package auth

import (
	"bytes"
	"compress/flate"
	"crypto"
	"crypto/ecdsa"
	"crypto/rsa"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/xml"
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/beevik/etree"
	"github.com/crewjam/saml"
	"github.com/gofiber/fiber/v2"
	dsig "github.com/russellhaering/goxmldsig"
	"github.com/valyala/fasthttp/fasthttpadaptor"
	"wucher/internal/config"
	"wucher/internal/service"
)

type Handler struct {
	cfg  config.Config
	auth *service.AuthService
}

func NewHandler(cfg config.Config, authSvc *service.AuthService) *Handler {
	return &Handler{cfg: cfg, auth: authSvc}
}

func (h *Handler) HandleSAMLCallback(c *fiber.Ctx) error {
	if SAMLMiddleware == nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "saml is not initialized"})
	}
	req, err := toHTTPRequest(c)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request"})
	}
	if err := req.ParseForm(); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid form payload"})
	}
	assertion, err := SAMLMiddleware.ServiceProvider.ParseResponse(req, []string{""})
	if err != nil {
		resp := fiber.Map{"error": "invalid saml response"}
		if strings.EqualFold(h.cfg.AppEnv, "development") || strings.EqualFold(h.cfg.AppEnv, "local") {
			resp["detail"] = err.Error()
			var invalidRespErr *saml.InvalidResponseError
			if errors.As(err, &invalidRespErr) && invalidRespErr.PrivateErr != nil {
				resp["private_detail"] = invalidRespErr.PrivateErr.Error()
			}
			if raw := c.FormValue("SAMLResponse"); raw != "" {
				if statusCode, statusMessage := samlResponseStatus(raw); statusCode != "" || statusMessage != "" {
					resp["status_code"] = statusCode
					resp["status_message"] = statusMessage
				}
			}
		}
		return c.Status(fiber.StatusUnauthorized).JSON(resp)
	}

	nameID := ""
	email := ""
	givenName := ""
	surname := ""
	displayName := ""
	if assertion.Subject != nil {
		nameID = assertion.Subject.NameID.Value
	}
	for _, stmt := range assertion.AttributeStatements {
		for _, attr := range stmt.Attributes {
			if len(attr.Values) == 0 {
				continue
			}
			val := attr.Values[0].Value
			switch attr.Name {
			case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "emailaddress":
				email = val
			case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "givenname":
				givenName = val
			case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", "surname":
				surname = val
			case "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "name":
				displayName = val
			}
		}
	}
	if displayName == "" {
		displayName = givenName + " " + surname
	}

	now := time.Now().UTC()
	session := UserSession{
		UserID:          nameID,
		MicrosoftNameID: nameID,
		Email:           email,
		Name:            displayName,
		LoggedInAt:      now,
		ExpiresAt:       now.Add(time.Duration(h.cfg.SessionTTLHour) * time.Hour),
	}
	token, err := CreateSession(session)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "create session failed"})
	}

	if h.auth != nil {
		claims := map[string]any{
			"sub":   nameID,
			"email": email,
			"name":  displayName,
			"oid":   nameID,
			"exp":   time.Now().UTC().Add(5 * time.Minute).Unix(),
		}
		identity, err := h.auth.ExchangeMicrosoftToken(c.UserContext(), "saml_access", "saml_id", claims)
		if err == nil && identity != nil {
			user, userErr := h.auth.GetUserByID(c.UserContext(), identity.UserID)
			if userErr == nil && user != nil {
				tokens, tokenErr := h.auth.IssueTokensWithClientForProviderAndDeviceType(
					c.UserContext(),
					user,
					c.Get(fiber.HeaderUserAgent),
					c.IP(),
					"microsoft",
					readDeviceTypeHint(c),
				)
				if tokenErr == nil {
					setAuthCookies(c, h.auth, tokens)
				}
			}
		}
	}

	c.Cookie(&fiber.Cookie{
		Name:     "session_token",
		Value:    token,
		HTTPOnly: true,
		Secure:   h.cfg.CookieSecure,
		SameSite: "lax",
		Path:     "/",
		Domain:   h.cfg.CookieDomain,
		Expires:  session.ExpiresAt,
	})
	return c.Redirect(h.cfg.FrontendURL+"/dashboard", fiber.StatusFound)
}

