package middleware

import (
	"context"
	"errors"
	"math"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/nexuscrm-api-host/origin"
)

const (
	// SessionControlCookieName uses the __Host- prefix so conforming browsers
	// require Secure, Path=/, and no Domain attribute.
	SessionControlCookieName = "__Host-nexuscrm-session-control"

	// HeaderSessionControlIntent is a fixed, non-simple request header. Browser
	// clients send SessionControlIntentValue when they intend to use the ambient
	// session-control cookie for first-party authentication or revocation.
	HeaderSessionControlIntent = "X-NexusCRM-Session-Control-Intent"
	SessionControlIntentValue  = "use"

	minSessionControlProofBytes = 32
	maxSessionControlProofBytes = 256
	headerOrigin                = "Origin"
	headerReferer               = "Referer"
	headerSecFetchSite          = "Sec-Fetch-Site"
	secFetchSiteSameOrigin      = "same-origin"
	secFetchSiteSameSite        = "same-site"
)

var (
	errInvalidSessionControlProof   = errors.New("invalid session-control proof")
	errInvalidSessionControlExpiry  = errors.New("invalid session-control cookie expiry")
	errSessionControlNotAdmitted    = errors.New("session-control request was not admitted")
	errSessionCredentialGuard       = errors.New("session credential response guard is required")
	errSessionCredentialCommitted   = errors.New("session credential response already committed")
	errInvalidSessionCredentialPair = errors.New("invalid paired session credentials")
)

type sessionControlProofContextKey struct{}

type sessionControlRequestState struct {
	admitted bool
	proof    string
}

// NewSessionControlAdmission returns middleware that marks first-party browser
// evidence as admitted and exposes a structurally valid session-control cookie,
// if present, to the request context. Admission requires the fixed intent
// header and first-party fetch metadata. Unsafe
// methods additionally require an exact allowed Origin. Same-site cross-origin
// requests always require an exact allowed Origin, while same-origin safe
// methods may use an exact Origin or Referer and tolerate their omission when
// Sec-Fetch-Site independently reports same-origin.
//
// allowedOrigins must be the host's static first-party frontend origins, not a
// dynamic CORS or OAuth connected-app allowlist. Register the returned handler
// before RequireAuth, OptionalAuth, or a login/revoke handler that reads the
// proof.
func NewSessionControlAdmission(allowedOrigins ...string) (gin.HandlerFunc, error) {
	admit, err := newSessionControlAdmission(allowedOrigins...)
	if err != nil {
		return nil, err
	}

	return func(c *gin.Context) {
		c.Request = admit(c.Request)
		c.Next()
	}, nil
}

// NewSessionControlHTTPAdmission exposes the same host-owned browser admission
// policy to AppServices that use net/http directly. It must wrap login,
// authenticated, and revoke routes before they inspect SessionControlProof.
func NewSessionControlHTTPAdmission(allowedOrigins ...string) (func(http.Handler) http.Handler, error) {
	admit, err := newSessionControlAdmission(allowedOrigins...)
	if err != nil {
		return nil, err
	}
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			next.ServeHTTP(w, admit(r))
		})
	}, nil
}

func newSessionControlAdmission(allowedOrigins ...string) (func(*http.Request) *http.Request, error) {
	allowed := make(map[string]struct{}, len(allowedOrigins))
	for _, raw := range allowedOrigins {
		canonical, ok := canonicalSessionControlOrigin(raw, false)
		if !ok {
			return nil, errors.New("invalid session-control allowed origin")
		}
		allowed[canonical] = struct{}{}
	}
	if len(allowed) == 0 {
		return nil, errors.New("at least one session-control allowed origin is required")
	}

	return func(request *http.Request) *http.Request {
		if request == nil {
			return nil
		}
		if state, attempted := sessionControlRequestAdmission(request, allowed); attempted {
			ctx := context.WithValue(request.Context(), sessionControlProofContextKey{}, state)
			return request.WithContext(ctx)
		}
		return request
	}, nil
}

// SessionControlRequestAdmitted reports whether NewSessionControlAdmission
// admitted the request's first-party browser evidence. Unlike
// SessionControlProof, it is true for an admitted initial login request that
// does not have a proof cookie yet.
func SessionControlRequestAdmitted(ctx context.Context) bool {
	state, ok := ctx.Value(sessionControlProofContextKey{}).(sessionControlRequestState)
	return ok && state.admitted
}

// SessionControlProof returns the admitted opaque proof, if one was installed
// by NewSessionControlAdmission. It is suitable for control-aware auth and
// revoke adapters; callers still own cryptographic/durable proof validation.
func SessionControlProof(ctx context.Context) (string, bool) {
	state, ok := ctx.Value(sessionControlProofContextKey{}).(sessionControlRequestState)
	return state.proof, ok && state.admitted && state.proof != ""
}

func sessionControlRequestAttempted(ctx context.Context) bool {
	_, ok := ctx.Value(sessionControlProofContextKey{}).(sessionControlRequestState)
	return ok
}

