package serviceconfiguration

import (
	"encoding/binary"
	"fmt"
	"sort"

	"github.com/pingcap-inc/nexuscrm-connectors/features/connector/credentialgeneration"
	catalogv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
)

const (
	generationCanonicalDomainV1 = "nexuscrm.connector-service-configuration-generation-canonical/v1"
	receiptCanonicalDomainV1    = "nexuscrm.connector-service-configuration-owner-receipt-canonical/v1"
)

// GenerationProtocol is one closed immutable whole-service generation recipe.
// Values can only be obtained through an exact authoring or retained-read
// resolver.
type GenerationProtocol struct {
	version GenerationProtocolVersion
}

// ReceiptProtocol is one closed compact owner-receipt recipe. Reader support
// is intentionally independent from new-authoring support.
type ReceiptProtocol struct {
	version ReceiptProtocolVersion
}

func ResolveGenerationProtocolForAuthoring(
	version GenerationProtocolVersion,
) (GenerationProtocol, error) {
	switch version {
	case GenerationProtocolV1:
		return GenerationProtocol{version: version}, nil
	default:
		return GenerationProtocol{}, fmt.Errorf("%w: %q", ErrUnsupportedGenerationProtocol, version)
	}
}

func ResolveGenerationProtocolForRead(
	version GenerationProtocolVersion,
) (GenerationProtocol, error) {
	switch version {
	case GenerationProtocolV1:
		return GenerationProtocol{version: version}, nil
	default:
		return GenerationProtocol{}, fmt.Errorf("%w: %q", ErrUnsupportedGenerationProtocol, version)
	}
}

func ResolveReceiptProtocolForAuthoring(
	version ReceiptProtocolVersion,
) (ReceiptProtocol, error) {
	switch version {
	case ReceiptProtocolV1:
		return ReceiptProtocol{version: version}, nil
	default:
		return ReceiptProtocol{}, fmt.Errorf("%w: %q", ErrUnsupportedReceiptProtocol, version)
	}
}

func ResolveReceiptProtocolForRead(
	version ReceiptProtocolVersion,
) (ReceiptProtocol, error) {
	switch version {
	case ReceiptProtocolV1:
		return ReceiptProtocol{version: version}, nil
	default:
		return ReceiptProtocol{}, fmt.Errorf("%w: %q", ErrUnsupportedReceiptProtocol, version)
	}
}

func (protocol GenerationProtocol) Version() GenerationProtocolVersion { return protocol.version }

func (protocol ReceiptProtocol) Version() ReceiptProtocolVersion { return protocol.version }

func deriveGeneration(
	input AuthoringInput,
	view catalogv1.IntegrationBindingView,
	credentialGenerations []credentialgeneration.Generation,
) (Generation, error) {
	identities := make([]credentialGenerationIdentity, len(credentialGenerations))
	if credentialGenerations == nil {
		identities = nil
	} else {
		for index := range credentialGenerations {
			identities[index] = credentialGenerations[index]
		}
	}
	return deriveGenerationWithIdentities(input, view, identities)
}

func deriveGenerationFromExactIdentities(
	input AuthoringInput,
	view catalogv1.IntegrationBindingView,
	credentialGenerations []credentialgeneration.ExactIdentity,
) (Generation, error) {
	identities := make([]credentialGenerationIdentity, len(credentialGenerations))
	if credentialGenerations == nil {
		identities = nil
	} else {
		for index := range credentialGenerations {
			identities[index] = credentialGenerations[index]
		}
	}
	return deriveGenerationWithIdentities(input, view, identities)
}

func deriveGenerationWithIdentities(
	input AuthoringInput,
	view catalogv1.IntegrationBindingView,
	credentialGenerations []credentialGenerationIdentity,
) (Generation, error) {
	generationProtocol, err := ResolveGenerationProtocolForAuthoring(
		input.GenerationProtocolVersion,
	)
	if err != nil {
		return Generation{}, err
	}
	receiptProtocol, err := ResolveReceiptProtocolForAuthoring(input.ReceiptProtocolVersion)
	if err != nil {
		return Generation{}, err
	}
	if err := validateAuthoringMaterial(input, view, credentialGenerations); err != nil {
		return Generation{}, err
	}
	return deriveCanonicalGeneration(
		generationProtocol,
		receiptProtocol,
		input,
		catalogReferenceFromView(view),
	)
}

func deriveStoredGeneration(input AuthoringInput, catalog catalogReference) (Generation, error) {
	generationProtocol, err := ResolveGenerationProtocolForRead(input.GenerationProtocolVersion)
	if err != nil {
		return Generation{}, err
	}
	receiptProtocol, err := ResolveReceiptProtocolForRead(input.ReceiptProtocolVersion)
	if err != nil {
		return Generation{}, err
	}
	if err := validateStoredSource(input, catalog); err != nil {
		return Generation{}, err
	}
	return deriveCanonicalGeneration(generationProtocol, receiptProtocol, input, catalog)
}

