package gateway

import (
	"strings"
	"sync/atomic"
)

// Route maps a request matcher to a named cluster. An empty field means
// "match any".
type Route struct {
	Host     string
	Prefix   string
	Method   string
	HeaderK  string // optional header predicate key (e.g. for canary steering)
	HeaderV  string
	Cluster  string
}

// RouteTable is an immutable set of routes; swap a whole table at once.
type RouteTable struct {
	Routes []Route
}

// Match returns the cluster for the most specific matching route. Routes
// are ranked the way the Kubernetes Gateway API / Envoy do it: longest
// path prefix first, then (among equal prefixes) routes with more
// constraints (a header predicate, then a method) win. Ties resolve to
// the first declared route.
//
// This specificity ordering is why a canary route ("/v1/play" +
// header=x-canary) beats the base route ("/v1/play") for a canary
// request, even though both have the same prefix length.
func (t *RouteTable) Match(host, method, path string, hdr func(string) string) (string, bool) {
	best := -1
	bestScore := -1
	for i := range t.Routes {
		r := &t.Routes[i]
		if r.Host != "" && r.Host != host {
			continue
		}
		if r.Method != "" && r.Method != method {
			continue
		}
		if !strings.HasPrefix(path, r.Prefix) {
			continue
		}
		if r.HeaderK != "" && hdr(r.HeaderK) != r.HeaderV {
			continue
		}
		// Prefix length dominates; header/method predicates break ties
		// toward the more specific route.
		score := len(r.Prefix) * 10
		if r.HeaderK != "" {
			score += 2
		}
		if r.Method != "" {
			score++
		}
		if score > bestScore {
			best, bestScore = i, score
		}
	}
	if best < 0 {
		return "", false
	}
	return t.Routes[best].Cluster, true
}

// Router holds an atomically swappable table for lock-free hot-reload:
// in-flight requests keep the snapshot they read; new requests see the
// new table; no request is ever dropped during a swap. This previews the
// control-plane push of gw-08.
type Router struct {
	tbl atomic.Pointer[RouteTable]
}

func NewRouter(t *RouteTable) *Router {
	r := &Router{}
	r.tbl.Store(t)
	return r
}

func (r *Router) Load() *RouteTable     { return r.tbl.Load() }
func (r *Router) Swap(t *RouteTable)     { r.tbl.Store(t) }

// RoutingFilter (inbound) selects the cluster for the request.
type RoutingFilter struct{ R *Router }

func (RoutingFilter) Type() Phase  { return Inbound }
func (RoutingFilter) Order() int   { return 50 } // after auth(10), before endpoint
func (RoutingFilter) Name() string { return "routing" }
func (RoutingFilter) ShouldFilter(c *RequestContext) bool { return true }

func (f RoutingFilter) Apply(c *RequestContext) {
	cluster, ok := f.R.Load().Match(c.Req.Host, c.Req.Method, c.Req.URL.Path, c.Req.Header.Get)
	if !ok {
		c.Resp.Status = 404
		c.Resp.Body = []byte("no route")
		c.Stop()
		return
	}
	c.RouteName = cluster
	c.Attributes["cluster"] = cluster
}
