package metadataadminreadv1

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

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

func ValidateObjectAdminIndexRequest(ctx context.Context, request ObjectAdminIndexRequest) error {
	return validateRequest(ctx, request.MinCatalogRevision)
}

func ValidateLayoutAdminWorkspaceRequest(ctx context.Context, request LayoutAdminWorkspaceRequest) error {
	if err := validateRequest(ctx, request.MinCatalogRevision); err != nil {
		return err
	}
	if !common.ValidObjectIdentifier(request.ObjectID) {
		return invalid(InvalidComponentOperation)
	}
	return nil
}

func ValidateStoredLayoutRequest(ctx context.Context, request StoredLayoutRequest) error {
	if err := validateRequest(ctx, request.MinCatalogRevision); err != nil {
		return err
	}
	if !validOpaque(request.LayoutID, MaxOpaqueIdentifierBytes) {
		return invalid(InvalidComponentOperation)
	}
	return nil
}

func ValidateValidationRuleAdminIndexRequest(ctx context.Context, request ValidationRuleAdminIndexRequest) error {
	if err := validateRequest(ctx, request.MinCatalogRevision); err != nil {
		return err
	}
	if !common.ValidObjectIdentifier(request.ObjectID) {
		return invalid(InvalidComponentOperation)
	}
	return nil
}

