package server

import (
	"context"
	"net/http"
	"net/url"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"

	"scrabble/backend/internal/session"
)

// headerUserID is the identity header the gateway injects after resolving a
// session to an internal account.
const headerUserID = "X-User-ID"

// contextKey is an unexported type for request-context keys set by this package.
type contextKey string

const userIDContextKey contextKey = "scrabble.user_id"

// RequireUserID returns middleware that requires a valid X-User-ID header and
// stores the parsed account id in the request context. Requests without a
// parseable UUID are rejected with 401. The backend treats X-User-ID as the
// sole identity input and never derives identity from the request body.
func RequireUserID() gin.HandlerFunc {
	return func(c *gin.Context) {
		id, err := uuid.Parse(c.GetHeader(headerUserID))
		if err != nil {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing or invalid X-User-ID"})
			return
		}
		c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), userIDContextKey, id))
		c.Next()
	}
}

// UserIDFromContext returns the authenticated account id stored by
// RequireUserID, and whether it was present.
func UserIDFromContext(ctx context.Context) (uuid.UUID, bool) {
	id, ok := ctx.Value(userIDContextKey).(uuid.UUID)
	return id, ok
}

// headerPlatform is the trusted execution-platform header the gateway injects after
// resolving a session's platform. Its value is "<kind>/<subtype>" (e.g. "vk/ios");
// it is omitted for an untrusted session, in which case platform(c) reports the
// zero Platform. Like X-User-ID, the value is the gateway's — never a client body.
const headerPlatform = "X-Platform"

// platformContext returns middleware that parses the gateway-injected X-Platform
// header into the request context, so handlers (and the link/merge session mint)
// read the caller's trusted platform. An absent or malformed header leaves the
// context without a platform (untrusted) and never rejects the request — X-Platform
// is a capability signal, not an identity gate.
func platformContext() gin.HandlerFunc {
	return func(c *gin.Context) {
		if p, ok := parsePlatformHeader(c.GetHeader(headerPlatform)); ok {
			c.Request = c.Request.WithContext(session.WithPlatform(c.Request.Context(), p))
		}
		c.Next()
	}
}

// parsePlatformHeader splits a "<kind>/<subtype>" X-Platform value into a Platform.
// A blank value or blank kind yields no platform (an untrusted session).
func parsePlatformHeader(h string) (session.Platform, bool) {
	if h == "" {
		return session.Platform{}, false
	}
	kind, subtype, _ := strings.Cut(h, "/")
	if kind == "" {
		return session.Platform{}, false
	}
	return session.Platform{Kind: kind, Subtype: subtype}, true
}

// platform returns the caller's trusted execution platform, or the zero Platform
// and false when the session was not attributed to one — an untrusted, view-only
// context for the payments gate.
func platform(c *gin.Context) (session.Platform, bool) {
	return session.PlatformFromContext(c.Request.Context())
}

// requireSameOrigin guards the admin console's state-changing requests: it rejects
// a non-safe request whose Origin (or, failing that, Referer) host does not match
// the request Host. The gateway authenticates the operator with Basic-Auth in front
// of /_gm; this same-origin check is the console's CSRF defence, stopping a
// cross-site form POST from riding the cached credential. Safe methods pass through.
func requireSameOrigin() gin.HandlerFunc {
	return func(c *gin.Context) {
		switch c.Request.Method {
		case http.MethodGet, http.MethodHead, http.MethodOptions:
			c.Next()
			return
		}
		if !sameOrigin(c.Request) {
			c.AbortWithStatus(http.StatusForbidden)
			return
		}
		c.Next()
	}
}

// sameOrigin reports whether the request's Origin (or, failing that, Referer) host
// matches the request Host. A state-changing request carrying neither header is
// rejected.
func sameOrigin(r *http.Request) bool {
	for _, h := range []string{r.Header.Get("Origin"), r.Header.Get("Referer")} {
		if h == "" {
			continue
		}
		u, err := url.Parse(h)
		if err != nil {
			return false
		}
		return u.Host == r.Host
	}
	return false
}

// codeAccountBlocked is the stable error code the suspension gate returns for a blocked account.
// It threads through the gateway unchanged as the Execute result_code, so the UI can detect the
// block from any call and switch to the terminal blocked screen.
const codeAccountBlocked = "account_blocked"

// blockStatusPath is the one /api/v1/user route exempt from the suspension gate: a blocked client
// must still reach it to fetch the block's expiry and reason for the blocked screen.
const blockStatusPath = "/api/v1/user/block-status"

// requireNotSuspended returns middleware that rejects a blocked account's requests with 403 and
// code "account_blocked", so the UI can detect an active block from any call. The block-status
// probe is exempt. It is a no-op when the account store is not wired. It runs after RequireUserID
// (which has already placed the account id in the context).
func (s *Server) requireNotSuspended() gin.HandlerFunc {
	return func(c *gin.Context) {
		if s.accounts == nil || c.FullPath() == blockStatusPath {
			c.Next()
			return
		}
		id, ok := userID(c)
		if !ok {
			c.Next() // RequireUserID runs first and has already rejected a missing id
			return
		}
		_, blocked, err := s.accounts.CurrentSuspension(c.Request.Context(), id)
		if err != nil {
			s.abortErr(c, err)
			return
		}
		if blocked {
			c.AbortWithStatusJSON(http.StatusForbidden, errorResponse{Error: errorBody{Code: codeAccountBlocked, Message: "account is blocked"}})
			return
		}
		c.Next()
	}
}
