package connector_inbound

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/pingcap-inc/nexuscrm-connectors/features/connector/capabilities"
)

// SignatureVerifier is a [connectorcapabilities.SignatureVerifierHandler] bound
// to a namespace and pack-supplied [SignatureSchemeConfig].
//
// The dispatcher (#1527, NCRM-INT-04) resolves the webhook secret once per
// request and threads it through [connectorcapabilities.VerifierInput.Secret],
// [connectorcapabilities.VerifierInput.Algorithm], and
// [connectorcapabilities.VerifierInput.SecretName]. SignatureVerifier reads
// those fields directly — it holds no DB or cipher dependency.
//
// Construct via [NewSignatureVerifier]. The zero value is not usable.
type SignatureVerifier struct {
	cfg SignatureSchemeConfig
	// now is injectable so tests can control clock-dependent skew checks
	// without needing real wall-clock delays.
	now func() time.Time
}

// NewSignatureVerifier constructs a [SignatureVerifier] using cfg.
//
// Returns [ErrUnknownScheme] when cfg.Scheme is not in the closed v1 set.
// Returns [ErrUnknownTemplateToken] when cfg.SigningTemplate contains a token
// not in the closed v1 set ({Method}, {RequestURI}, {RequestURL}, {RawBody},
// {Timestamp}).
// Returns [ErrInvalidSchemeConfig] when any required config field is absent or
// invalid.
func NewSignatureVerifier(
	cfg SignatureSchemeConfig,
) (*SignatureVerifier, error) {
	canonCfg, err := validateSchemeConfig(cfg)
	if err != nil {
		return nil, err
	}
	return &SignatureVerifier{
		cfg: canonCfg,
		now: time.Now,
	}, nil
}

// Verify implements [connectorcapabilities.SignatureVerifierHandler].
//
// Failure modes:
//   - input.Secret == nil → server fault (NOT [connectorcapabilities.ErrInvalidSignature]);
//     dispatcher maps this to a retriable 5xx. An empty secret is a config/server problem,
//     not a bad-signature verdict.
//   - Algorithm mismatch → wrapped [connectorcapabilities.ErrInvalidSignature]; dispatcher
//     maps this to a terminal 401.
//   - Missing headers, skew exceeded, HMAC mismatch → wrapped
//     [connectorcapabilities.ErrInvalidSignature]; dispatcher maps this to a terminal 401.
func (v *SignatureVerifier) Verify(_ context.Context, input *connectorcapabilities.VerifierInput) error {
	if input.Secret == nil {
		// Server-fault class: the dispatcher failed to resolve/inject the secret.
		// Wrapping WITHOUT ErrInvalidSignature ensures the dispatcher treats this
		// as retriable rather than permanently dropping the delivery as bad credentials.
		return fmt.Errorf("verify: secret not injected by dispatcher (SecretName=%q): server fault", input.SecretName)
	}

	// Algorithm mismatch is definitive: the pack-config and the resolved secret
	// disagree on the signing algorithm, which is a deployment misconfiguration
	// the pack cannot recover from by retrying.
	if !algorithmMatches(v.cfg.Scheme, input.Algorithm) {
		return fmt.Errorf("algorithm mismatch: scheme expects %q, secret has %q: %w",
			algorithmForScheme(v.cfg.Scheme), input.Algorithm,
			connectorcapabilities.ErrInvalidSignature)
	}

	sig, ok := input.Headers[v.cfg.SignatureHeader]
	if !ok || sig == "" {
		return fmt.Errorf("missing signature header %q: %w",
			v.cfg.SignatureHeader, connectorcapabilities.ErrInvalidSignature)
	}

	if v.cfg.Scheme == SchemeHMACSHA256Template {
		tsStr, ok := input.Headers[v.cfg.TimestampHeader]
		if !ok || tsStr == "" {
			return fmt.Errorf("missing timestamp header %q: %w",
				v.cfg.TimestampHeader, connectorcapabilities.ErrInvalidSignature)
		}
		now := v.now()
		ts, err := parseTimestamp(tsStr, now)
		if err != nil {
			return fmt.Errorf("invalid timestamp: %w",
				connectorcapabilities.ErrInvalidSignature)
		}
		if err := checkSkew(ts, now, v.cfg.SkewWindow); err != nil {
			return err
		}
	}
	if strings.Contains(v.cfg.SigningTemplate, "{RequestURL}") && input.RequestURL == "" {
		return fmt.Errorf("verify: RequestURL not injected by dispatcher (SecretName=%q): server fault", input.SecretName)
	}

	signingStr := expandTemplate(
		v.cfg.SigningTemplate,
		input.Headers,
		input.RawBody,
		input.Method,
		input.RequestURI,
		input.RequestURL,
		v.cfg.TimestampHeader,
	)

	switch v.cfg.Scheme {
	case SchemeHMACSHA256Template:
		if err := verifyHMACSHA256Template(signingStr, sig, input.Secret, v.cfg.SignatureEncoding); err != nil {
			return err
		}
	case SchemeSHA256SecretPrefixTemplate:
		if err := verifySHA256SecretPrefixTemplate(signingStr, sig, input.Secret, v.cfg.SignatureEncoding); err != nil {
			return err
		}
	default:
		return fmt.Errorf("verify: unsupported signature scheme %q: server fault", v.cfg.Scheme)
	}
	if input.EnableVerifiedIngressReceipt && v.cfg.DeliveryIdentityBodyPath != "" {
		raw, err := extractRaw(v.cfg.DeliveryIdentityBodyPath, json.RawMessage(input.RawBody))
		if err != nil {
			return fmt.Errorf("extract delivery identity body path %q: %v",
				v.cfg.DeliveryIdentityBodyPath, err)
		}
		identity, _, err := scalarStringFromRaw(v.cfg.DeliveryIdentityBodyPath, raw, false)
		if err != nil {
			return fmt.Errorf("extract delivery identity body path %q: %v",
				v.cfg.DeliveryIdentityBodyPath, err)
		}
		identity = strings.TrimSpace(identity)
		if identity == "" {
			return fmt.Errorf("missing delivery identity body path %q",
				v.cfg.DeliveryIdentityBodyPath)
		}
		input.DeliveryIdentity = identity
		input.ReceiptHeaderAllowlist = append([]string(nil), v.cfg.ReceiptHeaderAllowlist...)
	} else if input.EnableVerifiedIngressReceipt && v.cfg.DeliveryIdentityCollectionPath != "" {
		identity, err := deliveryIdentityFromCollection(json.RawMessage(input.RawBody), v.cfg.DeliveryIdentityCollectionPath, v.cfg.DeliveryIdentityCollectionKeyPaths)
		if err != nil {
			return err
		}
		input.DeliveryIdentity = identity
		input.ReceiptHeaderAllowlist = append([]string(nil), v.cfg.ReceiptHeaderAllowlist...)
	}
	return nil
}