// SessionControlRequestAttempted reports whether the fixed browser intent was
// present but may have failed first-party source or proof validation. Public
// and authenticated adapters use it to fail closed without treating ordinary
// non-browser requests as malformed control attempts.
func SessionControlRequestAttempted(ctx context.Context) bool {
	return sessionControlRequestAttempted(ctx)
}

// IssueSessionControlCookie schedules the initial proof cookie at the protected
// response boundary for a request admitted by NewSessionControlAdmission.
// expiresAt is the durable browser-session expiry chosen by the consumer; the
// helper emits matching Expires and Max-Age attributes.
func IssueSessionControlCookie(c *gin.Context, proof string, expiresAt time.Time) error {
	if c == nil || c.Request == nil {
		return errSessionCredentialGuard
	}
	return IssueSessionControlResponse(c.Request.Context(), proof, expiresAt)
}

// IssueSessionControlResponse schedules the protected browser proof cookie for
// a request admitted by NewSessionControlHTTPAdmission. The outer handler must
// be ProtectSessionCredentialResponses so downstream code cannot spoof or leak
// the credential on a partially committed response.
func IssueSessionControlResponse(ctx context.Context, proof string, expiresAt time.Time) error {
	if ctx == nil {
		return errSessionCredentialGuard
	}
	if !SessionControlRequestAdmitted(ctx) {
		return errSessionControlNotAdmitted
	}
	cookie, err := newSessionControlCookie(proof, expiresAt, time.Now())
	if err != nil {
		return err
	}
	return setSessionControlResponseCookie(ctx, cookie)
}

// IssueRenewedSessionResponse schedules a replacement bearer and matching
// control-proof cookie as one protected response credential pair. Partial
// publication is rejected so a browser cannot retain mismatched credentials.
func IssueRenewedSessionResponse(ctx context.Context, bearerToken, proof string, expiresAt time.Time) error {
	if ctx == nil {
		return errSessionCredentialGuard
	}
	if !SessionControlRequestAdmitted(ctx) {
		return errSessionControlNotAdmitted
	}
	cookie, err := newSessionControlCookie(proof, expiresAt, time.Now())
	if err != nil {
		return err
	}
	return setSessionCredentialResponse(ctx, bearerToken, cookie)
}

// ClearSessionControlCookie schedules cookie expiry, normally after the
// consumer atomically revokes the durable session.
func ClearSessionControlCookie(c *gin.Context) error {
	if c == nil || c.Request == nil {
		return errSessionCredentialGuard
	}
	return ClearSessionControlResponse(c.Request.Context())
}

// ClearSessionControlResponse schedules cookie expiry at the protected
// response boundary after the Runtime session owner has accepted revocation.
func ClearSessionControlResponse(ctx context.Context) error {
	if ctx == nil {
		return errSessionCredentialGuard
	}
	return setSessionControlResponseCookie(ctx, expiredSessionControlCookie())
}

func newSessionControlCookie(proof string, expiresAt, now time.Time) (*http.Cookie, error) {
	if !validSessionControlProof(proof) {
		return nil, errInvalidSessionControlProof
	}
	remaining := expiresAt.Sub(now)
	if expiresAt.IsZero() || remaining <= 0 {
		return nil, errInvalidSessionControlExpiry
	}
	maxAgeSeconds := math.Ceil(remaining.Seconds())
	maxInt := int(^uint(0) >> 1)
	if maxAgeSeconds > float64(maxInt) {
		return nil, errInvalidSessionControlExpiry
	}
	return sessionControlCookie(proof, expiresAt.UTC(), int(maxAgeSeconds)), nil
}

func expiredSessionControlCookie() *http.Cookie {
	return sessionControlCookie("", time.Unix(1, 0).UTC(), -1)
}

func sessionControlCookie(value string, expiresAt time.Time, maxAge int) *http.Cookie {
	return &http.Cookie{
		Name:     SessionControlCookieName,
		Value:    value,
		Path:     "/",
		Expires:  expiresAt,
		MaxAge:   maxAge,
		Secure:   true,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
	}
}

func sessionControlRequestAdmission(r *http.Request, allowed map[string]struct{}) (sessionControlRequestState, bool) {
	if r == nil || len(r.Header.Values(HeaderSessionControlIntent)) == 0 {
		return sessionControlRequestState{}, false
	}
	if !singleHeaderEquals(r.Header, HeaderSessionControlIntent, SessionControlIntentValue) {
		return sessionControlRequestState{}, true
	}
	fetchSite, ok := singleSessionControlFetchSite(r.Header)
	if !ok || !requestHasAllowedSessionControlSource(r, allowed, fetchSite) {
		return sessionControlRequestState{}, true
	}

	state := sessionControlRequestState{}
	if !requestPresentsSessionControlCookie(r.Header) {
		state.admitted = true
		return state, true
	}
	proof, ok := usableSessionControlProof(r.Header)
	if !ok {
		return state, true
	}
	state.admitted = true
	state.proof = proof
	return state, true
}

