package config

import (
	"fmt"
	"net"
	"net/textproto"
	"net/url"
	"path/filepath"
	"regexp"
	"slices"
	"strconv"
	"strings"

	"gopkg.in/yaml.v3"

	apiproxyv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-sidecar/apiproxy/v1"
	secretstorev1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-sidecar/secretstore/v1"
)

const (
	APIProxyNetworkUnix               = "unix"
	APIProxyNetworkTCP                = "tcp"
	APIProxyMiddlewareLarkAuthProxy   = "lark-auth-proxy"
	APIProxyAuthBearer                = "bearer"
	APIProxyAuthBasic                 = "basic"
	APIProxyAuthHeader                = "header"
	APIProxyAuthGitHubPATBasic        = "githubPATBasic"
	APIProxyHostPathSegment           = "pathSegment"
	APIProxyMiddlewareOperationPolicy = "operation-policy"
	APIProxyMiddlewareCredentialAuth  = "credential-auth"

	APIProxyRuntimeDir = "/run/agent-sidecar"
)

var hostTemplateParameterPattern = regexp.MustCompile(`\{([a-z][a-zA-Z0-9]*)\}`)
var httpMethodPattern = regexp.MustCompile(`^[A-Z]+$`)

type APIProxyServiceConfig struct {
	Listeners map[string]APIProxyListenerConfig `yaml:"listeners"`
	Routes    map[string]APIProxyRouteConfig    `yaml:"routes"`
}

// Reject removed and misspelled top-level proxy fields instead of silently
// disabling an old singleton listener configuration during migration.
func (c *APIProxyServiceConfig) UnmarshalYAML(node *yaml.Node) error {
	type plain APIProxyServiceConfig
	for i := 0; i+1 < len(node.Content); i += 2 {
		name := node.Content[i].Value
		if name != "listeners" && name != "routes" {
			return fmt.Errorf("unknown apiProxy field %q", name)
		}
	}
	return node.Decode((*plain)(c))
}

type APIProxyListenerConfig struct {
	Network               string   `yaml:"network"`
	Address               string   `yaml:"address"`
	Mode                  string   `yaml:"mode"`
	AllowedRequestHeaders []string `yaml:"allowedRequestHeaders"`
}

type APIProxyRouteConfig struct {
	ListenerID            string                     `yaml:"listenerId"`
	Prefix                string                     `yaml:"prefix"`
	Upstream              APIProxyUpstreamConfig     `yaml:"upstream"`
	Middlewares           []APIProxyMiddlewareConfig `yaml:"middlewares"`
	AllowedRequestHeaders []string                   `yaml:"allowedRequestHeaders"`
}

type APIProxyUpstreamConfig struct {
	BaseURL        string                                 `yaml:"baseUrl"`
	Scheme         string                                 `yaml:"scheme"`
	HostTemplate   string                                 `yaml:"hostTemplate"`
	HostParameters map[string]APIProxyHostParameterConfig `yaml:"hostParameters"`
}

type APIProxyHostParameterConfig struct {
	Source        string   `yaml:"source"`
	AllowedValues []string `yaml:"allowedValues"`
}

type APIProxyMiddlewareConfig struct {
	Type   string                         `yaml:"type"`
	Config APIProxyMiddlewareParamsConfig `yaml:"config"`
}

type APIProxyMiddlewareParamsConfig struct {
	KeyFrom    APIProxySecretRefConfig       `yaml:"keyFrom"`
	Allow      []APIProxyOperationConfig     `yaml:"allow"`
	Candidates []APIProxyAuthCandidateConfig `yaml:"candidates"`
}

type APIProxyAuthCandidateConfig struct {
	Type       string                  `yaml:"type"`
	HeaderName string                  `yaml:"headerName"`
	ValueFrom  APIProxySecretRefConfig `yaml:"valueFrom"`
}

type APIProxySecretRefConfig struct {
	Secret string `yaml:"secret"`
	Field  string `yaml:"field"`
}

type APIProxyOperationConfig struct {
	Method       string `yaml:"method"`
	PathExact    string `yaml:"pathExact"`
	PathPrefix   string `yaml:"pathPrefix"`
	PathTemplate string `yaml:"pathTemplate"`
}

