package catalogpublication

import (
	"bytes"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"math"

	catalogpublicationv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalogpublicationv1"
	layoutbundlekeyv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutbundlekeyv1"
)

const (
	candidateSourceRequestDomain       = "nexuscrm.catalog-candidate-source-request/v1"
	candidateSourceOutcomeDomain       = "nexuscrm.catalog-candidate-source-preparation-outcome/v1"
	candidateSourceFailureDomain       = "nexuscrm.catalog-candidate-source-failure-outcome/v1"
	maxCandidateSourceFailureBytes     = 652
	maxCandidateSourceFailureCodeBytes = 64
)

type validatedCandidateSourceRequest struct {
	request           CandidateSourceRequest
	canonicalSet      layoutbundlekeyv1.RequiredBundleKeySet
	setBytes          []byte
	setDigest         string
	canonicalAppSet   SourceAppSelectionSet
	appSelectionBytes []byte
	requestDigest     string
}

type activeCandidateSourceBaseline struct {
	active          Reference
	binding         *catalogpublicationv1.Binding
	receiptBackedV2 bool
}

func validateCandidateSourceRequest(request CandidateSourceRequest) (validatedCandidateSourceRequest, error) {
	if !validCommandID(request.WriterCommandID) {
		return validatedCandidateSourceRequest{}, fmt.Errorf(
			"catalog candidate source: WriterCommandID must be a lowercase UUIDv7",
		)
	}
	if err := validateOpaque("RuntimeAudience", request.RuntimeAudience, 255); err != nil {
		return validatedCandidateSourceRequest{}, fmt.Errorf("catalog candidate source: %v", err)
	}
	if err := catalogpublicationv1.ValidateCatalogSnapshotBinding(request.CatalogSnapshotBinding); err != nil {
		return validatedCandidateSourceRequest{}, fmt.Errorf("catalog candidate source: %w", err)
	}
	if err := validateReference("ExpectedPriorPublication", request.ExpectedPriorPublication); err != nil {
		return validatedCandidateSourceRequest{}, fmt.Errorf("catalog candidate source: %v", err)
	}

	canonicalSet, err := layoutbundlekeyv1.CanonicalizeRequiredBundleKeySet(request.RequiredBundleKeySet)
	if err != nil {
		return validatedCandidateSourceRequest{}, fmt.Errorf(
			"catalog candidate source: RequiredBundleKeySet: %w", err,
		)
	}
	setBytes, err := layoutbundlekeyv1.CanonicalRequiredBundleKeySetBytes(canonicalSet)
	if err != nil {
		return validatedCandidateSourceRequest{}, fmt.Errorf(
			"catalog candidate source: RequiredBundleKeySet: %w", err,
		)
	}
	setDigest, err := layoutbundlekeyv1.RequiredBundleKeySetDigest(canonicalSet)
	if err != nil {
		return validatedCandidateSourceRequest{}, fmt.Errorf(
			"catalog candidate source: RequiredBundleKeySet: %w", err,
		)
	}

	canonicalAppSet, err := CanonicalizeSourceAppSelectionSet(
		request.SourceAppSelectionSet,
		request.WriterCommandID,
		canonicalSet,
	)
	if err != nil {
		return validatedCandidateSourceRequest{}, fmt.Errorf(
			"catalog candidate source: SourceAppSelectionSet: %w", err,
		)
	}
	appSelectionBytes, err := CanonicalSourceAppSelectionSetBytes(
		canonicalAppSet,
		request.WriterCommandID,
		canonicalSet,
	)
	if err != nil {
		return validatedCandidateSourceRequest{}, fmt.Errorf(
			"catalog candidate source: SourceAppSelectionSet: %w", err,
		)
	}
	for _, selection := range canonicalAppSet.Entries {
		if selection.SelectionKind == SourceAppSelectionKindCandidate {
			return validatedCandidateSourceRequest{}, fmt.Errorf(
				"%w: CANDIDATE App source selection requires atomic Catalog App coordination",
				ErrSourceNotReady,
			)
		}
	}

	request.RequiredBundleKeySet = cloneRequiredSet(canonicalSet)
	request.SourceAppSelectionSet = cloneSourceAppSelectionSet(canonicalAppSet)
	validated := validatedCandidateSourceRequest{
		request:           request,
		canonicalSet:      canonicalSet,
		setBytes:          append([]byte(nil), setBytes...),
		setDigest:         setDigest,
		canonicalAppSet:   canonicalAppSet,
		appSelectionBytes: append([]byte(nil), appSelectionBytes...),
	}
	validated.requestDigest = candidateSourceRequestDigest(validated)
	return validated, nil
}

