package connector_inbound

import (
	"fmt"
	"net/http"
	"regexp"
	"strings"
	"time"
)

// SchemeName identifies a named, L0-shipped signature scheme.
// The closed set contains the generic signing mechanisms shipped by L0.
type SchemeName string

// SchemeHMACSHA256Template is the HMAC-SHA256 scheme over a pack-declared signing
// template. The scheme is a parameterised template, not a versioned scheme —
// the pack's SigningTemplate field controls the signing string construction.
// Algorithm string for secret matching: "hmac-sha256".
const SchemeHMACSHA256Template SchemeName = "hmac-sha256-template"

// SchemeSHA256SecretPrefixTemplate computes SHA-256 over the resolved secret
// bytes followed by the expanded signing template. It supports legacy provider
// contracts shaped as SHA-256(secret || raw-body) without weakening the HMAC
// scheme's mandatory timestamp/replay checks.
const SchemeSHA256SecretPrefixTemplate SchemeName = "sha256-secret-prefix-template"

// algorithmForScheme returns the algorithm string stored in the resolved secret's
// Algorithm field that corresponds to scheme s.
// An empty return means "no algorithm assertion" (accept any stored value).
func algorithmForScheme(s SchemeName) string {
	switch s {
	case SchemeHMACSHA256Template:
		return "hmac-sha256"
	case SchemeSHA256SecretPrefixTemplate:
		return "sha256"
	}
	return ""
}

// algorithmMatches reports whether the resolved secret's algorithm is
// compatible with scheme s. An empty resolved algorithm is accepted (the
// stored secret row predates algorithm tracking). A non-empty algorithm that
// disagrees is a definitive mismatch → ErrInvalidSignature.
func algorithmMatches(s SchemeName, resolvedAlgorithm string) bool {
	if resolvedAlgorithm == "" {
		return true
	}
	expected := algorithmForScheme(s)
	if expected == "" {
		return true // scheme has no algorithm assertion
	}
	return strings.EqualFold(resolvedAlgorithm, expected)
}

// SignatureEncoding describes how the HMAC digest bytes are encoded in the
// signature header. An empty value defaults to [EncodingHex] for
// back-compatibility. Pack-supplied configs should set this explicitly.
//
// Per Skeptic MED #1: required to support vendors that send base64-encoded
// signatures — adding a second connector must require zero engine changes,
// only pack config.
type SignatureEncoding string

const (
	// EncodingHex is the default: the HMAC digest is hex-encoded (lowercase).
	// Most webhook providers use hex encoding.
	EncodingHex SignatureEncoding = "hex"

	// EncodingBase64 indicates the HMAC digest is standard base64-encoded.
	EncodingBase64 SignatureEncoding = "base64"
)

// SignatureSchemeConfig is the pack-supplied configuration for one
// [SignatureVerifier] instance. All fields are set by the connector pack's
// init() function; the platform binary holds no vendor-specific defaults.
type SignatureSchemeConfig struct {
	// Scheme names the signing algorithm. Must be one of the closed v1 set.
	Scheme SchemeName

	// SigningTemplate is the template expanded before HMAC computation.
	// Allowed tokens: {Method}, {RequestURI}, {RequestURL}, {RawBody},
	// {Timestamp}.
	// Unknown tokens cause [NewSignatureVerifier] to fail at construction.
	SigningTemplate string

	// TimestampHeader is the inbound header name carrying the timestamp value
	// used both for skew checking and as the {Timestamp} template token.
	TimestampHeader string

	// SignatureHeader is the inbound header name carrying the encoded HMAC.
	// The encoding is controlled by [SignatureEncoding].
	SignatureHeader string

	// SignatureEncoding controls how the HMAC digest bytes are encoded in the
	// signature header. Empty defaults to [EncodingHex].
	SignatureEncoding SignatureEncoding

	// DeliveryIdentityBodyPath is an optional JSON path into RawBody carrying
	// the stable external delivery identity used for receipt-level idempotency.
	// When set, SigningTemplate must include {RawBody}; this prevents unsigned
	// body values from becoming durable receipt keys.
	DeliveryIdentityBodyPath string

	// DeliveryIdentityCollectionPath and DeliveryIdentityCollectionKeyPaths define
	// an optional app-neutral identity source for signed JSON collections. The
	// verifier extracts the collection, reads each configured scalar key path from
	// every item, canonicalizes the tuples, and emits one opaque delivery identity.
	// This is mutually exclusive with DeliveryIdentityBodyPath.
	DeliveryIdentityCollectionPath     string
	DeliveryIdentityCollectionKeyPaths []string

	// ReceiptHeaderAllowlist is an optional list of inbound header names the
	// pack author allows L0 to encrypt onto verified ingress receipts for
	// operator diagnostics. Empty means store no headers.
	ReceiptHeaderAllowlist []string

	// SkewWindow is the symmetric skew tolerance. |now − ts| > SkewWindow
	// rejects with [connectorcapabilities.ErrInvalidSignature]. Must be > 0.
	SkewWindow time.Duration

	// SecretName is the name passed to
	// [webhooks.ResolveWebhookSecretForVerification] to retrieve the secret.
	SecretName string

	// MethodHeader is the inbound header name carrying the HTTP method.
	//
	// Deprecated: kept for v1 wire compatibility only. The signing engine now
	// reads Method from VerifierInput.Method (server-observed via Request.Method
	// in the dispatcher). This field is NO LONGER consulted by the engine. Pack
	// manifests may retain it for documentation; the engine ignores it.
	MethodHeader string

	// URIHeader is the inbound header name carrying the request URI.
	//
	// Deprecated: kept for v1 wire compatibility only. The signing engine now
	// reads RequestURI from VerifierInput.RequestURI (the raw request target).
	// Full-URL signing templates should use {RequestURL}. This field is NO
	// LONGER consulted by the engine. Pack manifests may retain it for
	// documentation; the engine ignores it.
	URIHeader string
}

