// Package restapi exposes the unauthenticated public REST surface of the
// gateway.
package restapi

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"sync"

	"galaxy/gateway/internal/config"
	"galaxy/gateway/internal/telemetry"

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.uber.org/zap"
)

const (
	jsonContentType = "application/json; charset=utf-8"

	errorCodeInvalidRequest     = "invalid_request"
	errorCodeNotFound           = "not_found"
	errorCodeMethodNotAllowed   = "method_not_allowed"
	errorCodeInternalError      = "internal_error"
	errorCodeServiceUnavailable = "service_unavailable"

	publicRESTBaseBucketKeyPrefix = "public_rest/class="
)

// PublicRouteClass identifies the public traffic class assigned to an incoming
// REST request before route handling and edge policy evaluation.
type PublicRouteClass string

const (
	// PublicRouteClassPublicAuth identifies public authentication commands.
	PublicRouteClassPublicAuth PublicRouteClass = "public_auth"

	// PublicRouteClassBrowserBootstrap identifies browser bootstrap traffic such
	// as the main document request.
	PublicRouteClassBrowserBootstrap PublicRouteClass = "browser_bootstrap"

	// PublicRouteClassBrowserAsset identifies browser asset requests.
	PublicRouteClassBrowserAsset PublicRouteClass = "browser_asset"

	// PublicRouteClassPublicMisc identifies public traffic that does not match a
	// more specific class.
	PublicRouteClassPublicMisc PublicRouteClass = "public_misc"
)

var configureGinModeOnce sync.Once

// Normalized returns c when it belongs to the stable public route class set.
// Unknown or empty values collapse to PublicRouteClassPublicMisc so edge policy
// code can rely on a fixed anti-abuse namespace.
func (c PublicRouteClass) Normalized() PublicRouteClass {
	switch c {
	case PublicRouteClassPublicAuth,
		PublicRouteClassBrowserBootstrap,
		PublicRouteClassBrowserAsset,
		PublicRouteClassPublicMisc:
		return c
	default:
		return PublicRouteClassPublicMisc
	}
}

// BaseBucketKey returns the canonical base rate-limit namespace for c. The key
// stays scoped only by the normalized public route class; callers may append
// subject dimensions such as IP or identity without redefining the class
// namespace.
func (c PublicRouteClass) BaseBucketKey() string {
	return publicRESTBaseBucketKeyPrefix + string(c.Normalized())
}

// PublicTrafficClassifier maps public REST requests to the public anti-abuse
// class used by the gateway edge. The server normalizes classifier outputs to
// the stable class set before storing them in request context.
type PublicTrafficClassifier interface {
	Classify(*http.Request) PublicRouteClass
}

// ServerDependencies describes the optional collaborators used by the public
// REST server. The zero value is valid and keeps the process runnable with the
// built-in defaults.
type ServerDependencies struct {
	// Classifier assigns the public anti-abuse class before route handling.
	// When nil, the gateway default classifier is used.
	Classifier PublicTrafficClassifier

	// AuthService delegates public auth commands to the Auth / Session Service.
	// When nil, public auth routes remain mounted and return a stable
	// service-unavailable response.
	AuthService AuthServiceClient

	// Limiter applies the public REST rate-limit policy. When nil, a default
	// process-local in-memory limiter is used.
	Limiter PublicRequestLimiter

	// Observer records malformed-request telemetry for the public REST layer.
	// When nil, a no-op observer is used.
	Observer PublicRequestObserver

	// Logger writes structured transport logs for public REST traffic. When nil,
	// a no-op logger is used.
	Logger *zap.Logger

	// Telemetry records low-cardinality edge metrics. When nil, metrics are
	// disabled.
	Telemetry *telemetry.Runtime
}

// Server owns the public unauthenticated REST listener exposed by the gateway.
type Server struct {
	cfg config.PublicHTTPConfig

	handler http.Handler
	logger  *zap.Logger

	stateMu  sync.RWMutex
	server   *http.Server
	listener net.Listener
}

