package catalogapp

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"sort"
	"strconv"

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

// AppLayoutBundleSlotBinding is the immutable binding from one AppRelease to
// one already registered global App-owned slot definition version.
type AppLayoutBundleSlotBinding struct {
	AppID                     string
	AppReleaseDigest          string
	LocalComponentKey         layoutbundlekeyv1.LocalComponentKey
	DeclarationComponentID    string
	NamespaceID               string
	DefinitionVersion         uint64
	RevisionDigest            string
	TargetComponentID         string
	PreviousTargetComponentID *string
}

type transactionBeginner interface {
	BeginTx(context.Context, *sql.TxOptions) (*sql.Tx, error)
}

// beginCatalogSnapshotRead opens one transaction for a multi-query consistency
// check without asking the driver to enforce read-only mode. Read-only is a
// property of the Catalog code below: it issues only SELECT statements. The
// transaction is still required so the release binding and every lineage read
// observe one database snapshot. TiDB rejects database/sql's ReadOnly option
// unless its no-op compatibility functions are enabled, so that option cannot
// be part of the portable Catalog mechanism.
func beginCatalogSnapshotRead(ctx context.Context, db transactionBeginner) (*sql.Tx, error) {
	return db.BeginTx(ctx, nil)
}

// ReadLayoutBundleSlotLineage returns one complete validated global App-slot
// lineage. It never infers a current version from AppRelease or installation
// order; the returned revision slice is an independent copy.
func (a *Authority) ReadLayoutBundleSlotLineage(ctx context.Context, appID string, localKey layoutbundlekeyv1.LocalComponentKey) (layoutbundleslotv1.Lineage, error) {
	if err := appauthority.ValidateAppID(appID); err != nil {
		return layoutbundleslotv1.Lineage{}, err
	}
	if err := layoutbundlekeyv1.ValidateLocalComponentKey(localKey); err != nil {
		return layoutbundleslotv1.Lineage{}, err
	}
	tx, err := beginCatalogSnapshotRead(ctx, a.db)
	if err != nil {
		return layoutbundleslotv1.Lineage{}, err
	}
	defer rollback(tx)
	lineage, err := readLayoutBundleSlotLineageTx(ctx, tx, appID, localKey, false)
	if err != nil {
		return layoutbundleslotv1.Lineage{}, err
	}
	if err := tx.Commit(); err != nil {
		return layoutbundleslotv1.Lineage{}, err
	}
	return lineage, nil
}

