package gateway

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/tls"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"sync/atomic"
	"time"

	"github.com/pingcap-inc/nexuscrm-api-host/processhttp"
)

const (
	MetricsPath          = "/metrics"
	RequestIDHeader      = "X-Request-ID"
	defaultRetryAfterSec = "1"
	// Requests are completely buffered before proxying so an oversized body
	// can never be partially delivered to an AppService. Admission therefore
	// has two dimensions: request count and the maximum bytes retained by that
	// count. Reject a static generation whose worst-case buffer set can exceed
	// this process-owned memory budget.
	maxBufferedRequestBytes int64 = 64 << 20
)

var errBodyTooLarge = errors.New("gateway: request body exceeds configured envelope")

// Config contains process-local transport policy. Product paths and upstreams
// enter only through the immutable RouteConfig deployment artifact.
type Config struct {
	Version     string
	MaxInFlight int
	Routes      RouteConfig
}

// Gateway is one immutable edge routing generation. It never discovers,
// reloads, adds application-level retries, or falls back to another distribution.
type Gateway struct {
	version   string
	routes    RouteConfig
	compiled  map[string]*compiledRoute
	templates map[int][]compiledTemplateRoute
	admission chan struct{}
	ready     atomic.Bool
	live      atomic.Bool
	metrics   metrics
	ops       http.Handler
}

type compiledRoute struct {
	config Route
	proxy  *httputil.ReverseProxy
}

type compiledTemplateRoute struct {
	pattern routePath
	route   *compiledRoute
}

type edgeTLSContextKey struct{}

// New validates and compiles the complete static routing generation.
func New(config Config) (*Gateway, error) {
	if strings.TrimSpace(config.Version) == "" || config.Version != strings.TrimSpace(config.Version) {
		return nil, errors.New("gateway: version is required")
	}
	if config.MaxInFlight <= 0 {
		return nil, errors.New("gateway: max in-flight must be positive")
	}
	if config.MaxInFlight > 65536 {
		return nil, errors.New("gateway: max in-flight must not exceed 65536")
	}
	routes := cloneRouteConfig(config.Routes)
	integrity, err := semanticIntegrity(routes)
	if err != nil {
		return nil, err
	}
	if !routes.parsed || routes.Digest != routes.sourceDigest || integrity != routes.integrity {
		return nil, errors.New("gateway: route config must be an unmodified ParseRouteConfig result")
	}
	if err := routes.validate(); err != nil {
		return nil, err
	}
	var largestRequest int64
	for _, route := range routes.Routes {
		if route.MaxRequestBytes > largestRequest {
			largestRequest = route.MaxRequestBytes
		}
	}
	if largestRequest > maxBufferedRequestBytes/int64(config.MaxInFlight) {
		return nil, fmt.Errorf("gateway: max in-flight and route envelope exceed the %d-byte buffered request budget", maxBufferedRequestBytes)
	}
	base := http.DefaultTransport.(*http.Transport).Clone()
	// A deployment-authored upstream is the only allowed outbound hop.
	// Inheriting ProxyFromEnvironment or accepting a caller-authored transport
	// would create an undeclared second routing authority capable of observing
	// credentials and request bodies. Future private-root or mTLS support must
	// therefore enter as narrow validated TLS inputs, not a RoundTripper escape.
	base.Proxy = nil
	base.DisableCompression = true
	base.MaxIdleConns = config.MaxInFlight * 2
	base.MaxIdleConnsPerHost = config.MaxInFlight
	base.MaxConnsPerHost = config.MaxInFlight
	base.IdleConnTimeout = 90 * time.Second
	base.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
	gateway := &Gateway{
		version:   config.Version,
		routes:    routes,
		compiled:  make(map[string]*compiledRoute, len(routes.Routes)),
		templates: make(map[int][]compiledTemplateRoute),
		admission: make(chan struct{}, config.MaxInFlight),
	}
	for _, route := range routes.Routes {
		target, _ := parseUpstream(route.Upstream)
		compiled := &compiledRoute{
			config: route,
			proxy:  gateway.newProxy(target, base),
		}
		path, _ := parseRoutePath(routes.APIVersion, route.Path)
		if path.template {
			count := len(path.segments)
			gateway.templates[count] = append(
				gateway.templates[count],
				compiledTemplateRoute{pattern: path, route: compiled},
			)
		} else {
			gateway.compiled[route.Path] = compiled
		}
	}
	capability := "gateway.static-routing.v1"
	if routes.APIVersion == RouteConfigAPIVersionV2 {
		capability = "gateway.static-routing.v2"
	} else if routes.APIVersion == RouteConfigAPIVersionV3 {
		capability = "gateway.static-routing.v3"
	}
	operational, err := processhttp.NewHandler(processhttp.Config{
		Role:         "gateway",
		Version:      config.Version,
		Live:         gateway.live.Load,
		Ready:        gateway.ready.Load,
		Capabilities: []string{capability},
	})
	if err != nil {
		return nil, err
	}
	gateway.ops = operational
	gateway.live.Store(true)
	gateway.ready.Store(true)
	return gateway, nil
}

