package catalogapp

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"slices"
	"strings"

	"github.com/pingcap-inc/nexuscrm-contract/pkg/appauthority"
	"github.com/pingcap-inc/nexuscrm-controlplane/internal/catalogadmissionfence"
)

func LegacyMappingDigest(mapping VerifiedLegacyMapping) (string, error) {
	encoded, err := json.Marshal(struct {
		Schema                string                   `json:"schema"`
		LegacyAuthorityDigest string                   `json:"legacy_authority_digest"`
		SignerID              string                   `json:"signer_id"`
		AppID                 string                   `json:"app_id"`
		TargetReleaseDigest   string                   `json:"target_release_digest"`
		Components            []LegacyComponentMapping `json:"components"`
	}{"nexuscrm.legacy-app-mapping/v1", mapping.LegacyAuthorityDigest, mapping.SignerID,
		mapping.AppID, mapping.TargetReleaseDigest, mapping.Components})
	if err != nil {
		return "", err
	}
	return digestParts(string(encoded)), nil
}

// LegacyAuthorityDigest seals the complete caller-supplied legacy authority
// evidence without reading or mutating the legacy authority. R1b owns proving
// that these exact facts came from the selected legacy incarnation.
func LegacyAuthorityDigest(legacy LegacyInstalledPack) (string, error) {
	encoded, err := json.Marshal(struct {
		Schema string              `json:"schema"`
		Facts  LegacyInstalledPack `json:"facts"`
	}{"nexuscrm.legacy-installed-pack-authority/v1", legacy})
	if err != nil {
		return "", err
	}
	return digestParts(string(encoded)), nil
}

func (a *Authority) ConvertLegacyPack(ctx context.Context, request ConversionRequest) (Conversion, error) {
	normalizedOperation, release, operationRequestDigest, err := a.validateBeginOperation(ctx, request.Operation)
	if err != nil {
		return Conversion{}, err
	}
	request.Operation = normalizedOperation
	if err := validateConversionRequest(request, release); err != nil {
		return Conversion{}, err
	}
	encoded, err := json.Marshal(struct {
		Schema  string            `json:"schema"`
		Request ConversionRequest `json:"request"`
	}{"nexuscrm.catalog-legacy-conversion/v1", request})
	if err != nil {
		return Conversion{}, err
	}
	requestDigest := digestParts(string(encoded))
	migrationKey := hashKey(request.Legacy.OrgID, request.Legacy.PackInstallID, request.Legacy.AppliedPayloadHash, request.Mapping.TargetReleaseDigest)
	logicalKey := hashKey(request.Legacy.OrgID, request.Legacy.PackInstallID)

	tx, err := a.db.BeginTx(ctx, nil)
	if err != nil {
		return Conversion{}, err
	}
	defer rollback(tx)
	if _, err := catalogadmissionfence.LockExclusive(ctx, tx); err != nil {
		return Conversion{}, fmt.Errorf(
			"%w: publication admission fence: %v",
			ErrCorrupt,
			err,
		)
	}
	if existing, found, err := readConversionByKey(ctx, tx, migrationKey); err != nil {
		return Conversion{}, err
	} else if found {
		var existingDigest string
		if err := tx.QueryRowContext(ctx, `SELECT RequestDigest FROM _System_CatalogCRMConversionJournal WHERE MigrationId = ? FOR UPDATE`, existing.MigrationID).Scan(&existingDigest); err != nil {
			return Conversion{}, err
		}
		if existingDigest != requestDigest {
			return Conversion{}, ErrConflict
		}
		if err := tx.Commit(); err != nil {
			return Conversion{}, err
		}
		return existing, nil
	}
	var conflictingMigration string
	err = tx.QueryRowContext(ctx, `SELECT MigrationId FROM _System_CatalogCRMConversionJournal
WHERE LogicalLegacyKey = ? FOR UPDATE`, logicalKey).Scan(&conflictingMigration)
	if err == nil {
		return Conversion{}, ErrConflict
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return Conversion{}, err
	}
	migrationID, err := a.newID(a.now())
	if err != nil {
		return Conversion{}, err
	}
	operation, err := a.beginOperationAfterAdmissionFence(
		ctx,
		tx,
		request.Operation,
		release,
		operationRequestDigest,
		initiationLegacyConversion,
	)
	if err != nil {
		return Conversion{}, err
	}
	if err := a.insertConversionBindingsTx(ctx, tx, operation, request); err != nil {
		return Conversion{}, err
	}
	_, err = tx.ExecContext(ctx, `INSERT INTO _System_CatalogCRMConversionJournal
(MigrationId, MigrationKey, LogicalLegacyKey, OrgId, LegacyPackInstallId, AppliedPayloadHash,
LegacyAuthorityDigest, TargetAppReleaseDigest, MappingDigest, MappingVerificationDigest, RequestDigest,
AppInstallationId, OperationId, CreatedDate, LastModifiedDate)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP(6), CURRENT_TIMESTAMP(6))`,
		migrationID, migrationKey, logicalKey, request.Legacy.OrgID, request.Legacy.PackInstallID,
		request.Legacy.AppliedPayloadHash, request.Mapping.LegacyAuthorityDigest, request.Mapping.TargetReleaseDigest,
		request.Mapping.MappingDigest, request.Mapping.SignatureVerificationDigest, requestDigest,
		operation.AppInstallationID, operation.OperationID)
	if isDuplicate(err) {
		return Conversion{}, ErrConflict
	}
	if err != nil {
		return Conversion{}, err
	}
	if err := tx.Commit(); err != nil {
		return Conversion{}, err
	}
	return Conversion{MigrationID: migrationID, OperationID: operation.OperationID,
		AppInstallationID: operation.AppInstallationID, State: "PREPARING", Step: "CANDIDATE_COMMITTED"}, nil
}

