package catalogverificationtx

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"strconv"
	"strings"

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

var (
	ErrAppOwnerNotFound = errors.New("catalog verification transaction: App owner fact not found")
	ErrAppOwnerConflict = errors.New("catalog verification transaction: App owner fact conflict")
	ErrAppOwnerCorrupt  = errors.New("catalog verification transaction: App owner durable state is corrupt")
	ErrClaimOrder       = errors.New("catalog verification transaction: owner claim order conflict")
)

// AppOwnerRequest is the exact source-selected App tuple and required key.
// Only catalogpublication can obtain it from a locked source claim.
type AppOwnerRequest struct {
	OrgID             string
	AppID             string
	AppInstallationID string
	AppReleaseDigest  string
	LocalComponentKey layoutbundlekeyv1.LocalComponentKey
}

// AppOwnerFact is the validated immutable fact selected by one AppRelease.
// CatalogRevision and ActivationEpoch are retained as the authority fact
// version bound by the live transaction even though they are not evidence
// fields.
type AppOwnerFact struct {
	AppInstallationID              string
	AppReleaseDigest               string
	DeclarationComponentID         string
	DefinitionVersion              uint64
	LayoutBundleSlotRevisionDigest string
	CatalogRevision                uint64
	ActivationEpoch                uint64
}

type appInstallationRow struct {
	id               string
	orgID            string
	appID            string
	state            string
	activeRelease    sql.NullString
	candidateRelease sql.NullString
	activeOperation  sql.NullString
	liveAppKey       sql.NullString
	catalogRevision  uint64
	activationEpoch  uint64
}

// VerifyAppOwner holds the exact App installation, release, slot reservation,
// and complete slot lineage under current-read locks in the retained source
// transaction. It exposes no general query or callback seam.
func VerifyAppOwner(
	ctx context.Context,
	token *Token,
	db *sql.DB,
	request AppOwnerRequest,
) (AppOwnerFact, error) {
	if err := validateAppOwnerRequest(request); err != nil {
		return AppOwnerFact{}, err
	}
	tx, err := token.acquire(db)
	if err != nil {
		return AppOwnerFact{}, err
	}
	defer token.release()

	installation, err := lockExactActiveAppInstallation(ctx, tx, request)
	if err != nil {
		return AppOwnerFact{}, err
	}
	if err := lockExactAppRelease(ctx, tx, request.AppID, request.AppReleaseDigest); err != nil {
		return AppOwnerFact{}, err
	}
	selectedVersion, err := lockExactReleaseSlot(
		ctx,
		tx,
		request.AppID,
		request.AppReleaseDigest,
		request.LocalComponentKey,
	)
	if err != nil {
		return AppOwnerFact{}, err
	}
	lineage, err := lockAppSlotLineage(
		ctx,
		tx,
		request.AppID,
		request.LocalComponentKey,
	)
	if err != nil {
		return AppOwnerFact{}, err
	}
	if selectedVersion == 0 || selectedVersion > uint64(len(lineage.Revisions)) {
		return AppOwnerFact{}, fmt.Errorf(
			"%w: selected AppRelease slot version is absent from its lineage",
			ErrAppOwnerCorrupt,
		)
	}
	selected := lineage.Revisions[selectedVersion-1]
	return AppOwnerFact{
		AppInstallationID:              installation.id,
		AppReleaseDigest:               request.AppReleaseDigest,
		DeclarationComponentID:         lineage.DeclarationComponentID,
		DefinitionVersion:              selected.DefinitionVersion,
		LayoutBundleSlotRevisionDigest: selected.RevisionDigest,
		CatalogRevision:                installation.catalogRevision,
		ActivationEpoch:                installation.activationEpoch,
	}, nil
}

// VerifyAppOwnerClaim is the sole bridge from an opaque source App claim to
// the fixed-purpose App authority query. The claim never exposes its retained
// transaction or request.
func VerifyAppOwnerClaim(
	ctx context.Context,
	db *sql.DB,
	claim *AppClaim,
) (AppOwnerFact, error) {
	token, request, err := claim.acquireInput()
	if err != nil {
		return AppOwnerFact{}, err
	}
	fact, err := VerifyAppOwner(ctx, token, db, request)
	if err != nil {
		claim.Abort()
		return AppOwnerFact{}, err
	}
	if err := claim.lease.markClaimVerified(claim.lifecycle); err != nil {
		claim.Abort()
		return AppOwnerFact{}, err
	}
	return fact, nil
}

func validateAppOwnerRequest(request AppOwnerRequest) error {
	if strings.TrimSpace(request.OrgID) == "" || len(request.OrgID) > 255 {
		return fmt.Errorf("%w: OrgID is invalid", ErrAppOwnerConflict)
	}
	if err := appauthority.ValidateAppID(request.AppID); err != nil {
		return fmt.Errorf("%w: AppID: %v", ErrAppOwnerConflict, err)
	}
	if err := appauthority.ValidateAppInstallationID(request.AppInstallationID); err != nil {
		return fmt.Errorf("%w: AppInstallationID: %v", ErrAppOwnerConflict, err)
	}
	if err := appauthority.ValidateDigest(request.AppReleaseDigest); err != nil {
		return fmt.Errorf("%w: AppReleaseDigest: %v", ErrAppOwnerConflict, err)
	}
	if err := layoutbundlekeyv1.ValidateLocalComponentKey(request.LocalComponentKey); err != nil {
		return fmt.Errorf("%w: LocalComponentKey: %v", ErrAppOwnerConflict, err)
	}
	return nil
}

