package catalogapp

import (
	"context"
	"errors"
	"fmt"
)

const operationJournalStepMetadataSelectionCommitted = "METADATA_SELECTION_COMMITTED"

// CommitOperationMetadataSelection binds the first durable CatalogSnapshot
// selected by one lifecycle operation. It deliberately does not advance
// Operation.Step: derived view/receipt publication and HandlerBinding staging
// remain subsequent recoverable steps. The snapshot bytes must already be in
// the content-addressed store, so every committed selection is recoverable
// without reopening live metadata.
func (a *Authority) CommitOperationMetadataSelection(ctx context.Context, operationID, catalogSnapshotDigest string) (string, error) {
	return a.commitOperationMetadataSelection(
		ctx, operationID, catalogSnapshotDigest, nil,
	)
}

// CommitOperationMetadataSelectionWithEffectiveComponents atomically binds
// the CatalogSnapshot and the complete Catalog-derived consumer projection.
// A v2 consumer can therefore never observe a selected snapshot without its
// exact effective component set, including after a process crash.
func (a *Authority) CommitOperationMetadataSelectionWithEffectiveComponents(
	ctx context.Context,
	operationID string,
	catalogSnapshotDigest string,
	components []EffectiveMetadataComponent,
) (string, error) {
	return a.commitOperationMetadataSelection(
		ctx, operationID, catalogSnapshotDigest, components,
	)
}

func (a *Authority) commitOperationMetadataSelection(
	ctx context.Context,
	operationID string,
	catalogSnapshotDigest string,
	components []EffectiveMetadataComponent,
) (string, error) {
	if err := requireDigest("Catalog snapshot selection", catalogSnapshotDigest); err != nil {
		return "", err
	}
	tx, err := a.db.BeginTx(ctx, nil)
	if err != nil {
		return "", err
	}
	defer rollback(tx)
	operation, err := a.readOperation(ctx, tx, operationID, true)
	if err != nil {
		return "", err
	}
	if operation.Kind == OperationRemove || operation.Step != "CANDIDATE_COMMITTED" ||
		operation.State == StateFailed || operation.State == StateRemoved {
		return "", ErrNotReady
	}
	committed, err := readOperationMetadataSelection(ctx, tx, operationID, true)
	if err == nil {
		if committed != catalogSnapshotDigest {
			return "", fmt.Errorf("%w: operation metadata selection changed", ErrConflict)
		}
		capabilityName, requireErr := operationEffectiveMetadataCapabilityTx(
			ctx, tx, operation,
		)
		if requireErr != nil {
			return "", requireErr
		}
		if capabilityName != "" {
			if err := verifyEffectiveMetadataSetTx(
				ctx, tx, operation, committed, components,
			); err != nil {
				return "", err
			}
		} else if len(components) != 0 {
			return "", fmt.Errorf(
				"%w: effective metadata was supplied without a v2 consumer grant",
				ErrConflict,
			)
		}
		if err := tx.Commit(); err != nil {
			return "", err
		}
		return committed, nil
	} else if !errors.Is(err, ErrNotFound) {
		return "", err
	}
	if err := appendNextJournal(ctx, tx, operationID, operationJournalStepMetadataSelectionCommitted,
		string(operation.State), catalogSnapshotDigest); err != nil {
		return "", err
	}
	if err := commitEffectiveMetadataSetTx(
		ctx, tx, operation, catalogSnapshotDigest, components,
	); err != nil {
		return "", err
	}
	if err := tx.Commit(); err != nil {
		return "", err
	}
	return catalogSnapshotDigest, nil
}

// ReadOperationMetadataSelection returns only the operation-owned immutable
// snapshot identity. It has no current/latest fallback.
func (a *Authority) ReadOperationMetadataSelection(ctx context.Context, operationID string) (string, error) {
	return readOperationMetadataSelection(ctx, a.db, operationID, false)
}

func readOperationMetadataSelection(ctx context.Context, query sqlQueryer, operationID string, forUpdate bool) (string, error) {
	statement := `SELECT EvidenceDigest FROM _System_CatalogAppOperationJournal
WHERE OperationId = ? AND Step = ? ORDER BY SequenceNumber`
	if forUpdate {
		statement += " FOR UPDATE"
	}
	rows, err := query.QueryContext(ctx, statement, operationID, operationJournalStepMetadataSelectionCommitted)
	if err != nil {
		return "", err
	}
	defer rows.Close()
	var digest string
	count := 0
	for rows.Next() {
		count++
		if err := rows.Scan(&digest); err != nil {
			return "", err
		}
	}
	if err := rows.Err(); err != nil {
		return "", err
	}
	if count == 0 {
		return "", ErrNotFound
	}
	if count != 1 {
		return "", fmt.Errorf("%w: operation has multiple metadata selections", ErrConflict)
	}
	if err := requireDigest("committed Catalog snapshot selection", digest); err != nil {
		return "", fmt.Errorf("%w: %v", ErrConflict, err)
	}
	return digest, nil
}