// NewServer constructs a public REST server for the supplied listener
// configuration and dependency bundle. Nil dependencies are replaced with safe
// defaults so the gateway can still expose the documented public surface.
func NewServer(cfg config.PublicHTTPConfig, deps ServerDependencies) *Server {
	deps = normalizeServerDependencies(deps)

	return &Server{
		cfg:     cfg,
		handler: newPublicHandlerWithConfig(cfg, deps),
		logger:  deps.Logger.Named("public_http"),
	}
}

// Run binds the configured listener and serves the public REST surface until
// Shutdown closes the server.
func (s *Server) Run(ctx context.Context) error {
	if ctx == nil {
		return errors.New("run public REST server: nil context")
	}
	if err := ctx.Err(); err != nil {
		return err
	}

	listener, err := net.Listen("tcp", s.cfg.Addr)
	if err != nil {
		return fmt.Errorf("run public REST server: listen on %q: %w", s.cfg.Addr, err)
	}

	server := &http.Server{
		Handler:           s.handler,
		ReadHeaderTimeout: s.cfg.ReadHeaderTimeout,
		ReadTimeout:       s.cfg.ReadTimeout,
		IdleTimeout:       s.cfg.IdleTimeout,
	}

	s.stateMu.Lock()
	s.server = server
	s.listener = listener
	s.stateMu.Unlock()

	s.logger.Info("public REST server started", zap.String("addr", listener.Addr().String()))

	defer func() {
		s.stateMu.Lock()
		s.server = nil
		s.listener = nil
		s.stateMu.Unlock()
	}()

	err = server.Serve(listener)
	switch {
	case err == nil:
		return nil
	case errors.Is(err, http.ErrServerClosed):
		s.logger.Info("public REST server stopped")
		return nil
	default:
		return fmt.Errorf("run public REST server: serve on %q: %w", s.cfg.Addr, err)
	}
}

// Shutdown gracefully stops the public REST server within ctx.
func (s *Server) Shutdown(ctx context.Context) error {
	if ctx == nil {
		return errors.New("shutdown public REST server: nil context")
	}

	s.stateMu.RLock()
	server := s.server
	s.stateMu.RUnlock()

	if server == nil {
		return nil
	}

	if err := server.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
		return fmt.Errorf("shutdown public REST server: %w", err)
	}

	return nil
}

// PublicRouteClassFromContext returns the previously classified normalized
// public route class stored in ctx.
func PublicRouteClassFromContext(ctx context.Context) (PublicRouteClass, bool) {
	if ctx == nil {
		return "", false
	}

	class, ok := ctx.Value(publicRouteClassContextKey{}).(PublicRouteClass)
	if !ok {
		return "", false
	}

	return class.Normalized(), true
}

type publicRouteClassContextKey struct{}

type defaultPublicTrafficClassifier struct{}

// Classify maps the incoming request into a stable public route class that can
// later drive anti-abuse policy and rate limiting.
func (defaultPublicTrafficClassifier) Classify(r *http.Request) PublicRouteClass {
	switch {
	case isPublicAuthRequest(r):
		return PublicRouteClassPublicAuth
	case isBrowserBootstrapRequest(r):
		return PublicRouteClassBrowserBootstrap
	case isBrowserAssetRequest(r):
		return PublicRouteClassBrowserAsset
	default:
		return PublicRouteClassPublicMisc
	}
}

func normalizeServerDependencies(deps ServerDependencies) ServerDependencies {
	if deps.Classifier == nil {
		deps.Classifier = defaultPublicTrafficClassifier{}
	}
	if deps.AuthService == nil {
		deps.AuthService = unavailableAuthServiceClient{}
	}
	if deps.Limiter == nil {
		deps.Limiter = newInMemoryPublicRequestLimiter()
	}
	if deps.Observer == nil {
		deps.Observer = noopPublicRequestObserver{}
	}
	if deps.Logger == nil {
		deps.Logger = zap.NewNop()
	}

	return deps
}

func newPublicHandler(deps ServerDependencies) http.Handler {
	return newPublicHandlerWithConfig(config.DefaultPublicHTTPConfig(), deps)
}