// ReadReleaseLayoutBundleSlots returns the complete App-owned slot binding set
// for one registered immutable AppRelease, sorted by LocalComponentKey. A
// registered release with no declarations returns a present empty slice.
func (a *Authority) ReadReleaseLayoutBundleSlots(ctx context.Context, appID, appReleaseDigest string) ([]AppLayoutBundleSlotBinding, error) {
	if err := appauthority.ValidateAppID(appID); err != nil {
		return nil, err
	}
	if err := requireDigest("AppReleaseDigest", appReleaseDigest); err != nil {
		return nil, err
	}
	tx, err := beginCatalogSnapshotRead(ctx, a.db)
	if err != nil {
		return nil, err
	}
	defer rollback(tx)
	var storedAppID, storedDigest string
	if err := tx.QueryRowContext(ctx, `SELECT AppId, AppReleaseDigest FROM _System_CatalogAppRelease
WHERE AppId = ? AND AppReleaseDigest = ?`, appID, appReleaseDigest).Scan(&storedAppID, &storedDigest); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	if storedAppID != appID || storedDigest != appReleaseDigest {
		return nil, fmt.Errorf("%w: AppRelease durable identity does not exactly match its lookup", ErrCorrupt)
	}
	bindings, err := readReleaseLayoutBundleSlotsTx(ctx, tx, appID, appReleaseDigest)
	if err != nil {
		return nil, err
	}
	for _, binding := range bindings {
		lineage, err := readLayoutBundleSlotLineageTx(ctx, tx, appID, binding.LocalComponentKey, false)
		if err != nil {
			return nil, fmt.Errorf("%w: AppRelease layout-bundle slot lineage cannot be verified: %v", ErrCorrupt, err)
		}
		index := binding.DefinitionVersion - 1
		if index >= uint64(len(lineage.Revisions)) {
			return nil, fmt.Errorf("%w: AppRelease layout-bundle slot version is absent from its lineage", ErrCorrupt)
		}
		revision := lineage.Revisions[index]
		if revision.RevisionDigest != binding.RevisionDigest || revision.TargetComponentID != binding.TargetComponentID ||
			!equalOptionalString(revision.PreviousTargetComponentID, binding.PreviousTargetComponentID) {
			return nil, fmt.Errorf("%w: AppRelease layout-bundle slot binding differs from its lineage", ErrCorrupt)
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return bindings, nil
}

func registerReleaseLayoutBundleSlotsTx(ctx context.Context, tx *sql.Tx, appID, appReleaseDigest string, declarations []layoutbundleslotv1.Declaration) error {
	ordered := append([]layoutbundleslotv1.Declaration(nil), declarations...)
	sort.Slice(ordered, func(left, right int) bool {
		return ordered[left].LocalComponentKey < ordered[right].LocalComponentKey
	})
	for index, declaration := range ordered {
		if declaration.AppID != appID {
			return fmt.Errorf("layout-bundle slot declaration[%d] belongs to another App", index)
		}
		var current *layoutbundleslotv1.Lineage
		lineage, err := readLayoutBundleSlotLineageTx(ctx, tx, appID, declaration.LocalComponentKey, true)
		switch {
		case err == nil:
			current = &lineage
		case errors.Is(err, ErrNotFound):
		default:
			return err
		}
		transition, err := layoutbundleslotv1.EvaluateTransition(current, declaration)
		if err != nil {
			return fmt.Errorf("%w: App layout-bundle slot %s: %v", ErrConflict, declaration.LocalComponentKey, err)
		}
		switch transition.Kind {
		case layoutbundleslotv1.TransitionInitial:
			if _, err := tx.ExecContext(ctx, `INSERT INTO _System_CatalogAppLayoutBundleSlot
(AppId, LocalComponentKey, DeclarationComponentId, NamespaceId, HeadDefinitionVersion, HeadRevisionDigest, CreatedDate, LastModifiedDate)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP(6), CURRENT_TIMESTAMP(6))`, declaration.AppID, declaration.LocalComponentKey,
				declaration.DeclarationComponentID, declaration.NamespaceID, epochText(declaration.DefinitionVersion), declaration.RevisionDigest); err != nil {
				if isDuplicate(err) {
					return ErrConflict
				}
				return err
			}
			if err := insertLayoutBundleSlotVersionTx(ctx, tx, declaration.AppID, declaration.LocalComponentKey, transition.Candidate); err != nil {
				return err
			}
		case layoutbundleslotv1.TransitionExactReuse:
			// Historical reuse is a release binding only. It must not move the
			// global head or rewrite the immutable version row.
		case layoutbundleslotv1.TransitionAppendSuccessor:
			result, err := tx.ExecContext(ctx, `UPDATE _System_CatalogAppLayoutBundleSlot
SET HeadDefinitionVersion = ?, HeadRevisionDigest = ?, LastModifiedDate = CURRENT_TIMESTAMP(6)
WHERE AppId = ? AND LocalComponentKey = ? AND HeadDefinitionVersion = ? AND HeadRevisionDigest = ?`,
				epochText(declaration.DefinitionVersion), declaration.RevisionDigest, declaration.AppID, declaration.LocalComponentKey,
				epochText(transition.ExpectedHeadVersion), transition.ExpectedHeadRevisionDigest)
			if err != nil {
				return err
			}
			affected, err := result.RowsAffected()
			if err != nil {
				return err
			}
			if affected != 1 {
				return ErrConflict
			}
			if err := insertLayoutBundleSlotVersionTx(ctx, tx, declaration.AppID, declaration.LocalComponentKey, transition.Candidate); err != nil {
				return err
			}
		default:
			return fmt.Errorf("unknown layout-bundle slot transition %q", transition.Kind)
		}
		if _, err := tx.ExecContext(ctx, `INSERT INTO _System_CatalogAppReleaseLayoutBundleSlot
(AppId, AppReleaseDigest, LocalComponentKey, DefinitionVersion, CreatedDate)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP(6))`, appID, appReleaseDigest, declaration.LocalComponentKey, epochText(declaration.DefinitionVersion)); err != nil {
			if isDuplicate(err) {
				return ErrConflict
			}
			return err
		}
	}
	return nil
}

func insertLayoutBundleSlotVersionTx(ctx context.Context, tx *sql.Tx, appID string, localKey layoutbundlekeyv1.LocalComponentKey, revision layoutbundleslotv1.AcceptedRevision) error {
	var previous any
	if revision.PreviousTargetComponentID != nil {
		previous = *revision.PreviousTargetComponentID
	}
	_, err := tx.ExecContext(ctx, `INSERT INTO _System_CatalogAppLayoutBundleSlotVersion
(AppId, LocalComponentKey, DefinitionVersion, RevisionDigest, TargetComponentId, PreviousTargetComponentId, CreatedDate)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP(6))`, appID, localKey, epochText(revision.DefinitionVersion), revision.RevisionDigest,
		revision.TargetComponentID, previous)
	if isDuplicate(err) {
		return ErrConflict
	}
	return err
}

func verifyReleaseLayoutBundleSlotsTx(ctx context.Context, tx *sql.Tx, appID, appReleaseDigest string, expected []layoutbundleslotv1.Declaration) error {
	bindings, err := readReleaseLayoutBundleSlotsTx(ctx, tx, appID, appReleaseDigest)
	if err != nil {
		return err
	}
	ordered, err := compareReleaseLayoutBundleSlotBindings(bindings, appID, appReleaseDigest, expected)
	if err != nil {
		return err
	}
	for _, declaration := range ordered {
		lineage, err := readLayoutBundleSlotLineageTx(ctx, tx, appID, declaration.LocalComponentKey, true)
		if err != nil {
			return fmt.Errorf("%w: stored AppRelease layout-bundle slot lineage cannot be verified: %v", ErrCorrupt, err)
		}
		transition, err := layoutbundleslotv1.EvaluateTransition(&lineage, declaration)
		if err != nil || transition.Kind != layoutbundleslotv1.TransitionExactReuse {
			return fmt.Errorf("%w: stored AppRelease layout-bundle slot is not an exact registered lineage revision", ErrCorrupt)
		}
	}
	return nil
}

func compareReleaseLayoutBundleSlotBindings(bindings []AppLayoutBundleSlotBinding, appID, appReleaseDigest string, expected []layoutbundleslotv1.Declaration) ([]layoutbundleslotv1.Declaration, error) {
	ordered := append([]layoutbundleslotv1.Declaration(nil), expected...)
	sort.Slice(ordered, func(left, right int) bool {
		return ordered[left].LocalComponentKey < ordered[right].LocalComponentKey
	})
	if len(bindings) != len(ordered) {
		return nil, fmt.Errorf("%w: stored AppRelease layout-bundle slot set is incomplete", ErrCorrupt)
	}
	for index, declaration := range ordered {
		binding := bindings[index]
		if declaration.AppID != appID || binding.AppID != declaration.AppID || binding.AppReleaseDigest != appReleaseDigest ||
			binding.LocalComponentKey != declaration.LocalComponentKey ||
			binding.DeclarationComponentID != declaration.DeclarationComponentID || binding.NamespaceID != declaration.NamespaceID ||
			binding.DefinitionVersion != declaration.DefinitionVersion || binding.RevisionDigest != declaration.RevisionDigest ||
			binding.TargetComponentID != declaration.TargetComponentID ||
			!equalOptionalString(binding.PreviousTargetComponentID, declaration.PreviousTargetComponentID) {
			return nil, fmt.Errorf("%w: stored AppRelease layout-bundle slot binding differs at index %d", ErrCorrupt, index)
		}
	}
	return ordered, nil
}

func readLayoutBundleSlotLineageTx(ctx context.Context, tx *sql.Tx, appID string, localKey layoutbundlekeyv1.LocalComponentKey, forUpdate bool) (layoutbundleslotv1.Lineage, error) {
	reservationQuery := `SELECT AppId, LocalComponentKey, DeclarationComponentId, NamespaceId, HeadDefinitionVersion, HeadRevisionDigest
FROM _System_CatalogAppLayoutBundleSlot WHERE AppId = ? AND LocalComponentKey = ?`
	if forUpdate {
		reservationQuery += " FOR UPDATE"
	}
	var storedAppID, storedLocalKey, declarationComponentID, namespaceID, headRevisionDigest string
	var rawHeadVersion []byte
	if err := tx.QueryRowContext(ctx, reservationQuery, appID, localKey).Scan(&storedAppID, &storedLocalKey, &declarationComponentID, &namespaceID, &rawHeadVersion, &headRevisionDigest); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return layoutbundleslotv1.Lineage{}, ErrNotFound
		}
		return layoutbundleslotv1.Lineage{}, err
	}
	if storedAppID != appID || storedLocalKey != string(localKey) {
		return layoutbundleslotv1.Lineage{}, fmt.Errorf("%w: App layout-bundle slot durable identity does not exactly match its lookup", ErrCorrupt)
	}
	headVersion, err := parseSlotDefinitionVersion(rawHeadVersion)
	if err != nil {
		return layoutbundleslotv1.Lineage{}, fmt.Errorf("%w: stored App layout-bundle slot head: %v", ErrCorrupt, err)
	}
	versionQuery := `SELECT AppId, LocalComponentKey, DefinitionVersion, RevisionDigest, TargetComponentId, PreviousTargetComponentId
FROM _System_CatalogAppLayoutBundleSlotVersion
WHERE AppId = ? AND LocalComponentKey = ? ORDER BY DefinitionVersion`
	if forUpdate {
		versionQuery += " FOR UPDATE"
	}
	rows, err := tx.QueryContext(ctx, versionQuery, appID, localKey)
	if err != nil {
		return layoutbundleslotv1.Lineage{}, err
	}
	defer rows.Close()
	lineage := layoutbundleslotv1.Lineage{
		AppID: appID, NamespaceID: namespaceID, LocalComponentKey: localKey,
		DeclarationComponentID: declarationComponentID,
		Revisions:              []layoutbundleslotv1.AcceptedRevision{},
	}
	for rows.Next() {
		var versionAppID, versionLocalKey string
		var rawVersion []byte
		var revision layoutbundleslotv1.AcceptedRevision
		var previous sql.NullString
		if err := rows.Scan(&versionAppID, &versionLocalKey, &rawVersion, &revision.RevisionDigest, &revision.TargetComponentID, &previous); err != nil {
			return layoutbundleslotv1.Lineage{}, err
		}
		if versionAppID != appID || versionLocalKey != string(localKey) {
			return layoutbundleslotv1.Lineage{}, fmt.Errorf("%w: App layout-bundle slot version durable identity does not exactly match its lookup", ErrCorrupt)
		}
		revision.DefinitionVersion, err = parseSlotDefinitionVersion(rawVersion)
		if err != nil {
			return layoutbundleslotv1.Lineage{}, fmt.Errorf("%w: stored App layout-bundle slot version: %v", ErrCorrupt, err)
		}
		if previous.Valid {
			value := previous.String
			revision.PreviousTargetComponentID = &value
		}
		lineage.Revisions = append(lineage.Revisions, revision)
	}
	if err := rows.Err(); err != nil {
		return layoutbundleslotv1.Lineage{}, err
	}
	if err := layoutbundleslotv1.ValidateLineage(lineage); err != nil {
		return layoutbundleslotv1.Lineage{}, fmt.Errorf("%w: stored App layout-bundle slot lineage is invalid: %v", ErrCorrupt, err)
	}
	head := lineage.Revisions[len(lineage.Revisions)-1]
	if head.DefinitionVersion != headVersion || head.RevisionDigest != headRevisionDigest {
		return layoutbundleslotv1.Lineage{}, fmt.Errorf("%w: stored App layout-bundle slot head does not match its version ledger", ErrCorrupt)
	}
	return lineage, nil
}

