package credentialgeneration

import (
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"regexp"
	"strings"
	"unicode"
	"unicode/utf8"
)

const (
	transportConfigurationDomainV1 = "nexuscrm.connector-credential-transport-configuration.v1"
	ownerReceiptDomainV1           = "nexuscrm.connector-credential-generation-owner-receipt.v1"
	maxIdentifierBytes             = 255
)

var prefixedSHA256Pattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)

// ReceiptProtocol is one closed immutable owner-receipt recipe. Valid values
// can only be obtained through an exact authoring or retained-read resolver.
//
// V1 hashes a versioned domain, NUL, and one uint64-big-endian length frame
// containing the exact transport bytes. Its receipt uses a separate versioned
// domain and frames, in order, the receipt protocol, generation ID, stable
// identity, provider scope, connector account, secret-backend protocol, exact
// secret-backend version, transport bytes, and transport digest. Text is exact
// UTF-8 and binary configuration is exact bytes: neither is case-folded or
// normalized. Digests are lowercase sha256-prefixed hexadecimal strings.
type ReceiptProtocol struct {
	version ReceiptProtocolVersion
}

// ResolveReceiptProtocolForAuthoring selects an exact protocol permitted to
// author new generations. There is no default, alias, downgrade, or runtime
// registration path.
func ResolveReceiptProtocolForAuthoring(version ReceiptProtocolVersion) (ReceiptProtocol, error) {
	switch version {
	case ReceiptProtocolV1:
		return ReceiptProtocol{version: version}, nil
	default:
		return ReceiptProtocol{}, unsupportedReceiptProtocol(version)
	}
}

// ResolveReceiptProtocolForRead selects an exact retained reader. Reader
// support remains independent from authoring support so referenced historical
// generations never become unreadable merely because new authoring advances.
func ResolveReceiptProtocolForRead(version ReceiptProtocolVersion) (ReceiptProtocol, error) {
	switch version {
	case ReceiptProtocolV1:
		return ReceiptProtocol{version: version}, nil
	default:
		return ReceiptProtocol{}, unsupportedReceiptProtocol(version)
	}
}

// Version returns the exact immutable receipt protocol version.
func (protocol ReceiptProtocol) Version() ReceiptProtocolVersion { return protocol.version }

// NewGeneration derives every digest and receipt from the caller's complete
// exact input. The receipt protocol must be selected explicitly.
func NewGeneration(input AuthoringInput) (Generation, error) {
	protocol, err := ResolveReceiptProtocolForAuthoring(input.ReceiptProtocolVersion)
	if err != nil {
		return Generation{}, err
	}
	return protocol.derive(input)
}

func (protocol ReceiptProtocol) derive(input AuthoringInput) (Generation, error) {
	if protocol.version != input.ReceiptProtocolVersion {
		return Generation{}, invalidGeneration("receipt protocol selection disagrees")
	}
	if err := validateAuthoringInput(input); err != nil {
		return Generation{}, err
	}

	configuration := cloneBinary(input.TransportConfiguration)
	configurationDigest := digestPrefixed(transportConfigurationPreimage(configuration))
	receipt := ownerReceiptPreimage(protocol.version, input, configuration, configurationDigest)
	return Generation{
		receiptProtocolVersion:       protocol.version,
		id:                           input.ID,
		stableCredentialIdentity:     input.StableCredentialIdentity,
		providerScopeID:              input.ProviderScopeID,
		connectorAccountID:           input.ConnectorAccountID,
		secretBackendProtocol:        input.SecretBackendProtocol,
		secretBackendVersionRef:      input.SecretBackendVersionRef,
		transportConfiguration:       configuration,
		transportConfigurationDigest: configurationDigest,
		ownerReceipt:                 receipt,
		ownerReceiptDigest:           digestPrefixed(receipt),
	}, nil
}

