package searchv1

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

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

func ValidateRequest(ctx context.Context, request Request) error {
	if err := validateContext(ctx); err != nil {
		return err
	}
	return validateRequestShape(request)
}

func validateRequestShape(request Request) error {
	if request.Limit == 0 || request.Limit > MaxRecords ||
		strings.TrimSpace(request.Term) == "" || !utf8.ValidString(request.Term) ||
		len(request.Term) > MaxTermBytes {
		return invalid(common.InvalidComponentQueryPlan)
	}
	planned := len(request.ObjectPlans) > 0
	legacy := len(request.ObjectIDs) > 0 || len(request.SearchFields) > 0 ||
		len(request.SelectedFields) > 0
	if planned == legacy {
		return invalid(common.InvalidComponentQueryPlan)
	}
	if planned {
		if len(request.ObjectPlans) > MaxObjects {
			return invalid(common.InvalidComponentQueryPlan)
		}
		seenObjects := make(map[string]struct{}, len(request.ObjectPlans))
		for _, plan := range request.ObjectPlans {
			if !common.ValidObjectIdentifier(plan.ObjectID) {
				return invalid(common.InvalidComponentObjectID)
			}
			objectKey := strings.ToLower(plan.ObjectID)
			if _, duplicate := seenObjects[objectKey]; duplicate {
				return invalid(common.InvalidComponentQuerySelection)
			}
			seenObjects[objectKey] = struct{}{}
			if len(plan.SearchFields) == 0 || len(plan.SearchFields) > MaxSelectedFields ||
				len(plan.SelectedFields) == 0 || len(plan.SelectedFields) > MaxSelectedFields ||
				plan.Limit == 0 || plan.Limit > MaxRecords ||
				!uniqueFolded(plan.SearchFields) || !uniqueFolded(plan.SelectedFields) {
				return invalid(common.InvalidComponentQuerySelection)
			}
			for _, fieldID := range append(append([]string(nil), plan.SearchFields...), plan.SelectedFields...) {
				if !common.ValidFieldIdentifier(fieldID) {
					return invalid(common.InvalidComponentQuerySelection)
				}
			}
		}
	} else {
		if len(request.ObjectIDs) == 0 || len(request.ObjectIDs) > MaxObjects ||
			len(request.SearchFields) == 0 || len(request.SearchFields) > MaxSelectedFields ||
			len(request.SelectedFields) == 0 || len(request.SelectedFields) > MaxSelectedFields {
			return invalid(common.InvalidComponentQueryPlan)
		}
		for _, objectID := range request.ObjectIDs {
			if !common.ValidObjectIdentifier(objectID) {
				return invalid(common.InvalidComponentObjectID)
			}
		}
		for _, fieldID := range append(append([]string(nil), request.SearchFields...), request.SelectedFields...) {
			if !common.ValidFieldIdentifier(fieldID) {
				return invalid(common.InvalidComponentQuerySelection)
			}
		}
		if !uniqueFolded(request.ObjectIDs) || !uniqueFolded(request.SearchFields) ||
			!uniqueFolded(request.SelectedFields) {
			return invalid(common.InvalidComponentQuerySelection)
		}
	}
	return validateSize(request, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func ValidateResponse(request Request, response Response) error {
	if validateRequestShape(request) != nil ||
		!common.ValidReceipts(response.Receipts, CapabilityName) ||
		len(response.Records) > int(request.Limit) || len(response.Records) > MaxRecords {
		return internal()
	}
	if !responseWithinBudget(response) {
		return exhaustedResponse()
	}
	plans := ObjectPlansForExecution(request)
	requestObjects := make(map[string]map[string]struct{}, len(plans))
	objectLimits := make(map[string]uint32, len(plans))
	for _, plan := range plans {
		objectKey := strings.ToLower(plan.ObjectID)
		requestObjects[objectKey] = foldedSet(plan.SelectedFields)
		objectLimits[objectKey] = plan.Limit
	}
	seen := make(map[string]struct{}, len(response.Records))
	objectCounts := make(map[string]uint32, len(plans))
	for _, record := range response.Records {
		if !common.ValidObjectIdentifier(record.ObjectID) ||
			len(record.RecordID) == 0 || len(record.RecordID) > common.MaxRecordIdentifierBytes ||
			!utf8.ValidString(record.RecordID) || strings.TrimSpace(record.RecordID) != record.RecordID ||
			(record.Score != nil && (math.IsNaN(*record.Score) || math.IsInf(*record.Score, 0))) {
			return internal()
		}
		objectKey := strings.ToLower(record.ObjectID)
		selectedFields, ok := requestObjects[objectKey]
		if !ok {
			return internal()
		}
		objectCounts[objectKey]++
		if objectCounts[objectKey] > objectLimits[objectKey] {
			return internal()
		}
		key := objectKey + "\x00" + record.RecordID
		if _, duplicate := seen[key]; duplicate {
			return internal()
		}
		seen[key] = struct{}{}
		for _, field := range record.Fields {
			if _, ok := selectedFields[strings.ToLower(field.Name)]; !ok {
				return internal()
			}
		}
		receipts := response.Receipts
		receipts.CapabilityVersion = records.CapabilityName
		if err := records.ValidateResponse(records.Response{
			RecordID: record.RecordID, Fields: record.Fields, Receipts: receipts,
		}); err != nil {
			return internal()
		}
	}
	return validateSize(response, MaxSemanticResponseBytes, common.LimitSemanticResponse)
}

type semanticBudget struct {
	remaining int
	exceeded  bool
}

func (budget *semanticBudget) add(size int) {
	if budget.exceeded || size < 0 || size > budget.remaining {
		budget.exceeded = true
		budget.remaining = 0
		return
	}
	budget.remaining -= size
}

func (budget *semanticBudget) addString(value string) {
	budget.add(8)
	budget.add(len(value))
}

func responseWithinBudget(response Response) bool {
	budget := semanticBudget{remaining: MaxSemanticResponseBytes}
	budget.add(8)
	for _, record := range response.Records {
		budget.addString(record.ObjectID)
		budget.addString(record.RecordID)
		budget.add(1)
		if record.Score != nil {
			budget.add(8)
		}
		budget.add(8)
		for _, field := range record.Fields {
			budget.addString(field.Name)
			budget.addString(string(field.Kind))
			budget.add(1)
			if field.String != nil {
				budget.addString(*field.String)
			}
			if field.Boolean != nil {
				budget.add(1)
			}
			if field.Integer != nil {
				budget.add(8)
			}
			if field.Decimal != nil {
				budget.addString(field.Decimal.Coefficient)
				budget.add(4)
			}
			if field.Date != nil {
				budget.addString(*field.Date)
			}
			if field.DateTime != nil {
				budget.addString(*field.DateTime)
			}
			if field.Reference != nil {
				budget.addString(*field.Reference)
			}
			if budget.exceeded {
				return false
			}
		}
		if budget.exceeded {
			return false
		}
	}
	budget.addString(response.Receipts.ProtocolVersion)
	budget.add(8 * 4)
	budget.addString(response.Receipts.CapabilityVersion)
	return !budget.exceeded
}

func validateContext(ctx context.Context) error {
	if ctx == nil {
		return invalid(common.InvalidComponentDeadline)
	}
	if err := ctx.Err(); err != nil {
		if err == context.DeadlineExceeded {
			return &Failure{Category: common.FailureDeadlineExceeded}
		}
		return &Failure{Category: common.FailureCanceled}
	}
	if _, ok := ctx.Deadline(); !ok {
		return invalid(common.InvalidComponentDeadline)
	}
	return nil
}

func uniqueFolded(values []string) bool { return len(foldedSet(values)) == len(values) }

func foldedSet(values []string) map[string]struct{} {
	result := make(map[string]struct{}, len(values))
	for _, value := range values {
		result[strings.ToLower(value)] = struct{}{}
	}
	return result
}

func validateSize(value any, maximum int, kind common.LimitKind) error {
	encoded, err := json.Marshal(value)
	if err != nil {
		return internal()
	}
	if len(encoded) > maximum {
		return &Failure{Category: common.FailureResourceExhausted, LimitKind: kind, Maximum: uint64(maximum)}
	}
	return nil
}

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

func exhaustedResponse() *Failure {
	return &Failure{
		Category:  common.FailureResourceExhausted,
		LimitKind: common.LimitSemanticResponse,
		Maximum:   MaxSemanticResponseBytes,
	}
}

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