// Drain atomically removes readiness and rejects all new routed requests.
// Complete response transport draining belongs to Server.Shutdown.
func (g *Gateway) Drain() {
	g.ready.Store(false)
}

// Close marks the process dead after the owning HTTP server has stopped.
func (g *Gateway) Close() {
	g.ready.Store(false)
	g.live.Store(false)
}

func (g *Gateway) Ready() bool { return g.ready.Load() }

func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	setSecurityHeaders(w, r)
	if processhttp.IsUpgradeRequest(r) {
		w.Header().Set("Connection", "close")
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "protocol_upgrade_not_supported"})
		return
	}
	// Route artifacts authorize one canonical, unescaped path shape. URL.Path is
	// already decoded by net/http, so dispatching before this check would allow
	// encoded aliases such as %2F to collide with an authorized route or ops
	// endpoint.
	if r.URL.RawPath != "" || r.URL.EscapedPath() != r.URL.Path {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "route_not_found"})
		return
	}
	switch r.URL.Path {
	case processhttp.LivePath, processhttp.ReadyPath, processhttp.VersionPath, processhttp.CapabilitiesPath:
		g.ops.ServeHTTP(w, r)
		return
	case MetricsPath:
		if r.Method != http.MethodGet && r.Method != http.MethodHead {
			w.Header().Set("Allow", "GET, HEAD")
			writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
			return
		}
		w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
		g.metrics.writePrometheus(w, g.routes)
		return
	}
	route := g.matchRoute(r.URL.Path)
	if route == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "route_not_found"})
		return
	}
	if !route.config.allows(r.Method) {
		w.Header().Set("Allow", route.config.allowHeader())
		writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
		return
	}
	requestID := canonicalRequestID(r.Header.Get(RequestIDHeader))
	if requestID == "" {
		var err error
		requestID, err = newRequestID()
		if err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "correlation_unavailable"})
			return
		}
	}
	r.Header.Set(RequestIDHeader, requestID)
	w.Header().Set(RequestIDHeader, requestID)
	if !g.ready.Load() {
		g.metrics.rejectedDrain.Add(1)
		w.Header().Set("Retry-After", defaultRetryAfterSec)
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "gateway_draining"})
		return
	}
	select {
	case g.admission <- struct{}{}:
		defer func() { <-g.admission }()
	default:
		g.metrics.rejectedFull.Add(1)
		w.Header().Set("Retry-After", defaultRetryAfterSec)
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "gateway_overloaded"})
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), route.config.timeout())
	defer cancel()
	originalBody := r.Body
	stopBodyClose := context.AfterFunc(ctx, func() {
		if originalBody != nil {
			_ = originalBody.Close()
		}
	})
	body, err := readBoundedBody(w, r, route.config.MaxRequestBytes)
	stopBodyClose()
	if originalBody != nil {
		_ = originalBody.Close()
	}
	if err != nil {
		switch {
		case errors.Is(err, errBodyTooLarge):
			g.metrics.rejectedSize.Add(1)
			writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "request_too_large"})
		case errors.Is(ctx.Err(), context.DeadlineExceeded):
			g.metrics.timeouts.Add(1)
			writeJSON(w, http.StatusGatewayTimeout, map[string]string{"error": "gateway_timeout"})
		case errors.Is(ctx.Err(), context.Canceled):
			g.metrics.rejectedBody.Add(1)
			writeJSON(w, http.StatusRequestTimeout, map[string]string{"error": "request_canceled"})
		default:
			g.metrics.rejectedBody.Add(1)
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid_request_body"})
		}
		return
	}
	r.Body = io.NopCloser(bytes.NewReader(body))
	r.ContentLength = int64(len(body))
	r.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(body)), nil }
	r = r.WithContext(ctx)
	g.metrics.accepted.Add(1)
	g.metrics.inFlight.Add(1)
	started := time.Now()
	defer func() {
		g.metrics.inFlight.Add(-1)
		g.metrics.observeLatency(time.Since(started))
	}()
	route.proxy.ServeHTTP(w, r)
}

func (g *Gateway) matchRoute(path string) *compiledRoute {
	if route := g.compiled[path]; route != nil {
		return route
	}
	// Count first without allocating. An untrusted request whose shape cannot
	// match any configured template is rejected before strings.Split, and a
	// candidate shape is split exactly once rather than once per route.
	candidates := g.templates[strings.Count(path, "/")]
	if len(candidates) == 0 {
		return nil
	}
	segments, ok := splitRequestPath(path)
	if !ok {
		return nil
	}
	for _, candidate := range candidates {
		if candidate.pattern.matchesSegments(segments) {
			return candidate.route
		}
	}
	return nil
}