func (generation Generation) validateStored() error {
	if !prefixedSHA256Pattern.MatchString(generation.transportConfigurationDigest) ||
		!prefixedSHA256Pattern.MatchString(generation.ownerReceiptDigest) {
		return storedGenerationInvalid("digest shape is invalid")
	}
	protocol, err := ResolveReceiptProtocolForRead(generation.receiptProtocolVersion)
	if err != nil {
		return storedGenerationInvalid("receipt protocol: %v", err)
	}
	expected, err := protocol.derive(AuthoringInput{
		ReceiptProtocolVersion:   generation.receiptProtocolVersion,
		ID:                       generation.id,
		StableCredentialIdentity: generation.stableCredentialIdentity,
		ProviderScopeID:          generation.providerScopeID,
		ConnectorAccountID:       generation.connectorAccountID,
		SecretBackendProtocol:    generation.secretBackendProtocol,
		SecretBackendVersionRef:  generation.secretBackendVersionRef,
		TransportConfiguration:   generation.transportConfiguration,
	})
	if err != nil {
		return storedGenerationInvalid("cannot rederive generation: %v", err)
	}
	if !generation.equal(expected) {
		return storedGenerationInvalid("persisted fields disagree with canonical generation")
	}
	return nil
}

func validateAuthoringInput(input AuthoringInput) error {
	fields := []struct {
		name  string
		value string
	}{
		{name: "generation ID", value: input.ID},
		{name: "stable credential identity", value: input.StableCredentialIdentity},
		{name: "provider scope ID", value: input.ProviderScopeID},
		{name: "connector account ID", value: input.ConnectorAccountID},
		{name: "secret backend protocol", value: input.SecretBackendProtocol},
		{name: "secret backend version ref", value: input.SecretBackendVersionRef},
	}
	for _, field := range fields {
		if !validOpaqueIdentifier(field.value) {
			return invalidGeneration("%s is not canonical", field.name)
		}
	}
	if len(input.TransportConfiguration) > MaxTransportConfigurationBytes {
		return invalidGeneration(
			"transport configuration exceeds %d bytes",
			MaxTransportConfigurationBytes,
		)
	}
	return nil
}

func transportConfigurationPreimage(configuration []byte) []byte {
	preimage := canonicalPrefix(transportConfigurationDomainV1)
	return appendCanonicalBytes(preimage, configuration)
}

func ownerReceiptPreimage(
	version ReceiptProtocolVersion,
	input AuthoringInput,
	configuration []byte,
	configurationDigest string,
) []byte {
	receipt := canonicalPrefix(ownerReceiptDomainV1)
	receipt = appendCanonicalString(receipt, string(version))
	receipt = appendCanonicalString(receipt, input.ID)
	receipt = appendCanonicalString(receipt, input.StableCredentialIdentity)
	receipt = appendCanonicalString(receipt, input.ProviderScopeID)
	receipt = appendCanonicalString(receipt, input.ConnectorAccountID)
	receipt = appendCanonicalString(receipt, input.SecretBackendProtocol)
	receipt = appendCanonicalString(receipt, input.SecretBackendVersionRef)
	receipt = appendCanonicalBytes(receipt, configuration)
	receipt = appendCanonicalString(receipt, configurationDigest)
	return receipt
}

func canonicalPrefix(domain string) []byte {
	result := make([]byte, 0, len(domain)+1)
	result = append(result, domain...)
	return append(result, 0)
}

func appendCanonicalString(destination []byte, value string) []byte {
	return appendCanonicalBytes(destination, []byte(value))
}

func appendCanonicalBytes(destination, value []byte) []byte {
	destination = binary.BigEndian.AppendUint64(destination, uint64(len(value)))
	return append(destination, value...)
}

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

func validOpaqueIdentifier(value string) bool {
	if value == "" || len(value) > maxIdentifierBytes ||
		!utf8.ValidString(value) || strings.TrimSpace(value) != value ||
		strings.IndexByte(value, 0) >= 0 {
		return false
	}
	for _, character := range value {
		if unicode.IsControl(character) {
			return false
		}
	}
	return true
}

func unsupportedReceiptProtocol(version ReceiptProtocolVersion) error {
	return fmt.Errorf("%w: %q", ErrUnsupportedReceiptProtocol, version)
}

func invalidGeneration(format string, args ...any) error {
	return fmt.Errorf("%w: %s", ErrInvalidGeneration, fmt.Sprintf(format, args...))
}

func storedGenerationInvalid(format string, args ...any) error {
	return fmt.Errorf("%w: %s", ErrStoredGenerationInvalid, fmt.Sprintf(format, args...))
}