func (h *Handler) HandleSLO(c *fiber.Ctx) error {
	return h.HandleSAMLLogoutRequest(c)
}

func (h *Handler) HandleSAMLLogoutRequest(c *fiber.Ctx) error {
	if SAMLMiddleware == nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "saml is not initialized"})
	}
	log.Printf("saml logout request: has_saml_request=%t has_saml_response=%t method=%s", formOrQuery(c, "SAMLRequest") != "", formOrQuery(c, "SAMLResponse") != "", c.Method())
	rawReq := formOrQuery(c, "SAMLRequest")
	if rawReq == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "missing SAMLRequest"})
	}

	xmlBytes, err := decodeSAMLMessage(rawReq)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid SAMLRequest encoding"})
	}
	if c.Method() == fiber.MethodGet {
		if err := validateRedirectSAMLSignature(c); err != nil {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid SAMLRequest signature"})
		}
	} else {
		if err := validateLogoutRequestSignature(xmlBytes); err != nil {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid SAMLRequest signature"})
		}
	}

	var logoutReq saml.LogoutRequest
	if err := xml.Unmarshal(xmlBytes, &logoutReq); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid LogoutRequest XML"})
	}
	if err := validateSAMLMessageIssuer(logoutReq.Issuer.Value); err != nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid SAMLRequest issuer"})
	}
	nameID := strings.TrimSpace(logoutReq.NameID.Value)
	h.cleanupUserAuthSession(c, nameID)
	log.Printf("saml logout request processed: name_id_present=%t", nameID != "")

	relayState := formOrQuery(c, "RelayState")
	redirectURL, err := SAMLMiddleware.ServiceProvider.MakeRedirectLogoutResponse(logoutReq.ID, relayState)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to build LogoutResponse"})
	}
	return c.Redirect(redirectURL.String(), fiber.StatusFound)
}

func (h *Handler) HandleSAMLLogoutResponse(c *fiber.Ctx) error {
	if SAMLMiddleware == nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "saml is not initialized"})
	}
	rawQuery := c.Context().QueryArgs().String()
	hasSAMLResponse := formOrQuery(c, "SAMLResponse") != ""
	hasRelayState := formOrQuery(c, "RelayState") != ""
	hasSigAlg := formOrQuery(c, "SigAlg") != ""
	hasSignature := formOrQuery(c, "Signature") != ""
	sigAlg := formOrQuery(c, "SigAlg")
	log.Printf("saml logout response: method=%s path=%s hasSAMLResponse=%t hasRelayState=%t hasSigAlg=%t hasSignature=%t sigAlg=%q rawQueryLen=%d",
		c.Method(), c.Path(), hasSAMLResponse, hasRelayState, hasSigAlg, hasSignature, sigAlg, len(rawQuery))
	rawResp := formOrQuery(c, "SAMLResponse")
	if rawResp == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "missing SAMLResponse"})
	}

	if c.Method() == fiber.MethodGet && hasSigAlg && hasSignature {
		if err := validateRedirectSAMLSignatureRaw(rawQuery, "SAMLResponse"); err != nil {
			log.Printf("saml logout response: verification_result=failed mode=redirect reason=%v", err)
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid SAMLResponse signature"})
		}
		log.Printf("saml logout response: verification_result=ok mode=redirect")
	}

	xmlBytes, err := decodeSAMLMessage(rawResp)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid SAMLResponse encoding"})
	}

	if c.Method() == fiber.MethodGet && !(hasSigAlg && hasSignature) {
		if hasSigAlg != hasSignature {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "missing SAMLResponse signature"})
		}
		if !hasXMLSignature(xmlBytes) {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "missing SAMLResponse signature"})
		}
		if err := validateLogoutResponseSignature(xmlBytes); err != nil {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid SAMLResponse signature"})
		}
	} else if c.Method() != fiber.MethodGet {
		if err := validateLogoutResponseSignature(xmlBytes); err != nil {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid SAMLResponse signature"})
		}
	}

	var logoutResp saml.LogoutResponse
	if err := xml.Unmarshal(xmlBytes, &logoutResp); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid LogoutResponse XML"})
	}
	if err := validateSAMLMessageIssuer(logoutResp.Issuer.Value); err != nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid SAMLResponse issuer"})
	}
	if strings.TrimSpace(logoutResp.Status.StatusCode.Value) != saml.StatusSuccess {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid SAMLResponse status"})
	}
	h.cleanupUserAuthSession(c, "")
	return c.Redirect(h.logoutRedirectURL(), fiber.StatusFound)
}