// deriveCandidateSourceRelation closes source admission over exactly the two
// protocol relations. The relation is not caller input.
func deriveCandidateSourceRelation(
	request CandidateSourceRequest,
	writerCatalogRevision uint64,
	baseline activeCandidateSourceBaseline,
) (CandidateSourceRelation, error) {
	candidate := request.CatalogSnapshotBinding
	if !request.ExpectedPriorPublication.Present {
		if request.ExpectedPriorCatalogRevision != 0 || candidate.CatalogRevision != 1 {
			return "", fmt.Errorf(
				"initial source requires absent prior, expected revision zero, and candidate CatalogRevision 1",
			)
		}
		if baseline.active.Present || baseline.binding != nil || writerCatalogRevision != 0 {
			return "", fmt.Errorf("initial source conflicts with existing Catalog lineage")
		}
		return CandidateSourceCatalogSuccessor, nil
	}
	if request.ExpectedPriorCatalogRevision == 0 ||
		request.ExpectedPriorPublication != baseline.active ||
		baseline.binding == nil {
		return "", fmt.Errorf("source expected prior is not the exact active publication")
	}
	if baseline.binding.CatalogRevision != request.ExpectedPriorCatalogRevision ||
		writerCatalogRevision != request.ExpectedPriorCatalogRevision {
		return "", fmt.Errorf(
			"source expected Catalog revision does not match active writer lineage",
		)
	}

	switch candidate.CatalogRevision {
	case request.ExpectedPriorCatalogRevision:
		if !baseline.receiptBackedV2 {
			return "", fmt.Errorf(
				"exact-current source requires a receipt-backed v2 expected prior",
			)
		}
		if candidate != snapshotBindingFromPublication(*baseline.binding) {
			return "", fmt.Errorf(
				"exact-current source must retain the complete Catalog snapshot binding",
			)
		}
		return CandidateSourceExactCurrent, nil
	default:
		if request.ExpectedPriorCatalogRevision == math.MaxUint64 ||
			candidate.CatalogRevision != request.ExpectedPriorCatalogRevision+1 {
			return "", fmt.Errorf(
				"successor source candidate CatalogRevision must equal expected prior plus one",
			)
		}
		return CandidateSourceCatalogSuccessor, nil
	}
}

func snapshotBindingFromPublication(
	binding catalogpublicationv1.Binding,
) catalogpublicationv1.CatalogSnapshotBinding {
	return catalogpublicationv1.CatalogSnapshotBinding{
		OrgID:                 binding.OrgID,
		CatalogRevision:       binding.CatalogRevision,
		CatalogSnapshotDigest: binding.CatalogSnapshotDigest,
		SchemaGeneration:      binding.SchemaGeneration,
		EngineGeneration:      binding.EngineGeneration,
	}
}

func candidateSourceRequestDigest(value validatedCandidateSourceRequest) string {
	var canonical bytes.Buffer
	writeSourceString(&canonical, candidateSourceRequestDomain)
	writeSourceString(&canonical, value.request.RuntimeAudience)
	writeSourceString(&canonical, value.request.WriterCommandID)
	writeSnapshotBinding(&canonical, value.request.CatalogSnapshotBinding)
	writeSourceBytes(&canonical, value.setBytes)
	writeSourceString(&canonical, value.setDigest)
	writeReference(&canonical, value.request.ExpectedPriorPublication)
	writeSourceUint(&canonical, value.request.ExpectedPriorCatalogRevision)
	writeSourceBytes(&canonical, value.appSelectionBytes)
	return sourceDigest(canonical.Bytes())
}

func candidateSourcePreparationOutcomeDigest(record candidateSourceRecord) string {
	var canonical bytes.Buffer
	writeSourceString(&canonical, candidateSourceOutcomeDomain)
	writeSourceString(&canonical, record.OrgID)
	writeSourceString(&canonical, record.WriterCommandID)
	writeSourceString(&canonical, record.RequestDigest)
	writeSourceUint(&canonical, record.WriterReservationGeneration)
	return sourceDigest(canonical.Bytes())
}

func candidateSourceFailureOutcome(
	record candidateSourceRecord,
	failure CandidateSourceFailure,
	state CandidateSourceState,
) (CatalogCandidateSourceFailureOutcomeV1, string, error) {
	outcome := CatalogCandidateSourceFailureOutcomeV1{
		OrgID:                        record.OrgID,
		WriterCommandID:              record.WriterCommandID,
		WriterReservationGeneration:  record.WriterReservationGeneration,
		SourceRequestDigest:          record.RequestDigest,
		ExpectedPriorPublication:     record.ExpectedPriorPublication,
		ExpectedPriorCatalogRevision: record.ExpectedPriorCatalogRevision,
		FailureClass:                 failure.FailureClass,
		FailureCode:                  failure.FailureCode,
		FailureDetailDigest:          failure.FailureDetailDigest,
		RetryDisposition:             failure.RetryDisposition,
		SourceTerminalState:          state,
	}
	digest, err := catalogCandidateSourceFailureOutcomeDigest(outcome)
	return outcome, digest, err
}