func (c APIProxyHostParameterConfig) ContractSource() apiproxyv1.HostParameterSource {
	if c.Source == APIProxyHostPathSegment {
		return apiproxyv1.HostParameterSource_HOST_PARAMETER_SOURCE_PATH_SEGMENT
	}
	return apiproxyv1.HostParameterSource_HOST_PARAMETER_SOURCE_UNSPECIFIED
}

func (c APIProxyAuthCandidateConfig) ContractType() apiproxyv1.AuthType {
	switch c.Type {
	case APIProxyAuthBearer:
		return apiproxyv1.AuthType_AUTH_TYPE_BEARER
	case APIProxyAuthBasic:
		return apiproxyv1.AuthType_AUTH_TYPE_BASIC
	case APIProxyAuthHeader:
		return apiproxyv1.AuthType_AUTH_TYPE_HEADER
	case APIProxyAuthGitHubPATBasic:
		return apiproxyv1.AuthType_AUTH_TYPE_GITHUB_PAT_BASIC
	default:
		return apiproxyv1.AuthType_AUTH_TYPE_UNSPECIFIED
	}
}

func (c APIProxyOperationConfig) ContractOperation() *apiproxyv1.Operation {
	operation := &apiproxyv1.Operation{Method: c.Method}
	switch {
	case c.PathExact != "":
		operation.PathMatcher = &apiproxyv1.Operation_PathExact{PathExact: c.PathExact}
	case c.PathPrefix != "":
		operation.PathMatcher = &apiproxyv1.Operation_PathPrefix{PathPrefix: c.PathPrefix}
	case c.PathTemplate != "":
		operation.PathMatcher = &apiproxyv1.Operation_PathTemplate{PathTemplate: c.PathTemplate}
	}
	return operation
}

func (c APIProxyServiceConfig) Validate(store SecretStoreServiceConfig) error {
	if len(c.Listeners) == 0 && len(c.Routes) > 0 {
		return fmt.Errorf("listener must be configured when routes are present")
	}
	addresses := make(map[string]bool)
	prefixes := make(map[string][]string)
	for name, listener := range c.Listeners {
		if !logicalNamePattern.MatchString(name) {
			return fmt.Errorf("listener name %q is invalid", name)
		}
		switch listener.Network {
		case APIProxyNetworkUnix:
			if err := validateUnixSocket(listener.Address, listener.Mode); err != nil {
				return fmt.Errorf("listener %q: %w", name, err)
			}
		case APIProxyNetworkTCP:
			host, port, err := net.SplitHostPort(listener.Address)
			number, portErr := strconv.Atoi(port)
			ip := net.ParseIP(host)
			if err != nil || portErr != nil || number < 1 || number > 65535 || ip == nil || !ip.IsLoopback() {
				return fmt.Errorf("listener %q must use a loopback IP and TCP port", name)
			}
			if listener.Mode != "" {
				return fmt.Errorf("TCP listener %q cannot configure mode", name)
			}
		default:
			return fmt.Errorf("listener %q has unsupported network %q", name, listener.Network)
		}
		address := listener.Network + ":" + listener.Address
		if addresses[address] {
			return fmt.Errorf("listener address %q is duplicated", listener.Address)
		}
		addresses[address] = true
		if err := validateHeaderAllowlist(listener.AllowedRequestHeaders); err != nil {
			return fmt.Errorf("listener %q: %w", name, err)
		}
	}
	for name, route := range c.Routes {
		if !logicalNamePattern.MatchString(name) {
			return fmt.Errorf("route name %q is invalid", name)
		}
		if _, ok := c.Listeners[route.ListenerID]; !ok {
			return fmt.Errorf("route %q references unknown listener %q", name, route.ListenerID)
		}
		if err := validateRoute(name, route, store); err != nil {
			return err
		}
		prefixes[route.ListenerID] = append(prefixes[route.ListenerID], route.Prefix)
	}
	for name := range c.Listeners {
		if len(prefixes[name]) == 0 {
			return fmt.Errorf("listener %q must configure routes", name)
		}
		if err := validateRoutePrefixes(prefixes[name]); err != nil {
			return fmt.Errorf("listener %q: %w", name, err)
		}
	}
	return nil
}

