package gateway

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// AuthFilter (inbound, early) short-circuits requests without a token.
// Real identity comes from mTLS/JWT (gw-07); this is the shape.
type AuthFilter struct {
	// Exempt paths skip auth (e.g. health checks).
	Exempt map[string]bool
}

func (AuthFilter) Type() Phase  { return Inbound }
func (AuthFilter) Order() int   { return 10 } // before routing(50)
func (AuthFilter) Name() string { return "auth" }
func (a AuthFilter) ShouldFilter(c *RequestContext) bool {
	return !a.Exempt[c.Req.URL.Path]
}
func (AuthFilter) Apply(c *RequestContext) {
	if c.Req.Header.Get("Authorization") == "" {
		c.Resp.Status = http.StatusUnauthorized
		c.Resp.Body = []byte("missing token")
		c.Stop() // no routing, no origin call for a 401
		return
	}
	c.Attributes["identity"] = c.Req.Header.Get("Authorization")
}

// LocalEndpoint serves a fixed response for a path without an origin call
// (e.g. /healthz). Registered as the endpoint when no proxy is needed.
type LocalEndpoint struct {
	Path string
	Body string
}

func (LocalEndpoint) Type() Phase  { return Endpoint }
func (LocalEndpoint) Order() int   { return 0 }
func (LocalEndpoint) Name() string { return "local" }
func (e LocalEndpoint) ShouldFilter(c *RequestContext) bool { return c.Req.URL.Path == e.Path }
func (e LocalEndpoint) Apply(c *RequestContext) {
	c.Resp.Status = http.StatusOK
	c.Resp.Body = []byte(e.Body)
}

// AccessLog (outbound, last) records the outcome. Always runs, even on a
// short-circuited request — the access log must never be skipped.
type AccessLog struct {
	Sink func(line string) // default: discard; tests capture it
}

func (AccessLog) Type() Phase  { return Outbound }
func (AccessLog) Order() int   { return 1000 }
func (AccessLog) Name() string { return "accesslog" }
func (AccessLog) ShouldFilter(c *RequestContext) bool { return true }
func (l AccessLog) Apply(c *RequestContext) {
	if l.Sink == nil {
		return
	}
	l.Sink(fmt.Sprintf("%s %s -> %d route=%q",
		c.Req.Method, c.Req.URL.Path, c.Resp.Status, c.RouteName))
}

// ErrorNormalizer (outbound, first) guarantees a well-formed response and
// records the status class for metrics (gw-11).
type ErrorNormalizer struct{}

func (ErrorNormalizer) Type() Phase  { return Outbound }
func (ErrorNormalizer) Order() int   { return 1 }
func (ErrorNormalizer) Name() string { return "errornorm" }
func (ErrorNormalizer) ShouldFilter(c *RequestContext) bool { return true }
func (ErrorNormalizer) Apply(c *RequestContext) {
	if c.Resp.Status == 0 {
		c.Resp.Status = http.StatusInternalServerError
	}
	c.Attributes["status_class"] = c.Resp.Status / 100
}

// hopByHop headers a proxy must consume, not forward (RFC 9110 §7.6.1).
var hopByHop = []string{
	"Connection", "Proxy-Connection", "Keep-Alive", "Proxy-Authenticate",
	"Proxy-Authorization", "Te", "Trailer", "Transfer-Encoding", "Upgrade",
}

func sanitizeForOrigin(r *http.Request) {
	for _, name := range strings.Split(r.Header.Get("Connection"), ",") {
		if n := strings.TrimSpace(name); n != "" {
			r.Header.Del(n)
		}
	}
	for _, n := range hopByHop {
		r.Header.Del(n)
	}
	if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil && ip != "" {
		if prior := r.Header.Get("X-Forwarded-For"); prior != "" {
			r.Header.Set("X-Forwarded-For", prior+", "+ip)
		} else {
			r.Header.Set("X-Forwarded-For", ip)
		}
	}
}

// ProxyEndpoint forwards the request to the cluster's origin. The
// Transport seam is where gw-04's pooled/subsetted connection manager and
// gw-06's resilience policy get injected.
type ProxyEndpoint struct {
	Resolve   func(cluster string) (baseURL string, ok bool) // service discovery (gw-08)
	Transport http.RoundTripper
	Timeout   time.Duration
	MaxBody   int64
}

func (ProxyEndpoint) Type() Phase  { return Endpoint }
func (ProxyEndpoint) Order() int   { return 0 }
func (ProxyEndpoint) Name() string { return "proxy" }
func (p ProxyEndpoint) ShouldFilter(c *RequestContext) bool {
	return c.RouteName != "" && c.RouteName != "edge-fallback"
}

func (p ProxyEndpoint) Apply(c *RequestContext) {
	base, ok := p.Resolve(c.RouteName)
	if !ok {
		c.Resp.Status = http.StatusServiceUnavailable
		c.Resp.Body = []byte("no healthy origin")
		return
	}
	u, err := url.Parse(base)
	if err != nil {
		c.Resp.Status = http.StatusBadGateway
		return
	}

	timeout := p.Timeout
	if timeout <= 0 {
		timeout = 5 * time.Second
	}
	ctx, cancel := context.WithTimeout(c.Req.Context(), timeout)
	defer cancel()

	out := c.Req.Clone(ctx)
	out.URL.Scheme = u.Scheme
	out.URL.Host = u.Host
	out.Host = u.Host
	out.RequestURI = ""
	sanitizeForOrigin(out)

	tr := p.Transport
	if tr == nil {
		tr = http.DefaultTransport
	}
	resp, err := tr.RoundTrip(out)
	if err != nil {
		c.Resp.Status = statusForError(err) // 504 on timeout, 502 otherwise
		c.Resp.Body = []byte("origin error")
		c.Attributes["origin_error"] = err.Error()
		return
	}
	defer resp.Body.Close()

	c.Resp.Status = resp.StatusCode
	for k, vs := range resp.Header {
		for _, v := range vs {
			c.Resp.Header.Add(k, v)
		}
	}
	max := p.MaxBody
	if max <= 0 {
		max = 8 << 20
	}
	c.Resp.Body, _ = io.ReadAll(io.LimitReader(resp.Body, max))
}

func statusForError(err error) int {
	if errors.Is(err, context.DeadlineExceeded) {
		return http.StatusGatewayTimeout // 504
	}
	var ne net.Error
	if errors.As(err, &ne) && ne.Timeout() {
		return http.StatusGatewayTimeout
	}
	return http.StatusBadGateway // 502
}
