package catalogapp

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
)

const (
	OperationStepServingArtifactsCommitted               = "SERVING_ARTIFACTS_COMMITTED"
	operationJournalStepLegacyServingArtifactsBackfilled = "LEGACY_SERVING_ARTIFACTS_BACKFILLED"
)

type servingHandlerSelection struct {
	OrgID                      string `json:"org_id"`
	AppInstallationID          string `json:"app_installation_id"`
	AppReleaseDigest           string `json:"app_release_digest"`
	CapabilityID               string `json:"capability_id"`
	HandlerRef                 string `json:"handler_ref"`
	WorkloadID                 string `json:"workload_id"`
	ServiceReleaseDigest       string `json:"service_release_digest"`
	OwnershipEpoch             uint64 `json:"ownership_epoch"`
	CompatibilityReceiptDigest string `json:"compatibility_receipt_digest"`
}

type servingFrontendSelection struct {
	OrgID                      string `json:"org_id"`
	AppInstallationID          string `json:"app_installation_id"`
	AppReleaseDigest           string `json:"app_release_digest"`
	FrontendReleaseDigest      string `json:"frontend_release_digest"`
	CompatibilityReceiptDigest string `json:"compatibility_receipt_digest"`
}

type servingSelection struct {
	Schema   string                    `json:"schema"`
	Handlers []servingHandlerSelection `json:"handlers"`
	Frontend *servingFrontendSelection `json:"frontend,omitempty"`
}

// CommitServingArtifacts makes the complete operation-bound binding set the
// only serving selection that this operation may publish or activate. Binding
// rows own the per-capability receipts; the operation journal owns only one
// canonical commitment over that complete set, so multi-capability releases do
// not collapse into a false single-receipt model.
func (a *Authority) CommitServingArtifacts(ctx context.Context, operationID string) (string, error) {
	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
	}
	switch operation.Step {
	case "CANDIDATE_COMMITTED":
		if operation.State == StateFailed || operation.State == StateRemoved || operation.Kind == OperationRemove {
			return "", ErrNotReady
		}
		if err := verifyConsumerMetadataSelectionTx(ctx, tx, operation); err != nil {
			return "", err
		}
		if err := validateStagedBindingCoverage(ctx, tx, operation); err != nil {
			return "", err
		}
		digest, err := servingSelectionDigest(ctx, tx, operationID)
		if err != nil {
			return "", err
		}
		result, err := tx.ExecContext(ctx, `UPDATE _System_CatalogAppOperation
SET Step = ?, LastModifiedDate = CURRENT_TIMESTAMP(6)
WHERE OperationId = ? AND Step = 'CANDIDATE_COMMITTED' AND State = ?`,
			OperationStepServingArtifactsCommitted, operationID, transitionState(operation.Kind))
		if err != nil {
			return "", err
		}
		affected, err := result.RowsAffected()
		if err != nil || affected != 1 {
			return "", ErrConflict
		}
		if err := appendNextJournal(ctx, tx, operationID, OperationStepServingArtifactsCommitted, string(operation.State), digest); err != nil {
			return "", err
		}
		if err := tx.Commit(); err != nil {
			return "", err
		}
		return digest, nil
	case OperationStepServingArtifactsCommitted, "ACTIVATED":
		digest, err := verifyServingArtifactCommitment(ctx, tx, operationID)
		if err != nil {
			return "", err
		}
		if err := tx.Commit(); err != nil {
			return "", err
		}
		return digest, nil
	case "PROJECTION_ACKNOWLEDGED":
		digest, err := ensureServingArtifactCommitmentForActivation(ctx, tx, operation)
		if err != nil {
			return "", err
		}
		if err := tx.Commit(); err != nil {
			return "", err
		}
		return digest, nil
	default:
		return "", ErrNotReady
	}
}

// ReadServingArtifactCommitment returns the canonical binding-set commitment.
// It never derives a current/latest selection from metadata or artifact
// storage, and it fails if operation-bound binding facts drift from the
// journaled commitment.
func (a *Authority) ReadServingArtifactCommitment(ctx context.Context, operationID string) (string, error) {
	tx, err := a.db.BeginTx(ctx, nil)
	if err != nil {
		return "", err
	}
	defer rollback(tx)
	operation, err := a.readOperation(ctx, tx, operationID, false)
	if err != nil {
		return "", err
	}
	if operation.Step != OperationStepServingArtifactsCommitted && operation.Step != "PROJECTION_ACKNOWLEDGED" && operation.Step != "ACTIVATED" {
		return "", ErrNotReady
	}
	digest, err := verifyServingArtifactCommitment(ctx, tx, operationID)
	if err != nil {
		return "", err
	}
	if err := tx.Commit(); err != nil {
		return "", err
	}
	return digest, nil
}

func verifyServingArtifactCommitment(ctx context.Context, query sqlQueryer, operationID string) (string, error) {
	committed, err := readServingArtifactEvidence(ctx, query, operationID)
	if err != nil {
		return "", err
	}
	current, err := servingSelectionDigest(ctx, query, operationID)
	if err != nil {
		return "", err
	}
	matches := 0
	for _, evidence := range committed {
		if evidence == current {
			matches++
		}
	}
	if matches == 0 {
		return "", fmt.Errorf("%w: operation-bound serving selection changed after commitment", ErrConflict)
	}
	if matches != 1 {
		return "", fmt.Errorf(
			"%w: operation has multiple matching serving artifact commitments",
			ErrConflict,
		)
	}
	return current, nil
}