func (h *Handler) HandleMetadata(c *fiber.Ctx) error {
	if SAMLMiddleware == nil {
		return c.Status(fiber.StatusInternalServerError).SendString("saml is not initialized")
	}
	buf, err := xml.MarshalIndent(SAMLMiddleware.ServiceProvider.Metadata(), "", "  ")
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).SendString("failed to marshal metadata")
	}
	c.Set(fiber.HeaderContentType, "application/samlmetadata+xml")
	return c.Send(buf)
}

func (h *Handler) HandleLogin(c *fiber.Ctx) error {
	if SAMLMiddleware == nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "saml is not initialized"})
	}
	redirectURL, err := SAMLMiddleware.ServiceProvider.MakeRedirectAuthenticationRequest("")
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to build authn request"})
	}
	if wantsJSON(c) {
		return c.Status(fiber.StatusOK).JSON(fiber.Map{
			"data": fiber.Map{
				"type": "auth_microsoft_url",
				"attributes": fiber.Map{
					"url":          redirectURL.String(),
					"redirect_url": redirectURL.String(),
				},
			},
		})
	}
	return c.Redirect(redirectURL.String(), fiber.StatusFound)
}

func (h *Handler) HandleLogout(c *fiber.Ctx) error {
	return h.HandleBrowserLocalLogout(c)
}

func (h *Handler) HandleBrowserLocalLogout(c *fiber.Ctx) error {
	log.Printf("browser local logout: method=%s", c.Method())
	var nameID string
	if token := strings.TrimSpace(c.Cookies("session_token")); token != "" {
		if sess, err := GetSession(token); err == nil {
			nameID = strings.TrimSpace(sess.MicrosoftNameID)
		}
	}
	h.cleanupUserAuthSession(c, nameID)
	if nameID != "" && SAMLMiddleware != nil {
		redirectURL, err := SAMLMiddleware.ServiceProvider.MakeRedirectLogoutRequest(nameID, "")
		if err == nil {
			return c.Redirect(redirectURL.String(), fiber.StatusFound)
		}
	}
	return c.Redirect(h.logoutRedirectURL(), fiber.StatusFound)
}

func toHTTPRequest(c *fiber.Ctx) (*http.Request, error) {
	r := new(http.Request)
	if err := fasthttpadaptor.ConvertRequest(c.Context(), r, true); err != nil {
		return nil, err
	}
	return r, nil
}

func validateLogoutRequestSignature(xmlBytes []byte) error {
	certs, err := getIDPCertificates()
	if err != nil {
		return err
	}
	certStore := dsig.MemoryX509CertificateStore{Roots: certs}
	validator := dsig.NewDefaultValidationContext(&certStore)

	doc := etree.NewDocument()
	if err := doc.ReadFromBytes(xmlBytes); err != nil {
		return fmt.Errorf("parse xml: %w", err)
	}
	root := doc.Root()
	if root == nil {
		return fmt.Errorf("empty xml")
	}
	_, err = validator.Validate(root)
	return err
}