func readReleaseLayoutBundleSlotsTx(ctx context.Context, tx *sql.Tx, appID, appReleaseDigest string) ([]AppLayoutBundleSlotBinding, error) {
	rows, err := tx.QueryContext(ctx, `SELECT b.AppId, b.AppReleaseDigest, b.LocalComponentKey, b.DefinitionVersion,
s.DeclarationComponentId, s.NamespaceId, v.RevisionDigest, v.TargetComponentId, v.PreviousTargetComponentId
FROM _System_CatalogAppReleaseLayoutBundleSlot b
LEFT JOIN _System_CatalogAppLayoutBundleSlot s ON s.AppId = b.AppId AND s.LocalComponentKey = b.LocalComponentKey
LEFT JOIN _System_CatalogAppLayoutBundleSlotVersion v ON v.AppId = b.AppId AND v.LocalComponentKey = b.LocalComponentKey AND v.DefinitionVersion = b.DefinitionVersion
WHERE b.AppId = ? AND b.AppReleaseDigest = ? ORDER BY b.LocalComponentKey`, appID, appReleaseDigest)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	bindings := []AppLayoutBundleSlotBinding{}
	for rows.Next() {
		var binding AppLayoutBundleSlotBinding
		var storedAppID, storedReleaseDigest, localKey string
		var rawVersion []byte
		var declarationComponentID, namespaceID, revisionDigest, targetComponentID, previous sql.NullString
		if err := rows.Scan(&storedAppID, &storedReleaseDigest, &localKey, &rawVersion, &declarationComponentID, &namespaceID,
			&revisionDigest, &targetComponentID, &previous); err != nil {
			return nil, err
		}
		if storedAppID != appID || storedReleaseDigest != appReleaseDigest {
			return nil, fmt.Errorf("%w: AppRelease layout-bundle slot durable identity does not exactly match its lookup", ErrCorrupt)
		}
		if !declarationComponentID.Valid || !namespaceID.Valid || !revisionDigest.Valid || !targetComponentID.Valid {
			return nil, fmt.Errorf("%w: AppRelease layout-bundle slot binding has no complete reservation and version", ErrCorrupt)
		}
		binding.AppID, binding.AppReleaseDigest = storedAppID, storedReleaseDigest
		binding.LocalComponentKey = layoutbundlekeyv1.LocalComponentKey(localKey)
		binding.DeclarationComponentID, binding.NamespaceID = declarationComponentID.String, namespaceID.String
		binding.RevisionDigest, binding.TargetComponentID = revisionDigest.String, targetComponentID.String
		binding.DefinitionVersion, err = parseSlotDefinitionVersion(rawVersion)
		if err != nil {
			return nil, fmt.Errorf("%w: stored AppRelease layout-bundle slot version: %v", ErrCorrupt, err)
		}
		if previous.Valid {
			value := previous.String
			binding.PreviousTargetComponentID = &value
		}
		if err := validateStoredLayoutBundleSlotBinding(binding); err != nil {
			return nil, fmt.Errorf("%w: %v", ErrCorrupt, err)
		}
		bindings = append(bindings, binding)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return bindings, nil
}

func validateStoredLayoutBundleSlotBinding(binding AppLayoutBundleSlotBinding) error {
	if err := layoutbundlekeyv1.ValidateLocalComponentKey(binding.LocalComponentKey); err != nil {
		return fmt.Errorf("stored App layout-bundle slot key: %w", err)
	}
	if err := appauthority.ValidateNamespaceID(binding.NamespaceID); err != nil {
		return fmt.Errorf("stored App layout-bundle slot namespace: %w", err)
	}
	expectedComponentID := appauthority.ComponentID(binding.NamespaceID, layoutbundleslotv1.ComponentKind, string(binding.LocalComponentKey))
	if binding.DeclarationComponentID != expectedComponentID {
		return fmt.Errorf("stored App layout-bundle slot declaration identity is invalid")
	}
	for label, digest := range map[string]string{
		"revision": binding.RevisionDigest,
		"target":   binding.TargetComponentID,
	} {
		if err := appauthority.ValidateDigest(digest); err != nil {
			return fmt.Errorf("stored App layout-bundle slot %s digest: %w", label, err)
		}
	}
	if binding.DefinitionVersion == 1 && binding.PreviousTargetComponentID != nil {
		return fmt.Errorf("stored App layout-bundle slot definition version 1 has a predecessor")
	}
	if binding.DefinitionVersion > 1 {
		if binding.PreviousTargetComponentID == nil {
			return fmt.Errorf("stored App layout-bundle slot successor has no predecessor")
		}
		if err := appauthority.ValidateDigest(*binding.PreviousTargetComponentID); err != nil {
			return fmt.Errorf("stored App layout-bundle slot predecessor digest: %w", err)
		}
		if *binding.PreviousTargetComponentID == binding.TargetComponentID {
			return fmt.Errorf("stored App layout-bundle slot successor does not change target identity")
		}
	}
	return nil
}

func parseSlotDefinitionVersion(raw []byte) (uint64, error) {
	if len(raw) == 0 {
		return 0, fmt.Errorf("invalid layout-bundle slot definition version")
	}
	for _, character := range raw {
		if character < '0' || character > '9' {
			return 0, fmt.Errorf("invalid layout-bundle slot definition version")
		}
	}
	value, err := strconv.ParseUint(string(raw), 10, 64)
	if err != nil || value == 0 || strconv.FormatUint(value, 10) != string(raw) {
		return 0, fmt.Errorf("invalid layout-bundle slot definition version")
	}
	return value, nil
}

func equalOptionalString(left, right *string) bool {
	if left == nil || right == nil {
		return left == right
	}
	return *left == *right
}
