// Package gateway implements an API gateway in the Zuul 2 shape: a
// per-request context threaded through three ordered filter phases
// (inbound -> endpoint -> outbound), with short-circuit, guaranteed
// outbound execution, and panic isolation. It is stdlib-only.
package gateway

import (
	"net/http"
	"sort"
	"time"
)

// Phase is one of Zuul's three filter phases.
type Phase int

const (
	Inbound  Phase = iota // pre-routing: authn, routing, rate limit
	Endpoint              // the terminal action: proxy, or serve locally
	Outbound              // post-response: rewrite, log, metrics
)

// ResponseBuilder is the response being assembled by the chain.
type ResponseBuilder struct {
	Status int
	Header http.Header
	Body   []byte
}

// RequestContext is the mutable per-request state (Zuul's SessionContext).
type RequestContext struct {
	Req        *http.Request
	Resp       *ResponseBuilder
	RouteName  string
	Attributes map[string]any
	Timings    map[string]time.Duration

	stopped   bool
	startedAt time.Time
}

func (c *RequestContext) Stop()             { c.stopped = true }
func (c *RequestContext) Stopped() bool      { return c.stopped }
func (c *RequestContext) StartedAt() time.Time { return c.startedAt }

// Filter is the unit of gateway logic (Zuul's ZuulFilter).
type Filter interface {
	Type() Phase
	Order() int // lower runs first within a phase
	ShouldFilter(c *RequestContext) bool
	Apply(c *RequestContext)
}

// Named lets a filter report a stable name for timings/metrics.
type Named interface{ Name() string }

func filterName(f Filter) string {
	if n, ok := f.(Named); ok {
		return n.Name()
	}
	return "filter"
}

// Gateway runs the filter chain for each request.
type Gateway struct {
	inbound  []Filter
	endpoint Filter
	outbound []Filter

	// OnPanic, if set, is called when a filter panics (for logging/metrics).
	OnPanic func(c *RequestContext, f Filter, recovered any)
}

// Use registers a filter. Inbound/outbound are sorted by Order(); there
// is exactly one endpoint (last registration wins).
func (g *Gateway) Use(f Filter) {
	switch f.Type() {
	case Inbound:
		g.inbound = append(g.inbound, f)
		sort.SliceStable(g.inbound, func(i, j int) bool { return g.inbound[i].Order() < g.inbound[j].Order() })
	case Endpoint:
		g.endpoint = f
	case Outbound:
		g.outbound = append(g.outbound, f)
		sort.SliceStable(g.outbound, func(i, j int) bool { return g.outbound[i].Order() < g.outbound[j].Order() })
	}
}

func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	c := &RequestContext{
		Req:        r,
		Resp:       &ResponseBuilder{Header: http.Header{}},
		Attributes: map[string]any{},
		Timings:    map[string]time.Duration{},
		startedAt:  time.Now(),
	}

	// INBOUND: stop as soon as a filter short-circuits.
	for _, f := range g.inbound {
		if c.Stopped() {
			break
		}
		g.runOne(c, f)
	}

	// ENDPOINT: only if not short-circuited (e.g. by auth/cache).
	if !c.Stopped() && g.endpoint != nil && g.endpoint.ShouldFilter(c) {
		g.runOne(c, g.endpoint)
	}

	// OUTBOUND: ALWAYS runs — access logs / metrics must fire on 401/429 too.
	for _, f := range g.outbound {
		g.runOne(c, f)
	}

	g.write(w, c)
}

func (g *Gateway) runOne(c *RequestContext, f Filter) {
	if !f.ShouldFilter(c) {
		return
	}
	start := time.Now()
	defer func() {
		// A filter panic becomes a 502; the chain never crashes the
		// connection on one filter's bug.
		if rec := recover(); rec != nil {
			c.Resp.Status = http.StatusBadGateway
			c.Resp.Body = []byte("filter error")
			if g.OnPanic != nil {
				g.OnPanic(c, f, rec)
			}
		}
		c.Timings[filterName(f)] = time.Since(start)
	}()
	f.Apply(c)
}

func (g *Gateway) write(w http.ResponseWriter, c *RequestContext) {
	if c.Resp.Status == 0 {
		c.Resp.Status = http.StatusOK
	}
	for k, vs := range c.Resp.Header {
		for _, v := range vs {
			w.Header().Add(k, v)
		}
	}
	w.WriteHeader(c.Resp.Status)
	_, _ = w.Write(c.Resp.Body)
}