func ValidateObjectAdminIndexResponse(request ObjectAdminIndexRequest, response ObjectAdminIndexResponse) error {
	if err := validateReceipts(request.MinCatalogRevision, response.Receipts); err != nil {
		return err
	}
	if len(response.Objects) > MaxObjects {
		return exhausted(LimitRecordCount, MaxObjects)
	}
	if !objectAdminIndexWithinRawBudget(response) {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	previous := ""
	for index, object := range response.Objects {
		if !common.ValidObjectIdentifier(object.ObjectID) ||
			!validOptionalText(object.Description, MaxDescriptionBytes) ||
			!validText(object.DeleteMode, MaxNameBytes) ||
			(index > 0 && object.ObjectID <= previous) {
			return internal()
		}
		previous = object.ObjectID
	}
	return validateResponseSize(response)
}

func ValidateLayoutAdminWorkspaceResponse(request LayoutAdminWorkspaceRequest, response LayoutAdminWorkspaceResponse) error {
	if err := validateReceipts(request.MinCatalogRevision, response.Receipts); err != nil {
		return err
	}
	if !common.ValidObjectIdentifier(request.ObjectID) || response.ObjectID != request.ObjectID {
		return internal()
	}
	if len(response.Layouts) > MaxLayoutsPerObject {
		return exhausted(LimitRecordCount, MaxLayoutsPerObject)
	}
	if len(response.Assignments) > MaxAssignmentsPerObject {
		return exhausted(LimitRecordCount, MaxAssignmentsPerObject)
	}
	if !layoutAdminWorkspaceWithinRawBudget(response) {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	layoutIDs := make(map[string]struct{}, len(response.Layouts))
	previous := ""
	for index, layout := range response.Layouts {
		if !validOpaque(layout.LayoutID, MaxOpaqueIdentifierBytes) || layout.ObjectID != request.ObjectID ||
			!validRequiredText(layout.Name, MaxNameBytes) || !validText(layout.Type, MaxNameBytes) ||
			!validTimestamp(layout.CreatedAt) || !validTimestamp(layout.LastModifiedAt) ||
			(index > 0 && layout.LayoutID <= previous) {
			return internal()
		}
		layoutIDs[layout.LayoutID] = struct{}{}
		previous = layout.LayoutID
	}
	previous = ""
	for index, assignment := range response.Assignments {
		if !validOpaque(assignment.AssignmentID, MaxOpaqueIdentifierBytes) ||
			assignment.ObjectID != request.ObjectID ||
			!validOpaque(assignment.LayoutID, MaxOpaqueIdentifierBytes) ||
			!validOptionalOpaque(assignment.CreatedByID) ||
			!validTimestamp(assignment.CreatedAt) || !validTimestamp(assignment.LastModifiedAt) ||
			len(assignment.Dimensions) > MaxAssignmentDimensions ||
			assignment.Specificity != uint32(len(assignment.Dimensions)) ||
			(index > 0 && assignment.AssignmentID <= previous) {
			return internal()
		}
		previousDimension := ""
		for dimensionIndex, dimension := range assignment.Dimensions {
			if !validOpaque(dimension.Key, MaxNameBytes) ||
				!validOpaque(dimension.Value, MaxOpaqueIdentifierBytes) ||
				(dimensionIndex > 0 && dimension.Key <= previousDimension) {
				return internal()
			}
			previousDimension = dimension.Key
		}
		if _, found := layoutIDs[assignment.LayoutID]; !found {
			return internal()
		}
		previous = assignment.AssignmentID
	}
	return validateResponseSize(response)
}

func ValidateStoredLayoutResponse(request StoredLayoutRequest, response StoredLayoutResponse) error {
	if err := validateReceipts(request.MinCatalogRevision, response.Receipts); err != nil {
		return err
	}
	if !validOpaque(request.LayoutID, MaxOpaqueIdentifierBytes) ||
		!validOpaque(response.LayoutID, MaxOpaqueIdentifierBytes) ||
		response.LayoutID != request.LayoutID ||
		!common.ValidObjectIdentifier(response.ObjectID) ||
		len(response.StoredDocument) == 0 || !utf8.Valid(response.StoredDocument) {
		return internal()
	}
	if len(response.StoredDocument) > MaxStoredDocumentBytes {
		return exhausted(LimitSemanticResponse, MaxStoredDocumentBytes)
	}
	if !storedLayoutWithinRawBudget(response) {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	return validateResponseSize(response)
}

func ValidateValidationRuleAdminIndexResponse(
	request ValidationRuleAdminIndexRequest,
	response ValidationRuleAdminIndexResponse,
) error {
	if err := validateReceipts(request.MinCatalogRevision, response.Receipts); err != nil {
		return err
	}
	if !common.ValidObjectIdentifier(request.ObjectID) || !common.ValidObjectIdentifier(response.ObjectID) ||
		response.ObjectID != request.ObjectID {
		return internal()
	}
	if len(response.Rules) > MaxValidationRulesPerObject {
		return exhausted(LimitRecordCount, MaxValidationRulesPerObject)
	}
	remainingProjectionEntries := MaxValidationRuleProjectionEntries
	if len(response.Rules) > remainingProjectionEntries {
		return exhausted(LimitRecordCount, MaxValidationRuleProjectionEntries)
	}
	remainingProjectionEntries -= len(response.Rules)
	for _, rule := range response.Rules {
		if len(rule.RecordSetBindings) > MaxRecordSetBindingsPerRule {
			return exhausted(LimitRecordCount, MaxRecordSetBindingsPerRule)
		}
		if len(rule.RecordSetBindings) > remainingProjectionEntries {
			return exhausted(LimitRecordCount, MaxValidationRuleProjectionEntries)
		}
		remainingProjectionEntries -= len(rule.RecordSetBindings)
		for _, binding := range rule.RecordSetBindings {
			if len(binding.Filters) > MaxRecordSetFiltersPerBinding {
				return exhausted(LimitRecordCount, MaxRecordSetFiltersPerBinding)
			}
			if len(binding.Filters) > remainingProjectionEntries {
				return exhausted(LimitRecordCount, MaxValidationRuleProjectionEntries)
			}
			remainingProjectionEntries -= len(binding.Filters)
		}
	}
	if !validationRuleAdminIndexWithinRawBudget(response) {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	previous := ""
	for index, rule := range response.Rules {
		if !validOpaque(rule.RuleID, MaxOpaqueIdentifierBytes) || !common.ValidObjectIdentifier(rule.ObjectID) ||
			rule.ObjectID != request.ObjectID ||
			!validRequiredText(rule.Name, MaxNameBytes) ||
			!validOptionalText(rule.Description, MaxDescriptionBytes) ||
			!validRequiredText(rule.Condition, MaxDescriptionBytes) ||
			!validRequiredText(rule.ErrorMessage, MaxDescriptionBytes) ||
			!validOptionalOpaque(rule.ErrorDisplayField) ||
			!validOptionalText(rule.OwnerPack, MaxNameBytes) ||
			(index > 0 && rule.RuleID <= previous) {
			return internal()
		}
		for _, binding := range rule.RecordSetBindings {
			if !validRequiredText(binding.Name, MaxNameBytes) ||
				!validRequiredText(binding.Mode, MaxNameBytes) ||
				!common.ValidObjectIdentifier(binding.ObjectID) ||
				!validText(binding.FilterLogic, MaxDescriptionBytes) {
				return internal()
			}
			for _, filter := range binding.Filters {
				if len(filter.Value) > MaxFilterValueBytes {
					return exhausted(LimitSemanticResponse, MaxFilterValueBytes)
				}
				if !validRequiredText(filter.Field, MaxNameBytes) ||
					!validRequiredText(filter.Operator, MaxNameBytes) ||
					!validRequiredText(filter.ValueSource, MaxNameBytes) ||
					len(filter.Value) == 0 ||
					!utf8.Valid(filter.Value) ||
					!json.Valid(filter.Value) {
					return internal()
				}
			}
		}
		previous = rule.RuleID
	}
	return validateResponseSize(response)
}

func validateRequest(ctx context.Context, minimum *uint64) 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 minimum != nil && *minimum == 0 {
		return invalid(InvalidComponentOperation)
	}
	return nil
}

func validateReceipts(minimum *uint64, receipts Receipts) error {
	if minimum != nil && *minimum == 0 {
		return invalid(InvalidComponentOperation)
	}
	if !common.ValidReceipts(receipts, CapabilityName) {
		return incompatible()
	}
	if minimum != nil && receipts.CatalogRevision < *minimum {
		return internal()
	}
	return nil
}

type rawResponseBudget struct{ remaining int }

func newRawResponseBudget(receipts Receipts) rawResponseBudget {
	budget := rawResponseBudget{remaining: MaxSemanticResponseBytes}
	_ = budget.add(receipts.ProtocolVersion)
	_ = budget.add(receipts.CapabilityVersion)
	return budget
}

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

func (budget *rawResponseBudget) addOptional(value *string) bool {
	return value == nil || budget.add(*value)
}

func objectAdminIndexWithinRawBudget(response ObjectAdminIndexResponse) bool {
	budget := newRawResponseBudget(response.Receipts)
	for _, object := range response.Objects {
		if !budget.add(object.ObjectID, object.DeleteMode) || !budget.addOptional(object.Description) {
			return false
		}
	}
	return true
}

func layoutAdminWorkspaceWithinRawBudget(response LayoutAdminWorkspaceResponse) bool {
	budget := newRawResponseBudget(response.Receipts)
	if !budget.add(response.ObjectID) {
		return false
	}
	for _, layout := range response.Layouts {
		if !budget.add(
			layout.LayoutID, layout.ObjectID, layout.Name, layout.Type,
			layout.CreatedAt, layout.LastModifiedAt,
		) {
			return false
		}
	}
	for _, assignment := range response.Assignments {
		if !budget.add(
			assignment.AssignmentID, assignment.ObjectID, assignment.LayoutID,
			assignment.CreatedAt, assignment.LastModifiedAt,
		) || !budget.addOptional(assignment.CreatedByID) {
			return false
		}
		for _, dimension := range assignment.Dimensions {
			if !budget.add(dimension.Key, dimension.Value) {
				return false
			}
		}
	}
	return true
}

func storedLayoutWithinRawBudget(response StoredLayoutResponse) bool {
	budget := newRawResponseBudget(response.Receipts)
	if !budget.add(response.LayoutID, response.ObjectID) || len(response.StoredDocument) > budget.remaining {
		return false
	}
	budget.remaining -= len(response.StoredDocument)
	return true
}

func validationRuleAdminIndexWithinRawBudget(response ValidationRuleAdminIndexResponse) bool {
	budget := newRawResponseBudget(response.Receipts)
	if !budget.add(response.ObjectID) {
		return false
	}
	for _, rule := range response.Rules {
		if !budget.add(rule.RuleID, rule.ObjectID, rule.Name, rule.Condition, rule.ErrorMessage) ||
			!budget.addOptional(rule.Description) || !budget.addOptional(rule.ErrorDisplayField) ||
			!budget.addOptional(rule.OwnerPack) {
			return false
		}
		for _, binding := range rule.RecordSetBindings {
			if !budget.add(binding.Name, binding.Mode, binding.ObjectID, binding.FilterLogic) {
				return false
			}
			for _, filter := range binding.Filters {
				if !budget.add(filter.Field, filter.Operator, filter.ValueSource) ||
					len(filter.Value) > budget.remaining {
					return false
				}
				budget.remaining -= len(filter.Value)
			}
		}
	}
	return true
}

func validateResponseSize(response any) error {
	encoded, err := json.Marshal(response)
	if err != nil {
		return internal()
	}
	if len(encoded) > MaxSemanticResponseBytes {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	return nil
}

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

func validOptionalOpaque(value *string) bool {
	return value == nil || validOpaque(*value, MaxOpaqueIdentifierBytes)
}

func validText(value string, maximum int) bool {
	return len(value) <= maximum && utf8.ValidString(value)
}

func validRequiredText(value string, maximum int) bool {
	return validOpaque(value, maximum)
}

func validOptionalText(value *string, maximum int) bool {
	return value == nil || validText(*value, maximum)
}

func validTimestamp(value string) bool {
	if value == "" {
		return true
	}
	parsed, err := time.Parse(time.RFC3339Nano, value)
	return err == nil && parsed.Location() == time.UTC && strings.HasSuffix(value, "Z")
}

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 incompatible() *Failure {
	return &Failure{Category: FailureIncompatible, RequiredCapability: CapabilityName,
		SupportedMajor: ProtocolMajor, SupportedMinorMin: ProtocolMinor, SupportedMinorMax: ProtocolMinor}
}

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