func validateUnixSocket(address, mode string) error {
	if !filepath.IsAbs(address) || filepath.Dir(address) != APIProxyRuntimeDir {
		return fmt.Errorf("socket address must be directly under %s", APIProxyRuntimeDir)
	}
	if filepath.Clean(address) == APIProxyRuntimeDir {
		return fmt.Errorf("socket address must name a file")
	}
	parsed, err := strconv.ParseUint(mode, 8, 32)
	if err != nil || (parsed != 0o600 && parsed != 0o660) {
		return fmt.Errorf("socket mode %q is invalid", mode)
	}
	return nil
}

func validateRoute(name string, route APIProxyRouteConfig, store SecretStoreServiceConfig) error {
	if err := validateAbsolutePath(route.Prefix); err != nil || route.Prefix == "/healthz" {
		return fmt.Errorf("route %q has invalid prefix %q", name, route.Prefix)
	}
	if route.Prefix != "/" && strings.HasSuffix(route.Prefix, "/") {
		return fmt.Errorf("route %q prefix must not end with slash", name)
	}
	if err := validateUpstream(route.Upstream); err != nil {
		return fmt.Errorf("route %q: %w", name, err)
	}
	if err := validateMiddlewares(route.Middlewares, store); err != nil {
		return fmt.Errorf("route %q: %w", name, err)
	}
	for _, middleware := range route.Middlewares {
		if middleware.Type != APIProxyMiddlewareLarkAuthProxy {
			continue
		}
		if route.Prefix != "/" || (route.Upstream.BaseURL != "https://open.feishu.cn" && route.Upstream.BaseURL != "https://open.larksuite.com") {
			return fmt.Errorf("lark-auth-proxy requires root prefix and a fixed official HTTPS origin")
		}
		auth := route.Middlewares[len(route.Middlewares)-1].Config.Candidates
		if len(auth) != 1 || auth[0].Type != APIProxyAuthBearer {
			return fmt.Errorf("lark-auth-proxy requires exactly one bearer credential")
		}
	}
	if err := validateHeaderAllowlist(route.AllowedRequestHeaders); err != nil {
		return fmt.Errorf("route %q: %w", name, err)
	}
	return nil
}

func validateMiddlewares(middlewares []APIProxyMiddlewareConfig, store SecretStoreServiceConfig) error {
	operationPolicyIndex := -1
	credentialAuthIndex := -1
	for index, middleware := range middlewares {
		if middleware.Type != APIProxyMiddlewareLarkAuthProxy && middleware.Config.KeyFrom != (APIProxySecretRefConfig{}) {
			return fmt.Errorf("keyFrom is only supported by lark-auth-proxy")
		}
		switch middleware.Type {
		case APIProxyMiddlewareLarkAuthProxy:
			if index != 0 {
				return fmt.Errorf("lark-auth-proxy must be first and configured once")
			}
			if len(middleware.Config.Allow) != 0 || len(middleware.Config.Candidates) != 0 {
				return fmt.Errorf("lark-auth-proxy cannot configure operations or candidates")
			}
			if err := validateSecretRef(middleware.Config.KeyFrom, store); err != nil {
				return err
			}
		case APIProxyMiddlewareOperationPolicy:
			if operationPolicyIndex >= 0 {
				return fmt.Errorf("middleware %q must be configured exactly once", middleware.Type)
			}
			if len(middleware.Config.Allow) == 0 {
				return fmt.Errorf("middleware %q must configure allowed operations", middleware.Type)
			}
			if len(middleware.Config.Candidates) != 0 {
				return fmt.Errorf("middleware %q cannot configure auth candidates", middleware.Type)
			}
			for _, operation := range middleware.Config.Allow {
				if err := validateOperation(operation); err != nil {
					return fmt.Errorf("middleware %q: %w", middleware.Type, err)
				}
			}
			operationPolicyIndex = index
		case APIProxyMiddlewareCredentialAuth:
			if credentialAuthIndex >= 0 {
				return fmt.Errorf("middleware %q must be configured exactly once", middleware.Type)
			}
			if len(middleware.Config.Candidates) == 0 {
				return fmt.Errorf("middleware %q must configure auth candidates", middleware.Type)
			}
			if len(middleware.Config.Allow) != 0 {
				return fmt.Errorf("middleware %q cannot configure allowed operations", middleware.Type)
			}
			for _, candidate := range middleware.Config.Candidates {
				if err := validateAuthCandidate(candidate, store); err != nil {
					return fmt.Errorf("middleware %q: %w", middleware.Type, err)
				}
			}
			credentialAuthIndex = index
		default:
			return fmt.Errorf("unsupported middleware type %q", middleware.Type)
		}
	}
	if operationPolicyIndex < 0 {
		return fmt.Errorf("middleware %q must be configured exactly once", APIProxyMiddlewareOperationPolicy)
	}
	if credentialAuthIndex < 0 {
		return fmt.Errorf("middleware %q must be configured exactly once", APIProxyMiddlewareCredentialAuth)
	}
	if operationPolicyIndex > credentialAuthIndex {
		return fmt.Errorf("middleware %q must run before %q", APIProxyMiddlewareOperationPolicy, APIProxyMiddlewareCredentialAuth)
	}
	return nil
}

