package middleware

import (
	"context"
	"log"
	"maxwarden/auth"
	"maxwarden/config"
	"maxwarden/constants"
	"maxwarden/security"
	"maxwarden/users"
	"net/http"
	"net/url"
	"strings"
	"time"
)

type identityKey struct{}

func LoadIdentity(h http.HandlerFunc, requireAuth bool) http.HandlerFunc {
	loginPath := constants.IDENTITY_LOGIN_PATH
	logoutPath := constants.IDENTITY_LOGOUT_PATH
	defaultPath := constants.IDENTITY_DEFAULT_PATH
	redirect := constants.IDENTITY_AUTH_REDIRECT

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var identity *auth.Identity

		token := r.Header.Get("Authorization")

		isToken := token != ""

		// if bearer token present, use token auth, else use cookies
		if isToken {
			redirect = false
			splitToken := strings.Split(token, "Bearer ")

			if len(splitToken) >= 2 {
				token = splitToken[1]
				identity, _ = security.DecryptData[auth.Identity](
					[]byte(security.DecodeBase58(token)),
					config.GetConfig().IdentityPrivateKey,
				)
			}

			if identity == nil {
				blankIdentity := &auth.Identity{Authenticated: false}

				if requireAuth {
					http.Error(w, "Error: Unauthorized", http.StatusUnauthorized)
					return
				}

				ctx := context.WithValue(r.Context(), identityKey{}, blankIdentity)
				h.ServeHTTP(w, r.WithContext(ctx))
				return
			}
		} else {
			identityCookie, err := r.Cookie(constants.IDENTITY_COOKIE_NAME)
			if err == nil {
				identity, _ = security.DecryptData[auth.Identity](
					[]byte(security.DecodeBase58(identityCookie.Value)),
					config.GetConfig().IdentityPrivateKey,
				)
			}

			if identity == nil {
				blankIdentity := &auth.Identity{Authenticated: false}

				if requireAuth {
					if redirect && loginPath != r.URL.Path && logoutPath != r.URL.Path {
						redirectPath := loginPath + "?redirect=" + url.QueryEscape(r.URL.String())
						http.Redirect(w, r, redirectPath, http.StatusFound)

						return
					} else if !redirect && loginPath != r.URL.Path {
						http.Error(w, "Error: Unauthorized", http.StatusUnauthorized)
						return
					}
				}

				ctx := context.WithValue(r.Context(), identityKey{}, blankIdentity)
				h.ServeHTTP(w, r.WithContext(ctx))
				return
			}
		}

		// fetch the current user according to the database, and validate that the security stamp hasn't changed.
		// if it has, invalidate the login session.
		latestUser, _ := users.FetchById(identity.UserID)

		securityCheckFailed := latestUser.SecurityStamp != identity.SecurityStamp
		notAuthenticated := requireAuth && !identity.Authenticated
		identityExpired := identity.Expiration.Before(time.Now())

		if securityCheckFailed || notAuthenticated || identityExpired {
			if isToken {
				http.Error(w, "Error: Unauthorized", http.StatusUnauthorized)
				return
			} else {
				DeleteIdentityCookie(w, r)
				http.Redirect(w, r, loginPath, http.StatusFound)
				return
			}
		}

		if redirect && loginPath == r.URL.Path {
			http.Redirect(w, r, defaultPath, http.StatusFound)
			return
		}

		ctx := context.WithValue(r.Context(), identityKey{}, identity)
		h.ServeHTTP(w, r.WithContext(ctx))
	})
}

func GetIdentity(r *http.Request) *auth.Identity {
	identity := r.Context().Value(identityKey{}).(*auth.Identity)
	return identity
}

func PutIdentityCookie(w http.ResponseWriter, r *http.Request, identity *auth.Identity) {
	cookies := r.Cookies()

	// calculate total bytes used by other cookies
	var totalBytes int
	for _, cookie := range cookies {
		if cookie.Name == constants.IDENTITY_COOKIE_NAME {
			continue
		} else {
			totalBytes += len(cookie.Value)
		}
	}

	// A cookie serializer is a better way to handle session data. they are still
	// generated, validated, and read only by the server, but they are stored on the
	// client in a cookie.

	// For example, when a user logs into a web service, all of their auth data is
	// packed into a serialized encrypted string, which is sent via a cookie. this
	// cookie can be sent back to the page, decrypted, and de-serialized to retrieve
	// auth information in code. this is extremely fast and cheap, since you do not
	// need to store this data in a database, or even in memory.

	// Of course with this approach you must be careful not to leak the encryption
	// key, since it can be used to decrypt legitimate keys, and sign faulty ones.
	// The key should not be checked into VCS, and be regenerated if theft is
	// suspected. Resetting the key will log *everyone* out, since no sessions
	// or identities will validate.
	identityData, err := security.EncryptData(identity, config.GetConfig().IdentityPrivateKey)
	if err != nil {
		return
	}

	length := len(identityData) + 8 // 8 additional bytes coming from somewhere ¯\_(ツ)_/¯

	if length+totalBytes > 4096 {
		log.Println("Attempt to generate cookie exceeding 4096 bytes")
		return
	}

	httpCookie := &http.Cookie{
		Name:     constants.IDENTITY_COOKIE_NAME,
		Value:    security.EncodeBase58(identityData),
		HttpOnly: true,
		Secure:   r.URL.Scheme == "https",
		Path:     "/",
		SameSite: http.SameSiteStrictMode,
	}

	// if no expiry is set, cookie defaults to clear after browser closes

	http.SetCookie(w, httpCookie)
}

func DeleteIdentityCookie(w http.ResponseWriter, r *http.Request) {
	http.SetCookie(w, &http.Cookie{
		Name:    constants.IDENTITY_COOKIE_NAME,
		MaxAge:  -1,
		Expires: time.Now().Add(-100 * time.Hour),
		Path:    "/",
	})
}