// knownSchemes is the closed set of generic L0 signature mechanisms.
var knownSchemes = map[SchemeName]bool{
	SchemeHMACSHA256Template:         true,
	SchemeSHA256SecretPrefixTemplate: true,
}

// templateTokenRE matches any {…} placeholder in a signing template.
var templateTokenRE = regexp.MustCompile(`\{[^}]+\}`)

// allowedTemplateTokens is the closed v1 set of template token strings.
var allowedTemplateTokens = map[string]bool{
	"{Method}":     true,
	"{RequestURI}": true,
	"{RequestURL}": true,
	"{RawBody}":    true,
	"{Timestamp}":  true,
}

// validateSchemeConfig validates cfg and returns a canonicalized copy on success.
// It is the pre-flight check shared by [NewSignatureVerifier].
//
// Canonicalization order for required header-name fields (TimestampHeader,
// SignatureHeader): TrimSpace → reject-if-empty → CanonicalHeaderKey.
// This ordering is deliberate and must not be changed: a whitespace-only value
// such as "   " is NOT a literal empty string, so checking for "" before
// trimming would silently accept it, store http.CanonicalHeaderKey("") == "",
// and cause runtime Verify to look up input.Headers[""] → "missing header" →
// ErrInvalidSignature → every legitimate delivery for that pack rejected as a
// terminal 401 (silent-drop class). By trimming first we fail closed at
// construction time instead of at runtime.
//
// The deprecated optional fields (MethodHeader, URIHeader) follow the same
// trim-then-canonicalize hygiene but do not reject whitespace-only collapse to
// "" because those fields are not consulted by the signing engine (the engine
// reads Method / RequestURI from VerifierInput.Method / VerifierInput.RequestURI).
//
// Additional rejections (fail-loud construction-time guards):
//   - TimestampHeader == SignatureHeader after canonicalization: reading both from
//     the same slot causes Verify to parse the HMAC digest as a timestamp → terminal
//     401 for every delivery.
//   - a delivery identity source is configured without {RawBody}: body-derived
//     receipt-level idempotency must be derived only from signed material.
//   - SecretName whitespace-only (trims to ""): a blank NamedCredential developer
//     name causes secret resolution to fail from the first delivery.
func validateSchemeConfig(cfg SignatureSchemeConfig) (SignatureSchemeConfig, error) {
	// Validate SignatureEncoding early — an unknown value silently falls through
	// to hex behaviour and ships a config that fails every delivery as
	// ErrInvalidSignature.
	switch cfg.SignatureEncoding {
	case "", EncodingHex, EncodingBase64:
		// ok
	default:
		return cfg, fmt.Errorf("%w: SignatureEncoding %q is not one of {\"\", %q, %q}",
			ErrInvalidSchemeConfig, cfg.SignatureEncoding, EncodingHex, EncodingBase64)
	}

	if !knownSchemes[cfg.Scheme] {
		return cfg, fmt.Errorf("%w: %q", ErrUnknownScheme, cfg.Scheme)
	}
	if cfg.SigningTemplate == "" {
		return cfg, fmt.Errorf("%w: SigningTemplate is empty", ErrInvalidSchemeConfig)
	}
	// Reject unknown tokens — bad config is a deployment bug.
	tokens := templateTokenRE.FindAllString(cfg.SigningTemplate, -1)
	for _, tok := range tokens {
		if !allowedTemplateTokens[tok] {
			return cfg, fmt.Errorf("%w: %q", ErrUnknownTemplateToken, tok)
		}
	}

	// The HMAC scheme is timestamp-bound. The legacy secret-prefix scheme is
	// deliberately timestamp-free and cannot opt into the Timestamp token.
	cfg.TimestampHeader = strings.TrimSpace(cfg.TimestampHeader)
	switch cfg.Scheme {
	case SchemeHMACSHA256Template:
		if cfg.TimestampHeader == "" {
			return cfg, fmt.Errorf("%w: TimestampHeader is empty", ErrInvalidSchemeConfig)
		}
		cfg.TimestampHeader = http.CanonicalHeaderKey(cfg.TimestampHeader)
		if cfg.SkewWindow <= 0 {
			return cfg, fmt.Errorf("%w: SkewWindow must be > 0", ErrInvalidSchemeConfig)
		}
	case SchemeSHA256SecretPrefixTemplate:
		if strings.Contains(cfg.SigningTemplate, "{Timestamp}") {
			return cfg, fmt.Errorf("%w: scheme %q does not support {Timestamp}", ErrInvalidSchemeConfig, cfg.Scheme)
		}
		if cfg.TimestampHeader != "" || cfg.SkewWindow != 0 {
			return cfg, fmt.Errorf("%w: scheme %q is timestamp-free; TimestampHeader and SkewWindow must be empty", ErrInvalidSchemeConfig, cfg.Scheme)
		}
	}

	cfg.SignatureHeader = strings.TrimSpace(cfg.SignatureHeader)
	if cfg.SignatureHeader == "" {
		return cfg, fmt.Errorf("%w: SignatureHeader is empty", ErrInvalidSchemeConfig)
	}
	cfg.SignatureHeader = http.CanonicalHeaderKey(cfg.SignatureHeader)

	// TimestampHeader and SignatureHeader must be distinct after canonicalization.
	// Reading both from the same header slot causes Verify to parse the HMAC digest
	// as a timestamp → timestamp parse error → ErrInvalidSignature for every delivery
	// (terminal 401, silent-drop class).
	if cfg.TimestampHeader != "" && cfg.TimestampHeader == cfg.SignatureHeader {
		return cfg, fmt.Errorf("%w: TimestampHeader and SignatureHeader must differ (got %q) — reading both from the same header slot causes Verify to parse the HMAC digest as a timestamp", ErrInvalidSchemeConfig, cfg.TimestampHeader)
	}
	cfg.DeliveryIdentityBodyPath = strings.TrimSpace(cfg.DeliveryIdentityBodyPath)
	cfg.DeliveryIdentityCollectionPath = strings.TrimSpace(cfg.DeliveryIdentityCollectionPath)
	for i, path := range cfg.DeliveryIdentityCollectionKeyPaths {
		cfg.DeliveryIdentityCollectionKeyPaths[i] = strings.TrimSpace(path)
	}
	hasScalarIdentity := cfg.DeliveryIdentityBodyPath != ""
	hasCollectionIdentity := cfg.DeliveryIdentityCollectionPath != "" || len(cfg.DeliveryIdentityCollectionKeyPaths) > 0
	if hasScalarIdentity && hasCollectionIdentity {
		return cfg, fmt.Errorf("%w: DeliveryIdentityBodyPath is mutually exclusive with DeliveryIdentityCollectionPath/DeliveryIdentityCollectionKeyPaths", ErrInvalidSchemeConfig)
	}
	if hasScalarIdentity || hasCollectionIdentity {
		if !strings.Contains(cfg.SigningTemplate, "{RawBody}") {
			return cfg, fmt.Errorf("%w: delivery identity source is configured but SigningTemplate does not include {RawBody}", ErrInvalidSchemeConfig)
		}
	}
	if hasScalarIdentity {
		if _, err := parsePath(cfg.DeliveryIdentityBodyPath); err != nil {
			return cfg, fmt.Errorf("%w: DeliveryIdentityBodyPath %q: %v", ErrInvalidSchemeConfig, cfg.DeliveryIdentityBodyPath, err)
		}
	}
	if hasCollectionIdentity {
		if cfg.DeliveryIdentityCollectionPath == "" {
			return cfg, fmt.Errorf("%w: DeliveryIdentityCollectionPath is required when DeliveryIdentityCollectionKeyPaths is set", ErrInvalidSchemeConfig)
		}
		if len(cfg.DeliveryIdentityCollectionKeyPaths) == 0 {
			return cfg, fmt.Errorf("%w: DeliveryIdentityCollectionKeyPaths is required when DeliveryIdentityCollectionPath is set", ErrInvalidSchemeConfig)
		}
		if _, err := parsePath(cfg.DeliveryIdentityCollectionPath); err != nil {
			return cfg, fmt.Errorf("%w: DeliveryIdentityCollectionPath %q: %v", ErrInvalidSchemeConfig, cfg.DeliveryIdentityCollectionPath, err)
		}
		seen := make(map[string]struct{}, len(cfg.DeliveryIdentityCollectionKeyPaths))
		for i, path := range cfg.DeliveryIdentityCollectionKeyPaths {
			if path == "" {
				return cfg, fmt.Errorf("%w: DeliveryIdentityCollectionKeyPaths[%d] is empty", ErrInvalidSchemeConfig, i)
			}
			if _, err := parsePath(path); err != nil {
				return cfg, fmt.Errorf("%w: DeliveryIdentityCollectionKeyPaths[%d] %q: %v", ErrInvalidSchemeConfig, i, path, err)
			}
			if _, ok := seen[path]; ok {
				return cfg, fmt.Errorf("%w: DeliveryIdentityCollectionKeyPaths contains duplicate path %q", ErrInvalidSchemeConfig, path)
			}
			seen[path] = struct{}{}
		}
	}

	// SecretName: trim whitespace first, then reject empty.
	// SecretName is a NamedCredential developer name, NOT an HTTP header, so
	// CanonicalHeaderKey is NOT applied here.
	cfg.SecretName = strings.TrimSpace(cfg.SecretName)
	if cfg.SecretName == "" {
		return cfg, fmt.Errorf("%w: SecretName is empty", ErrInvalidSchemeConfig)
	}

	// Deprecated optional fields: trim-then-canonicalize for hygiene; whitespace-only
	// collapsing to "" is acceptable because the engine never reads these fields.
	if cfg.MethodHeader != "" {
		cfg.MethodHeader = strings.TrimSpace(cfg.MethodHeader)
		if cfg.MethodHeader != "" {
			cfg.MethodHeader = http.CanonicalHeaderKey(cfg.MethodHeader)
		}
	}
	if cfg.URIHeader != "" {
		cfg.URIHeader = strings.TrimSpace(cfg.URIHeader)
		if cfg.URIHeader != "" {
			cfg.URIHeader = http.CanonicalHeaderKey(cfg.URIHeader)
		}
	}
	cfg.ReceiptHeaderAllowlist = canonicalHeaderAllowlist(cfg.ReceiptHeaderAllowlist)

	// Note: {Method}, {RequestURI}, and {RequestURL} tokens are resolved from
	// VerifierInput. {Method} is server-observed Request.Method; {RequestURI} is
	// the raw request target; {RequestURL} is the dispatcher-reconstructed full
	// URL. No header config is required or consulted for these tokens.
	return cfg, nil
}

func canonicalHeaderAllowlist(names []string) []string {
	if len(names) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(names))
	out := make([]string, 0, len(names))
	for _, name := range names {
		canonical := http.CanonicalHeaderKey(strings.TrimSpace(name))
		if canonical == "" {
			continue
		}
		key := strings.ToLower(canonical)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, canonical)
	}
	return out
}

// expandTemplate replaces the allowed tokens in tmpl with the corresponding
// values. method and requestURI MUST be server-observed values from
// VerifierInput.Method / VerifierInput.RequestURI / VerifierInput.RequestURL
// (populated by the dispatcher); they are NOT read from headers.
// {RawBody} is inserted as the UTF-8 string of rawBody.
func expandTemplate(tmpl string, headers map[string]string, rawBody []byte, method, requestURI, requestURL, tsHeader string) string {
	r := strings.NewReplacer(
		"{Method}", method,
		"{RequestURI}", requestURI,
		"{RequestURL}", requestURL,
		"{RawBody}", string(rawBody),
		"{Timestamp}", headers[tsHeader],
	)
	return r.Replace(tmpl)
}