func validateRedirectSAMLSignature(c *fiber.Ctx) error {
	rawQuery := c.Context().QueryArgs().String()
	return validateRedirectSAMLSignatureRaw(rawQuery, "")
}

func validateRedirectSAMLSignatureRaw(rawQuery, expectedMessageKey string) error {
	params, err := url.ParseQuery(rawQuery)
	if err != nil {
		return fmt.Errorf("invalid query: %w", err)
	}

	sigAlg := params.Get("SigAlg")
	signatureB64 := params.Get("Signature")
	if sigAlg == "" || signatureB64 == "" {
		return fmt.Errorf("missing SigAlg or Signature")
	}

	signedPayload, err := buildRedirectSignaturePayload(rawQuery, expectedMessageKey)
	if err != nil {
		return err
	}
	log.Printf("saml redirect signature: canonicalLen=%d", len(signedPayload))

	signature, err := base64.StdEncoding.DecodeString(signatureB64)
	if err != nil {
		return fmt.Errorf("invalid signature encoding: %w", err)
	}

	certs, err := getIDPCertificates()
	if err != nil {
		return err
	}
	for _, cert := range certs {
		log.Printf("saml redirect signature: certThumbprint=%s", certificateThumbprintSHA1(cert))
		if err := verifyRedirectSignature(cert.PublicKey, sigAlg, signedPayload, signature); err == nil {
			return nil
		}
	}
	return fmt.Errorf("signature verification failed")
}

func buildRedirectSignaturePayload(rawQuery, expectedMessageKey string) ([]byte, error) {
	var samlReq, samlResp, relay, sigAlg string
	for _, part := range strings.Split(rawQuery, "&") {
		if part == "" {
			continue
		}
		key, val, found := strings.Cut(part, "=")
		if !found {
			continue
		}
		switch key {
		case "SAMLRequest":
			if samlReq == "" {
				samlReq = val
			}
		case "SAMLResponse":
			if samlResp == "" {
				samlResp = val
			}
		case "RelayState":
			if relay == "" {
				relay = val
			}
		case "SigAlg":
			if sigAlg == "" {
				sigAlg = val
			}
		}
	}

	msgKey := "SAMLRequest"
	msgVal := samlReq
	if msgVal == "" {
		msgKey = "SAMLResponse"
		msgVal = samlResp
	}
	if expectedMessageKey != "" && msgKey != expectedMessageKey {
		return nil, fmt.Errorf("missing %s", expectedMessageKey)
	}
	if msgVal == "" || sigAlg == "" {
		return nil, fmt.Errorf("missing SAML payload or SigAlg")
	}

	payload := msgKey + "=" + msgVal
	if relay != "" {
		payload += "&RelayState=" + relay
	}
	payload += "&SigAlg=" + sigAlg
	return []byte(payload), nil
}

func validateLogoutResponseSignature(xmlBytes []byte) error {
	return validateLogoutRequestSignature(xmlBytes)
}

func hasXMLSignature(xmlBytes []byte) bool {
	doc := etree.NewDocument()
	if err := doc.ReadFromBytes(xmlBytes); err != nil {
		return false
	}
	root := doc.Root()
	if root == nil {
		return false
	}
	for _, el := range root.FindElements(".//*") {
		if strings.EqualFold(el.Tag, "Signature") || strings.HasSuffix(el.Tag, ":Signature") {
			return true
		}
	}
	return false
}

func certificateThumbprintSHA1(cert *x509.Certificate) string {
	if cert == nil {
		return ""
	}
	sum := sha1.Sum(cert.Raw)
	return strings.ToUpper(hex.EncodeToString(sum[:]))
}

func validateSAMLMessageIssuer(issuer string) error {
	issuer = strings.TrimSpace(issuer)
	if issuer == "" {
		return fmt.Errorf("missing issuer")
	}
	if SAMLMiddleware == nil || SAMLMiddleware.ServiceProvider.IDPMetadata == nil {
		return fmt.Errorf("missing idp metadata")
	}
	expected := strings.TrimSpace(SAMLMiddleware.ServiceProvider.IDPMetadata.EntityID)
	if expected == "" {
		return fmt.Errorf("missing expected issuer")
	}
	if issuer != expected {
		return fmt.Errorf("unexpected issuer")
	}
	return nil
}

