package catalogassignmentidentityv1

import (
	"fmt"
	"strings"
	"unicode/utf8"

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

// NormalizeObjectAPIName validates an exact source API name and returns its
// portable ASCII-lowercase key. No trimming or Unicode normalization occurs.
func NormalizeObjectAPIName(value string) (ObjectKey, error) {
	if len(value) == 0 || len(value) > MaxObjectAPINameBytes {
		return ObjectKey{}, fmt.Errorf("ObjectAPIName must be 1..%d ASCII bytes", MaxObjectAPINameBytes)
	}
	canonical := make([]byte, len(value))
	for index, char := range []byte(value) {
		switch {
		case char >= 'A' && char <= 'Z':
			canonical[index] = char + ('a' - 'A')
		case char >= 'a' && char <= 'z', char >= '0' && char <= '9', char == '_':
			canonical[index] = char
		default:
			return ObjectKey{}, fmt.Errorf("ObjectAPIName must match [A-Za-z_][A-Za-z0-9_]*")
		}
	}
	if first := canonical[0]; (first < 'a' || first > 'z') && first != '_' {
		return ObjectKey{}, fmt.Errorf("ObjectAPIName must match [A-Za-z_][A-Za-z0-9_]*")
	}
	return ObjectKey{NormalizedAPIName: string(canonical)}, nil
}

func ValidateObjectKey(key ObjectKey) error {
	if len(key.NormalizedAPIName) == 0 || len(key.NormalizedAPIName) > MaxObjectAPINameBytes {
		return fmt.Errorf("ObjectKey.NormalizedAPIName must be 1..%d bytes", MaxObjectAPINameBytes)
	}
	for index, char := range []byte(key.NormalizedAPIName) {
		if index == 0 {
			if (char < 'a' || char > 'z') && char != '_' {
				return fmt.Errorf("ObjectKey.NormalizedAPIName must match [a-z_][a-z0-9_]*")
			}
			continue
		}
		if (char < 'a' || char > 'z') && (char < '0' || char > '9') && char != '_' {
			return fmt.Errorf("ObjectKey.NormalizedAPIName must match [a-z_][a-z0-9_]*")
		}
	}
	return nil
}

func ValidateRecordTypeKey(key RecordTypeKey) error {
	if err := validateNamespacedKey(key.NamespaceID, key.LocalKey); err != nil {
		return fmt.Errorf("RecordTypeKey: %w", err)
	}
	if key.LocalKey == MasterRecordTypeLocalKey && key.NamespaceID != SystemNamespaceID {
		return fmt.Errorf("RecordTypeKey: %q is reserved to namespace %q", MasterRecordTypeLocalKey, SystemNamespaceID)
	}
	return nil
}

func ValidateProductAppKey(key ProductAppKey) error {
	if err := appauthority.ValidateAppID(key.AppID); err != nil {
		return fmt.Errorf("ProductAppKey.AppID: %w", err)
	}
	return nil
}

func ValidateNavigationAppKey(key NavigationAppKey) error {
	if err := validateNamespacedKey(key.NamespaceID, key.LocalKey); err != nil {
		return fmt.Errorf("NavigationAppKey: %w", err)
	}
	if key.NamespaceID == SystemNamespaceID && key.LocalKey == MasterRecordTypeLocalKey {
		return fmt.Errorf("NavigationAppKey: reserved Master record-type key is not a navigation key")
	}
	return nil
}

// ValidateView validates the complete value without consulting a Catalog
// ledger, artifact store, current pointer, or Runtime state.
func ValidateView(view View) error {
	if err := catalogpublicationv1.ValidateCatalogSnapshotBinding(view.SourceCatalogSnapshot); err != nil {
		return fmt.Errorf("View.SourceCatalogSnapshot: %w", err)
	}
	if view.CanonicalizationVersion != CanonicalizationVersion {
		return fmt.Errorf("View.CanonicalizationVersion must equal %q", CanonicalizationVersion)
	}
	if view.KeyGrammarVersion != KeyGrammarVersion {
		return fmt.Errorf("View.KeyGrammarVersion must equal %q", KeyGrammarVersion)
	}
	if view.ObjectKeyNormalizationVersion != ObjectKeyNormalizationVersion {
		return fmt.Errorf("View.ObjectKeyNormalizationVersion must equal %q", ObjectKeyNormalizationVersion)
	}
	if view.Objects == nil || view.RecordTypes == nil || view.ProductApps == nil || view.NavigationApps == nil {
		return fmt.Errorf("View identity arrays must be present")
	}
	if len(view.Objects) > MaxEntriesPerDimension {
		return fmt.Errorf("View.Objects exceeds %d entries", MaxEntriesPerDimension)
	}
	if len(view.RecordTypes) > MaxEntriesPerDimension {
		return fmt.Errorf("View.RecordTypes exceeds %d entries", MaxEntriesPerDimension)
	}
	if len(view.ProductApps) > MaxEntriesPerDimension {
		return fmt.Errorf("View.ProductApps exceeds %d entries", MaxEntriesPerDimension)
	}
	if len(view.NavigationApps) > MaxEntriesPerDimension {
		return fmt.Errorf("View.NavigationApps exceeds %d entries", MaxEntriesPerDimension)
	}
	if err := validateCanonicalViewSize(view, MaxCanonicalViewBytes); err != nil {
		return err
	}

	objectsByKey := make(map[string]IdentityState, len(view.Objects))
	objectIDs := make(map[string]struct{}, len(view.Objects))
	for index, identity := range view.Objects {
		if err := ValidateObjectKey(identity.Key); err != nil {
			return fmt.Errorf("View.Objects[%d]: %w", index, err)
		}
		if err := validateTargetLocalID(identity.ResolvedObjectID); err != nil {
			return fmt.Errorf("View.Objects[%d].ResolvedObjectID: %w", index, err)
		}
		if err := validateState(identity.State); err != nil {
			return fmt.Errorf("View.Objects[%d].State: %w", index, err)
		}
		key := identity.Key.NormalizedAPIName
		if _, exists := objectsByKey[key]; exists {
			return fmt.Errorf("View.Objects contains duplicate ObjectKey")
		}
		if _, exists := objectIDs[identity.ResolvedObjectID]; exists {
			return fmt.Errorf("View.Objects contains duplicate ResolvedObjectID")
		}
		objectsByKey[key] = identity.State
		objectIDs[identity.ResolvedObjectID] = struct{}{}
	}

	recordTypeKeys := make(map[string]struct{}, len(view.RecordTypes))
	recordTypeIDs := make(map[string]struct{}, len(view.RecordTypes))
	for index, identity := range view.RecordTypes {
		if err := ValidateObjectKey(identity.ObjectKey); err != nil {
			return fmt.Errorf("View.RecordTypes[%d].ObjectKey: %w", index, err)
		}
		parentState, exists := objectsByKey[identity.ObjectKey.NormalizedAPIName]
		if !exists {
			return fmt.Errorf("View.RecordTypes[%d] names a missing ObjectKey", index)
		}
		if err := ValidateRecordTypeKey(identity.Key); err != nil {
			return fmt.Errorf("View.RecordTypes[%d]: %w", index, err)
		}
		if err := validateTargetLocalID(identity.ResolvedRecordTypeID); err != nil {
			return fmt.Errorf("View.RecordTypes[%d].ResolvedRecordTypeID: %w", index, err)
		}
		if err := validateState(identity.State); err != nil {
			return fmt.Errorf("View.RecordTypes[%d].State: %w", index, err)
		}
		if identity.State == IdentityStateActive && parentState != IdentityStateActive {
			return fmt.Errorf("View.RecordTypes[%d] is active under a tombstoned ObjectKey", index)
		}
		key := recordTypeIdentity(identity)
		if _, duplicate := recordTypeKeys[key]; duplicate {
			return fmt.Errorf("View.RecordTypes contains duplicate complete key")
		}
		if _, duplicate := recordTypeIDs[identity.ResolvedRecordTypeID]; duplicate {
			return fmt.Errorf("View.RecordTypes contains duplicate ResolvedRecordTypeID")
		}
		recordTypeKeys[key] = struct{}{}
		recordTypeIDs[identity.ResolvedRecordTypeID] = struct{}{}
	}

	productAppKeys := make(map[string]struct{}, len(view.ProductApps))
	productAppIDs := make(map[string]struct{}, len(view.ProductApps))
	for index, identity := range view.ProductApps {
		if err := ValidateProductAppKey(identity.Key); err != nil {
			return fmt.Errorf("View.ProductApps[%d]: %w", index, err)
		}
		if err := validateState(identity.State); err != nil {
			return fmt.Errorf("View.ProductApps[%d].State: %w", index, err)
		}
		if err := validateProductAppResolvedTarget(identity.State, identity.ResolvedTarget); err != nil {
			return fmt.Errorf("View.ProductApps[%d].ResolvedTarget: %w", index, err)
		}
		if _, duplicate := productAppKeys[identity.Key.AppID]; duplicate {
			return fmt.Errorf("View.ProductApps contains duplicate ProductAppKey")
		}
		if identity.ResolvedTarget.Kind == ProductAppResolvedTargetKindAppInstallationID {
			if _, duplicate := productAppIDs[identity.ResolvedTarget.ResolvedAppInstallationID]; duplicate {
				return fmt.Errorf("View.ProductApps contains duplicate present ResolvedAppInstallationID")
			}
			productAppIDs[identity.ResolvedTarget.ResolvedAppInstallationID] = struct{}{}
		}
		productAppKeys[identity.Key.AppID] = struct{}{}
	}

	navigationAppKeys := make(map[string]struct{}, len(view.NavigationApps))
	navigationAppIDs := make(map[string]struct{}, len(view.NavigationApps))
	for index, identity := range view.NavigationApps {
		if err := ValidateNavigationAppKey(identity.Key); err != nil {
			return fmt.Errorf("View.NavigationApps[%d]: %w", index, err)
		}
		if err := validateTargetLocalID(identity.ResolvedAppConfigID); err != nil {
			return fmt.Errorf("View.NavigationApps[%d].ResolvedAppConfigID: %w", index, err)
		}
		if err := validateState(identity.State); err != nil {
			return fmt.Errorf("View.NavigationApps[%d].State: %w", index, err)
		}
		key := namespacedKeyIdentity(identity.Key.NamespaceID, identity.Key.LocalKey)
		if _, duplicate := navigationAppKeys[key]; duplicate {
			return fmt.Errorf("View.NavigationApps contains duplicate NavigationAppKey")
		}
		if _, duplicate := navigationAppIDs[identity.ResolvedAppConfigID]; duplicate {
			return fmt.Errorf("View.NavigationApps contains duplicate ResolvedAppConfigID")
		}
		navigationAppKeys[key] = struct{}{}
		navigationAppIDs[identity.ResolvedAppConfigID] = struct{}{}
	}

	return nil
}

func validateNamespacedKey(namespaceID, localKey string) error {
	if err := validateNamespacedKeyPart("NamespaceID", namespaceID, 2); err != nil {
		return fmt.Errorf("NamespaceID: %w", err)
	}
	if err := validateNamespacedKeyPart("LocalKey", localKey, 1); err != nil {
		return fmt.Errorf("LocalKey: %w", err)
	}
	return nil
}

func validateNamespacedKeyPart(label, value string, minimumSegments int) error {
	if len(value) == 0 || len(value) > MaxKeyPartBytes {
		return fmt.Errorf("%s must be 1..%d ASCII bytes", label, MaxKeyPartBytes)
	}
	segments := strings.Split(value, ".")
	if len(segments) < minimumSegments {
		return fmt.Errorf("%s must contain at least %d dot-separated segments", label, minimumSegments)
	}
	for segmentIndex, segment := range segments {
		if len(segment) == 0 || len(segment) > MaxKeySegmentBytes {
			return fmt.Errorf("%s segment %d must be 1..%d ASCII bytes", label, segmentIndex, MaxKeySegmentBytes)
		}
		for index := 0; index < len(segment); index++ {
			character := segment[index]
			if index == 0 {
				if character < 'a' || character > 'z' {
					return fmt.Errorf("%s segment %d must begin with a lowercase ASCII letter", label, segmentIndex)
				}
				continue
			}
			if (character < 'a' || character > 'z') &&
				(character < '0' || character > '9') && character != '-' {
				return fmt.Errorf("%s segment %d contains a non-canonical byte", label, segmentIndex)
			}
		}
		if segment[len(segment)-1] == '-' {
			return fmt.Errorf("%s segment %d must not end in '-'", label, segmentIndex)
		}
	}
	return nil
}

func validateProductAppResolvedTarget(state IdentityState, target ProductAppResolvedTarget) error {
	switch target.Kind {
	case ProductAppResolvedTargetKindAbsent:
		if target.ResolvedAppInstallationID != "" {
			return fmt.Errorf("ABSENT must carry no ResolvedAppInstallationID")
		}
		if state != IdentityStateTombstone {
			return fmt.Errorf("ACTIVE requires APP_INSTALLATION_ID")
		}
	case ProductAppResolvedTargetKindAppInstallationID:
		if err := appauthority.ValidateAppInstallationID(target.ResolvedAppInstallationID); err != nil {
			return fmt.Errorf("ResolvedAppInstallationID: %w", err)
		}
	default:
		return fmt.Errorf("Kind must be ABSENT or APP_INSTALLATION_ID")
	}
	return nil
}

func validateTargetLocalID(value string) error {
	if len(value) == 0 || len(value) > MaxTargetLocalIDBytes || !utf8.ValidString(value) {
		return fmt.Errorf("must be well-formed UTF-8 with 1..%d bytes", MaxTargetLocalIDBytes)
	}
	if value[0] == ' ' || value[len(value)-1] == ' ' {
		return fmt.Errorf("must not have surrounding ASCII space")
	}
	for _, char := range []byte(value) {
		if char < 0x20 || char == 0x7f {
			return fmt.Errorf("must not contain ASCII control or DEL bytes")
		}
	}
	return nil
}

func validateState(state IdentityState) error {
	if state != IdentityStateActive && state != IdentityStateTombstone {
		return fmt.Errorf("must be ACTIVE or TOMBSTONE")
	}
	return nil
}

func namespacedKeyIdentity(namespaceID, localKey string) string {
	return namespaceID + "\x00" + localKey
}

func recordTypeIdentity(identity RecordTypeIdentity) string {
	return identity.ObjectKey.NormalizedAPIName + "\x00" + namespacedKeyIdentity(identity.Key.NamespaceID, identity.Key.LocalKey)
}