// requestPresentsSessionControlCookie reports whether the request attaches the
// session-control cookie at all, by either the raw header or the parsed view. A
// request that attaches none is a first-party request without a browser
// credential, which is different from one that attaches an unusable credential.
func requestPresentsSessionControlCookie(header http.Header) bool {
	if sessionControlCookieOccurrenceCount(header) > 0 {
		return true
	}
	return len((&http.Request{Header: header}).CookiesNamed(SessionControlCookieName)) > 0
}

// usableSessionControlProof returns the one structurally valid session-control
// proof a request presents.
//
// This is the single proof-cookie parser at the authentication boundary, and both
// the session-control admission and the credential lane selector must use it. Two
// parsers there is a vulnerability rather than a duplication: if one accepts a
// value the other rejects, a request can be classified into the browser lane by
// the lane selector while admission considers it to carry no credential.
//
// Every rejection below is a distinct way a value can look usable but is not:
//
//   - the raw header must contain the cookie exactly once, because Go's parser
//     silently drops malformed duplicates that a browser or proxy may still have
//     sent;
//   - the parsed view must also yield exactly one cookie;
//   - a quoted value is rejected. Go strips the quotes and records Quoted, so the
//     unquoted value can look perfectly valid while the bytes on the wire were
//     not what any peer agreed on;
//   - the value must satisfy the proof's structural rule.
func usableSessionControlProof(header http.Header) (string, bool) {
	cookies := (&http.Request{Header: header}).CookiesNamed(SessionControlCookieName)
	if sessionControlCookieOccurrenceCount(header) != 1 ||
		len(cookies) != 1 ||
		cookies[0].Quoted ||
		!validSessionControlProof(cookies[0].Value) {
		return "", false
	}
	return cookies[0].Value, true
}

func sessionControlCookieOccurrenceCount(header http.Header) int {
	count := 0
	for _, line := range header.Values("Cookie") {
		for len(line) > 0 {
			part, rest, _ := strings.Cut(line, ";")
			line = rest
			name, _, _ := strings.Cut(strings.TrimSpace(part), "=")
			if strings.TrimSpace(name) == SessionControlCookieName {
				count++
			}
		}
	}
	return count
}

func requestHasAllowedSessionControlSource(r *http.Request, allowed map[string]struct{}, fetchSite string) bool {
	originPresent, originAllowed := singleAllowedHeaderOrigin(r.Header, headerOrigin, allowed, false)
	if originPresent && !originAllowed {
		return false
	}

	refererPresent, refererAllowed := singleAllowedHeaderOrigin(r.Header, headerReferer, allowed, true)
	if refererPresent && !refererAllowed {
		return false
	}

	if fetchSite == secFetchSiteSameSite || !safeSessionControlMethod(r.Method) {
		return originPresent && originAllowed
	}
	return true
}

func singleSessionControlFetchSite(header http.Header) (string, bool) {
	values := header.Values(headerSecFetchSite)
	if len(values) != 1 {
		return "", false
	}
	switch values[0] {
	case secFetchSiteSameOrigin, secFetchSiteSameSite:
		return values[0], true
	default:
		return "", false
	}
}

func singleAllowedHeaderOrigin(header http.Header, name string, allowed map[string]struct{}, allowURL bool) (bool, bool) {
	values := header.Values(name)
	if len(values) == 0 {
		return false, false
	}
	if len(values) != 1 {
		return true, false
	}
	canonical, ok := canonicalSessionControlOrigin(values[0], allowURL)
	if !ok {
		return true, false
	}
	_, ok = allowed[canonical]
	return true, ok
}

func canonicalSessionControlOrigin(raw string, allowURL bool) (string, bool) {
	var (
		canonical string
		ok        bool
	)
	if allowURL {
		canonical, ok = origin.CanonicalOriginFromURL(raw)
	} else {
		canonical, ok = origin.CanonicalOrigin(raw)
		if ok {
			// Apply the same HTTPS-or-loopback rule used for absolute browser
			// URLs; origin.CanonicalOrigin alone intentionally permits generic
			// HTTP origins for non-cookie CORS callers.
			canonical, ok = origin.CanonicalOriginFromURL(canonical)
		}
	}
	if !ok {
		return "", false
	}
	return strings.ToLower(canonical), true
}

func singleHeaderEquals(header http.Header, name, expected string) bool {
	values := header.Values(name)
	return len(values) == 1 && values[0] == expected
}

func safeSessionControlMethod(method string) bool {
	switch method {
	case http.MethodGet, http.MethodHead, http.MethodOptions:
		return true
	default:
		return false
	}
}

func validSessionControlProof(proof string) bool {
	if len(proof) < minSessionControlProofBytes || len(proof) > maxSessionControlProofBytes || proof != strings.TrimSpace(proof) {
		return false
	}
	for i := 0; i < len(proof); i++ {
		b := proof[i]
		if b < 0x21 || b > 0x7e || b == '"' || b == ',' || b == ';' || b == '\\' {
			return false
		}
	}
	cookie := &http.Cookie{Name: SessionControlCookieName, Value: proof}
	return cookie.Valid() == nil
}