func validateConversionRequest(request ConversionRequest, release RegisteredRelease) error {
	legacy := request.Legacy
	if request.Operation.Kind != OperationPrepare || request.Operation.OrgID != legacy.OrgID || request.Operation.AppInstallationID != "" {
		return fmt.Errorf("legacy conversion must create one Catalog-minted installation through PREPARE")
	}
	// Legacy conversion stages every service handler as one atomic adoption.
	// Empty selection is the durable all-handler representation; an explicit
	// selection belongs to ordinary reconcile and must fail before an operation
	// or conversion journal is created.
	if len(request.Operation.SelectedHandlerCapabilities) != 0 {
		return fmt.Errorf("legacy conversion does not accept selected handler capabilities")
	}
	if len(request.Operation.WorkloadHandlerAdmissions) != 0 {
		return fmt.Errorf("legacy conversion does not accept workload handler admissions")
	}
	if err := validateOrgAndOpaqueID(legacy.OrgID, "legacy PackInstallId", legacy.PackInstallID); err != nil {
		return err
	}
	if len(legacy.PackInstallID) > 255 {
		return fmt.Errorf("legacy PackInstallId must fit 255 bytes")
	}
	if err := requireDigest("legacy desired payload hash", legacy.DesiredPayloadHash); err != nil {
		return err
	}
	if err := requireDigest("legacy applied payload hash", legacy.AppliedPayloadHash); err != nil {
		return err
	}
	for label, value := range map[string]string{
		"legacy host requirements":   legacy.HostRequirementsDigest,
		"legacy Runtime declaration": legacy.RuntimeDeclarationDigest,
		"legacy deep verification":   legacy.DeepVerificationDigest,
	} {
		if err := requireDigest(label, value); err != nil {
			return err
		}
	}
	if legacy.DesiredPayloadHash != legacy.AppliedPayloadHash {
		return fmt.Errorf("%w: legacy desired and applied payloads have not converged", ErrNotReady)
	}
	for label, value := range map[string]string{
		"pack name": legacy.PackName, "category": legacy.Category, "pack version": legacy.PackVersion,
		"manifest format version": legacy.ManifestFormatVersion, "platform compatibility": legacy.PlatformCompatibility,
		"payload hash algorithm": legacy.PayloadHashAlgorithm, "source kind": legacy.SourceKind,
		"source URI": legacy.SourceURI, "source revision": legacy.SourceRevision,
	} {
		if strings.TrimSpace(value) == "" {
			return fmt.Errorf("legacy %s is required", label)
		}
	}
	if legacy.Status != "active" || legacy.IsDeleted {
		return fmt.Errorf("%w: legacy installation is not active", ErrNotReady)
	}
	mapping := request.Mapping
	legacyAuthorityDigest, err := LegacyAuthorityDigest(legacy)
	if err != nil {
		return err
	}
	if err := requireDigest("legacy authority", mapping.LegacyAuthorityDigest); err != nil {
		return err
	}
	if mapping.LegacyAuthorityDigest != legacyAuthorityDigest {
		return fmt.Errorf("verified legacy mapping does not bind the exact legacy authority facts")
	}
	if strings.TrimSpace(mapping.SignerID) == "" || len(mapping.SignerID) > 255 {
		return fmt.Errorf("verified legacy mapping signer is required and must fit 255 bytes")
	}
	if err := requireDigest("legacy mapping signature verification", mapping.SignatureVerificationDigest); err != nil {
		return err
	}
	if mapping.VerifiedAt.IsZero() || mapping.AppID != request.Operation.AppID || mapping.AppID != release.AppID ||
		mapping.TargetReleaseDigest != request.Operation.TargetAppReleaseDigest || mapping.TargetReleaseDigest != release.AppReleaseDigest {
		return fmt.Errorf("verified legacy mapping does not bind the target App release")
	}
	expectedMappingDigest, err := LegacyMappingDigest(mapping)
	if err != nil {
		return err
	}
	if mapping.MappingDigest != expectedMappingDigest {
		return fmt.Errorf("verified legacy mapping digest does not match its closed content")
	}
	if len(legacy.Components) != len(mapping.Components) || len(mapping.Components) != len(release.Package.Bundle.Components) {
		return fmt.Errorf("legacy component conversion is not complete and one-to-one")
	}
	legacyKeys := make(map[string]struct{}, len(legacy.Components))
	for i, component := range legacy.Components {
		key := component.ComponentKind + "\x00" + component.BusinessKey
		if strings.TrimSpace(component.ComponentKind) == "" || strings.TrimSpace(component.BusinessKey) == "" {
			return fmt.Errorf("legacy component[%d] kind and business key are required", i)
		}
		if err := requireDigest("legacy component content", component.ContentDigest); err != nil {
			return err
		}
		if _, duplicate := legacyKeys[key]; duplicate {
			return fmt.Errorf("legacy components contain duplicate %q", key)
		}
		if i > 0 {
			prior := legacy.Components[i-1].ComponentKind + "\x00" + legacy.Components[i-1].BusinessKey
			if key <= prior {
				return fmt.Errorf("legacy components must be strictly sorted and duplicate-free")
			}
		}
		legacyKeys[key] = struct{}{}
	}
	targets := make(map[string]appauthority.Component, len(release.Package.Bundle.Components))
	for _, component := range release.Package.Bundle.Components {
		targets[component.ComponentID] = component
	}
	seenLegacy, seenTarget := map[string]struct{}{}, map[string]struct{}{}
	for i, conversion := range mapping.Components {
		legacyKey := conversion.LegacyComponentKind + "\x00" + conversion.LegacyBusinessKey
		if i > 0 {
			prior := mapping.Components[i-1].LegacyComponentKind + "\x00" + mapping.Components[i-1].LegacyBusinessKey
			if legacyKey <= prior {
				return fmt.Errorf("legacy component mapping must be strictly sorted and duplicate-free")
			}
		}
		if _, duplicate := seenLegacy[legacyKey]; duplicate {
			return fmt.Errorf("legacy component mapping duplicates %q", legacyKey)
		}
		if _, duplicate := seenTarget[conversion.TargetComponentID]; duplicate {
			return fmt.Errorf("legacy component mapping reuses target %q", conversion.TargetComponentID)
		}
		target, found := targets[conversion.TargetComponentID]
		if !found {
			return fmt.Errorf("legacy component mapping targets an unknown component")
		}
		legacyIndex := slices.IndexFunc(legacy.Components, func(component LegacyComponent) bool {
			return component.ComponentKind == conversion.LegacyComponentKind && component.BusinessKey == conversion.LegacyBusinessKey
		})
		if legacyIndex < 0 || legacy.Components[legacyIndex].ContentDigest != target.ContentDigest {
			return fmt.Errorf("legacy component content does not match its target component")
		}
		seenLegacy[legacyKey], seenTarget[conversion.TargetComponentID] = struct{}{}, struct{}{}
	}
	serviceCapabilities := make([]appauthority.ProvidedCapability, 0)
	for _, capability := range release.Package.Manifest.ProvidedCapabilities {
		if capability.Kind == appauthority.CapabilityKindServiceHandler {
			serviceCapabilities = append(serviceCapabilities, capability)
		}
	}
	if len(request.HandlerBindings) != len(serviceCapabilities) {
		return fmt.Errorf("conversion HandlerBindings do not cover every service capability")
	}
	for i, binding := range request.HandlerBindings {
		if binding.OperationID != "" || binding.AppInstallationID != "" || binding.AppReleaseDigest != "" ||
			binding.CapabilityID != serviceCapabilities[i].CapabilityID || binding.HandlerRef != serviceCapabilities[i].HandlerRef || binding.ExpectedOwnershipEpoch != 0 {
			return fmt.Errorf("conversion HandlerBinding[%d] does not match the release or initial ownership epoch", i)
		}
		if !slices.Contains(request.Operation.AuthorizedWorkloads, binding.WorkloadID) {
			return fmt.Errorf("conversion HandlerBinding[%d] workload is not authorized", i)
		}
		if err := validateHandlerConsumerGrantAuthority(
			serviceCapabilities[i].RequiredRuntimeCapabilities,
			binding.WorkloadID,
			request.Operation.ConsumerGrants,
		); err != nil {
			return fmt.Errorf("conversion HandlerBinding[%d]: %w", i, err)
		}
		if err := requireDigest("conversion service release", binding.ServiceReleaseDigest); err != nil {
			return err
		}
		if err := requireDigest("conversion compatibility receipt", binding.CompatibilityReceiptDigest); err != nil {
			return err
		}
	}
	frontendRequired := release.Package.Manifest.FrontendCompatibility.Mode == appauthority.FrontendModeRequired
	if frontendRequired != (request.FrontendBinding != nil) {
		return fmt.Errorf("conversion frontend binding does not match the release")
	}
	if request.FrontendBinding != nil {
		if request.FrontendBinding.OperationID != "" || request.FrontendBinding.AppInstallationID != "" || request.FrontendBinding.AppReleaseDigest != "" {
			return fmt.Errorf("conversion binding target is Catalog-minted")
		}
		if err := requireDigest("conversion frontend release", request.FrontendBinding.FrontendReleaseDigest); err != nil {
			return err
		}
		if err := requireDigest("conversion frontend receipt", request.FrontendBinding.CompatibilityReceiptDigest); err != nil {
			return err
		}
	}
	return nil
}

