// Package gateway owns the app-blind HTTP edge process used to route exact,
// deployment-authored public paths to independently deployed AppServices.
package gateway

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/url"
	"os"
	"regexp"
	"sort"
	"strings"
	"time"
)

const (
	// RouteConfigAPIVersion is the original exact-path grammar. Keep accepting
	// it so existing immutable D1 route artifacts remain valid.
	RouteConfigAPIVersion = "gateway.nexuscrm.io/v1"
	// RouteConfigAPIVersionV2 adds one app-blind route shape: a single opaque
	// terminal path segment written as :name. It deliberately does not add
	// prefix, wildcard, regex, or multi-segment matching.
	RouteConfigAPIVersionV2 = "gateway.nexuscrm.io/v2"
	// RouteConfigAPIVersionV3 permits one or more bounded opaque path segments
	// written as :name. Every request must still have the exact configured
	// segment count and fixed segments; this is not prefix or wildcard routing.
	RouteConfigAPIVersionV3 = "gateway.nexuscrm.io/v3"
	maxRouteCount           = 256
	maxRequestBodyBytes     = 64 << 20
	maxRouteTimeout         = 30 * time.Second
)

var identifierPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$`)
var routeParameterPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{0,63}$`)

// RouteConfig is a load-once deployment artifact. The Gateway deliberately has
// no reload, route epoch, discovery, or fallback behavior in D1.
type RouteConfig struct {
	APIVersion string  `json:"apiVersion"`
	Revision   string  `json:"revision"`
	Routes     []Route `json:"routes"`
	Digest     string  `json:"-"`

	parsed       bool
	integrity    [sha256.Size]byte
	sourceDigest string
}

// Route maps one public path shape to one upstream origin. Route names and
// paths are opaque deployment facts; they carry no product meaning in L0. V1
// accepts exact paths only. V2 additionally accepts one opaque terminal
// segment, for example /api/v1/items/:id. V3 accepts multiple bounded opaque
// segments, for example /api/v1/items/:object/:id.
type Route struct {
	Name            string   `json:"name"`
	Path            string   `json:"path"`
	Methods         []string `json:"methods"`
	Upstream        string   `json:"upstream"`
	TimeoutMillis   int64    `json:"timeoutMillis"`
	MaxRequestBytes int64    `json:"maxRequestBytes"`
}

// LoadRouteConfig reads and validates the complete static route artifact.
func LoadRouteConfig(path string) (RouteConfig, error) {
	path = strings.TrimSpace(path)
	if path == "" {
		return RouteConfig{}, errors.New("gateway: route config path is required")
	}
	raw, err := os.ReadFile(path)
	if err != nil {
		return RouteConfig{}, fmt.Errorf("gateway: read route config: %w", err)
	}
	return ParseRouteConfig(raw)
}

// ParseRouteConfig rejects unknown fields and trailing values so a deployment
// cannot silently rely on configuration this binary does not enforce.
func ParseRouteConfig(raw []byte) (RouteConfig, error) {
	if err := rejectDuplicateJSONMembers(raw); err != nil {
		return RouteConfig{}, err
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var config RouteConfig
	if err := decoder.Decode(&config); err != nil {
		return RouteConfig{}, fmt.Errorf("gateway: decode route config: %w", err)
	}
	if err := requireJSONEOF(decoder); err != nil {
		return RouteConfig{}, err
	}
	if err := config.validate(); err != nil {
		return RouteConfig{}, err
	}
	digest := sha256.Sum256(raw)
	config.Digest = "sha256:" + hex.EncodeToString(digest[:])
	config.parsed = true
	integrity, err := semanticIntegrity(config)
	if err != nil {
		return RouteConfig{}, err
	}
	config.integrity = integrity
	config.sourceDigest = config.Digest
	return config, nil
}

// rejectDuplicateJSONMembers closes the ambiguity that encoding/json leaves
// open by default: later duplicate object members silently replace earlier
// ones. A route artifact is authorization input, so every fact must have one
// unambiguous representation at every nesting level.
func rejectDuplicateJSONMembers(raw []byte) error {
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.UseNumber()
	var walk func() error
	walk = func() error {
		token, err := decoder.Token()
		if err != nil {
			return err
		}
		delimiter, composite := token.(json.Delim)
		if !composite {
			return nil
		}
		switch delimiter {
		case '{':
			seen := make(map[string]struct{})
			for decoder.More() {
				keyToken, err := decoder.Token()
				if err != nil {
					return err
				}
				key, ok := keyToken.(string)
				if !ok {
					return errors.New("gateway: route config object member is not a string")
				}
				if _, duplicate := seen[key]; duplicate {
					return fmt.Errorf("gateway: route config contains duplicate JSON member %q", key)
				}
				seen[key] = struct{}{}
				if err := walk(); err != nil {
					return err
				}
			}
			closing, err := decoder.Token()
			if err != nil {
				return err
			}
			if closing != json.Delim('}') {
				return errors.New("gateway: malformed route config object")
			}
		case '[':
			for decoder.More() {
				if err := walk(); err != nil {
					return err
				}
			}
			closing, err := decoder.Token()
			if err != nil {
				return err
			}
			if closing != json.Delim(']') {
				return errors.New("gateway: malformed route config array")
			}
		default:
			return errors.New("gateway: malformed route config delimiter")
		}
		return nil
	}
	if err := walk(); err != nil {
		return fmt.Errorf("gateway: decode route config: %w", err)
	}
	if _, err := decoder.Token(); err != io.EOF {
		if err == nil {
			return errors.New("gateway: route config contains trailing JSON value")
		}
		return fmt.Errorf("gateway: decode trailing route config: %w", err)
	}
	return nil
}

func requireJSONEOF(decoder *json.Decoder) error {
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		if err == nil {
			return errors.New("gateway: route config contains trailing JSON value")
		}
		return fmt.Errorf("gateway: decode trailing route config: %w", err)
	}
	return nil
}