type collectionDeliveryIdentityPayload struct {
	Version        int        `json:"version"`
	CollectionPath string     `json:"collectionPath"`
	KeyPaths       []string   `json:"keyPaths"`
	Items          [][]string `json:"items"`
}

func deliveryIdentityFromCollection(body json.RawMessage, collectionPath string, keyPaths []string) (string, error) {
	items, err := extractArray(collectionPath, body)
	if err != nil {
		return "", fmt.Errorf("extract delivery identity collection path %q: %v", collectionPath, err)
	}
	if len(items) == 0 {
		return "", fmt.Errorf("missing delivery identity collection path %q", collectionPath)
	}
	tuples := make([][]string, 0, len(items))
	for itemIndex, item := range items {
		tuple := make([]string, 0, len(keyPaths))
		for _, keyPath := range keyPaths {
			raw, err := extractRaw(keyPath, item)
			if err != nil {
				return "", fmt.Errorf("extract delivery identity collection key path %q at item %d: %v", keyPath, itemIndex, err)
			}
			part, _, err := scalarStringFromRaw(keyPath, raw, false)
			if err != nil {
				return "", fmt.Errorf("extract delivery identity collection key path %q at item %d: %v", keyPath, itemIndex, err)
			}
			part = strings.TrimSpace(part)
			if part == "" {
				return "", fmt.Errorf("missing delivery identity collection key path %q at item %d", keyPath, itemIndex)
			}
			tuple = append(tuple, part)
		}
		tuples = append(tuples, tuple)
	}
	sort.Slice(tuples, func(i, j int) bool {
		return tupleLess(tuples[i], tuples[j])
	})
	payload := collectionDeliveryIdentityPayload{
		Version:        1,
		CollectionPath: collectionPath,
		KeyPaths:       append([]string(nil), keyPaths...),
		Items:          tuples,
	}
	canonical, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("canonicalize delivery identity collection: %v", err)
	}
	sum := sha256.Sum256(canonical)
	return "collection-sha256-v1:" + hex.EncodeToString(sum[:]), nil
}

func tupleLess(left, right []string) bool {
	for i := 0; i < len(left) && i < len(right); i++ {
		if left[i] == right[i] {
			continue
		}
		return left[i] < right[i]
	}
	return len(left) < len(right)
}
