package externaleffectv1

import (
	"context"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"hash"
	"regexp"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

var (
	identity = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_.:/-]*$`)
	method   = regexp.MustCompile(`^[A-Z]{3,16}$`)
	header   = regexp.MustCompile(`^[a-z0-9!#$%&'*+.^_` + "`" + `|~-]+$`)
)

var allowedRequestHeaders = map[string]struct{}{
	"accept":           {},
	"content-type":     {},
	"idempotency-key":  {},
	"if-match":         {},
	"if-none-match":    {},
	"prefer":           {},
	"user-agent":       {},
	"x-correlation-id": {},
	"x-request-id":     {},
}

func ValidateClaimRequest(ctx context.Context, request ClaimRequest) error {
	if err := validateContext(ctx); err != nil {
		return err
	}
	if !validOpaque(request.WorkerID) || request.Limit == 0 || request.Limit > MaxEffectsPerClaim {
		return invalid(common.InvalidComponentCommandData)
	}
	return validateSize(request, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func ValidateClaimResponse(response ClaimResponse) error {
	if !validBoundReceipts(response.Receipts) || len(response.Effects) > MaxEffectsPerClaim {
		return internal()
	}
	// Reject an oversized aggregate before validation clones, sorts, or hashes
	// any nested collection. The final JSON-size check below remains the exact
	// wire-independent semantic ceiling; this raw-byte budget is its cheap
	// allocation-amplification guard.
	if !claimResponseWithinRawBudget(response, MaxSemanticResponseBytes) {
		return &Failure{Category: common.FailureResourceExhausted, LimitKind: common.LimitSemanticResponse, Maximum: uint64(MaxSemanticResponseBytes)}
	}
	seen := map[string]struct{}{}
	for _, effect := range response.Effects {
		if err := validateEffect(effect); err != nil {
			return internal()
		}
		if _, duplicate := seen[effect.EffectID]; duplicate {
			return internal()
		}
		seen[effect.EffectID] = struct{}{}
	}
	return validateSize(response, MaxSemanticResponseBytes, common.LimitSemanticResponse)
}

func ValidateClaimResponseFor(request ClaimRequest, response ClaimResponse) error {
	if err := ValidateClaimResponse(response); err != nil {
		return err
	}
	if len(response.Effects) > int(request.Limit) ||
		(request.MinCatalogRevision != nil && response.Receipts.CatalogRevision < *request.MinCatalogRevision) {
		return internal()
	}
	return nil
}

func ValidateCompleteRequest(ctx context.Context, request CompleteRequest) error {
	if err := validateContext(ctx); err != nil {
		return err
	}
	if !validOpaque(request.EffectID) || !validOpaque(request.ClaimToken) || request.CatalogRevision == 0 ||
		len(request.Results) == 0 || len(request.Results) > MaxOperationsPerEffect {
		return invalid(common.InvalidComponentCommandData)
	}
	if !completeRequestWithinRawBudget(request, MaxSemanticRequestBytes) {
		return &Failure{Category: common.FailureResourceExhausted, LimitKind: common.LimitSemanticRequest, Maximum: uint64(MaxSemanticRequestBytes)}
	}
	seen := map[string]struct{}{}
	terminal := false
	for _, result := range request.Results {
		if err := validateOperationResult(result, terminal); err != nil {
			return err
		}
		if _, duplicate := seen[result.OperationID]; duplicate {
			return invalid(common.InvalidComponentCommandData)
		}
		seen[result.OperationID] = struct{}{}
		if result.Outcome != OutcomeResponseReceived || result.StatusCode < 200 || result.StatusCode >= 300 {
			terminal = true
		}
	}
	return validateSize(request, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

// ValidateCompleteRequestForClaim binds completion to the exact immutable
// operation set returned by Claim. Shape validation alone deliberately cannot
// establish this relationship; callers on both sides of the wire must use this
// function before accepting or submitting a completion.
func ValidateCompleteRequestForClaim(
	ctx context.Context,
	claim ClaimResponse,
	request CompleteRequest,
) error {
	if err := ValidateCompleteRequest(ctx, request); err != nil {
		return err
	}
	if err := ValidateClaimResponse(claim); err != nil {
		return internal()
	}
	if request.CatalogRevision != claim.Receipts.CatalogRevision {
		return invalid(common.InvalidComponentCommandData)
	}
	var claimed *Effect
	for i := range claim.Effects {
		if claim.Effects[i].EffectID == request.EffectID {
			claimed = &claim.Effects[i]
			break
		}
	}
	if claimed == nil || claimed.ClaimToken != request.ClaimToken ||
		len(claimed.Operations) != len(request.Results) {
		return invalid(common.InvalidComponentCommandData)
	}
	for i := range claimed.Operations {
		if request.Results[i].OperationID != claimed.Operations[i].OperationID ||
			request.Results[i].RequestDigest != claimed.Operations[i].RequestDigest {
			return invalid(common.InvalidComponentCommandData)
		}
	}
	return nil
}

func ValidateCompleteResponse(request CompleteRequest, response CompleteResponse) error {
	if response.EffectID != request.EffectID || !validOpaque(response.EffectID) ||
		!response.State.Valid() || !validBoundReceipts(response.Receipts) ||
		response.Receipts.CatalogRevision != request.CatalogRevision {
		return internal()
	}
	return validateSize(response, MaxSemanticResponseBytes, common.LimitSemanticResponse)
}

func validateEffect(effect Effect) error {
	if !validOpaque(effect.EffectID) || !validOpaque(effect.ClaimToken) ||
		effect.LeaseExpiresAtUnixMillis <= 0 || len(effect.Operations) == 0 ||
		len(effect.Operations) > MaxOperationsPerEffect {
		return internal()
	}
	seen := map[string]struct{}{}
	for _, operation := range effect.Operations {
		if err := validateOperation(operation); err != nil {
			return err
		}
		if _, duplicate := seen[operation.OperationID]; duplicate {
			return internal()
		}
		seen[operation.OperationID] = struct{}{}
	}
	return nil
}

func validateOperation(operation Operation) error {
	if !validOpaque(operation.OperationID) || !validDigest(operation.RequestDigest) ||
		!method.MatchString(operation.Method) || !validRelativePath(operation.RelativePath) ||
		!validOpaque(operation.CredentialRef) || !validIdentity(operation.EndpointClass) ||
		len(operation.Body) > MaxRequestBodyBytes ||
		!validHeaders(operation.Headers) || !validQuery(operation.Query) ||
		!validRetryPolicy(operation.RetryPolicy) {
		return internal()
	}
	// Digest computation clones and sorts bounded collections. Keep it after
	// every per-field bound so an invalid response cannot amplify allocation
	// before the declared resource ceilings are enforced.
	if ComputeRequestDigest(operation) != operation.RequestDigest {
		return internal()
	}
	return nil
}

func validateOperationResult(result OperationResult, terminal bool) error {
	if !validOpaque(result.OperationID) || !validDigest(result.RequestDigest) ||
		!result.Outcome.Valid() || len(result.Body) > MaxResponseBodyBytes {
		return invalid(common.InvalidComponentCommandData)
	}
	if terminal && result.Outcome != OutcomeSkipped {
		return invalid(common.InvalidComponentCommandData)
	}
	switch result.Outcome {
	case OutcomeResponseReceived:
		// The worker reports one final provider response, never an informational
		// 1xx frame. Accepting 1xx here could incorrectly terminate an operation
		// chain before the final response arrives.
		if result.StatusCode < 200 || result.StatusCode > 599 || result.FailureCode != "" {
			return invalid(common.InvalidComponentCommandData)
		}
	case OutcomeNoResponse, OutcomeNotDispatched:
		if result.StatusCode != 0 || len(result.Body) != 0 || !validFailureCode(result.FailureCode) {
			return invalid(common.InvalidComponentCommandData)
		}
	case OutcomeSkipped:
		if !terminal || result.StatusCode != 0 || len(result.Body) != 0 || result.FailureCode != "" {
			return invalid(common.InvalidComponentCommandData)
		}
	}
	return nil
}

func validFailureCode(value string) bool {
	switch value {
	case FailureEndpointUnavailable,
		FailureCredentialUnavailable,
		FailureInvalidRequest,
		FailureInsecureEndpoint,
		FailureUnsupportedAuth,
		FailureNoResponse:
		return true
	default:
		return false
	}
}

// ComputeRequestDigest binds the exact frozen request without credential
// secrets. Its language-independent v1 grammar is:
//
//	ASCII domain tag; then every scalar byte string as uint64 big-endian length
//	plus UTF-8/raw bytes; every collection as uint64 count plus its elements;
//	and every integer as uint64 big-endian. Query pairs, headers/header values,
//	and retryable status codes are sorted lexicographically/numerically first.
//
// Query/header/value and retry-code order is deliberately semantic-set order,
// not caller sequence order. Validators require this same canonical ordering,
// so no accepted operation has a second ordering with the same digest. This
// also avoids implementation-specific JSON and nil-versus-empty identity.
func ComputeRequestDigest(operation Operation) string {
	query := append([]QueryParameter(nil), operation.Query...)
	sort.Slice(query, func(i, j int) bool {
		if query[i].Name == query[j].Name {
			return query[i].Value < query[j].Value
		}
		return query[i].Name < query[j].Name
	})
	headers := cloneHeaders(operation.Headers)
	sort.Slice(headers, func(i, j int) bool { return headers[i].Name < headers[j].Name })
	statuses := append([]uint32(nil), operation.RetryPolicy.RetryableStatusCodes...)
	sort.Slice(statuses, func(i, j int) bool { return statuses[i] < statuses[j] })

	digest := sha256.New()
	_, _ = digest.Write([]byte("nexuscrm.external-effect.request-digest.v1\x00"))
	writeDigestString(digest, operation.Method)
	writeDigestString(digest, operation.RelativePath)
	writeDigestString(digest, operation.CredentialRef)
	writeDigestString(digest, operation.EndpointClass)
	writeDigestUint64(digest, uint64(len(query)))
	for _, item := range query {
		writeDigestString(digest, item.Name)
		writeDigestString(digest, item.Value)
	}
	writeDigestUint64(digest, uint64(len(headers)))
	for _, item := range headers {
		writeDigestString(digest, item.Name)
		writeDigestUint64(digest, uint64(len(item.Values)))
		for _, value := range item.Values {
			writeDigestString(digest, value)
		}
	}
	writeDigestBytes(digest, operation.Body)
	writeDigestUint64(digest, uint64(operation.RetryPolicy.MaxAttempts))
	writeDigestUint64(digest, operation.RetryPolicy.BaseDelayMillis)
	writeDigestUint64(digest, uint64(operation.RetryPolicy.MultiplierMilli))
	writeDigestUint64(digest, operation.RetryPolicy.MaxDelayMillis)
	writeDigestUint64(digest, uint64(len(statuses)))
	for _, status := range statuses {
		writeDigestUint64(digest, uint64(status))
	}
	return "sha256:" + hex.EncodeToString(digest.Sum(nil))
}

func writeDigestString(destination hash.Hash, value string) {
	writeDigestBytes(destination, []byte(value))
}

func writeDigestBytes(destination hash.Hash, value []byte) {
	writeDigestUint64(destination, uint64(len(value)))
	_, _ = destination.Write(value)
}

func writeDigestUint64(destination hash.Hash, value uint64) {
	var encoded [8]byte
	binary.BigEndian.PutUint64(encoded[:], value)
	_, _ = destination.Write(encoded[:])
}

func cloneHeaders(values []Header) []Header {
	out := make([]Header, len(values))
	for i := range values {
		out[i] = Header{Name: values[i].Name, Values: append([]string(nil), values[i].Values...)}
		sort.Strings(out[i].Values)
	}
	return out
}

func validRetryPolicy(policy RetryPolicy) bool {
	if policy.MaxAttempts == 0 || policy.MaxAttempts > 10 ||
		(policy.MaxAttempts > 1 && policy.MultiplierMilli == 0) ||
		policy.MultiplierMilli > 10000 || policy.BaseDelayMillis > 300000 || policy.MaxDelayMillis > 300000 ||
		len(policy.RetryableStatusCodes) > 16 {
		return false
	}
	for index, status := range policy.RetryableStatusCodes {
		if status < 100 || status > 599 ||
			(index > 0 && policy.RetryableStatusCodes[index-1] >= status) {
			return false
		}
	}
	return true
}

func validHeaders(values []Header) bool {
	if len(values) > MaxHeadersPerMessage {
		return false
	}
	seen := map[string]struct{}{}
	for index, value := range values {
		if !header.MatchString(value.Name) || len(value.Values) == 0 || len(value.Values) > MaxHeaderValues {
			return false
		}
		if _, duplicate := seen[value.Name]; duplicate {
			return false
		}
		seen[value.Name] = struct{}{}
		if index > 0 && values[index-1].Name >= value.Name {
			return false
		}
		if _, allowed := allowedRequestHeaders[value.Name]; !allowed {
			return false
		}
		for valueIndex, item := range value.Values {
			if !validHeaderValue(item) {
				return false
			}
			if valueIndex > 0 && value.Values[valueIndex-1] > item {
				return false
			}
		}
	}
	return true
}

func validQuery(values []QueryParameter) bool {
	if len(values) > 128 {
		return false
	}
	for index, value := range values {
		if value.Name == "" || !validBoundedText(value.Name, 255) || !validBoundedText(value.Value, 4096) {
			return false
		}
		if index > 0 && (values[index-1].Name > value.Name ||
			(values[index-1].Name == value.Name && values[index-1].Value > value.Value)) {
			return false
		}
	}
	return true
}

func validRelativePath(value string) bool {
	return value != "" && len(value) <= 8192 && strings.HasPrefix(value, "/") &&
		!strings.HasPrefix(value, "//") &&
		!strings.ContainsAny(value, "\\?#") && validBoundedText(value, 8192)
}

type rawSemanticBudget struct{ remaining int }

func (b *rawSemanticBudget) take(size int) bool {
	if size < 0 || size > b.remaining {
		return false
	}
	b.remaining -= size
	return true
}

func claimResponseWithinRawBudget(response ClaimResponse, maximum int) bool {
	budget := rawSemanticBudget{remaining: maximum}
	if !budget.take(len(response.Receipts.ProtocolVersion)) ||
		!budget.take(len(response.Receipts.CapabilityVersion)) {
		return false
	}
	for _, effect := range response.Effects {
		if !budget.take(len(effect.EffectID)) || !budget.take(len(effect.ClaimToken)) {
			return false
		}
		// Leave malformed cardinality to the shape validator without traversing it.
		if len(effect.Operations) > MaxOperationsPerEffect {
			return true
		}
		for _, operation := range effect.Operations {
			if !budget.take(len(operation.OperationID)) || !budget.take(len(operation.RequestDigest)) ||
				!budget.take(len(operation.Method)) || !budget.take(len(operation.RelativePath)) ||
				!budget.take(len(operation.CredentialRef)) || !budget.take(len(operation.EndpointClass)) ||
				!budget.take(len(operation.Body)) {
				return false
			}
			if len(operation.Headers) > MaxHeadersPerMessage || len(operation.Query) > 128 {
				return true
			}
			for _, item := range operation.Headers {
				if !budget.take(len(item.Name)) {
					return false
				}
				if len(item.Values) > MaxHeaderValues {
					return true
				}
				for _, value := range item.Values {
					if !budget.take(len(value)) {
						return false
					}
				}
			}
			for _, item := range operation.Query {
				if !budget.take(len(item.Name)) || !budget.take(len(item.Value)) {
					return false
				}
			}
		}
	}
	return true
}

func completeRequestWithinRawBudget(request CompleteRequest, maximum int) bool {
	budget := rawSemanticBudget{remaining: maximum}
	if !budget.take(len(request.EffectID)) || !budget.take(len(request.ClaimToken)) {
		return false
	}
	for _, result := range request.Results {
		if !budget.take(len(result.OperationID)) || !budget.take(len(result.RequestDigest)) ||
			!budget.take(len(result.Body)) || !budget.take(len(result.FailureCode)) {
			return false
		}
	}
	return true
}

func validHeaderValue(value string) bool {
	return validBoundedText(value, 8192) && !strings.ContainsAny(value, "\r\n")
}

func validBoundedText(value string, maximum int) bool {
	if len(value) > maximum || !utf8.ValidString(value) {
		return false
	}
	for _, r := range value {
		if unicode.IsControl(r) {
			return false
		}
	}
	return true
}

func validIdentity(value string) bool {
	return len(value) <= MaxIdentifierBytes && identity.MatchString(value)
}

func validOpaque(value string) bool {
	return value != "" && len(value) <= MaxIdentifierBytes && strings.TrimSpace(value) == value && validBoundedText(value, MaxIdentifierBytes)
}

func validDigest(value string) bool {
	if !strings.HasPrefix(value, "sha256:") || len(value) != 71 {
		return false
	}
	_, err := hex.DecodeString(strings.TrimPrefix(value, "sha256:"))
	return err == nil && strings.ToLower(value) == value
}

func validateContext(ctx context.Context) error {
	if ctx == nil {
		return invalid(common.InvalidComponentDeadline)
	}
	if err := ctx.Err(); err != nil {
		if err == context.DeadlineExceeded {
			return &Failure{Category: common.FailureDeadlineExceeded}
		}
		return &Failure{Category: common.FailureCanceled}
	}
	if _, ok := ctx.Deadline(); !ok {
		return invalid(common.InvalidComponentDeadline)
	}
	return nil
}

func validateSize(value any, maximum int, kind common.LimitKind) error {
	encoded, err := json.Marshal(value)
	if err != nil {
		return internal()
	}
	if len(encoded) > maximum {
		return &Failure{Category: common.FailureResourceExhausted, LimitKind: kind, Maximum: uint64(maximum)}
	}
	return nil
}

func validBoundReceipts(receipts Receipts) bool {
	return common.ValidReceipts(receipts, CapabilityName) &&
		receipts.CatalogRevision > 0 && receipts.SchemaGeneration > 0 &&
		receipts.EngineGeneration > 0 && receipts.SecurityEpoch > 0
}

func invalid(component common.InvalidComponent) *Failure {
	return &Failure{Category: common.FailureInvalidRequest, InvalidComponent: component}
}
func internal() *Failure { return &Failure{Category: common.FailureInternal} }