func catalogCandidateSourceFailureOutcomeDigest(
	outcome CatalogCandidateSourceFailureOutcomeV1,
) (string, error) {
	if err := validateCandidateSourceFailureOutcome(outcome); err != nil {
		return "", err
	}
	size := candidateSourceFailureCanonicalSize(outcome)
	if size > maxCandidateSourceFailureBytes {
		return "", fmt.Errorf(
			"catalog candidate source: failure outcome exceeds %d canonical bytes",
			maxCandidateSourceFailureBytes,
		)
	}
	var canonical bytes.Buffer
	canonical.Grow(size)
	writeSourceString(&canonical, candidateSourceFailureDomain)
	writeSourceString(&canonical, outcome.OrgID)
	writeSourceString(&canonical, outcome.WriterCommandID)
	writeSourceUint(&canonical, outcome.WriterReservationGeneration)
	writeSourceString(&canonical, outcome.SourceRequestDigest)
	writeReference(&canonical, outcome.ExpectedPriorPublication)
	writeSourceUint(&canonical, outcome.ExpectedPriorCatalogRevision)
	canonical.WriteByte(candidateSourceFailureClassByte(outcome.FailureClass))
	writeSourceString(&canonical, outcome.FailureCode)
	writeSourceString(&canonical, outcome.FailureDetailDigest)
	canonical.WriteByte(candidateSourceRetryDispositionByte(outcome.RetryDisposition))
	canonical.WriteByte(candidateSourceTerminalStateByte(outcome.SourceTerminalState))
	if canonical.Len() != size {
		return "", fmt.Errorf("%w: failure outcome canonical size mismatch", ErrSourceCorrupt)
	}
	return sourceDigest(canonical.Bytes()), nil
}

func validateCandidateSourceFailureOutcome(
	outcome CatalogCandidateSourceFailureOutcomeV1,
) error {
	if err := validateOpaque("OrgID", outcome.OrgID, 255); err != nil {
		return fmt.Errorf("catalog candidate source: failure outcome: %v", err)
	}
	if !validCommandID(outcome.WriterCommandID) {
		return fmt.Errorf(
			"catalog candidate source: failure outcome WriterCommandID must be a lowercase UUIDv7",
		)
	}
	if outcome.WriterReservationGeneration == 0 {
		return fmt.Errorf(
			"catalog candidate source: failure outcome writer generation must be positive",
		)
	}
	if err := validateSHA256Digest(outcome.SourceRequestDigest); err != nil {
		return fmt.Errorf("catalog candidate source: failure outcome source digest: %w", err)
	}
	if err := validateReference(
		"failure outcome ExpectedPriorPublication",
		outcome.ExpectedPriorPublication,
	); err != nil {
		return fmt.Errorf("catalog candidate source: %v", err)
	}
	if candidateSourceFailureClassByte(outcome.FailureClass) == 0 {
		return fmt.Errorf(
			"catalog candidate source: unknown failure class %q",
			outcome.FailureClass,
		)
	}
	if !validCandidateSourceFailureCode(outcome.FailureCode) {
		return fmt.Errorf(
			"catalog candidate source: FailureCode must match ^[A-Z][A-Z0-9_]{0,63}$",
		)
	}
	if err := validateSHA256Digest(outcome.FailureDetailDigest); err != nil {
		return fmt.Errorf("catalog candidate source: FailureDetailDigest: %w", err)
	}
	if candidateSourceRetryDispositionByte(outcome.RetryDisposition) == 0 {
		return fmt.Errorf(
			"catalog candidate source: unknown retry disposition %q",
			outcome.RetryDisposition,
		)
	}
	if candidateSourceTerminalStateByte(outcome.SourceTerminalState) == 0 {
		return fmt.Errorf(
			"catalog candidate source: failure outcome state must be TERMINATED or STALE",
		)
	}
	return nil
}