func deriveCanonicalGeneration(
	generationProtocol GenerationProtocol,
	receiptProtocol ReceiptProtocol,
	input AuthoringInput,
	catalog catalogReference,
) (Generation, error) {
	if generationProtocol.version != input.GenerationProtocolVersion ||
		receiptProtocol.version != input.ReceiptProtocolVersion {
		return Generation{}, invalidGeneration("protocol selection disagrees with authoring input")
	}

	generation := Generation{
		generationProtocolVersion: generationProtocol.version,
		receiptProtocolVersion:    receiptProtocol.version,
		id:                        input.ID,
		catalog:                   catalog,
		ownerRevision:             input.OwnerRevision,
		accounts:                  make([]AccountConfiguration, len(input.Accounts)),
	}
	for accountIndex, inputAccount := range input.Accounts {
		account := AccountConfiguration{
			connectorAccountID: inputAccount.ConnectorAccountID,
			providerScopeID:    inputAccount.ProviderScopeID,
			accountRevision:    inputAccount.AccountRevision,
			providerRevision:   inputAccount.ProviderRevision,
			bindings:           make([]BindingConfiguration, len(inputAccount.Bindings)),
		}
		for bindingIndex, inputBinding := range inputAccount.Bindings {
			binding := BindingConfiguration{
				catalogBindingOrdinal:      inputBinding.CatalogBindingOrdinal,
				configurationRevision:      inputBinding.ConfigurationRevision,
				configurationFormatVersion: inputBinding.ConfigurationFormatVersion,
				canonicalConfiguration:     cloneBinary(inputBinding.CanonicalConfiguration),
				credentialSelections: make(
					[]CredentialSelection,
					len(inputBinding.CredentialSelections),
				),
			}
			for selectionIndex, inputSelection := range inputBinding.CredentialSelections {
				binding.credentialSelections[selectionIndex] = CredentialSelection{
					credentialRef:          inputSelection.CredentialRef,
					credentialGenerationID: inputSelection.CredentialGenerationID,
				}
			}
			sort.Slice(binding.credentialSelections, func(left, right int) bool {
				return binding.credentialSelections[left].credentialRef <
					binding.credentialSelections[right].credentialRef
			})
			account.bindings[bindingIndex] = binding
		}
		sort.Slice(account.bindings, func(left, right int) bool {
			return account.bindings[left].catalogBindingOrdinal <
				account.bindings[right].catalogBindingOrdinal
		})
		generation.accounts[accountIndex] = account
	}
	sort.Slice(generation.accounts, func(left, right int) bool {
		return generation.accounts[left].connectorAccountID <
			generation.accounts[right].connectorAccountID
	})

	for accountIndex := range generation.accounts {
		account := &generation.accounts[accountIndex]
		account.ordinal = uint64(accountIndex)
		account.rowID = computeAccountRowID(generation.id, account.ordinal, account.connectorAccountID)
		generation.bindingCount += uint64(len(account.bindings))
		for bindingIndex := range account.bindings {
			binding := &account.bindings[bindingIndex]
			binding.accountRowID = account.rowID
			binding.rowID = computeBindingRowID(account.rowID, binding.catalogBindingOrdinal)
			generation.credentialSelectionCount += uint64(len(binding.credentialSelections))
			for selectionIndex := range binding.credentialSelections {
				selection := &binding.credentialSelections[selectionIndex]
				selection.ordinal = uint64(selectionIndex)
				selection.bindingRowID = binding.rowID
				selection.rowID = computeCredentialSelectionRowID(
					binding.rowID,
					selection.ordinal,
					selection.credentialRef,
				)
			}
		}
	}
	generation.accountCount = uint64(len(generation.accounts))

	canonical, err := generationCanonicalBytes(generation)
	if err != nil {
		return Generation{}, err
	}
	generation.generationDigest = digestPrefixed(canonical)
	generation.ownerReceipt, err = receiptCanonicalBytes(generation)
	if err != nil {
		return Generation{}, err
	}
	generation.ownerReceiptDigest = digestPrefixed(generation.ownerReceipt)
	return generation, nil
}

func catalogReferenceFromView(view catalogv1.IntegrationBindingView) catalogReference {
	return catalogReference{
		orgID:                view.OrgID,
		appInstallationID:    view.AppInstallationID,
		authorizedWorkloadID: view.AuthorizedWorkloadID,
		lineage:              view.Lineage,
		receipts:             view.Receipts,
	}
}

