package systemlayoutbundleauthorityv1

import (
	"encoding/hex"
	"fmt"
	"strings"

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

// ValidateBundleKey validates the explicit versioned system key grammar. It
// proves canonical text only, not that a registry row or history exists.
func ValidateBundleKey(key BundleKey) error {
	if key.BundleKeyVersion != layoutbundlekeyv1.BundleKeyVersion {
		return fmt.Errorf("BundleKey.BundleKeyVersion must equal %q", layoutbundlekeyv1.BundleKeyVersion)
	}
	if key.SlotOwnerKind != layoutbundlekeyv1.SlotOwnerKindSystem {
		return fmt.Errorf("BundleKey.SlotOwnerKind must equal system")
	}
	if key.SlotOwnerNamespace != layoutbundlekeyv1.SystemSlotOwnerNamespace {
		return fmt.Errorf("BundleKey.SlotOwnerNamespace must equal %q", layoutbundlekeyv1.SystemSlotOwnerNamespace)
	}
	if err := layoutbundlekeyv1.ValidateBundleKey(portableBundleKey(key)); err != nil {
		return err
	}
	return nil
}

// ValidateSlotValue validates one complete ACTIVE or TOMBSTONE value without
// consulting current state or history.
func ValidateSlotValue(value SystemLayoutBundleSlotV1) error {
	if value.ValueVersion != SlotValueVersion {
		return fmt.Errorf("SystemLayoutBundleSlotV1.ValueVersion must equal %q", SlotValueVersion)
	}
	if err := ValidateBundleKey(value.BundleKey); err != nil {
		return fmt.Errorf("SystemLayoutBundleSlotV1.BundleKey: %w", err)
	}
	switch value.State {
	case SlotStateActive, SlotStateTombstone:
		return nil
	default:
		return fmt.Errorf("SystemLayoutBundleSlotV1.State must be ACTIVE or TOMBSTONE")
	}
}

// ValidateCommandID rejects every spelling except lowercase canonical UUIDv7.
func ValidateCommandID(commandID CommandID) error {
	value := string(commandID)
	if len(value) != 36 || value[8] != '-' || value[13] != '-' || value[18] != '-' || value[23] != '-' {
		return fmt.Errorf("CommandID must be a lowercase canonical UUIDv7")
	}
	for index := range value {
		switch index {
		case 8, 13, 18, 23:
			continue
		}
		if !isLowerHex(value[index]) {
			return fmt.Errorf("CommandID must be a lowercase canonical UUIDv7")
		}
	}
	if value[14] != '7' || !strings.ContainsRune("89ab", rune(value[19])) {
		return fmt.Errorf("CommandID must be a lowercase canonical UUIDv7")
	}
	return nil
}

// ValidateCommandRequest validates the complete typed request. In v1 the
// ACTIVE predecessor digest of a RETIRE request is derivable from the fixed
// value version and immutable BundleKey, so a different digest is rejected.
func ValidateCommandRequest(request SystemLayoutBundleSlotCommandRequest) error {
	if err := ValidateBundleKey(request.TargetBundleKey); err != nil {
		return fmt.Errorf("SystemLayoutBundleSlotCommandRequest.TargetBundleKey: %w", err)
	}
	if err := ValidateSlotValue(request.DesiredSlotValue); err != nil {
		return fmt.Errorf("SystemLayoutBundleSlotCommandRequest.DesiredSlotValue: %w", err)
	}
	if request.TargetBundleKey != request.DesiredSlotValue.BundleKey {
		return fmt.Errorf("SystemLayoutBundleSlotCommandRequest TargetBundleKey must equal DesiredSlotValue.BundleKey")
	}

	switch request.Operation {
	case OperationCreate:
		if request.DesiredSlotValue.State != SlotStateActive || request.ExpectedPrior.Variant != ExpectedPriorAbsent || request.ExpectedPrior.SlotValueDigest != "" {
			return fmt.Errorf("CREATE requires desired ACTIVE and expected ABSENT with no digest")
		}
	case OperationRetire:
		if request.DesiredSlotValue.State != SlotStateTombstone || request.ExpectedPrior.Variant != ExpectedPriorExact {
			return fmt.Errorf("RETIRE requires desired TOMBSTONE and expected EXACT active digest")
		}
		if err := validateDigest(request.ExpectedPrior.SlotValueDigest); err != nil {
			return fmt.Errorf("ExpectedPrior.SlotValueDigest: %w", err)
		}
		active := request.DesiredSlotValue
		active.State = SlotStateActive
		activeDigest, err := SlotValueDigest(active)
		if err != nil {
			return fmt.Errorf("derive active ExpectedPrior: %w", err)
		}
		if request.ExpectedPrior.SlotValueDigest != activeDigest {
			return fmt.Errorf("ExpectedPrior.SlotValueDigest must equal the exact ACTIVE slot-value digest")
		}
	default:
		return fmt.Errorf("SystemLayoutBundleSlotCommandRequest.Operation must be CREATE or RETIRE")
	}
	return nil
}

// ValidateEvidence validates the closed evidence bytes without consulting a
// source handle, registry current row, history, command, or outcome.
func ValidateEvidence(evidence SystemLayoutBundleAuthorityEvidenceV1) error {
	if evidence.EvidenceVersion != EvidenceVersion {
		return fmt.Errorf("SystemLayoutBundleAuthorityEvidenceV1.EvidenceVersion must equal %q", EvidenceVersion)
	}
	if err := catalogpublicationv1.ValidateCatalogSnapshotBinding(evidence.SourceCatalogSnapshot); err != nil {
		return fmt.Errorf("SystemLayoutBundleAuthorityEvidenceV1.SourceCatalogSnapshot: %w", err)
	}
	if err := ValidateBundleKey(evidence.BundleKey); err != nil {
		return fmt.Errorf("SystemLayoutBundleAuthorityEvidenceV1.BundleKey: %w", err)
	}
	if evidence.SlotValueVersion != SlotValueVersion {
		return fmt.Errorf("SystemLayoutBundleAuthorityEvidenceV1.SlotValueVersion must equal %q", SlotValueVersion)
	}
	if evidence.SlotState != SlotStateActive {
		return fmt.Errorf("SystemLayoutBundleAuthorityEvidenceV1.SlotState must equal ACTIVE")
	}
	return nil
}

func portableBundleKey(key BundleKey) layoutbundlekeyv1.BundleKey {
	return layoutbundlekeyv1.BundleKey{
		SlotOwnerKind:      key.SlotOwnerKind,
		SlotOwnerNamespace: key.SlotOwnerNamespace,
		LocalComponentKey:  key.LocalComponentKey,
	}
}

func validateDigest(value string) error {
	const prefix = "sha256:"
	if len(value) != len(prefix)+64 || !strings.HasPrefix(value, prefix) {
		return fmt.Errorf("must be sha256:<64 lowercase hex>")
	}
	hexText := value[len(prefix):]
	if strings.ToLower(hexText) != hexText {
		return fmt.Errorf("hex must be lowercase")
	}
	if _, err := hex.DecodeString(hexText); err != nil {
		return fmt.Errorf("hex is invalid")
	}
	return nil
}

func isLowerHex(value byte) bool {
	return value >= '0' && value <= '9' || value >= 'a' && value <= 'f'
}