func validateUpstream(upstream APIProxyUpstreamConfig) error {
	if (upstream.BaseURL == "") == (upstream.HostTemplate == "") {
		return fmt.Errorf("upstream must configure exactly one of baseUrl or hostTemplate")
	}
	if upstream.BaseURL != "" {
		parsed, err := url.Parse(upstream.BaseURL)
		if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.RawPath != "" || parsed.ForceQuery || parsed.Opaque != "" {
			return fmt.Errorf("upstream baseUrl must be an absolute HTTPS URL without userinfo, query, or fragment")
		}
		if len(upstream.HostParameters) != 0 || upstream.Scheme != "" {
			return fmt.Errorf("fixed upstream cannot configure scheme or hostParameters")
		}
		if parsed.Path != "" {
			if err := validateAbsolutePath(parsed.Path); err != nil {
				return fmt.Errorf("upstream baseUrl path is invalid")
			}
		}
		return nil
	}
	if upstream.Scheme != "https" {
		return fmt.Errorf("templated upstream scheme must be https")
	}
	matches := hostTemplateParameterPattern.FindAllStringSubmatch(upstream.HostTemplate, -1)
	if len(matches) == 0 || strings.ContainsAny(upstream.HostTemplate, "/:@?#\\") {
		return fmt.Errorf("hostTemplate must be a host with declared parameters")
	}
	declared := make(map[string]struct{}, len(matches))
	for _, match := range matches {
		name := match[1]
		if _, duplicate := declared[name]; duplicate {
			return fmt.Errorf("host parameter %q appears more than once", name)
		}
		declared[name] = struct{}{}
		parameter, ok := upstream.HostParameters[name]
		if !ok || parameter.ContractSource() != apiproxyv1.HostParameterSource_HOST_PARAMETER_SOURCE_PATH_SEGMENT || len(parameter.AllowedValues) == 0 {
			return fmt.Errorf("host parameter %q must use pathSegment with allowedValues", name)
		}
		for _, value := range parameter.AllowedValues {
			if !validHostLabel(value) {
				return fmt.Errorf("host parameter %q has invalid allowed value %q", name, value)
			}
		}
	}
	if len(declared) != len(upstream.HostParameters) {
		return fmt.Errorf("hostParameters contains an undeclared parameter")
	}
	remaining := hostTemplateParameterPattern.ReplaceAllString(upstream.HostTemplate, "x")
	if parsed, err := url.Parse("https://" + remaining); err != nil || parsed.Host != remaining {
		return fmt.Errorf("hostTemplate is invalid")
	}
	return nil
}

func validateAuthCandidate(candidate APIProxyAuthCandidateConfig, store SecretStoreServiceConfig) error {
	switch candidate.ContractType() {
	case apiproxyv1.AuthType_AUTH_TYPE_BEARER, apiproxyv1.AuthType_AUTH_TYPE_BASIC, apiproxyv1.AuthType_AUTH_TYPE_GITHUB_PAT_BASIC:
		if candidate.HeaderName != "" {
			return fmt.Errorf("auth type %q cannot configure headerName", candidate.Type)
		}
	case apiproxyv1.AuthType_AUTH_TYPE_HEADER:
		if isProtectedHeader(candidate.HeaderName) {
			return fmt.Errorf("auth header %q is forbidden", candidate.HeaderName)
		}
	default:
		return fmt.Errorf("unsupported auth type %q", candidate.Type)
	}
	return validateSecretRef(candidate.ValueFrom, store)
}