func (generation Generation) validateStored() error {
	if !validDigest(generation.generationDigest) ||
		!validDigest(generation.ownerReceiptDigest) ||
		len(generation.ownerReceipt) > MaxCanonicalReceiptBytes {
		return storedGenerationInvalid("digest or receipt shape is invalid")
	}
	input := authoringInputFromGeneration(generation)
	expected, err := deriveStoredGeneration(input, generation.catalog)
	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 authoringInputFromGeneration(generation Generation) AuthoringInput {
	input := AuthoringInput{
		GenerationProtocolVersion: generation.generationProtocolVersion,
		ReceiptProtocolVersion:    generation.receiptProtocolVersion,
		ID:                        generation.id,
		OwnerRevision:             generation.ownerRevision,
		Accounts:                  make([]AccountInput, len(generation.accounts)),
	}
	for accountIndex, account := range generation.accounts {
		input.Accounts[accountIndex] = AccountInput{
			ConnectorAccountID: account.connectorAccountID,
			ProviderScopeID:    account.providerScopeID,
			AccountRevision:    account.accountRevision,
			ProviderRevision:   account.providerRevision,
			Bindings:           make([]BindingInput, len(account.bindings)),
		}
		for bindingIndex, binding := range account.bindings {
			inputBinding := BindingInput{
				CatalogBindingOrdinal:      binding.catalogBindingOrdinal,
				ConfigurationRevision:      binding.configurationRevision,
				ConfigurationFormatVersion: binding.configurationFormatVersion,
				CanonicalConfiguration:     cloneBinary(binding.canonicalConfiguration),
				CredentialSelections: make(
					[]CredentialSelectionInput,
					len(binding.credentialSelections),
				),
			}
			for selectionIndex, selection := range binding.credentialSelections {
				inputBinding.CredentialSelections[selectionIndex] = CredentialSelectionInput{
					CredentialRef:          selection.credentialRef,
					CredentialGenerationID: selection.credentialGenerationID,
				}
			}
			input.Accounts[accountIndex].Bindings[bindingIndex] = inputBinding
		}
	}
	return input
}

func generationCanonicalBytes(generation Generation) ([]byte, error) {
	size := canonicalGenerationSize(generation)
	if size > MaxCanonicalGenerationBytes {
		return nil, invalidGeneration(
			"canonical generation exceeds %d bytes",
			MaxCanonicalGenerationBytes,
		)
	}
	canonical := make([]byte, 0, int(size))
	canonical = append(canonical, canonicalPrefix(generationCanonicalDomainV1)...)
	canonical = appendCanonicalString(canonical, string(generation.generationProtocolVersion))
	canonical = appendCanonicalString(canonical, string(generation.receiptProtocolVersion))
	canonical = appendCanonicalString(canonical, generation.id)
	canonical = appendCanonicalCatalogReference(canonical, generation.catalog)
	canonical = appendCanonicalRevision(canonical, generation.ownerRevision)
	canonical = binary.BigEndian.AppendUint64(canonical, uint64(len(generation.accounts)))
	for _, account := range generation.accounts {
		canonical = appendCanonicalString(canonical, account.connectorAccountID)
		canonical = appendCanonicalString(canonical, account.providerScopeID)
		canonical = appendCanonicalRevision(canonical, account.accountRevision)
		canonical = appendCanonicalRevision(canonical, account.providerRevision)
		canonical = binary.BigEndian.AppendUint64(canonical, uint64(len(account.bindings)))
		for _, binding := range account.bindings {
			canonical = binary.BigEndian.AppendUint32(canonical, binding.catalogBindingOrdinal)
			canonical = appendCanonicalRevision(canonical, binding.configurationRevision)
			canonical = appendCanonicalString(canonical, binding.configurationFormatVersion)
			canonical = appendCanonicalBytes(canonical, binding.canonicalConfiguration)
			canonical = binary.BigEndian.AppendUint64(
				canonical,
				uint64(len(binding.credentialSelections)),
			)
			for _, selection := range binding.credentialSelections {
				canonical = appendCanonicalString(canonical, selection.credentialRef)
				canonical = appendCanonicalString(canonical, selection.credentialGenerationID)
			}
		}
	}
	return canonical, nil
}

func receiptCanonicalBytes(generation Generation) ([]byte, error) {
	receipt := canonicalPrefix(receiptCanonicalDomainV1)
	receipt = appendCanonicalString(receipt, string(generation.receiptProtocolVersion))
	receipt = appendCanonicalString(receipt, string(generation.generationProtocolVersion))
	receipt = appendCanonicalString(receipt, generation.id)
	receipt = appendCanonicalCatalogReference(receipt, generation.catalog)
	receipt = appendCanonicalRevision(receipt, generation.ownerRevision)
	receipt = appendCanonicalString(receipt, generation.generationDigest)
	receipt = binary.BigEndian.AppendUint64(receipt, generation.accountCount)
	receipt = binary.BigEndian.AppendUint64(receipt, generation.bindingCount)
	receipt = binary.BigEndian.AppendUint64(receipt, generation.credentialSelectionCount)
	if len(receipt) > MaxCanonicalReceiptBytes {
		return nil, invalidGeneration(
			"canonical owner receipt exceeds %d bytes",
			MaxCanonicalReceiptBytes,
		)
	}
	return receipt, nil
}

func appendCanonicalCatalogReference(destination []byte, catalog catalogReference) []byte {
	destination = appendCanonicalString(destination, catalog.orgID)
	destination = appendCanonicalString(destination, catalog.appInstallationID)
	destination = appendCanonicalString(destination, catalog.authorizedWorkloadID)
	destination = binary.BigEndian.AppendUint64(destination, catalog.lineage.CatalogRevision)
	destination = binary.BigEndian.AppendUint64(destination, catalog.lineage.SchemaGeneration)
	destination = binary.BigEndian.AppendUint64(destination, catalog.lineage.EngineGeneration)
	destination = appendCanonicalString(destination, catalog.lineage.SourceSnapshotDigest)
	destination = appendCanonicalString(destination, catalog.lineage.ViewDigest)
	destination = appendCanonicalString(destination, catalog.receipts.ProtocolVersion)
	destination = binary.BigEndian.AppendUint64(destination, catalog.receipts.CatalogRevision)
	destination = binary.BigEndian.AppendUint64(destination, catalog.receipts.SchemaGeneration)
	destination = binary.BigEndian.AppendUint64(destination, catalog.receipts.EngineGeneration)
	destination = binary.BigEndian.AppendUint64(destination, catalog.receipts.SecurityEpoch)
	return appendCanonicalString(destination, catalog.receipts.CapabilityVersion)
}

func authoringCanonicalSize(input AuthoringInput, catalog catalogReference) (uint64, bool) {
	size := uint64(len(generationCanonicalDomainV1) + 1)
	add := func(value uint64) bool {
		if value > ^uint64(0)-size {
			return false
		}
		size += value
		return true
	}
	for _, value := range []uint64{
		canonicalBytesSize(len(input.GenerationProtocolVersion)),
		canonicalBytesSize(len(input.ReceiptProtocolVersion)),
		canonicalBytesSize(len(input.ID)),
		canonicalCatalogReferenceSize(catalog),
		canonicalRevisionSize(input.OwnerRevision),
		8,
	} {
		if !add(value) {
			return 0, false
		}
	}
	for _, account := range input.Accounts {
		for _, value := range []uint64{
			canonicalBytesSize(len(account.ConnectorAccountID)),
			canonicalBytesSize(len(account.ProviderScopeID)),
			canonicalRevisionSize(account.AccountRevision),
			canonicalRevisionSize(account.ProviderRevision),
			8,
		} {
			if !add(value) {
				return 0, false
			}
		}
		for _, binding := range account.Bindings {
			for _, value := range []uint64{
				4,
				canonicalRevisionSize(binding.ConfigurationRevision),
				canonicalBytesSize(len(binding.ConfigurationFormatVersion)),
				canonicalBytesSize(len(binding.CanonicalConfiguration)),
				8,
			} {
				if !add(value) {
					return 0, false
				}
			}
			for _, selection := range binding.CredentialSelections {
				if !add(canonicalBytesSize(len(selection.CredentialRef))) ||
					!add(canonicalBytesSize(len(selection.CredentialGenerationID))) {
					return 0, false
				}
			}
		}
	}
	return size, true
}

func canonicalGenerationSize(generation Generation) uint64 {
	size, ok := authoringCanonicalSize(authoringInputFromGeneration(generation), generation.catalog)
	if !ok {
		return ^uint64(0)
	}
	return size
}

func canonicalCatalogReferenceSize(catalog catalogReference) uint64 {
	return canonicalBytesSize(len(catalog.orgID)) +
		canonicalBytesSize(len(catalog.appInstallationID)) +
		canonicalBytesSize(len(catalog.authorizedWorkloadID)) +
		24 +
		canonicalBytesSize(len(catalog.lineage.SourceSnapshotDigest)) +
		canonicalBytesSize(len(catalog.lineage.ViewDigest)) +
		canonicalBytesSize(len(catalog.receipts.ProtocolVersion)) +
		32 +
		canonicalBytesSize(len(catalog.receipts.CapabilityVersion))
}

func canonicalRevisionSize(revision ExactOwnerRevision) uint64 {
	return canonicalBytesSize(len(revision.RevisionID)) +
		canonicalBytesSize(len(revision.ValueDigest))
}

func canonicalBytesSize(length int) uint64 { return 8 + uint64(length) }
