package recorddefaultsv1

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

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

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

func SemanticResponseSize(response Response) (int, error) {
	data, err := json.Marshal(response)
	return len(data), 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 !common.ValidObjectIdentifier(request.ObjectID) {
		if len(request.ObjectID) > MaxObjectIdentifierBytes {
			return exhausted(LimitObjectID, MaxObjectIdentifierBytes)
		}
		return invalid(InvalidComponentObjectID)
	}
	if request.MinCatalogRevision != nil && *request.MinCatalogRevision == 0 {
		return invalid(InvalidComponentOperation)
	}
	if (request.ParentFieldID == nil) != (request.ParentRecordID == nil) {
		if request.ParentFieldID == nil {
			return invalid(InvalidComponentRelationshipField)
		}
		return invalid(InvalidComponentRecordID)
	}
	if request.ParentFieldID != nil {
		if !common.ValidFieldIdentifier(*request.ParentFieldID) {
			if len(*request.ParentFieldID) > MaxFieldIdentifierBytes {
				return exhausted(LimitFieldID, MaxFieldIdentifierBytes)
			}
			return invalid(InvalidComponentRelationshipField)
		}
		if len(*request.ParentRecordID) > MaxRecordIdentifierBytes {
			return exhausted(LimitRecordID, MaxRecordIdentifierBytes)
		}
		if strings.TrimSpace(*request.ParentRecordID) == "" ||
			!utf8.ValidString(*request.ParentRecordID) {
			return invalid(InvalidComponentRecordID)
		}
	}
	size, err := SemanticRequestSize(request)
	if err != nil {
		return invalid(InvalidComponentResponse)
	}
	if size > MaxSemanticRequestBytes {
		return exhausted(LimitSemanticRequest, MaxSemanticRequestBytes)
	}
	return nil
}

// ValidateResponse validates both the response envelope and its freshness
// against the exact request. A Runtime response is untrusted input even when
// transport authentication succeeded.
func ValidateResponse(request Request, response Response) error {
	if len(response.Fields) > MaxDefaultFields {
		return exhausted(LimitFieldCount, MaxDefaultFields)
	}
	// Bound every response-owned string before receipt checks, UTF-8 scans,
	// sorting, or JSON serialization. The exact JSON size check below still
	// accounts for structure and escaping, but can now allocate only from an
	// already bounded semantic envelope.
	if !rawResponseStringsWithinBudget(response, MaxSemanticResponseBytes) {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	if !validResponseRequest(request) {
		return internal()
	}
	if !common.ValidReceipts(response.Receipts, CapabilityName) {
		return incompatible()
	}
	if request.MinCatalogRevision != nil &&
		response.Receipts.CatalogRevision < *request.MinCatalogRevision {
		return internal()
	}
	if !sort.SliceIsSorted(response.Fields, func(i, j int) bool {
		return response.Fields[i].Name < response.Fields[j].Name
	}) {
		return internal()
	}
	previous := ""
	for _, field := range response.Fields {
		if !common.ValidFieldIdentifier(field.Name) || field.Name == previous ||
			!field.Kind.Valid() || !field.Provenance.Valid() {
			return internal()
		}
		previous = field.Name
		if err := validateField(field); err != nil {
			return err
		}
	}
	size, err := SemanticResponseSize(response)
	if err != nil {
		return internal()
	}
	if size > MaxSemanticResponseBytes {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	return nil
}

func rawResponseStringsWithinBudget(response Response, budget int) bool {
	if !stringsWithinBudget(&budget,
		response.Receipts.ProtocolVersion,
		response.Receipts.CapabilityVersion,
	) {
		return false
	}
	for _, field := range response.Fields {
		if !stringsWithinBudget(&budget, field.Name, string(field.Kind), string(field.Provenance)) {
			return false
		}
		if field.Reference != nil && !stringsWithinBudget(&budget, *field.Reference) {
			return false
		}
		if field.DisplayValue != nil && !stringsWithinBudget(&budget, *field.DisplayValue) {
			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 validResponseRequest(request Request) bool {
	if !common.ValidObjectIdentifier(request.ObjectID) ||
		(request.ParentFieldID == nil) != (request.ParentRecordID == nil) ||
		(request.MinCatalogRevision != nil && *request.MinCatalogRevision == 0) {
		return false
	}
	if request.ParentFieldID == nil {
		return true
	}
	return common.ValidFieldIdentifier(*request.ParentFieldID) &&
		len(*request.ParentRecordID) <= MaxRecordIdentifierBytes &&
		strings.TrimSpace(*request.ParentRecordID) != "" &&
		utf8.ValidString(*request.ParentRecordID)
}

func validateField(field Field) error {
	if field.DisplayValue != nil && !utf8.ValidString(*field.DisplayValue) {
		return internal()
	}
	switch field.Provenance {
	case ProvenanceSiblingMaxPlusOne:
		if field.Kind != ValueInteger || field.Integer == nil ||
			field.Reference != nil || field.DisplayValue != nil {
			return internal()
		}
	case ProvenanceCurrentPrincipal:
		if field.Kind != ValueReference || field.Integer != nil ||
			field.Reference == nil || strings.TrimSpace(*field.Reference) == "" ||
			!utf8.ValidString(*field.Reference) || len(*field.Reference) > MaxRecordIdentifierBytes {
			return internal()
		}
	default:
		return internal()
	}
	return nil
}

func contextFailure(ctx context.Context) error {
	if ctx == nil {
		return invalid(InvalidComponentDeadline)
	}
	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} }
