package catalogpublicationv1

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

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

// ValidateBinding fails closed unless every member of the complete
// publication identity is present and canonical. It intentionally does not
// compare CatalogRevision with SecurityEpoch: those axes advance independently.
func ValidateBinding(binding Binding) error {
	catalog := CatalogSchemaBinding{
		CatalogSnapshot: CatalogSnapshotBinding{
			OrgID: binding.OrgID, CatalogRevision: binding.CatalogRevision,
			CatalogSnapshotDigest: binding.CatalogSnapshotDigest,
			SchemaGeneration:      binding.SchemaGeneration, EngineGeneration: binding.EngineGeneration,
		},
		LayoutValidationViewDigest: binding.LayoutValidationViewDigest,
	}
	if err := ValidateCatalogSchemaBinding(catalog); err != nil {
		return fmt.Errorf("catalog publication binding: %w", err)
	}
	if err := validateDigest(binding.LayoutActivationSetDigest); err != nil {
		return fmt.Errorf("catalog publication binding: LayoutActivationSetDigest: %w", err)
	}
	security := layoutroutingsecurityv1.Binding{
		OrgID: binding.OrgID, SecurityEpoch: binding.SecurityEpoch,
		LayoutRoutingSecurityViewDigest: binding.LayoutRoutingSecurityViewDigest,
	}
	if err := layoutroutingsecurityv1.ValidateBinding(security); err != nil {
		return fmt.Errorf("catalog publication binding: %w", err)
	}
	return nil
}

// ValidateCatalogSnapshotBinding validates one complete Catalog snapshot
// reference without consulting storage or current state.
func ValidateCatalogSnapshotBinding(binding CatalogSnapshotBinding) error {
	if err := validateOrgID(binding.OrgID); err != nil {
		return fmt.Errorf("CatalogSnapshotBinding.OrgID: %w", err)
	}
	if binding.CatalogRevision == 0 {
		return fmt.Errorf("CatalogSnapshotBinding.CatalogRevision must be positive")
	}
	if err := validateDigest(binding.CatalogSnapshotDigest); err != nil {
		return fmt.Errorf("CatalogSnapshotBinding.CatalogSnapshotDigest: %w", err)
	}
	if binding.SchemaGeneration == 0 {
		return fmt.Errorf("CatalogSnapshotBinding.SchemaGeneration must be positive")
	}
	if binding.EngineGeneration == 0 {
		return fmt.Errorf("CatalogSnapshotBinding.EngineGeneration must be positive")
	}
	return nil
}

// ValidateCatalogSchemaBinding validates a snapshot and its exact derived
// layout-validation view.
func ValidateCatalogSchemaBinding(binding CatalogSchemaBinding) error {
	if err := ValidateCatalogSnapshotBinding(binding.CatalogSnapshot); err != nil {
		return err
	}
	if err := validateDigest(binding.LayoutValidationViewDigest); err != nil {
		return fmt.Errorf("CatalogSchemaBinding.LayoutValidationViewDigest: %w", err)
	}
	return nil
}

// ComposeBinding composes the single publication identity from its
// independently owned Catalog and routing-security bindings plus one complete
// activation-set digest.
func ComposeBinding(
	catalog CatalogSchemaBinding,
	layoutActivationSetDigest string,
	security layoutroutingsecurityv1.Binding,
) (Binding, error) {
	if err := ValidateCatalogSchemaBinding(catalog); err != nil {
		return Binding{}, err
	}
	if err := validateDigest(layoutActivationSetDigest); err != nil {
		return Binding{}, fmt.Errorf("LayoutActivationSetDigest: %w", err)
	}
	if err := layoutroutingsecurityv1.ValidateBinding(security); err != nil {
		return Binding{}, err
	}
	if catalog.CatalogSnapshot.OrgID != security.OrgID {
		return Binding{}, fmt.Errorf("Catalog and routing-security OrgIDs must match exactly")
	}
	return Binding{
		OrgID:                           catalog.CatalogSnapshot.OrgID,
		CatalogRevision:                 catalog.CatalogSnapshot.CatalogRevision,
		CatalogSnapshotDigest:           catalog.CatalogSnapshot.CatalogSnapshotDigest,
		SchemaGeneration:                catalog.CatalogSnapshot.SchemaGeneration,
		EngineGeneration:                catalog.CatalogSnapshot.EngineGeneration,
		LayoutValidationViewDigest:      catalog.LayoutValidationViewDigest,
		LayoutActivationSetDigest:       layoutActivationSetDigest,
		SecurityEpoch:                   security.SecurityEpoch,
		LayoutRoutingSecurityViewDigest: security.LayoutRoutingSecurityViewDigest,
	}, nil
}

// BindingComponents decomposes one validated publication identity without
// inventing another release or selector identity.
func BindingComponents(binding Binding) (CatalogSchemaBinding, string, layoutroutingsecurityv1.Binding, error) {
	if err := ValidateBinding(binding); err != nil {
		return CatalogSchemaBinding{}, "", layoutroutingsecurityv1.Binding{}, err
	}
	return CatalogSchemaBinding{
			CatalogSnapshot: CatalogSnapshotBinding{
				OrgID: binding.OrgID, CatalogRevision: binding.CatalogRevision,
				CatalogSnapshotDigest: binding.CatalogSnapshotDigest,
				SchemaGeneration:      binding.SchemaGeneration, EngineGeneration: binding.EngineGeneration,
			},
			LayoutValidationViewDigest: binding.LayoutValidationViewDigest,
		}, binding.LayoutActivationSetDigest, layoutroutingsecurityv1.Binding{
			OrgID: binding.OrgID, SecurityEpoch: binding.SecurityEpoch,
			LayoutRoutingSecurityViewDigest: binding.LayoutRoutingSecurityViewDigest,
		}, nil
}

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

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
}
