package pagelayoutresolutionv1

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

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

func ValidateRequest(ctx context.Context, request Request) 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}
	}
	if _, ok := ctx.Deadline(); !ok {
		return invalid(InvalidComponentDeadline)
	}
	if len(request.RecordTypeIDs) > MaxRecordTypeSelectors {
		return exhausted(LimitRecordCount, MaxRecordTypeSelectors)
	}
	if !validRequestShape(request) {
		return invalid(InvalidComponentOperation)
	}
	return nil
}

func ValidateResponseForRequest(request Request, response Response) error {
	if !validRequestShape(request) {
		return internal()
	}
	if len(response.Selections) != expectedSelectionCount(request) ||
		len(response.Layouts) == 0 || len(response.Layouts) > expectedSelectionCount(request) {
		return internal()
	}
	if err := validateResponseCounts(response); err != nil {
		return err
	}
	if !semanticResponseWithinBudget(response, MaxSemanticResponseBytes) {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	if response.ObjectID != request.ObjectID ||
		!common.ValidReceipts(response.Receipts, CapabilityName) ||
		(request.MinCatalogRevision != nil && response.Receipts.CatalogRevision < *request.MinCatalogRevision) {
		return internal()
	}
	selectedLayouts := make(map[string]struct{}, len(response.Selections))
	for index, selection := range response.Selections {
		if !selectionMatchesRequest(request, index, selection) || !validOpaque(selection.LayoutID) {
			return internal()
		}
		selectedLayouts[selection.LayoutID] = struct{}{}
	}
	previousLayoutID := ""
	for _, layout := range response.Layouts {
		if !validOpaque(layout.LayoutID) || layout.LayoutID <= previousLayoutID ||
			layout.DocumentKind != PageLayoutDocumentKind ||
			layout.DocumentFormatVersion != PageLayoutDocumentFormatVersion ||
			len(layout.LayoutDocument) == 0 || len(layout.LayoutDocument) > MaxLayoutDocumentBytes ||
			!utf8.Valid(layout.LayoutDocument) || !validJSONObject(layout.LayoutDocument) {
			return internal()
		}
		if _, selected := selectedLayouts[layout.LayoutID]; !selected {
			return internal()
		}
		delete(selectedLayouts, layout.LayoutID)
		previousLayoutID = layout.LayoutID
		previousResolution := ""
		for _, resolution := range layout.RelatedLists {
			if !validResolution(resolution) {
				return internal()
			}
			key := resolution.DisplayObjectID + "\x00" + resolution.DisplayLookupField
			if key <= previousResolution {
				return internal()
			}
			previousResolution = key
		}
	}
	if len(selectedLayouts) != 0 {
		return internal()
	}
	return nil
}

// validateResponseCounts closes every collection before any content scan. The
// following budget and semantic passes therefore perform bounded work even for
// a malformed response received from Runtime.
func validateResponseCounts(response Response) error {
	remaining := MaxResolutionProjectionEntries
	for _, layout := range response.Layouts {
		if len(layout.LayoutDocument) > MaxLayoutDocumentBytes {
			return exhausted(LimitSemanticResponse, MaxLayoutDocumentBytes)
		}
		if len(layout.RelatedLists) > MaxRelatedListResolutions {
			return exhausted(LimitRecordCount, MaxRelatedListResolutions)
		}
		if len(layout.RelatedLists) > remaining {
			return exhausted(LimitRecordCount, MaxResolutionProjectionEntries)
		}
		remaining -= len(layout.RelatedLists)
		for _, resolution := range layout.RelatedLists {
			mappings := len(resolution.Create.PrefillMappings)
			if mappings > MaxPrefillMappingsPerResolution {
				return exhausted(LimitRecordCount, MaxPrefillMappingsPerResolution)
			}
			if mappings > remaining {
				return exhausted(LimitRecordCount, MaxResolutionProjectionEntries)
			}
			remaining -= mappings
		}
	}
	return nil
}

// semanticResponseWithinBudget counts the transport-independent semantic
// payload without allocating a serialized copy. Protobuf message size and
// framing remain wire-owned facts.
func semanticResponseWithinBudget(response Response, remaining int) bool {
	consume := func(values ...string) bool {
		for _, value := range values {
			if len(value) > remaining {
				return false
			}
			remaining -= len(value)
		}
		return true
	}
	if !consume(response.ObjectID, response.Receipts.ProtocolVersion, response.Receipts.CapabilityVersion) {
		return false
	}
	for _, selection := range response.Selections {
		if selection.RecordTypeID != nil && !consume(*selection.RecordTypeID) {
			return false
		}
		if !consume(selection.LayoutID) {
			return false
		}
	}
	for _, layout := range response.Layouts {
		if !consume(layout.LayoutID, layout.DocumentKind) || len(layout.LayoutDocument) > remaining {
			return false
		}
		remaining -= len(layout.LayoutDocument)
		for _, resolution := range layout.RelatedLists {
			if !consume(
				resolution.DisplayObjectID, resolution.DisplayLookupField,
				resolution.Query.ObjectID, resolution.Query.LookupField, resolution.Query.ParentFieldID,
				resolution.Detail.ObjectID,
				resolution.List.ObjectID, resolution.List.FilterField, resolution.List.ParentFieldID,
				resolution.Create.Kind, resolution.Create.ObjectID,
			) {
				return false
			}
			for _, mapping := range resolution.Create.PrefillMappings {
				if !consume(mapping.TargetFieldID, mapping.SourceFieldID) {
					return false
				}
			}
		}
	}
	return true
}

func validRequestShape(request Request) bool {
	if !common.ValidObjectIdentifier(request.ObjectID) ||
		len(request.RecordTypeIDs) > MaxRecordTypeSelectors ||
		(request.MinCatalogRevision != nil && *request.MinCatalogRevision == 0) {
		return false
	}
	previous := ""
	for _, recordTypeID := range request.RecordTypeIDs {
		if !validOpaque(recordTypeID) || recordTypeID <= previous {
			return false
		}
		previous = recordTypeID
	}
	return true
}

func expectedSelectionCount(request Request) int {
	if len(request.RecordTypeIDs) == 0 {
		return 1
	}
	return len(request.RecordTypeIDs)
}

func selectionMatchesRequest(request Request, index int, selection Selection) bool {
	if len(request.RecordTypeIDs) == 0 {
		return index == 0 && selection.RecordTypeID == nil
	}
	return selection.RecordTypeID != nil && *selection.RecordTypeID == request.RecordTypeIDs[index]
}

func validResolution(value RelatedListResolution) bool {
	if !common.ValidObjectIdentifier(value.DisplayObjectID) || !common.ValidFieldIdentifier(value.DisplayLookupField) ||
		!common.ValidObjectIdentifier(value.Query.ObjectID) || !common.ValidFieldIdentifier(value.Query.LookupField) ||
		!validOptionalFieldIdentifier(value.Query.ParentFieldID) || !common.ValidObjectIdentifier(value.Detail.ObjectID) ||
		!common.ValidObjectIdentifier(value.List.ObjectID) || !common.ValidFieldIdentifier(value.List.FilterField) ||
		!validOptionalFieldIdentifier(value.List.ParentFieldID) {
		return false
	}
	switch value.Create.Kind {
	case "standard":
		if !common.ValidObjectIdentifier(value.Create.ObjectID) {
			return false
		}
	case "upload", "none":
		if value.Create.ObjectID != "" {
			return false
		}
	default:
		return false
	}
	previous := ""
	for _, mapping := range value.Create.PrefillMappings {
		if !common.ValidFieldIdentifier(mapping.TargetFieldID) || !common.ValidFieldIdentifier(mapping.SourceFieldID) ||
			mapping.TargetFieldID <= previous {
			return false
		}
		previous = mapping.TargetFieldID
	}
	return true
}

func validJSONObject(value []byte) bool {
	trimmed := bytes.TrimSpace(value)
	return len(trimmed) >= 2 && trimmed[0] == '{' && trimmed[len(trimmed)-1] == '}' && json.Valid(trimmed)
}

func validOpaque(value string) bool {
	return value != "" && len(value) <= MaxOpaqueIdentifierBytes &&
		utf8.ValidString(value) && strings.TrimSpace(value) == value
}

func validOptionalFieldIdentifier(value string) bool {
	return value == "" || common.ValidFieldIdentifier(value)
}

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

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

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