func newPublicHandlerWithConfig(cfg config.PublicHTTPConfig, deps ServerDependencies) http.Handler {
	configureGinModeOnce.Do(func() {
		gin.SetMode(gin.ReleaseMode)
	})

	deps = normalizeServerDependencies(deps)

	router := gin.New()
	router.HandleMethodNotAllowed = true
	router.Use(gin.CustomRecovery(func(c *gin.Context, _ any) {
		abortWithError(c, http.StatusInternalServerError, errorCodeInternalError, "internal server error")
	}))
	router.Use(otelgin.Middleware("galaxy-edge-gateway-public"))
	router.Use(withPublicObservability(deps.Logger.Named("public_http"), deps.Telemetry))
	// CORS runs before the route classifier and anti-abuse layers so
	// preflight OPTIONS calls answer with 204 immediately and never
	// count against any rate-limit bucket.
	router.Use(withCORS(cfg.CORSAllowedOrigins))
	router.Use(withPublicRouteClass(deps.Classifier))
	router.Use(withPublicAntiAbuse(cfg.AntiAbuse, deps.Limiter, deps.Observer))

	router.GET("/healthz", handleHealthz)
	router.GET("/readyz", handleReadyz)
	router.POST("/api/v1/public/auth/send-email-code", handleSendEmailCode(deps.AuthService, cfg.AuthUpstreamTimeout))
	router.POST("/api/v1/public/auth/confirm-email-code", handleConfirmEmailCode(deps.AuthService, cfg.AuthUpstreamTimeout))

	router.NoMethod(func(c *gin.Context) {
		allowMethods := allowedMethodsForPath(c.Request.URL.Path)
		if allowMethods != "" {
			c.Header("Allow", allowMethods)
		}

		abortWithError(c, http.StatusMethodNotAllowed, errorCodeMethodNotAllowed, "request method is not allowed for this route")
	})
	router.NoRoute(func(c *gin.Context) {
		abortWithError(c, http.StatusNotFound, errorCodeNotFound, "resource was not found")
	})

	return router
}

func handleHealthz(c *gin.Context) {
	c.JSON(http.StatusOK, statusResponse{Status: "ok"})
}

func handleReadyz(c *gin.Context) {
	c.JSON(http.StatusOK, statusResponse{Status: "ready"})
}

func withPublicRouteClass(classifier PublicTrafficClassifier) gin.HandlerFunc {
	return func(c *gin.Context) {
		class := classifier.Classify(c.Request).Normalized()
		ctx := context.WithValue(c.Request.Context(), publicRouteClassContextKey{}, class)
		c.Request = c.Request.WithContext(ctx)
		c.Next()
	}
}

func isPublicAuthRequest(r *http.Request) bool {
	return r.Method == http.MethodPost && isPublicAuthPath(r.URL.Path)
}

func isBrowserBootstrapRequest(r *http.Request) bool {
	if r.Method == http.MethodGet && r.URL.Path == "/" {
		return true
	}

	return matchesBrowserBootstrapRequestShape(r)
}

func isBrowserAssetRequest(r *http.Request) bool {
	if r.Method != http.MethodGet && r.Method != http.MethodHead {
		return false
	}

	return matchesBrowserAssetRequestShape(r)
}

type statusResponse struct {
	Status string `json:"status"`
}

type errorResponse struct {
	Error errorBody `json:"error"`
}

type errorBody struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func abortWithError(c *gin.Context, statusCode int, code string, message string) {
	if c != nil {
		c.Set(publicErrorCodeContextKey, code)
	}
	c.AbortWithStatusJSON(statusCode, errorResponse{
		Error: errorBody{
			Code:    code,
			Message: message,
		},
	})
}

const publicErrorCodeContextKey = "public_error_code"

func allowedMethodsForPath(requestPath string) string {
	switch requestPath {
	case "/healthz", "/readyz":
		return http.MethodGet
	case "/api/v1/public/auth/send-email-code", "/api/v1/public/auth/confirm-email-code":
		return http.MethodPost
	default:
		return ""
	}
}

func (s *Server) listenAddr() string {
	s.stateMu.RLock()
	defer s.stateMu.RUnlock()

	if s.listener == nil {
		return ""
	}

	return s.listener.Addr().String()
}