func validateSecretRef(ref APIProxySecretRefConfig, store SecretStoreServiceConfig) error {
	runtime, ok := store.Runtimes[ref.Secret]
	if !ok {
		return fmt.Errorf("auth references unknown secret %q", ref.Secret)
	}
	if ref.Field == "" {
		return fmt.Errorf("auth secret field is required")
	}
	if runtime.Source.ContractFormat() == secretstorev1.SecretFormat_SECRET_FORMAT_OPAQUE && ref.Field != "value" {
		return fmt.Errorf("opaque secret %q only exposes field value", ref.Secret)
	}
	return nil
}

func validateOperation(operation APIProxyOperationConfig) error {
	method := strings.ToUpper(operation.Method)
	if method != operation.Method || !httpMethodPattern.MatchString(method) || method == "CONNECT" || method == "TRACE" {
		return fmt.Errorf("operation method %q is invalid", operation.Method)
	}
	matchers := []string{operation.PathExact, operation.PathPrefix, operation.PathTemplate}
	configured := 0
	for _, matcher := range matchers {
		if matcher != "" {
			configured++
			if err := validateAbsolutePath(matcher); err != nil {
				return fmt.Errorf("operation path %q is invalid", matcher)
			}
		}
	}
	if configured != 1 {
		return fmt.Errorf("operation must configure exactly one path matcher")
	}
	if strings.ContainsAny(operation.PathExact+operation.PathPrefix, "{}*") {
		return fmt.Errorf("exact and prefix matchers cannot contain templates")
	}
	if operation.PathTemplate != "" {
		greedySegments := 0
		for _, segment := range strings.Split(strings.TrimPrefix(operation.PathTemplate, "/"), "/") {
			if strings.ContainsAny(segment, "{}") && !regexp.MustCompile(`^\{[a-z][a-zA-Z0-9]*(\.\.\.)?\}$`).MatchString(segment) {
				return fmt.Errorf("path template segment %q is invalid", segment)
			}
			if strings.HasSuffix(segment, "...}") {
				greedySegments++
			}
		}
		if greedySegments > 1 {
			return fmt.Errorf("path template can contain at most one multi-segment parameter")
		}
	}
	return nil
}

func validateRoutePrefixes(prefixes []string) error {
	slices.Sort(prefixes)
	for i, prefix := range prefixes {
		if i > 0 && prefix == prefixes[i-1] {
			return fmt.Errorf("duplicate route prefix %q", prefix)
		}
	}
	return nil
}

func validateHeaderAllowlist(headers []string) error {
	for _, header := range headers {
		if textproto.CanonicalMIMEHeaderKey(header) == "" || isProtectedHeader(header) {
			return fmt.Errorf("request header %q is forbidden", header)
		}
	}
	return nil
}

func isProtectedHeader(header string) bool {
	lower := strings.ToLower(header)
	if textproto.CanonicalMIMEHeaderKey(header) == "" {
		return true
	}
	protected := []string{"authorization", "connection", "content-length", "cookie", "expect", "forwarded", "host", "keep-alive", "proxy-authenticate", "proxy-authorization", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade"}
	if slices.Contains(protected, lower) || strings.HasPrefix(lower, "x-forwarded-") || strings.HasPrefix(lower, "x-lark-proxy-") || lower == "x-lark-body-sha256" || lower == "x-lark-mcp-uat" || lower == "x-lark-mcp-tat" {
		return true
	}
	for _, prefix := range []string{"x-amz-", "x-aws-", "x-kubernetes-", "x-tidbcloud-", "x-pingcap-", "x-internal-"} {
		if strings.HasPrefix(lower, prefix) {
			return true
		}
	}
	return false
}

func validateAbsolutePath(value string) error {
	if value == "" || value[0] != '/' || strings.ContainsAny(value, "\\?#%") {
		return fmt.Errorf("path must be normalized and absolute")
	}
	cleaned := filepath.Clean(value)
	if strings.HasSuffix(value, "/") && value != "/" {
		cleaned += "/"
	}
	if cleaned != value || strings.Contains(value, "//") {
		return fmt.Errorf("path must be normalized and absolute")
	}
	return nil
}

func validHostLabel(value string) bool {
	if value == "" || len(value) > 63 || value[0] == '-' || value[len(value)-1] == '-' {
		return false
	}
	for _, ch := range value {
		if (ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') && ch != '-' {
			return false
		}
	}
	return true
}