func (c *RouteConfig) validate() error {
	if c.APIVersion != RouteConfigAPIVersion &&
		c.APIVersion != RouteConfigAPIVersionV2 &&
		c.APIVersion != RouteConfigAPIVersionV3 {
		return fmt.Errorf("gateway: unsupported route config apiVersion %q", c.APIVersion)
	}
	if !identifierPattern.MatchString(c.Revision) {
		return errors.New("gateway: route config revision must be canonical and at most 128 bytes")
	}
	if len(c.Routes) == 0 || len(c.Routes) > maxRouteCount {
		return fmt.Errorf("gateway: route count must be between 1 and %d", maxRouteCount)
	}
	seenNames := make(map[string]struct{}, len(c.Routes))
	paths := make([]routePath, 0, len(c.Routes))
	for i := range c.Routes {
		route := &c.Routes[i]
		path, err := route.validate(c.APIVersion)
		if err != nil {
			return fmt.Errorf("gateway: route %d: %w", i, err)
		}
		if _, exists := seenNames[route.Name]; exists {
			return fmt.Errorf("gateway: duplicate route name %q", route.Name)
		}
		seenNames[route.Name] = struct{}{}
		for priorIndex, prior := range paths {
			if path.overlaps(prior) {
				return fmt.Errorf("gateway: route path %q overlaps route %d path %q", route.Path, priorIndex, c.Routes[priorIndex].Path)
			}
		}
		paths = append(paths, path)
	}
	return nil
}

func (r *Route) validate(apiVersion string) (routePath, error) {
	if !identifierPattern.MatchString(r.Name) {
		return routePath{}, errors.New("name must be canonical and at most 128 bytes")
	}
	path, err := parseRoutePath(apiVersion, r.Path)
	if err != nil {
		return routePath{}, err
	}
	if len(r.Methods) == 0 {
		return routePath{}, errors.New("methods are required")
	}
	seenMethods := make(map[string]struct{}, len(r.Methods))
	for _, method := range r.Methods {
		if method == "" || method != strings.ToUpper(method) || !validMethod(method) {
			return routePath{}, fmt.Errorf("method %q is not canonical", method)
		}
		if _, exists := seenMethods[method]; exists {
			return routePath{}, fmt.Errorf("duplicate method %q", method)
		}
		seenMethods[method] = struct{}{}
	}
	sort.Strings(r.Methods)
	if _, err := parseUpstream(r.Upstream); err != nil {
		return routePath{}, err
	}
	timeout := time.Duration(r.TimeoutMillis) * time.Millisecond
	if r.TimeoutMillis <= 0 || timeout > maxRouteTimeout {
		return routePath{}, fmt.Errorf("timeoutMillis must be between 1 and %d", maxRouteTimeout.Milliseconds())
	}
	if r.MaxRequestBytes <= 0 || r.MaxRequestBytes > maxRequestBodyBytes {
		return routePath{}, fmt.Errorf("maxRequestBytes must be between 1 and %d", maxRequestBodyBytes)
	}
	return path, nil
}

type routePath struct {
	exact    string
	segments []routePathSegment
	template bool
}

type routePathSegment struct {
	literal   string
	parameter bool
}

func (p routePath) overlaps(other routePath) bool {
	switch {
	case !p.template && !other.template:
		return p.exact == other.exact
	case p.template && other.template:
		if len(p.segments) != len(other.segments) {
			return false
		}
		for index := range p.segments {
			left, right := p.segments[index], other.segments[index]
			if !left.parameter && !right.parameter && left.literal != right.literal {
				return false
			}
		}
		return true
	case p.template:
		return p.matches(other.exact)
	default:
		return other.matches(p.exact)
	}
}

func (p routePath) matches(path string) bool {
	if !p.template {
		return path == p.exact
	}
	segments, ok := splitRequestPath(path)
	return ok && p.matchesSegments(segments)
}

func (p routePath) matchesSegments(segments []string) bool {
	if !p.template || len(segments) != len(p.segments) {
		return false
	}
	for index, segment := range segments {
		pattern := p.segments[index]
		if !pattern.parameter && segment != pattern.literal {
			return false
		}
	}
	return true
}

