package outboundintentv1

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"strings"
	"unicode"
	"unicode/utf8"

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

func ValidateListRequest(ctx context.Context, request ListRequest) error {
	if err := contextError(ctx); err != nil {
		return err
	}
	if request.Limit == 0 || request.Limit > MaxIntentsPerList {
		return invalid()
	}
	if request.MaxOperationSetVersion > CurrentOperationSetVersion {
		return invalid()
	}
	if request.MinCatalogRevision != nil && *request.MinCatalogRevision == 0 {
		return invalid()
	}
	return validateSize(request, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func ValidateGenerationRequest(ctx context.Context, request GenerationRequest) error {
	if err := contextError(ctx); err != nil {
		return err
	}
	return validateSize(request, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func ValidateGenerationResponse(
	request GenerationRequest,
	response GenerationResponse,
) error {
	if response.Generation < request.AfterGeneration {
		return internal()
	}
	if err := validateReceipts(response.Receipts); err != nil {
		return internal()
	}
	return validateSize(response, MaxSemanticResponseBytes, common.LimitSemanticResponse)
}

func ValidateListResponseFor(request ListRequest, response ListResponse) error {
	if len(response.Intents) > int(request.Limit) {
		return internal()
	}
	if len(response.Intents) > MaxIntentsPerList {
		return internal()
	}
	if !rawPayloadsWithin(response.Intents, MaxSemanticResponseBytes) {
		return exhausted(common.LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	if err := validateReceipts(response.Receipts); err != nil {
		return internal()
	}
	if request.MinCatalogRevision != nil && response.Receipts.CatalogRevision < *request.MinCatalogRevision {
		return internal()
	}
	seen := make(map[string]struct{}, len(response.Intents))
	for _, intent := range response.Intents {
		if err := validateIntent(intent); err != nil {
			return internal()
		}
		if operationSetVersion(intent) > effectiveMaxOperationSetVersion(request) {
			return internal()
		}
		if _, ok := seen[intent.IntentID]; ok {
			return internal()
		}
		seen[intent.IntentID] = struct{}{}
		if request.MinCatalogRevision != nil && intent.CatalogRevision < *request.MinCatalogRevision {
			return internal()
		}
		if response.Receipts.CatalogRevision != intent.CatalogRevision {
			return internal()
		}
	}
	return validateSize(response, MaxSemanticResponseBytes, common.LimitSemanticResponse)
}

func effectiveMaxOperationSetVersion(request ListRequest) uint32 {
	if request.MaxOperationSetVersion == 0 {
		return OperationSetVersionV1
	}
	return request.MaxOperationSetVersion
}

func operationSetVersion(intent Intent) uint32 {
	if intent.Operation == OperationSendMail {
		if intent.PayloadFormatVersion == MailPayloadFormatV3 {
			return OperationSetVersionV3
		}
		return OperationSetVersionV2
	}
	return OperationSetVersionV1
}

func ValidateIntent(intent Intent) error {
	if err := validateIntent(intent); err != nil {
		return invalid()
	}
	return nil
}

func validateIntent(intent Intent) error {
	for label, value := range map[string]string{
		"intent ID": intent.IntentID, "idempotency key": intent.IdempotencyKey,
		"source object": intent.SourceObject, "source record ID": intent.SourceRecordID,
		"connector owner key": intent.ConnectorOwnerKey, "action slot ID": intent.ActionSlotID,
		"Flow developer name": intent.FlowDeveloperName, "Flow action name": intent.FlowActionName,
		"destination slot ID": intent.DestinationSlotID,
	} {
		if err := identifier(label, value); err != nil {
			return err
		}
	}
	if intent.PayloadFormatVersion == 0 {
		return fmt.Errorf("payload format version must be positive")
	}
	if intent.EffectRootID != "" || intent.EffectGenerationID != "" {
		if err := identifier("effect root ID", intent.EffectRootID); err != nil {
			return err
		}
		if err := identifier("effect generation ID", intent.EffectGenerationID); err != nil {
			return err
		}
		if !validLogicalEffectKey(intent.IntentID, logicalEffectIntentIDPrefix) ||
			!validLogicalEffectKey(intent.IdempotencyKey, logicalEffectIdempotencyPrefix) {
			return fmt.Errorf("logical-effect keys are not canonical")
		}
	}
	if !digest(intent.PayloadDigest) {
		return fmt.Errorf("payload digest must be canonical sha256")
	}
	if intent.CatalogRevision == 0 {
		return fmt.Errorf("Catalog revision must be positive")
	}
	if !intent.Operation.Valid() {
		return fmt.Errorf("operation is invalid")
	}
	if len(intent.Bindings) == 0 || len(intent.Bindings) > MaxBindingsPerIntent {
		return fmt.Errorf("binding reference count is invalid")
	}
	seen := map[string]struct{}{}
	for i, binding := range intent.Bindings {
		if err := ValidateBindingReference(binding); err != nil {
			return fmt.Errorf("bindings[%d]: %w", i, err)
		}
		key := strings.Join([]string{string(binding.Kind), binding.OwnerPack, binding.PackInstallationID, binding.BusinessKey}, "\x1f")
		if _, ok := seen[key]; ok {
			return fmt.Errorf("bindings[%d]: duplicate binding", i)
		}
		seen[key] = struct{}{}
	}
	if len(intent.CanonicalPayload) == 0 || len(intent.CanonicalPayload) > MaxPayloadBytes {
		return fmt.Errorf("canonical payload size is invalid")
	}
	sum := sha256.Sum256(intent.CanonicalPayload)
	if intent.PayloadDigest != "sha256:"+hex.EncodeToString(sum[:]) {
		return fmt.Errorf("payload digest does not match canonical payload")
	}
	return nil
}

func ValidateBindingReference(binding BindingReference) error {
	if err := validateBindingReference(binding); err != nil {
		return invalid()
	}
	return nil
}

func validateBindingReference(binding BindingReference) error {
	for label, value := range map[string]string{
		"owner pack":           binding.OwnerPack,
		"pack installation ID": binding.PackInstallationID, "business key": binding.BusinessKey,
	} {
		if err := identifier(label, value); err != nil {
			return err
		}
	}
	if !binding.Kind.Valid() || binding.DefinitionVersion == 0 {
		return fmt.Errorf("binding kind or definition version is invalid")
	}
	if !packPayloadDigest(binding.PackPayloadDigest) {
		return fmt.Errorf("pack payload digest must be canonical sha256")
	}
	return nil
}

func ValidateAcknowledgeRequest(ctx context.Context, request AcknowledgeRequest) error {
	if err := contextError(ctx); err != nil {
		return err
	}
	if err := validateAcknowledgeRequestData(request); err != nil {
		return invalid()
	}
	return validateSize(request, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func validateAcknowledgeRequestData(request AcknowledgeRequest) error {
	if err := identifier("intent ID", request.IntentID); err != nil {
		return err
	}
	if !digest(request.PayloadDigest) {
		return fmt.Errorf("payload digest must be canonical sha256")
	}
	if request.CatalogRevision == 0 {
		return fmt.Errorf("Catalog revision must be positive")
	}
	if !request.Outcome.Valid() {
		return fmt.Errorf("admission outcome is invalid")
	}
	switch request.Outcome {
	case AdmissionAccepted:
		if err := identifier("Integration command ID", request.IntegrationCommandID); err != nil {
			return err
		}
		if request.RejectionCode != "" {
			return fmt.Errorf("accepted admission cannot include a rejection code")
		}
	case AdmissionRejected:
		if request.IntegrationCommandID != "" {
			return fmt.Errorf("rejected admission cannot include an Integration command ID")
		}
		if err := identifier("rejection code", request.RejectionCode); err != nil {
			return err
		}
	}
	return nil
}

func ValidateAcknowledgeResponse(request AcknowledgeRequest, response AcknowledgeResponse) error {
	if response.IntentID != request.IntentID || response.Outcome != request.Outcome ||
		response.Receipts.CatalogRevision != request.CatalogRevision {
		return internal()
	}
	if err := validateReceipts(response.Receipts); err != nil {
		return internal()
	}
	return validateSize(response, MaxSemanticResponseBytes, common.LimitSemanticResponse)
}

// ValidateAcknowledgeRequestForIntent closes the admission receipt over the
// immutable intent selected under the implementation's transaction/lock. A
// syntactically valid request is not sufficient evidence for a different
// intent ID, payload digest, or Catalog revision.
func ValidateAcknowledgeRequestForIntent(intent Intent, request AcknowledgeRequest) error {
	if validateIntent(intent) != nil || validateAcknowledgeRequestData(request) != nil ||
		intent.IntentID != request.IntentID ||
		intent.PayloadDigest != request.PayloadDigest ||
		intent.CatalogRevision != request.CatalogRevision {
		return invalid()
	}
	return nil
}

func ValidateCommitResultRequest(ctx context.Context, request CommitResultRequest) error {
	if err := contextError(ctx); err != nil {
		return err
	}
	if err := validateCommitResultRequestData(request); err != nil {
		return invalid()
	}
	return validateSize(request, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func validateCommitResultRequestData(request CommitResultRequest) error {
	for label, value := range map[string]string{
		"intent ID": request.IntentID, "Integration command ID": request.IntegrationCommandID,
	} {
		if err := identifier(label, value); err != nil {
			return err
		}
	}
	if !digest(request.PayloadDigest) {
		return fmt.Errorf("payload digest must be canonical sha256")
	}
	if request.CatalogRevision == 0 {
		return fmt.Errorf("Catalog revision must be positive")
	}
	typed := request.ResultFormatVersion != 0 || request.ResultDigest != "" || len(request.CanonicalResult) != 0
	if !typed {
		if err := identifier("remote ID", request.RemoteID); err != nil {
			return err
		}
		return nil
	}
	if request.RemoteID != "" ||
		!digest(request.ResultDigest) || len(request.CanonicalResult) == 0 ||
		len(request.CanonicalResult) > MaxMailResultBytes {
		return fmt.Errorf("typed result is invalid")
	}
	sum := sha256.Sum256(request.CanonicalResult)
	if request.ResultDigest != "sha256:"+hex.EncodeToString(sum[:]) {
		return fmt.Errorf("result digest does not match canonical result")
	}
	var resultErr error
	switch request.ResultFormatVersion {
	case MailResultFormatV1:
		_, resultErr = DecodeMailDeliveryResultV1(request.CanonicalResult)
	case ResourceNoEffectResultFormatV1:
		_, resultErr = DecodeResourceNoEffectResultV1(request.CanonicalResult)
	default:
		return fmt.Errorf("typed result format is invalid")
	}
	if resultErr != nil {
		return fmt.Errorf("typed result is invalid")
	}
	return nil
}

// ValidateCommitResultRequestForIntent closes result commit over the exact
// immutable intent selected under the implementation transaction/lock. The
// implementation additionally verifies IntegrationCommandID against the
// accepted admission stored on that row.
func ValidateCommitResultRequestForIntent(intent Intent, request CommitResultRequest) error {
	if validateIntent(intent) != nil || validateCommitResultRequestData(request) != nil ||
		intent.IntentID != request.IntentID || intent.PayloadDigest != request.PayloadDigest ||
		intent.CatalogRevision != request.CatalogRevision {
		return invalid()
	}
	if intent.Operation == OperationSendMail {
		var payloadErr error
		switch intent.PayloadFormatVersion {
		case MailPayloadFormatV1:
			_, payloadErr = DecodeMailDeliveryV1(intent)
		case MailPayloadFormatV2:
			_, payloadErr = DecodeMailDeliveryV2(intent)
		case MailPayloadFormatV3:
			_, payloadErr = DecodeMailDeliveryV3(intent)
		default:
			payloadErr = invalid()
		}
		if payloadErr != nil || request.ResultFormatVersion != MailResultFormatV1 ||
			request.RemoteID != "" {
			return invalid()
		}
	} else {
		switch request.ResultFormatVersion {
		case 0:
			if len(request.CanonicalResult) != 0 {
				return invalid()
			}
		case ResourceNoEffectResultFormatV1:
			if intent.Operation != OperationUpsert && intent.Operation != OperationDelete {
				return invalid()
			}
		default:
			return invalid()
		}
	}
	return nil
}

func ValidateCommitResultResponse(request CommitResultRequest, response CommitResultResponse) error {
	if validateCommitResultRequestData(request) != nil ||
		response.IntentID != request.IntentID ||
		response.Receipts.CatalogRevision != request.CatalogRevision {
		return internal()
	}
	if err := validateReceipts(response.Receipts); err != nil {
		return internal()
	}
	return validateSize(response, MaxSemanticResponseBytes, common.LimitSemanticResponse)
}

func ValidateCommitNoEffectRequest(ctx context.Context, request CommitNoEffectRequest) error {
	if err := contextError(ctx); err != nil {
		return err
	}
	if err := validateCommitNoEffectRequestData(request); err != nil {
		return invalid()
	}
	return validateSize(request, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func validateCommitNoEffectRequestData(request CommitNoEffectRequest) error {
	for label, value := range map[string]string{
		"intent ID": request.IntentID, "Integration command ID": request.IntegrationCommandID,
	} {
		if err := identifier(label, value); err != nil {
			return err
		}
	}
	if !digest(request.PayloadDigest) {
		return fmt.Errorf("payload digest must be canonical sha256")
	}
	if request.CatalogRevision == 0 {
		return fmt.Errorf("Catalog revision must be positive")
	}
	if !request.Reason.Valid() {
		return fmt.Errorf("no-effect reason is invalid")
	}
	return nil
}

// ValidateCommitNoEffectRequestForIntent closes the terminal no-effect
// disposition over the exact immutable intent selected under the
// implementation transaction/lock. The implementation additionally verifies
// IntegrationCommandID against the accepted admission stored on that row.
func ValidateCommitNoEffectRequestForIntent(intent Intent, request CommitNoEffectRequest) error {
	if validateIntent(intent) != nil || validateCommitNoEffectRequestData(request) != nil ||
		intent.IntentID != request.IntentID || intent.PayloadDigest != request.PayloadDigest ||
		intent.CatalogRevision != request.CatalogRevision {
		return invalid()
	}
	return nil
}

func ValidateCommitNoEffectResponse(request CommitNoEffectRequest, response CommitNoEffectResponse) error {
	if validateCommitNoEffectRequestData(request) != nil ||
		response.IntentID != request.IntentID || response.Reason != request.Reason ||
		response.Receipts.CatalogRevision <= request.CatalogRevision {
		return internal()
	}
	if err := validateReceipts(response.Receipts); err != nil {
		return internal()
	}
	return validateSize(response, MaxSemanticResponseBytes, common.LimitSemanticResponse)
}

func ValidateOpenMailAttachmentRequest(ctx context.Context, request OpenMailAttachmentRequest) error {
	if err := contextError(ctx); err != nil {
		return err
	}
	if validateOpenMailAttachmentRequestData(request) != nil {
		return invalid()
	}
	return validateSize(request, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func validateOpenMailAttachmentRequestData(request OpenMailAttachmentRequest) error {
	if identifier("intent ID", request.IntentID) != nil ||
		identifier("Integration command ID", request.IntegrationCommandID) != nil ||
		!digest(request.PayloadDigest) || request.CatalogRevision == 0 ||
		request.AttachmentIndex >= MaxMailAttachments {
		return fmt.Errorf("mail attachment request is invalid")
	}
	return nil
}

// ValidateOpenMailAttachmentRequestForAcceptedIntent closes content disclosure
// over the exact immutable V3 intent and Integration command selected from the
// implementation's accepted row. It returns the only attachment reference the
// caller may open; file identity is never supplied by the request.
func ValidateOpenMailAttachmentRequestForAcceptedIntent(
	intent Intent,
	acceptedIntegrationCommandID string,
	request OpenMailAttachmentRequest,
) (MailAttachmentV1, error) {
	if validateIntent(intent) != nil ||
		identifier("Integration command ID", acceptedIntegrationCommandID) != nil ||
		validateOpenMailAttachmentRequestData(request) != nil ||
		intent.IntentID != request.IntentID || intent.PayloadDigest != request.PayloadDigest ||
		intent.CatalogRevision != request.CatalogRevision ||
		acceptedIntegrationCommandID != request.IntegrationCommandID {
		return MailAttachmentV1{}, invalid()
	}
	payload, err := DecodeMailDeliveryV3(intent)
	if err != nil || int(request.AttachmentIndex) >= len(payload.Attachments) {
		return MailAttachmentV1{}, invalid()
	}
	return payload.Attachments[request.AttachmentIndex], nil
}

// ValidateMailAttachmentChunk validates either the first metadata-bearing
// frame or a data-only continuation frame. Attachments are non-empty, so every
// frame must make progress and the stream has a finite frame count.
func ValidateMailAttachmentChunk(chunk MailAttachmentChunk) error {
	if len(chunk.Data) == 0 || len(chunk.Data) > MaxMailAttachmentChunkBytes {
		return invalid()
	}
	metadata := chunk.Receipts != nil || chunk.FileName != "" || chunk.ContentType != "" ||
		chunk.ContentLength != 0 || chunk.ContentDigest != ""
	if !metadata {
		return nil
	}
	if chunk.Receipts == nil || validateReceipts(*chunk.Receipts) != nil ||
		!boundedText(chunk.FileName, MaxMailAttachmentNameBytes, false) || chunk.FileName == "" ||
		!boundedText(chunk.ContentType, MaxMailContentTypeBytes, false) || chunk.ContentType == "" ||
		chunk.ContentLength == 0 || chunk.ContentLength > MaxMailAttachmentBytes ||
		uint64(len(chunk.Data)) > chunk.ContentLength || !digest(chunk.ContentDigest) {
		return invalid()
	}
	return nil
}

// NewMailAttachmentStreamState binds stream verification to the immutable
// reference decoded from the accepted intent, never to metadata self-reported
// by the first wire frame.
func NewMailAttachmentStreamState(expected MailAttachmentV1) (*MailAttachmentStreamState, error) {
	if validateMailAttachment(expected) != nil {
		return nil, invalid()
	}
	return &MailAttachmentStreamState{
		expected: expected, ready: true, length: expected.ContentLength,
		digest: expected.ContentDigest, hasher: sha256.New(),
	}, nil
}

func (state *MailAttachmentStreamState) Observe(chunk MailAttachmentChunk) error {
	if state == nil || !state.ready || state.complete || ValidateMailAttachmentChunk(chunk) != nil {
		return invalid()
	}
	if !state.chunkSeen {
		if chunk.Receipts == nil || chunk.FileName != state.expected.FileName ||
			chunk.ContentType != state.expected.ContentType ||
			chunk.ContentLength != state.expected.ContentLength ||
			chunk.ContentDigest != state.expected.ContentDigest {
			return invalid()
		}
		state.chunkSeen = true
	} else if chunk.Receipts != nil || chunk.FileName != "" || chunk.ContentType != "" ||
		chunk.ContentLength != 0 || chunk.ContentDigest != "" {
		return invalid()
	}
	state.received += uint64(len(chunk.Data))
	if state.received > state.length || state.hasher == nil {
		return invalid()
	}
	_, _ = state.hasher.Write(chunk.Data)
	return nil
}

// ValidateOpenedMailAttachment rechecks the fully buffered boundary value and
// all Runtime receipts before provider dispatch.
func ValidateOpenedMailAttachment(
	request OpenMailAttachmentRequest,
	expected MailAttachmentV1,
	opened OpenedMailAttachment,
) error {
	if validateMailAttachment(expected) != nil ||
		validateOpenMailAttachmentRequestData(request) != nil ||
		validateReceipts(opened.Receipts) != nil ||
		opened.Receipts.CatalogRevision != request.CatalogRevision ||
		opened.FileName != expected.FileName || opened.ContentType != expected.ContentType ||
		opened.ContentLength != expected.ContentLength || opened.ContentDigest != expected.ContentDigest ||
		uint64(len(opened.Data)) != expected.ContentLength {
		return invalid()
	}
	sum := sha256.Sum256(opened.Data)
	if expected.ContentDigest != "sha256:"+hex.EncodeToString(sum[:]) {
		return invalid()
	}
	return nil
}

func (state *MailAttachmentStreamState) Complete() error {
	if state == nil || !state.ready || state.complete || !state.chunkSeen || state.received != state.length || state.hasher == nil ||
		state.digest != "sha256:"+hex.EncodeToString(state.hasher.Sum(nil)) {
		return invalid()
	}
	state.complete = true
	return nil
}

func identifier(label, value string) error {
	return identifierWithin(label, value, MaxIdentifierBytes)
}

func identifierWithin(label, value string, maximum int) error {
	if strings.TrimSpace(value) != value || value == "" || !utf8.ValidString(value) || len(value) > maximum {
		return fmt.Errorf("%s is invalid", label)
	}
	for _, current := range value {
		if unicode.IsControl(current) {
			return fmt.Errorf("%s is invalid", label)
		}
	}
	return nil
}

func validLogicalEffectKey(value string, prefix string) bool {
	if len(value) != len(prefix)+sha256.Size*2 || !strings.HasPrefix(value, prefix) ||
		value != strings.ToLower(value) {
		return false
	}
	_, err := hex.DecodeString(strings.TrimPrefix(value, prefix))
	return err == nil
}

func digest(value string) bool {
	if len(value) != len("sha256:")+sha256.Size*2 || !strings.HasPrefix(value, "sha256:") || value != strings.ToLower(value) {
		return false
	}
	_, err := hex.DecodeString(strings.TrimPrefix(value, "sha256:"))
	return err == nil
}

func packPayloadDigest(value string) bool {
	const prefix = "nexus-pack-sha256-v1:"
	if len(value) != len(prefix)+sha256.Size*2 || !strings.HasPrefix(value, prefix) ||
		value != strings.ToLower(value) {
		return false
	}
	_, err := hex.DecodeString(strings.TrimPrefix(value, prefix))
	return err == nil
}

func validateReceipts(receipts Receipts) error {
	if receipts.ProtocolVersion != "1.0" || receipts.CapabilityVersion != CapabilityName || receipts.CatalogRevision == 0 || receipts.SchemaGeneration == 0 || receipts.EngineGeneration == 0 || receipts.SecurityEpoch == 0 {
		return fmt.Errorf("outbound intent receipts are incomplete")
	}
	return nil
}

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

func rawPayloadsWithin(intents []Intent, maximum int) bool {
	total := 0
	for i := range intents {
		if len(intents[i].CanonicalPayload) > maximum-total {
			return false
		}
		total += len(intents[i].CanonicalPayload)
	}
	return true
}

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 exhausted(kind, maximum)
	}
	return nil
}

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

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

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

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