func (h *Handler) cleanupUserAuthSession(c *fiber.Ctx, nameID string) {
	if strings.TrimSpace(nameID) != "" {
		_ = DeleteSessionByNameID(nameID)
	}
	if token := strings.TrimSpace(c.Cookies("session_token")); token != "" {
		_ = DeleteSession(token)
	}
	clearSessionCookie(c, h.cfg.CookieDomain, h.cfg.CookieSecure)
	if h.auth == nil {
		return
	}
	accessToken := strings.TrimSpace(c.Cookies(h.auth.AccessCookieName()))
	refreshToken := strings.TrimSpace(c.Cookies(h.auth.RefreshCookieName()))
	if accessToken != "" {
		if userID, err := h.auth.ParseAccessToken(c.UserContext(), accessToken); err == nil && len(userID) == 16 {
			_ = h.auth.RevokeAllUserSessions(c.UserContext(), userID)
			log.Printf("logout cleanup: revoked app sessions user_id=%x", userID)
		}
	}
	if refreshToken != "" {
		_ = h.auth.RevokeRefreshToken(c.UserContext(), refreshToken)
	}
	clearAuthCookies(c, h.auth)
}

func clearSessionCookie(c *fiber.Ctx, domain string, secure bool) {
	c.Cookie(&fiber.Cookie{
		Name:     "session_token",
		Value:    "",
		Path:     "/",
		Domain:   domain,
		HTTPOnly: true,
		Secure:   secure,
		SameSite: "lax",
		MaxAge:   -1,
		Expires:  time.Unix(0, 0),
	})
}

func clearAuthCookies(c *fiber.Ctx, svc *service.AuthService) {
	if svc == nil {
		return
	}
	c.Cookie(&fiber.Cookie{
		Name:     svc.AccessCookieName(),
		Value:    "",
		HTTPOnly: true,
		Secure:   svc.CookieSecure(),
		SameSite: parseSameSite(svc.CookieSameSite()),
		Domain:   svc.CookieDomain(),
		Path:     "/",
		MaxAge:   -1,
	})
	c.Cookie(&fiber.Cookie{
		Name:     svc.RefreshCookieName(),
		Value:    "",
		HTTPOnly: true,
		Secure:   svc.CookieSecure(),
		SameSite: parseSameSite(svc.CookieSameSite()),
		Domain:   svc.CookieDomain(),
		Path:     "/api/v1/auth/refresh",
		MaxAge:   -1,
	})
}

func (h *Handler) logoutRedirectURL() string {
	base := strings.TrimRight(strings.TrimSpace(h.cfg.FrontendURL), "/")
	if base == "" {
		return "/signin?loggedOut=true"
	}
	return base + "/signin?loggedOut=true"
}

func verifyRedirectSignature(pub any, sigAlg string, payload, signature []byte) error {
	switch sigAlg {
	case "http://www.w3.org/2000/09/xmldsig#rsa-sha1":
		sum := sha1.Sum(payload)
		key, ok := pub.(*rsa.PublicKey)
		if !ok {
			return fmt.Errorf("unexpected key type %T", pub)
		}
		return rsa.VerifyPKCS1v15(key, crypto.SHA1, sum[:], signature)
	case "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256":
		sum := sha256.Sum256(payload)
		key, ok := pub.(*rsa.PublicKey)
		if !ok {
			return fmt.Errorf("unexpected key type %T", pub)
		}
		return rsa.VerifyPKCS1v15(key, crypto.SHA256, sum[:], signature)
	case "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384":
		sum := sha512.Sum384(payload)
		key, ok := pub.(*rsa.PublicKey)
		if !ok {
			return fmt.Errorf("unexpected key type %T", pub)
		}
		return rsa.VerifyPKCS1v15(key, crypto.SHA384, sum[:], signature)
	case "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512":
		sum := sha512.Sum512(payload)
		key, ok := pub.(*rsa.PublicKey)
		if !ok {
			return fmt.Errorf("unexpected key type %T", pub)
		}
		return rsa.VerifyPKCS1v15(key, crypto.SHA512, sum[:], signature)
	case "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256":
		sum := sha256.Sum256(payload)
		key, ok := pub.(*ecdsa.PublicKey)
		if !ok {
			return fmt.Errorf("unexpected key type %T", pub)
		}
		if ecdsa.VerifyASN1(key, sum[:], signature) {
			return nil
		}
		return fmt.Errorf("ecdsa verify failed")
	default:
		return fmt.Errorf("unsupported SigAlg %q", sigAlg)
	}
}

