package server

import (
	"context"
	"net/http"
	"slices"
	"strings"
	"time"

	httputil "git.mstar.dev/mstar/goutils/http"
	"github.com/rs/zerolog/hlog"
	"github.com/rs/zerolog/log"

	"git.mstar.dev/mstar/linstrom/config"
	"git.mstar.dev/mstar/linstrom/storage"
)

type HandlerBuilder func(http.Handler) http.Handler

func ChainMiddlewares(base http.Handler, links ...HandlerBuilder) http.Handler {
	slices.Reverse(links)
	for _, f := range links {
		base = f(base)
	}
	return base
}

func ContextValsMiddleware(pairs map[any]any) HandlerBuilder {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := r.Context()
			for key, val := range pairs {
				ctx = context.WithValue(ctx, key, val)
			}
			newRequest := r.WithContext(ctx)
			h.ServeHTTP(w, newRequest)
		})
	}
}

func LoggingMiddleware(handler http.Handler) http.Handler {
	return ChainMiddlewares(handler,
		hlog.NewHandler(log.Logger),
		hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
			if strings.HasPrefix(r.URL.Path, "/assets") {
				return
			}
			hlog.FromRequest(r).Info().
				Str("method", r.Method).
				Stringer("url", r.URL).
				Int("status", status).
				Int("size", size).
				Dur("duration", duration).
				Send()
		}),
		hlog.RemoteAddrHandler("ip"),
		hlog.UserAgentHandler("user_agent"),
		hlog.RefererHandler("referer"),
		hlog.RequestIDHandler("req_id", "Request-Id"),
	)
}

func passkeyIdToAccountIdTransformerMiddleware(handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s := StorageFromRequest(r)
		if s == nil {
			return
		}
		log := hlog.FromRequest(r)
		passkeyId, ok := r.Context().Value(ContextKeyPasskeyUsername).(string)
		if !ok {
			httputil.HttpErr(
				w,
				HttpErrIdMissingContextValue,
				"Actor name missing",
				http.StatusInternalServerError,
			)
			return
		}
		log.Debug().Bytes("passkey-bytes", []byte(passkeyId)).Msg("Id from passkey auth")
		acc, err := s.FindAccountByPasskeyId([]byte(passkeyId))
		if err != nil {
			httputil.HttpErr(
				w,
				HttpErrIdDbFailure,
				"Failed to get account from storage",
				http.StatusInternalServerError,
			)
			return
		}
		r = r.WithContext(context.WithValue(r.Context(), ContextKeyActorId, acc.ID))
		handler.ServeHTTP(w, r)
	})
}

func profilingAuthenticationMiddleware(handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.FormValue("password") != config.GlobalConfig.Admin.ProfilingPassword {
			httputil.HttpErr(w, HttpErrIdNotAuthenticated, "Bad password", http.StatusUnauthorized)
			return
		}
		handler.ServeHTTP(w, r)
	})
}

// Middleware for inserting a logged in account's id into the request context if a session exists
// Does not cancel requests ever. If an error occurs, it's treated as if no session is set
func checkSessionMiddleware(handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		cookie, err := r.Cookie("sid")
		log := hlog.FromRequest(r)
		if err != nil {
			// No cookie is ok, this function is only for inserting account id into the context
			// if one exists, not for checking permissions
			log.Debug().Msg("No session cookie, passing along")
			handler.ServeHTTP(w, r)
			return
		}
		store := StorageFromRequest(r)
		session, ok := store.GetSession(cookie.Value)
		if !ok {
			// Failed to get session from cookie id. Log, then move on as if no session is set
			log.Warn().
				Str("session-id", cookie.Value).
				Msg("Cookie with session id found, but session doesn't exist")
			handler.ServeHTTP(w, r)
			return
		}
		if session.Expires.Before(time.Now()) {
			// Session expired. Move on as if no session was set
			store.DeleteSession(cookie.Value)
			handler.ServeHTTP(w, r)
			return
		}
		acc, err := store.FindAccountByPasskeyId(session.UserID)
		switch err {
		case storage.ErrEntryNotFound:
			log.Info().Msg("No account for the given passkey id found. It probably got deleted")
			handler.ServeHTTP(w, r)
			return
		case nil:
		default:
			// Failed to get account for passkey id. Log, then move on as if no session is set
			log.Error().
				Err(err).
				Bytes("passkey-id", session.UserID).
				Msg("Failed to get account with passkey id while checking session. Ignoring session")
			handler.ServeHTTP(w, r)
			return
		}
		handler.ServeHTTP(
			w,
			r.WithContext(
				context.WithValue(
					r.Context(),
					ContextKeyActorId,
					acc.ID,
				),
			),
		)
	})
}

func requireValidSessionMiddleware(
	h func(http.ResponseWriter, *http.Request),
) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		_, ok := r.Context().Value(ContextKeyActorId).(string)
		if !ok {
			httputil.HttpErr(
				w,
				HttpErrIdNotAuthenticated,
				"Not authenticated",
				http.StatusUnauthorized,
			)
			return
		}
		h(w, r)
	}
}

func buildRequirePermissionsMiddleware(permissionRole *storage.Role) HandlerBuilder {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			accId, ok := r.Context().Value(ContextKeyActorId).(string)
			if !ok {
				httputil.HttpErr(
					w,
					HttpErrIdNotAuthenticated,
					"Not authenticated",
					http.StatusUnauthorized,
				)
				return
			}
			store := StorageFromRequest(r)
			log := hlog.FromRequest(r)
			acc, err := store.FindAccountById(accId)
			// Assumption: If this handler is hit, the middleware for checking if a session exists at all has already passed
			// and thus a valid account id must exist in the context
			if err != nil {
				log.Error().
					Err(err).
					Str("account-id", accId).
					Msg("Error while getting account from session")
				httputil.HttpErr(
					w,
					HttpErrIdDbFailure,
					"Error while getting account from session",
					http.StatusInternalServerError,
				)
				return
			}
			roles, err := store.FindRolesByNames(acc.Roles)
			// Assumption: There will always be at least two roles per user, default user and user-specific one
			if err != nil {
				httputil.HttpErr(
					w,
					HttpErrIdDbFailure,
					"Failed to get roles for account",
					http.StatusInternalServerError,
				)
				return
			}
			collapsedRole := storage.CollapseRolesIntoOne(roles...)
			if !storage.CompareRoles(&collapsedRole, permissionRole) {
				httputil.HttpErr(
					w,
					HttpErrIdNotAuthenticated,
					"Insufficient permisions",
					http.StatusForbidden,
				)
				return
			}
			h.ServeHTTP(w, r)
		})
	}
}