func validateCandidateSourceFailureInput(failure CandidateSourceFailure) error {
	if candidateSourceFailureClassByte(failure.FailureClass) == 0 {
		return fmt.Errorf(
			"catalog candidate source: unknown failure class %q",
			failure.FailureClass,
		)
	}
	if !validCandidateSourceFailureCode(failure.FailureCode) {
		return fmt.Errorf(
			"catalog candidate source: FailureCode must match ^[A-Z][A-Z0-9_]{0,63}$",
		)
	}
	if err := validateSHA256Digest(failure.FailureDetailDigest); err != nil {
		return fmt.Errorf("catalog candidate source: FailureDetailDigest: %w", err)
	}
	if candidateSourceRetryDispositionByte(failure.RetryDisposition) == 0 {
		return fmt.Errorf(
			"catalog candidate source: unknown retry disposition %q",
			failure.RetryDisposition,
		)
	}
	return nil
}

func validCandidateSourceFailureCode(value string) bool {
	if len(value) < 1 || len(value) > maxCandidateSourceFailureCodeBytes ||
		value[0] < 'A' || value[0] > 'Z' {
		return false
	}
	for _, character := range []byte(value[1:]) {
		if (character < 'A' || character > 'Z') &&
			(character < '0' || character > '9') &&
			character != '_' {
			return false
		}
	}
	return true
}

func candidateSourceFailureClassByte(value CandidateSourceFailureClass) byte {
	switch value {
	case CandidateSourceSemanticInvalidity:
		return 0x01
	case CandidateSourceSourceInvalidity:
		return 0x02
	default:
		return 0
	}
}

func candidateSourceRetryDispositionByte(value CandidateSourceRetryDisposition) byte {
	switch value {
	case CandidateSourceRetryWithNewOuterCommand:
		return 0x01
	case CandidateSourceRetryTerminal:
		return 0x02
	default:
		return 0
	}
}

func candidateSourceTerminalStateByte(value CandidateSourceState) byte {
	switch value {
	case CandidateSourceTerminated:
		return 0x01
	case CandidateSourceStale:
		return 0x02
	default:
		return 0
	}
}

func candidateSourceFailureCanonicalSize(
	outcome CatalogCandidateSourceFailureOutcomeV1,
) int {
	return sourceStringSize(candidateSourceFailureDomain) +
		sourceStringSize(outcome.OrgID) +
		sourceStringSize(outcome.WriterCommandID) +
		sourceUintSize(outcome.WriterReservationGeneration) +
		sourceStringSize(outcome.SourceRequestDigest) +
		sourceReferenceSize(outcome.ExpectedPriorPublication) +
		sourceUintSize(outcome.ExpectedPriorCatalogRevision) +
		1 +
		sourceStringSize(outcome.FailureCode) +
		sourceStringSize(outcome.FailureDetailDigest) +
		1 +
		1
}

func sourceStringSize(value string) int {
	return sourceUintSize(uint64(len(value))) + len(value)
}

func sourceReferenceSize(value Reference) int {
	if !value.Present {
		return 1
	}
	return 1 + sourceStringSize(value.Digest)
}

func sourceUintSize(value uint64) int {
	var encoded [binary.MaxVarintLen64]byte
	return binary.PutUvarint(encoded[:], value)
}

func writeSnapshotBinding(
	out *bytes.Buffer,
	binding catalogpublicationv1.CatalogSnapshotBinding,
) {
	writeSourceString(out, binding.OrgID)
	writeSourceUint(out, binding.CatalogRevision)
	writeSourceString(out, binding.CatalogSnapshotDigest)
	writeSourceUint(out, binding.SchemaGeneration)
	writeSourceUint(out, binding.EngineGeneration)
}

func writeSourceString(out *bytes.Buffer, value string) {
	writeSourceUint(out, uint64(len(value)))
	out.WriteString(value)
}

func writeSourceBytes(out *bytes.Buffer, value []byte) {
	writeSourceUint(out, uint64(len(value)))
	out.Write(value)
}

func writeSourceUint(out *bytes.Buffer, value uint64) {
	var encoded [binary.MaxVarintLen64]byte
	count := binary.PutUvarint(encoded[:], value)
	out.Write(encoded[:count])
}

func sourceDigest(value []byte) string {
	digest := sha256.Sum256(value)
	return "sha256:" + hex.EncodeToString(digest[:])
}

func cloneRequiredSet(
	value layoutbundlekeyv1.RequiredBundleKeySet,
) layoutbundlekeyv1.RequiredBundleKeySet {
	return layoutbundlekeyv1.RequiredBundleKeySet{
		Version:    value.Version,
		BundleKeys: append([]layoutbundlekeyv1.BundleKey{}, value.BundleKeys...),
	}
}

func cloneSourceAppSelectionSet(value SourceAppSelectionSet) SourceAppSelectionSet {
	return SourceAppSelectionSet{
		SourceAppSelectionSetVersion: value.SourceAppSelectionSetVersion,
		Entries:                      append([]SourceAppSelection{}, value.Entries...),
	}
}