func (a *Authority) insertConversionBindingsTx(ctx context.Context, tx *sql.Tx, operation Operation, request ConversionRequest) error {
	for _, binding := range request.HandlerBindings {
		id, err := a.newID(a.now())
		if err != nil {
			return err
		}
		naturalKey := hashKey(operation.AppInstallationID, binding.CapabilityID, "1")
		_, err = tx.ExecContext(ctx, `INSERT INTO _System_CatalogHandlerBinding
(HandlerBindingId, OrgId, AppInstallationId, AppReleaseDigest, CapabilityId, HandlerRef, WorkloadId,
ServiceReleaseDigest, OwnershipEpoch, State, CompatibilityReceiptDigest, NaturalKey, ActiveKey, OperationId,
CreatedDate, LastModifiedDate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, 'STAGED', ?, ?, NULL, ?, CURRENT_TIMESTAMP(6), CURRENT_TIMESTAMP(6))`,
			id, operation.OrgID, operation.AppInstallationID, operation.TargetAppReleaseDigest, binding.CapabilityID,
			binding.HandlerRef, binding.WorkloadID, binding.ServiceReleaseDigest, binding.CompatibilityReceiptDigest,
			naturalKey, operation.OperationID)
		if err != nil {
			if isDuplicate(err) {
				return ErrConflict
			}
			return err
		}
	}
	if request.FrontendBinding != nil {
		id, err := a.newID(a.now())
		if err != nil {
			return err
		}
		_, err = tx.ExecContext(ctx, `INSERT INTO _System_CatalogFrontendBinding
(FrontendBindingId, OrgId, AppInstallationId, AppReleaseDigest, FrontendReleaseDigest, State,
CompatibilityReceiptDigest, ActiveKey, OperationId, OperationKey, StagingKey, CreatedDate, LastModifiedDate)
VALUES (?, ?, ?, ?, ?, 'STAGED', ?, NULL, ?, ?, NULL, CURRENT_TIMESTAMP(6), CURRENT_TIMESTAMP(6))`, id, operation.OrgID,
			operation.AppInstallationID, operation.TargetAppReleaseDigest, request.FrontendBinding.FrontendReleaseDigest,
			request.FrontendBinding.CompatibilityReceiptDigest, operation.OperationID,
			hashKey(operation.OperationID))
		if err != nil {
			if isDuplicate(err) {
				return ErrConflict
			}
			return err
		}
	}
	return nil
}