func (g *Gateway) maximumRouteTimeout() time.Duration {
	var maximum time.Duration
	for _, route := range g.routes.Routes {
		if timeout := route.timeout(); timeout > maximum {
			maximum = timeout
		}
	}
	return maximum
}

func (g *Gateway) newProxy(target *url.URL, transport http.RoundTripper) *httputil.ReverseProxy {
	return &httputil.ReverseProxy{
		Transport: transport,
		Rewrite: func(request *httputil.ProxyRequest) {
			originalHost := request.In.Host
			originalProto := "http"
			if request.In.TLS != nil {
				originalProto = "https"
			}
			stripUntrustedProxyIdentityHeaders(request.Out.Header)
			request.SetURL(target)
			// The public Host is application-visible request authority. The
			// private upstream is routing state and must not replace it.
			request.Out.Host = request.In.Host
			request.Out = request.Out.WithContext(context.WithValue(request.Out.Context(), edgeTLSContextKey{}, request.In.TLS != nil))
			request.SetXForwarded()
			request.Out.Header.Set("X-Forwarded-Host", originalHost)
			request.Out.Header.Set("X-Forwarded-Proto", originalProto)
		},
		ModifyResponse: func(response *http.Response) error {
			applySecurityHeaders(response.Header, response.Request.Context().Value(edgeTLSContextKey{}) == true)
			response.Header.Set(RequestIDHeader, response.Request.Header.Get(RequestIDHeader))
			return nil
		},
		ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
			switch {
			case errors.Is(r.Context().Err(), context.DeadlineExceeded):
				g.metrics.timeouts.Add(1)
				writeJSON(w, http.StatusGatewayTimeout, map[string]string{"error": "gateway_timeout"})
			case errors.Is(r.Context().Err(), context.Canceled):
				writeJSON(w, http.StatusRequestTimeout, map[string]string{"error": "request_canceled"})
			default:
				g.metrics.upstreamErrors.Add(1)
				writeJSON(w, http.StatusBadGateway, map[string]string{"error": "upstream_unavailable"})
			}
		},
	}
}

func stripUntrustedProxyIdentityHeaders(header http.Header) {
	for name := range header {
		canonical := http.CanonicalHeaderKey(name)
		if strings.HasPrefix(canonical, "X-Forwarded-") {
			header.Del(name)
			continue
		}
		switch canonical {
		case "Forwarded", "Via", "X-Real-Ip", "True-Client-Ip", "Cf-Connecting-Ip",
			"Fastly-Client-Ip", "X-Client-Ip", "X-Cluster-Client-Ip", "X-Original-Forwarded-For":
			header.Del(name)
		}
	}
}

func readBoundedBody(w http.ResponseWriter, r *http.Request, limit int64) ([]byte, error) {
	if r.ContentLength > limit {
		return nil, errBodyTooLarge
	}
	if r.Body == nil {
		return nil, nil
	}
	reader := http.MaxBytesReader(w, r.Body, limit)
	defer reader.Close()
	body, err := io.ReadAll(reader)
	if err != nil {
		var maxBytesError *http.MaxBytesError
		if errors.As(err, &maxBytesError) {
			return nil, errBodyTooLarge
		}
		return nil, err
	}
	return body, nil
}

func canonicalRequestID(value string) string {
	if len(value) == 0 || len(value) > 128 {
		return ""
	}
	for _, ch := range value {
		if !(ch >= 'a' && ch <= 'z') && !(ch >= 'A' && ch <= 'Z') && !(ch >= '0' && ch <= '9') && !strings.ContainsRune("._:-", ch) {
			return ""
		}
	}
	return value
}

func newRequestID() (string, error) {
	var random [16]byte
	if _, err := rand.Read(random[:]); err != nil {
		return "", fmt.Errorf("gateway: operating system random source: %w", err)
	}
	return hex.EncodeToString(random[:]), nil
}

func setSecurityHeaders(w http.ResponseWriter, r *http.Request) {
	applySecurityHeaders(w.Header(), r.TLS != nil)
}

func applySecurityHeaders(header http.Header, secure bool) {
	// Edge policy has one owner. Remove values supplied by an upstream before
	// installing the canonical generation, including protocol advertisement
	// that could redirect clients around this Gateway.
	for _, name := range []string{"X-Content-Type-Options", "X-Frame-Options", "Content-Security-Policy", "Referrer-Policy", "Strict-Transport-Security", "Alt-Svc"} {
		header.Del(name)
	}
	header.Set("X-Content-Type-Options", "nosniff")
	header.Set("X-Frame-Options", "DENY")
	header.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'")
	header.Set("Referrer-Policy", "no-referrer")
	if secure {
		header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
	}
}

func writeJSON(w http.ResponseWriter, status int, value any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(value)
}