func lockExactActiveAppInstallation(
	ctx context.Context,
	tx *sql.Tx,
	request AppOwnerRequest,
) (appInstallationRow, error) {
	// The retained source transaction already owns the org writer row. App
	// lifecycle changes that add or remove a non-removed installation must
	// acquire that row, so it stabilizes set membership while these row locks
	// stabilize every existing member. Read the complete set: LiveAppKey alone
	// cannot prove uniqueness because NULL values do not conflict in its unique
	// index.
	rows, err := tx.QueryContext(ctx, `SELECT AppInstallationId, OrgId, AppId, State,
ActiveReleaseDigest, CandidateReleaseDigest, CatalogRevision, ActivationEpoch,
ActiveOperationId, LiveAppKey
FROM _System_CatalogAppInstallation
WHERE OrgId = ? AND AppId = ? AND State <> 'REMOVED'
ORDER BY AppInstallationId LIMIT 2 FOR UPDATE`,
		request.OrgID,
		request.AppID,
	)
	if err != nil {
		return appInstallationRow{}, err
	}
	defer rows.Close()

	installations := make([]appInstallationRow, 0, 2)
	for rows.Next() {
		row, err := scanAppInstallationRow(rows)
		if err != nil {
			return appInstallationRow{}, err
		}
		installations = append(installations, row)
	}
	if err := rows.Err(); err != nil {
		return appInstallationRow{}, err
	}
	switch len(installations) {
	case 0:
		return appInstallationRow{}, ErrAppOwnerNotFound
	case 1:
	default:
		return appInstallationRow{}, fmt.Errorf(
			"%w: OrgID/AppID has multiple non-removed App installations",
			ErrAppOwnerCorrupt,
		)
	}
	row := installations[0]
	if row.id != request.AppInstallationID ||
		row.orgID != request.OrgID ||
		row.appID != request.AppID {
		return appInstallationRow{}, fmt.Errorf(
			"%w: source-selected App installation identity differs",
			ErrAppOwnerConflict,
		)
	}
	if row.state != "ACTIVE" ||
		!row.activeRelease.Valid ||
		row.activeRelease.String != request.AppReleaseDigest ||
		row.candidateRelease.Valid ||
		row.activeOperation.Valid ||
		!row.liveAppKey.Valid ||
		row.liveAppKey.String != appLiveKey(request.OrgID, request.AppID) {
		return appInstallationRow{}, fmt.Errorf(
			"%w: source-selected App tuple is not the exact quiescent active tuple",
			ErrAppOwnerConflict,
		)
	}
	return row, nil
}

func scanAppInstallationRow(rows *sql.Rows) (appInstallationRow, error) {
	var row appInstallationRow
	var rawCatalogRevision, rawActivationEpoch []byte
	if err := rows.Scan(
		&row.id,
		&row.orgID,
		&row.appID,
		&row.state,
		&row.activeRelease,
		&row.candidateRelease,
		&rawCatalogRevision,
		&rawActivationEpoch,
		&row.activeOperation,
		&row.liveAppKey,
	); err != nil {
		return appInstallationRow{}, err
	}
	var err error
	row.catalogRevision, err = parseUint(
		"App installation CatalogRevision",
		rawCatalogRevision,
		true,
	)
	if err != nil {
		return appInstallationRow{}, err
	}
	row.activationEpoch, err = parseUint(
		"App installation ActivationEpoch",
		rawActivationEpoch,
		false,
	)
	if err != nil {
		return appInstallationRow{}, err
	}
	return row, nil
}

func lockExactAppRelease(
	ctx context.Context,
	tx *sql.Tx,
	appID string,
	releaseDigest string,
) error {
	var storedAppID, storedReleaseDigest string
	err := tx.QueryRowContext(ctx, `SELECT AppId, AppReleaseDigest
FROM _System_CatalogAppRelease
	WHERE AppId = ? AND AppReleaseDigest = ? FOR UPDATE`, appID, releaseDigest).
		Scan(&storedAppID, &storedReleaseDigest)
	if errors.Is(err, sql.ErrNoRows) {
		return ErrAppOwnerNotFound
	}
	if err != nil {
		return err
	}
	if storedAppID != appID || storedReleaseDigest != releaseDigest {
		return fmt.Errorf(
			"%w: AppRelease durable identity differs from its exact lookup",
			ErrAppOwnerCorrupt,
		)
	}
	return nil
}