func formOrQuery(c *fiber.Ctx, key string) string {
	if v := strings.TrimSpace(c.FormValue(key)); v != "" {
		return v
	}
	return strings.TrimSpace(c.Query(key))
}

func decodeSAMLMessage(raw string) ([]byte, error) {
	decoded, err := base64.StdEncoding.DecodeString(raw)
	if err != nil {
		return nil, err
	}
	if bytes.Contains(decoded, []byte("<")) {
		return decoded, nil
	}

	reader := flate.NewReader(bytes.NewReader(decoded))
	defer reader.Close()
	var out bytes.Buffer
	if _, err := out.ReadFrom(reader); err != nil {
		return nil, err
	}
	return out.Bytes(), nil
}

func samlResponseStatus(raw string) (string, string) {
	decoded, err := base64.StdEncoding.DecodeString(raw)
	if err != nil {
		return "", ""
	}

	var resp saml.Response
	if err := xml.Unmarshal(decoded, &resp); err != nil {
		return "", ""
	}

	if resp.Status.StatusCode.Value == "" && resp.Status.StatusMessage == nil {
		return "", ""
	}

	statusMessage := ""
	if resp.Status.StatusMessage != nil {
		statusMessage = strings.TrimSpace(resp.Status.StatusMessage.Value)
	}
	return resp.Status.StatusCode.Value, statusMessage
}

func readDeviceTypeHint(c *fiber.Ctx) string {
	return strings.TrimSpace(c.Get("X-Device-Type"))
}

func setAuthCookies(c *fiber.Ctx, svc *service.AuthService, tokens service.TokenPair) {
	if svc == nil {
		return
	}
	refreshTTL := tokens.RefreshTTL
	if refreshTTL <= 0 {
		refreshTTL = svc.RefreshTTL()
	}
	c.Cookie(&fiber.Cookie{
		Name:     svc.AccessCookieName(),
		Value:    tokens.AccessToken,
		HTTPOnly: true,
		Secure:   svc.CookieSecure(),
		SameSite: parseSameSite(svc.CookieSameSite()),
		Domain:   svc.CookieDomain(),
		Path:     "/",
		MaxAge:   int(svc.AccessTTL().Seconds()),
	})
	c.Cookie(&fiber.Cookie{
		Name:     svc.RefreshCookieName(),
		Value:    tokens.RefreshToken,
		HTTPOnly: true,
		Secure:   svc.CookieSecure(),
		SameSite: parseSameSite(svc.CookieSameSite()),
		Domain:   svc.CookieDomain(),
		Path:     "/api/v1/auth/refresh",
		MaxAge:   int(refreshTTL.Seconds()),
	})
}

func parseSameSite(v string) string {
	switch strings.ToLower(strings.TrimSpace(v)) {
	case "none":
		return "none"
	case "strict":
		return "strict"
	default:
		return "lax"
	}
}

func wantsJSON(c *fiber.Ctx) bool {
	accept := strings.ToLower(strings.TrimSpace(c.Get(fiber.HeaderAccept)))
	return strings.Contains(accept, "application/json") || strings.Contains(accept, "application/vnd.api+json")
}
