package recordattachmentsv1

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 ResolveRequest) (int, error) {
	encoded, err := json.Marshal(request)
	return len(encoded), err
}

func SemanticResponseSize(response ResolveResponse) (int, error) {
	encoded, err := json.Marshal(response)
	return len(encoded), err
}

func ValidateRequest(ctx context.Context, request ResolveRequest) error {
	if err := contextFailure(ctx); err != nil {
		return err
	}
	if _, ok := ctx.Deadline(); !ok {
		return invalid(InvalidComponentDeadline)
	}
	if err := validateObjectID(request.ParentObjectID); err != nil {
		return err
	}
	if err := validateRecordID(request.ParentRecordID); err != nil {
		return err
	}
	if err := validateFieldID(request.ResolverID); err != nil {
		return err
	}
	if len(request.Sources) == 0 {
		return invalid(InvalidComponentOperation)
	}
	if len(request.Sources) > MaxSources {
		return exhausted(LimitOperationCount, MaxSources)
	}
	// Preserve identifier-specific limit failures before the aggregate
	// preflight, while ensuring that untrusted source values cannot cause
	// proportional UTF-8 scanning or JSON allocation.
	for _, source := range request.Sources {
		if len(source.SourceFieldID) > MaxFieldIdentifierBytes {
			return exhausted(LimitFieldID, MaxFieldIdentifierBytes)
		}
	}
	// This is deliberately a lower bound. The exact JSON check below still
	// accounts for structure and escaping, but it can allocate only after all
	// caller-owned strings are bounded by the semantic envelope.
	if !rawRequestStringsWithinBudget(request, MaxSemanticRequestBytes) {
		return exhausted(LimitSemanticRequest, MaxSemanticRequestBytes)
	}
	for _, source := range request.Sources {
		if err := validateFieldID(source.SourceFieldID); err != nil {
			return err
		}
		if source.SourceValue == "" || !utf8.ValidString(source.SourceValue) {
			return invalid(InvalidComponentFieldValue)
		}
	}
	if size, err := SemanticRequestSize(request); err != nil {
		return invalid(InvalidComponentResponse)
	} else if size > MaxSemanticRequestBytes {
		return exhausted(LimitSemanticRequest, MaxSemanticRequestBytes)
	}
	return nil
}

func ValidateResponse(request ResolveRequest, response ResolveResponse) error {
	if len(response.Results) != len(request.Sources) || len(response.Results) > MaxSources {
		return internal()
	}
	// Responses arrive from a remote Runtime and are therefore untrusted too.
	// Preflight every string before receipt validation or JSON sizing so a
	// malformed FileName (or receipt) cannot trigger proportional allocation.
	if !rawResponseStringsWithinBudget(response, MaxSemanticResponseBytes) {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	if !common.ValidReceipts(response.Receipts, CapabilityName) {
		return incompatible()
	}
	for index, result := range response.Results {
		if result.Index != uint32(index) || !result.Outcome.Valid() {
			return internal()
		}
		switch result.Outcome {
		case SourceResolved:
			if !validRecordIDText(result.FileID) || result.FileName == "" || !utf8.ValidString(result.FileName) {
				return internal()
			}
		default:
			if result.FileID != "" || result.FileName != "" {
				return internal()
			}
		}
	}
	if size, err := SemanticResponseSize(response); err != nil {
		return internal()
	} else if size > MaxSemanticResponseBytes {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	return nil
}

func rawRequestStringsWithinBudget(request ResolveRequest, budget int) bool {
	if !stringsWithinBudget(&budget, request.ParentObjectID, request.ParentRecordID, request.ResolverID) {
		return false
	}
	for _, source := range request.Sources {
		if !stringsWithinBudget(&budget, source.SourceFieldID, source.SourceValue) {
			return false
		}
	}
	return true
}

func rawResponseStringsWithinBudget(response ResolveResponse, budget int) bool {
	if !stringsWithinBudget(&budget, response.Receipts.ProtocolVersion, response.Receipts.CapabilityVersion) {
		return false
	}
	for _, result := range response.Results {
		if !stringsWithinBudget(&budget, string(result.Outcome), result.FileID, result.FileName) {
			return false
		}
	}
	return true
}

func stringsWithinBudget(remaining *int, values ...string) bool {
	for _, value := range values {
		if len(value) > *remaining {
			return false
		}
		*remaining -= len(value)
	}
	return true
}

func validateObjectID(value string) error {
	if len(value) > MaxObjectIdentifierBytes {
		return exhausted(LimitObjectID, MaxObjectIdentifierBytes)
	}
	if !validIdentifier(value, MaxObjectIdentifierBytes) {
		return invalid(InvalidComponentObjectID)
	}
	return nil
}

func validateFieldID(value string) error {
	if len(value) > MaxFieldIdentifierBytes {
		return exhausted(LimitFieldID, MaxFieldIdentifierBytes)
	}
	if !validIdentifier(value, MaxFieldIdentifierBytes) {
		return invalid(InvalidComponentField)
	}
	return nil
}

func validateRecordID(value string) error {
	if len(value) > MaxRecordIdentifierBytes {
		return exhausted(LimitRecordID, MaxRecordIdentifierBytes)
	}
	if !validRecordIDText(value) {
		return invalid(InvalidComponentRecordID)
	}
	return nil
}

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

func validRecordIDText(value string) bool {
	return value != "" && len(value) <= MaxRecordIdentifierBytes && utf8.ValidString(value) && strings.TrimSpace(value) == 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} }