func readConversionByKey(ctx context.Context, tx *sql.Tx, migrationKey string) (Conversion, bool, error) {
	var conversion Conversion
	err := tx.QueryRowContext(ctx, `SELECT c.MigrationId, c.OperationId, c.AppInstallationId, o.State, o.Step
FROM _System_CatalogCRMConversionJournal c JOIN _System_CatalogAppOperation o ON o.OperationId = c.OperationId
WHERE c.MigrationKey = ? FOR UPDATE`, migrationKey).Scan(&conversion.MigrationID, &conversion.OperationID,
		&conversion.AppInstallationID, &conversion.State, &conversion.Step)
	if errors.Is(err, sql.ErrNoRows) {
		return Conversion{}, false, nil
	}
	if err != nil {
		return Conversion{}, false, err
	}
	return conversion, true, nil
}

func (a *Authority) ReadConversion(ctx context.Context, migrationID string) (Conversion, error) {
	var conversion Conversion
	err := a.db.QueryRowContext(ctx, `SELECT c.MigrationId, c.OperationId, c.AppInstallationId, o.State, o.Step
FROM _System_CatalogCRMConversionJournal c JOIN _System_CatalogAppOperation o ON o.OperationId = c.OperationId
WHERE c.MigrationId = ?`, migrationID).Scan(&conversion.MigrationID, &conversion.OperationID,
		&conversion.AppInstallationID, &conversion.State, &conversion.Step)
	if errors.Is(err, sql.ErrNoRows) {
		return Conversion{}, ErrNotFound
	}
	if err != nil {
		return Conversion{}, err
	}
	return conversion, nil
}