func lockExactReleaseSlot(
	ctx context.Context,
	tx *sql.Tx,
	appID string,
	releaseDigest string,
	localKey layoutbundlekeyv1.LocalComponentKey,
) (uint64, error) {
	var storedAppID, storedReleaseDigest, storedLocalKey string
	var rawVersion []byte
	err := tx.QueryRowContext(ctx, `SELECT AppId, AppReleaseDigest, LocalComponentKey, DefinitionVersion
FROM _System_CatalogAppReleaseLayoutBundleSlot
	WHERE AppId = ? AND AppReleaseDigest = ? AND LocalComponentKey = ? FOR UPDATE`,
		appID, releaseDigest, localKey,
	).Scan(&storedAppID, &storedReleaseDigest, &storedLocalKey, &rawVersion)
	if errors.Is(err, sql.ErrNoRows) {
		return 0, ErrAppOwnerNotFound
	}
	if err != nil {
		return 0, err
	}
	if storedAppID != appID ||
		storedReleaseDigest != releaseDigest ||
		storedLocalKey != string(localKey) {
		return 0, fmt.Errorf(
			"%w: AppRelease slot durable identity differs from its exact lookup",
			ErrAppOwnerCorrupt,
		)
	}
	return parseUint("AppRelease slot DefinitionVersion", rawVersion, false)
}

func lockAppSlotLineage(
	ctx context.Context,
	tx *sql.Tx,
	appID string,
	localKey layoutbundlekeyv1.LocalComponentKey,
) (layoutbundleslotv1.Lineage, error) {
	var storedAppID, storedLocalKey, declarationComponentID, namespaceID, headRevisionDigest string
	var rawHeadVersion []byte
	err := tx.QueryRowContext(ctx, `SELECT AppId, LocalComponentKey, DeclarationComponentId,
NamespaceId, HeadDefinitionVersion, HeadRevisionDigest
FROM _System_CatalogAppLayoutBundleSlot
	WHERE AppId = ? AND LocalComponentKey = ? FOR UPDATE`, appID, localKey).
		Scan(
			&storedAppID,
			&storedLocalKey,
			&declarationComponentID,
			&namespaceID,
			&rawHeadVersion,
			&headRevisionDigest,
		)
	if errors.Is(err, sql.ErrNoRows) {
		return layoutbundleslotv1.Lineage{}, ErrAppOwnerNotFound
	}
	if err != nil {
		return layoutbundleslotv1.Lineage{}, err
	}
	if storedAppID != appID || storedLocalKey != string(localKey) {
		return layoutbundleslotv1.Lineage{}, fmt.Errorf(
			"%w: App slot reservation durable identity differs from its exact lookup",
			ErrAppOwnerCorrupt,
		)
	}
	headVersion, err := parseUint("App slot HeadDefinitionVersion", rawHeadVersion, false)
	if err != nil {
		return layoutbundleslotv1.Lineage{}, err
	}

	rows, err := tx.QueryContext(ctx, `SELECT AppId, LocalComponentKey, DefinitionVersion,
RevisionDigest, TargetComponentId, PreviousTargetComponentId
FROM _System_CatalogAppLayoutBundleSlotVersion
WHERE AppId = ? AND LocalComponentKey = ?
	ORDER BY DefinitionVersion FOR UPDATE`, 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 slot version durable identity differs from its exact lookup",
				ErrAppOwnerCorrupt,
			)
		}
		revision.DefinitionVersion, err = parseUint("App slot DefinitionVersion", rawVersion, false)
		if err != nil {
			return layoutbundleslotv1.Lineage{}, 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: App slot lineage is invalid: %v",
			ErrAppOwnerCorrupt,
			err,
		)
	}
	head := lineage.Revisions[len(lineage.Revisions)-1]
	if head.DefinitionVersion != headVersion || head.RevisionDigest != headRevisionDigest {
		return layoutbundleslotv1.Lineage{}, fmt.Errorf(
			"%w: App slot head differs from its version ledger",
			ErrAppOwnerCorrupt,
		)
	}
	return lineage, nil
}

func parseUint(label string, raw []byte, allowZero bool) (uint64, error) {
	if len(raw) == 0 {
		return 0, fmt.Errorf("%w: %s is empty", ErrAppOwnerCorrupt, label)
	}
	for _, character := range raw {
		if character < '0' || character > '9' {
			return 0, fmt.Errorf("%w: %s is not canonical decimal", ErrAppOwnerCorrupt, label)
		}
	}
	value, err := strconv.ParseUint(string(raw), 10, 64)
	if err != nil ||
		strconv.FormatUint(value, 10) != string(raw) ||
		(!allowZero && value == 0) {
		return 0, fmt.Errorf("%w: %s is not canonical decimal", ErrAppOwnerCorrupt, label)
	}
	return value, nil
}

func appLiveKey(orgID, appID string) string {
	hash := sha256.New()
	for _, part := range []string{orgID, appID} {
		_, _ = hash.Write([]byte{0})
		_, _ = hash.Write([]byte(part))
	}
	return hex.EncodeToString(hash.Sum(nil))
}
