package recordaccessv1

import (
	"context"
	"regexp"
	"sort"
	"strings"
	"unicode/utf8"

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

var identifierPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)

// ValidateRequest validates the transport-independent business envelope.
// Object existence, identifier canonical case, type resolution, and every
// authorization decision remain Runtime work.
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 request.ObjectID != nil {
		if !validObjectIdentifier(*request.ObjectID) {
			if len(*request.ObjectID) > MaxObjectIdentifierBytes {
				return exhausted(LimitObjectID, MaxObjectIdentifierBytes)
			}
			return invalid(InvalidComponentObjectID)
		}
	}
	if len(request.RecordIDs) > MaxRecords {
		return exhausted(LimitRecordCount, MaxRecords)
	}
	for _, recordID := range request.RecordIDs {
		if !utf8.ValidString(recordID) {
			return invalid(InvalidComponentRecordID)
		}
		if len(recordID) > MaxRecordIdentifierBytes {
			return exhausted(LimitRecordID, MaxRecordIdentifierBytes)
		}
	}
	return nil
}

// ValidateResponse validates both the response shape and its correlation to
// the submitted batch. A result for an identifier outside request would leak
// record existence/access and therefore fails closed as INTERNAL_FAILURE.
func ValidateResponse(request Request, response Response) error {
	if !common.ValidReceipts(response.Receipts, CapabilityName) {
		return incompatible()
	}
	if len(response.Results) > MaxRecords {
		return exhausted(LimitRecordCount, MaxRecords)
	}
	if !sort.SliceIsSorted(response.Results, func(i, j int) bool {
		return response.Results[i].RecordID < response.Results[j].RecordID
	}) {
		return internal()
	}
	previous := ""
	requested := make(map[string]struct{}, len(request.RecordIDs))
	for _, recordID := range request.RecordIDs {
		requested[recordID] = struct{}{}
	}
	for i, result := range response.Results {
		if strings.TrimSpace(result.RecordID) == "" || !utf8.ValidString(result.RecordID) || len(result.RecordID) > MaxRecordIdentifierBytes {
			return internal()
		}
		if _, submitted := requested[result.RecordID]; !submitted {
			return internal()
		}
		if i > 0 && result.RecordID == previous {
			return internal()
		}
		previous = result.RecordID
		if !result.Access.Read {
			return internal()
		}
		if result.Access.Locked && (result.Access.Update || result.Access.Delete) {
			return internal()
		}
	}
	return nil
}

func validObjectIdentifier(value string) bool {
	return value != "" && len(value) <= MaxObjectIdentifierBytes && identifierPattern.MatchString(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}
}