// ensureServingArtifactCommitmentForActivation closes the only upgrade-time
// compatibility window. Older binaries could durably acknowledge a projection
// before this commitment step existed. Once such an operation is observed at
// PROJECTION_ACKNOWLEDGED, its operation-bound binding rows are already frozen
// by the current state machine. We therefore validate the complete staged set
// and journal its exact digest before activation. New operations cannot enter
// this path without a commitment because current projection admission rejects
// CANDIDATE_COMMITTED.
func ensureServingArtifactCommitmentForActivation(ctx context.Context, tx *sql.Tx, operation Operation) (string, error) {
	digest, err := verifyServingArtifactCommitment(ctx, tx, operation.OperationID)
	if err == nil {
		return digest, nil
	}
	if !errors.Is(err, ErrNotFound) || operation.Step != "PROJECTION_ACKNOWLEDGED" ||
		operation.Kind == OperationRemove || operation.State == StateFailed || operation.State == StateRemoved {
		return "", err
	}
	if err := validateStagedBindingCoverage(ctx, tx, operation); err != nil {
		return "", fmt.Errorf("validate legacy projection binding set: %w", err)
	}
	digest, err = servingSelectionDigest(ctx, tx, operation.OperationID)
	if err != nil {
		return "", err
	}
	if err := appendNextJournal(ctx, tx, operation.OperationID,
		operationJournalStepLegacyServingArtifactsBackfilled, string(operation.State), digest); err != nil {
		return "", err
	}
	return digest, nil
}

func servingSelectionDigest(ctx context.Context, query sqlQueryer, operationID string) (string, error) {
	selection := servingSelection{
		Schema:   "nexuscrm.catalog-serving-selection/v1",
		Handlers: []servingHandlerSelection{},
	}
	rows, err := query.QueryContext(ctx, `SELECT OrgId, AppInstallationId, AppReleaseDigest, CapabilityId, HandlerRef, WorkloadId, ServiceReleaseDigest,
OwnershipEpoch, CompatibilityReceiptDigest FROM _System_CatalogHandlerBinding
WHERE OperationId = ? AND State <> 'FAILED' ORDER BY CapabilityId`, operationID)
	if err != nil {
		return "", err
	}
	for rows.Next() {
		var binding servingHandlerSelection
		var rawEpoch []byte
		if err := rows.Scan(&binding.OrgID, &binding.AppInstallationID, &binding.AppReleaseDigest,
			&binding.CapabilityID, &binding.HandlerRef, &binding.WorkloadID,
			&binding.ServiceReleaseDigest, &rawEpoch, &binding.CompatibilityReceiptDigest); err != nil {
			rows.Close()
			return "", err
		}
		binding.OwnershipEpoch, err = parseEpoch(rawEpoch)
		if err != nil {
			rows.Close()
			return "", err
		}
		selection.Handlers = append(selection.Handlers, binding)
	}
	if err := rows.Close(); err != nil {
		return "", err
	}
	rows, err = query.QueryContext(ctx, `SELECT OrgId, AppInstallationId, AppReleaseDigest, FrontendReleaseDigest, CompatibilityReceiptDigest
FROM _System_CatalogFrontendBinding WHERE OperationId = ? AND State <> 'FAILED' ORDER BY FrontendBindingId`, operationID)
	if err != nil {
		return "", err
	}
	frontendCount := 0
	for rows.Next() {
		frontendCount++
		if frontendCount > 1 {
			rows.Close()
			return "", fmt.Errorf("%w: operation has multiple frontend bindings", ErrConflict)
		}
		var frontend servingFrontendSelection
		if err := rows.Scan(&frontend.OrgID, &frontend.AppInstallationID, &frontend.AppReleaseDigest,
			&frontend.FrontendReleaseDigest, &frontend.CompatibilityReceiptDigest); err != nil {
			rows.Close()
			return "", err
		}
		selection.Frontend = &frontend
	}
	if err := rows.Close(); err != nil {
		return "", err
	}
	encoded, err := json.Marshal(selection)
	if err != nil {
		return "", err
	}
	return digestParts(string(encoded)), nil
}

func readServingArtifactEvidence(ctx context.Context, query sqlQueryer, operationID string) ([]string, error) {
	rows, err := query.QueryContext(ctx, `SELECT EvidenceDigest FROM _System_CatalogAppOperationJournal
WHERE OperationId = ? AND Step IN (?, ?) ORDER BY SequenceNumber`, operationID,
		OperationStepServingArtifactsCommitted, operationJournalStepLegacyServingArtifactsBackfilled)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var evidence []string
	for rows.Next() {
		var digest string
		if err := rows.Scan(&digest); err != nil {
			return nil, err
		}
		if err := requireDigest("serving artifact journal evidence", digest); err != nil {
			return nil, errors.Join(ErrConflict, err)
		}
		evidence = append(evidence, digest)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	if len(evidence) == 0 {
		return nil, ErrNotFound
	}
	return evidence, nil
}
