package relatedcountsv1

import (
	"context"
	"encoding/json"
	"regexp"
	"strings"
	"unicode/utf8"

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

var identifierPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)

func SemanticRequestSize(request Request) (int, error) {
	encoded, err := json.Marshal(request)
	return len(encoded), err
}

func ValidateRequest(ctx context.Context, request Request) error {
	if err := contextFailure(ctx); err != nil {
		return err
	}
	if _, ok := ctx.Deadline(); !ok {
		return invalid(InvalidComponentDeadline)
	}
	if len(request.Counts) > MaxCounts {
		return exhausted(LimitOperationCount, MaxCounts)
	}
	// Enforce individual identifier bounds before the aggregate preflight so a
	// malformed identifier retains its more specific, stable failure detail.
	for _, count := range request.Counts {
		if len(count.ParentRecordID) > MaxRecordIdentifierBytes {
			return exhausted(LimitRecordID, MaxRecordIdentifierBytes)
		}
		if len(count.ChildObjectID) > MaxObjectIdentifierBytes {
			return exhausted(LimitObjectID, MaxObjectIdentifierBytes)
		}
		if len(count.RelationshipField) > MaxFieldIdentifierBytes {
			return exhausted(LimitFieldID, MaxFieldIdentifierBytes)
		}
	}
	// Bound caller-owned bytes before UTF-8 scanning or JSON serialization.
	// This preflight is intentionally only a lower bound: the exact semantic
	// size check below still accounts for JSON structure and escaping, but it
	// can now allocate only in proportion to an already bounded input.
	if !rawRequestStringsWithinBudget(request, MaxSemanticRequestBytes) {
		return exhausted(LimitSemanticRequest, MaxSemanticRequestBytes)
	}
	for _, count := range request.Counts {
		if strings.TrimSpace(count.ParentRecordID) == "" || !utf8.ValidString(count.ParentRecordID) {
			return invalid(InvalidComponentRecordID)
		}
		if !validIdentifier(count.ChildObjectID, MaxObjectIdentifierBytes) {
			return invalid(InvalidComponentObjectID)
		}
		if !validIdentifier(count.RelationshipField, MaxFieldIdentifierBytes) {
			return invalid(InvalidComponentRelationshipField)
		}
		if !utf8.ValidString(count.MatchValue) {
			return invalid(InvalidComponentFieldValue)
		}
	}
	size, err := SemanticRequestSize(request)
	if err != nil {
		return invalid(InvalidComponentResponse)
	}
	if size > MaxSemanticRequestBytes {
		return exhausted(LimitSemanticRequest, MaxSemanticRequestBytes)
	}
	return nil
}

func rawRequestStringsWithinBudget(request Request, budget int) bool {
	remaining := budget
	for _, count := range request.Counts {
		for _, value := range [...]string{count.ParentRecordID, count.ChildObjectID, count.RelationshipField, count.MatchValue} {
			if len(value) > remaining {
				return false
			}
			remaining -= len(value)
		}
	}
	return true
}

func ValidateResponse(request Request, response Response) error {
	if !common.ValidReceipts(response.Receipts, CapabilityName) {
		return incompatible()
	}
	if len(response.Results) != len(request.Counts) || len(response.Results) > MaxCounts {
		return internal()
	}
	for index, result := range response.Results {
		if result.Index != uint32(index) {
			return internal()
		}
		if !result.Outcome.Valid() {
			return internal()
		}
		if result.Outcome == CountOutcomeNotAvailable && result.Count != 0 {
			return internal()
		}
	}
	return nil
}

func validIdentifier(value string, maximum int) bool {
	return value != "" && len(value) <= maximum && identifierPattern.MatchString(value)
}

func contextFailure(ctx context.Context) error {
	if err := ctx.Err(); err != nil {
		if err == context.DeadlineExceeded {
			return &Failure{Category: FailureDeadlineExceeded}
		}
		return &Failure{Category: FailureCanceled}
	}
	return nil
}

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

func exhausted(kind LimitKind, maximum uint64) *Failure {
	return &Failure{Category: FailureResourceExhausted, LimitKind: kind, Maximum: maximum}
}

func incompatible() *Failure {
	return &Failure{Category: FailureIncompatible, RequiredCapability: CapabilityName, SupportedMajor: ProtocolMajor, SupportedMinorMin: ProtocolMinor, SupportedMinorMax: ProtocolMinor}
}

func internal() *Failure { return &Failure{Category: FailureInternal} }