func splitRequestPath(path string) ([]string, bool) {
	if path == "" || path[0] != '/' || strings.ContainsAny(path, "?#\\") ||
		strings.Contains(path, "//") {
		return nil, false
	}
	segments := strings.Split(path, "/")[1:]
	for _, segment := range segments {
		if segment == "" || segment == "." || segment == ".." {
			return nil, false
		}
	}
	return segments, true
}

func parseRoutePath(apiVersion, path string) (routePath, error) {
	if path == "" || path[0] != '/' || path == "/" || len(path) > 2048 {
		return routePath{}, errors.New("path must be an absolute non-root path of at most 2048 bytes")
	}
	if strings.ContainsAny(path, "?#\\") || strings.Contains(path, "//") || strings.Contains(path, "*") {
		return routePath{}, errors.New("path must be canonical")
	}
	u, err := url.ParseRequestURI(path)
	if err != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != path {
		return routePath{}, errors.New("path must be an unescaped canonical URL path")
	}
	segments := strings.Split(path, "/")[1:]
	parameterCount := 0
	fixedCount := 0
	seenParameters := make(map[string]struct{})
	parsedSegments := make([]routePathSegment, 0, len(segments))
	for index, segment := range segments {
		if segment == "" || segment == "." || segment == ".." {
			return routePath{}, errors.New("path contains an empty or relative segment")
		}
		// A colon-leading segment was an opaque exact-path segment in v1.
		// Interpret it as template syntax only in explicitly versioned template
		// grammars so existing immutable v1 artifacts keep their exact meaning.
		if (apiVersion == RouteConfigAPIVersionV2 || apiVersion == RouteConfigAPIVersionV3) &&
			strings.HasPrefix(segment, ":") {
			name := strings.TrimPrefix(segment, ":")
			if apiVersion == RouteConfigAPIVersionV2 &&
				!routeParameterPattern.MatchString(name) {
				return routePath{}, errors.New("path template must contain one canonical terminal parameter")
			}
			if !routeParameterPattern.MatchString(name) {
				return routePath{}, errors.New("path template contains a non-canonical parameter")
			}
			if apiVersion == RouteConfigAPIVersionV2 &&
				(parameterCount >= 1 || index != len(segments)-1) {
				return routePath{}, errors.New("path template must contain one canonical terminal parameter")
			}
			if _, duplicate := seenParameters[name]; duplicate {
				return routePath{}, errors.New("path template parameter names must be unique")
			}
			seenParameters[name] = struct{}{}
			parameterCount++
			parsedSegments = append(parsedSegments, routePathSegment{parameter: true})
			continue
		}
		fixedCount++
		parsedSegments = append(parsedSegments, routePathSegment{literal: segment})
	}
	if parameterCount > 0 {
		if fixedCount == 0 {
			return routePath{}, errors.New("path template requires at least one fixed segment")
		}
		return routePath{segments: parsedSegments, template: true}, nil
	}
	switch path {
	case "/livez", "/readyz", "/version", "/capabilities", "/metrics":
		return routePath{}, errors.New("path conflicts with the operational surface")
	}
	return routePath{exact: path}, nil
}

func parseUpstream(raw string) (*url.URL, error) {
	if raw == "" || raw != strings.TrimSpace(raw) {
		return nil, errors.New("upstream must be a canonical origin")
	}
	u, err := url.Parse(raw)
	if err != nil || !u.IsAbs() || u.Host == "" || u.User != nil {
		return nil, errors.New("upstream must be an absolute origin without credentials")
	}
	if u.RawQuery != "" || u.Fragment != "" || u.Path != "" {
		return nil, errors.New("upstream must be an origin without path, query, or fragment")
	}
	if u.Scheme != "https" && u.Scheme != "http" {
		return nil, errors.New("upstream scheme must be http or https")
	}
	u.Path = ""
	return u, nil
}

func validMethod(method string) bool {
	if len(method) > 32 {
		return false
	}
	for _, ch := range method {
		if !(ch >= 'A' && ch <= 'Z') && !(ch >= '0' && ch <= '9') && !strings.ContainsRune("!#$%&'*+-.^_`|~", ch) {
			return false
		}
	}
	return true
}

func (r Route) timeout() time.Duration {
	return time.Duration(r.TimeoutMillis) * time.Millisecond
}

func (r Route) allows(method string) bool {
	index := sort.SearchStrings(r.Methods, method)
	return index < len(r.Methods) && r.Methods[index] == method
}

func (r Route) allowHeader() string {
	return strings.Join(r.Methods, ", ")
}

func semanticIntegrity(config RouteConfig) ([sha256.Size]byte, error) {
	canonical, err := json.Marshal(config)
	if err != nil {
		return [sha256.Size]byte{}, fmt.Errorf("gateway: canonicalize route config: %w", err)
	}
	return sha256.Sum256(canonical), nil
}

func cloneRouteConfig(source RouteConfig) RouteConfig {
	clone := source
	clone.Routes = make([]Route, len(source.Routes))
	for i, route := range source.Routes {
		clone.Routes[i] = route
		clone.Routes[i].Methods = append([]string(nil), route.Methods...)
	}
	return clone
}
