package plan

import (
	"bytes"
	"context"
	"database/sql"
	"fmt"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	planlang "github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

type PublishDraftParams struct {
	WorkspaceID                  string
	PlanID                       string
	ExpectedPlanRevision         int64
	DraftRevision                int64
	Artifact                     []byte
	CompilerVersion              string
	Digest                       string
	MakeDefault                  bool
	Actor                        Actor
	IdempotencyKey               string
	RequireNoActiveAgentEditWake bool
	Now                          time.Time
}

// PublishFrozenDraftParams publishes the exact draft snapshot previously
// frozen by a surrounding domain transaction. The current draft is still
// locked and must match Source, Layout, and DraftRevision before any immutable
// version is appended.
type PublishFrozenDraftParams struct {
	WorkspaceID          string
	PlanID               string
	ExpectedPlanRevision int64
	// DraftRevision is the revision of the exact mutable snapshot being
	// published. It may be greater than CommandDraftRevision when the
	// surrounding domain transaction first saves the caller's draft.
	DraftRevision int64
	// CommandDraftRevision is the external command's expected draft revision.
	// When set, it is used only for the immutable idempotency receipt; the
	// published version still records DraftRevision above.
	CommandDraftRevision int64
	Source               string
	Layout               []byte
	Artifact             []byte
	CompilerVersion      string
	Digest               string
	MakeDefault          bool
	Actor                Actor
	// CommandRequestDigest is an optional domain-owned idempotency fence. It
	// must be derived from the external command fields before mutable CAS
	// state is changed. Ordinary PublishDraft/RepublishVersion calls keep the
	// store-generated request digest semantics.
	CommandRequestDigest string
	IdempotencyKey       string
	Now                  time.Time
}

// PublishApprovedSnapshotParams appends the exact immutable source/artifact
// already authorized by an approval domain. Unlike ordinary draft publish, it
// intentionally does not require the mutable draft to remain at the reviewed
// revision.
type PublishApprovedSnapshotParams PublishFrozenDraftParams

type RepublishVersionParams struct {
	WorkspaceID          string
	PlanID               string
	SourceVersionID      string
	ExpectedPlanRevision int64
	Artifact             []byte
	CompilerVersion      string
	Digest               string
	MakeDefault          bool
	Actor                Actor
	IdempotencyKey       string
	Now                  time.Time
}

type ListVersionsParams struct {
	WorkspaceID       string
	PlanID            string
	Limit             int
	BeforePublishedAt time.Time
	BeforeID          string
}

type SetDefaultVersionParams struct {
	WorkspaceID      string
	PlanID           string
	ExpectedRevision int64
	VersionID        string
	Actor            Actor
	Now              time.Time
}

type publishVersionRequest struct {
	WorkspaceID                  string
	PlanID                       string
	ExpectedPlanRevision         int64
	DraftRevision                *int64
	SourceVersionID              *string
	Artifact                     []byte
	CompilerVersion              string
	Digest                       string
	MakeDefault                  bool
	Actor                        Actor
	IdempotencyKey               string
	RequireNoActiveAgentEditWake bool
	RequestDigest                string
	CommandDraftRevision         *int64
	CommandRequestDigest         string
	Now                          time.Time
	FrozenSource                 *string
	FrozenLayout                 []byte
	AllowHistoricalSnapshot      bool
}

func (store *Store) PublishDraft(ctx context.Context, params PublishDraftParams) (PublishResult, error) {
	request, err := publishDraftRequest(params)
	if err != nil {
		return PublishResult{}, err
	}
	return store.publishVersion(ctx, request)
}

// PublishDraft publishes the current exact draft inside the caller-owned
// transaction.
func (store *TxStore) PublishDraft(ctx context.Context, params PublishDraftParams) (PublishResult, error) {
	if err := store.validate(); err != nil {
		return PublishResult{}, err
	}
	request, err := publishDraftRequest(params)
	if err != nil {
		return PublishResult{}, err
	}
	return publishVersionInTx(ctx, store.tx, request)
}

// PublishFrozenDraft publishes a previously frozen draft snapshot inside the
// caller-owned transaction. It fails closed if the mutable draft changed after
// the snapshot was prepared.
func (store *TxStore) PublishFrozenDraft(ctx context.Context, params PublishFrozenDraftParams) (PublishResult, error) {
	if err := store.validate(); err != nil {
		return PublishResult{}, err
	}
	draftRevision := params.DraftRevision
	commandDraftRevision := params.CommandDraftRevision
	if commandDraftRevision <= 0 {
		commandDraftRevision = draftRevision
	}
	source := params.Source
	request, err := normalizePublishRequest(publishVersionRequest{
		WorkspaceID:          params.WorkspaceID,
		PlanID:               params.PlanID,
		ExpectedPlanRevision: params.ExpectedPlanRevision,
		DraftRevision:        &draftRevision,
		CommandDraftRevision: &commandDraftRevision,
		Artifact:             params.Artifact,
		CompilerVersion:      params.CompilerVersion,
		Digest:               params.Digest,
		MakeDefault:          params.MakeDefault,
		Actor:                params.Actor,
		CommandRequestDigest: params.CommandRequestDigest,
		IdempotencyKey:       params.IdempotencyKey,
		Now:                  params.Now,
		FrozenSource:         &source,
		FrozenLayout:         append([]byte(nil), params.Layout...),
	})
	if err != nil {
		return PublishResult{}, err
	}
	return publishVersionInTx(ctx, store.tx, request)
}

func (store *TxStore) PublishApprovedSnapshot(ctx context.Context, params PublishApprovedSnapshotParams) (PublishResult, error) {
	if err := store.validate(); err != nil {
		return PublishResult{}, err
	}
	draftRevision := params.DraftRevision
	source := params.Source
	request, err := normalizePublishRequest(publishVersionRequest{
		WorkspaceID:             params.WorkspaceID,
		PlanID:                  params.PlanID,
		ExpectedPlanRevision:    params.ExpectedPlanRevision,
		DraftRevision:           &draftRevision,
		Artifact:                params.Artifact,
		CompilerVersion:         params.CompilerVersion,
		Digest:                  params.Digest,
		MakeDefault:             params.MakeDefault,
		Actor:                   params.Actor,
		IdempotencyKey:          params.IdempotencyKey,
		Now:                     params.Now,
		FrozenSource:            &source,
		FrozenLayout:            append([]byte(nil), params.Layout...),
		AllowHistoricalSnapshot: true,
	})
	if err != nil {
		return PublishResult{}, err
	}
	return publishVersionInTx(ctx, store.tx, request)
}

func publishDraftRequest(params PublishDraftParams) (publishVersionRequest, error) {
	draftRevision := params.DraftRevision
	return normalizePublishRequest(publishVersionRequest{
		WorkspaceID:                  params.WorkspaceID,
		PlanID:                       params.PlanID,
		ExpectedPlanRevision:         params.ExpectedPlanRevision,
		DraftRevision:                &draftRevision,
		Artifact:                     params.Artifact,
		CompilerVersion:              params.CompilerVersion,
		Digest:                       params.Digest,
		MakeDefault:                  params.MakeDefault,
		Actor:                        params.Actor,
		IdempotencyKey:               params.IdempotencyKey,
		RequireNoActiveAgentEditWake: params.RequireNoActiveAgentEditWake,
		Now:                          params.Now,
	})
}

func (store *Store) RepublishVersion(ctx context.Context, params RepublishVersionParams) (PublishResult, error) {
	sourceVersionID := params.SourceVersionID
	request, err := normalizePublishRequest(publishVersionRequest{
		WorkspaceID:          params.WorkspaceID,
		PlanID:               params.PlanID,
		ExpectedPlanRevision: params.ExpectedPlanRevision,
		SourceVersionID:      &sourceVersionID,
		Artifact:             params.Artifact,
		CompilerVersion:      params.CompilerVersion,
		Digest:               params.Digest,
		MakeDefault:          params.MakeDefault,
		Actor:                params.Actor,
		IdempotencyKey:       params.IdempotencyKey,
		Now:                  params.Now,
	})
	if err != nil {
		return PublishResult{}, err
	}
	return store.publishVersion(ctx, request)
}

func (store *Store) publishVersion(ctx context.Context, request publishVersionRequest) (PublishResult, error) {
	if err := store.Validate(); err != nil {
		return PublishResult{}, err
	}
	var result PublishResult
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var txErr error
		result, txErr = publishVersionInTx(ctx, tx, request)
		return txErr
	})
	return result, err
}

func publishVersionInTx(ctx context.Context, tx coredb.Tx, request publishVersionRequest) (PublishResult, error) {
	plan, err := loadPlan(ctx, tx, request.WorkspaceID, request.PlanID, true)
	if err != nil {
		return PublishResult{}, err
	}
	existing, found, err := loadVersionByPublishIdempotencyForUpdate(ctx, tx, request.WorkspaceID, request.PlanID, request.Actor, request.IdempotencyKey)
	if err != nil {
		return PublishResult{}, err
	}
	if found {
		if existing.PublishRequestDigest != request.RequestDigest {
			return PublishResult{}, IdempotencyConflictError{Operation: "publish"}
		}
		return PublishResult{Plan: plan, Version: existing, Replayed: true}, nil
	}
	if err := requirePlanRevision(plan, request.ExpectedPlanRevision); err != nil {
		return PublishResult{}, err
	}
	if plan.ArchivedAt != nil {
		return PublishResult{}, ErrArchived
	}
	if request.RequireNoActiveAgentEditWake {
		if err := requireNoMatchingActiveAgentEditWake(
			ctx, tx, plan, request.Actor,
		); err != nil {
			return PublishResult{}, err
		}
	}

	var source, layout string
	var draftRevision *int64
	var sourceVersionID *string
	if request.DraftRevision != nil {
		if request.AllowHistoricalSnapshot {
			if request.FrozenSource == nil {
				return PublishResult{}, storeerr.BadRequest("historical draft publication requires frozen source")
			}
			source, layout = *request.FrozenSource, string(request.FrozenLayout)
		} else {
			draft, err := loadDraft(ctx, tx, request.WorkspaceID, request.PlanID, true)
			if err != nil {
				return PublishResult{}, err
			}
			if draft.Revision != *request.DraftRevision {
				return PublishResult{}, RevisionConflictError{Resource: "plan draft", Expected: *request.DraftRevision, Actual: draft.Revision}
			}
			if request.FrozenSource != nil {
				// Layout is stored in a native JSON column, so TiDB is allowed to
				// rewrite insignificant whitespace and object key order. The snapshot
				// fence remains strict about the source, revision, and decoded layout
				// value while accepting those storage-level JSON rewrites.
				if draft.Source != *request.FrozenSource || !model.JSONDocumentsEqual(draft.Layout, request.FrozenLayout) {
					return PublishResult{}, RevisionConflictError{Resource: "plan draft snapshot", Expected: *request.DraftRevision, Actual: draft.Revision}
				}
				source, layout = *request.FrozenSource, string(request.FrozenLayout)
			} else {
				source, layout = draft.Source, string(draft.Layout)
			}
		}
		draftRevision = request.DraftRevision
	} else {
		origin, err := loadVersionByIDForUpdate(ctx, tx, request.WorkspaceID, request.PlanID, *request.SourceVersionID)
		if err != nil {
			return PublishResult{}, err
		}
		source, layout = origin.Source, string(origin.Layout)
		originID := origin.ID
		sourceVersionID = &originID
	}
	// The artifact is compiler-derived data, so it is deliberately excluded
	// from the logical idempotency digest above. For a new command, however,
	// the concrete persistence boundary must prove that those derived bytes
	// came from the exact source row locked by this transaction. Otherwise a
	// caller could persist a valid artifact compiled from another Plan while
	// snapshotting this Plan's source alongside it. Receipt replay returns
	// before this point, preserving exact retries across compiler upgrades.
	if err := bindArtifactToLockedSource(source, &request); err != nil {
		return PublishResult{}, err
	}

	versionID, err := newID("plv", "version")
	if err != nil {
		return PublishResult{}, err
	}
	now, err := nextVersionPublishedAt(ctx, tx, request.WorkspaceID, request.PlanID, request.Now)
	if err != nil {
		return PublishResult{}, err
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO workflow_plan_versions (
    id, workspace_id, plan_id,
    draft_revision, source_version_id, source, layout, artifact,
    compiler_version, digest, publish_idempotency_key, publish_request_digest,
    published_by_member_id, published_by_actor_type, published_by_actor_id,
    published_at, created_at
) VALUES (
    $1, $2, $3,
    $4, $5, $6, $7, $8,
    $9, $10, $11, $12,
    $13, $14, $15,
    $16, $16
)
`, versionID, request.WorkspaceID, request.PlanID,
		draftRevision, sourceVersionID, source, layout, string(request.Artifact),
		request.CompilerVersion, request.Digest, request.IdempotencyKey, request.RequestDigest,
		request.Actor.MemberID, request.Actor.Type, request.Actor.ID, now); err != nil {
		return PublishResult{}, fmt.Errorf("insert workflow plan version: %w", mapWriteError(err))
	}
	update, err := tx.Exec(ctx, `
UPDATE workflow_plans
SET current_version_id = $1,
    default_version_id = CASE WHEN default_version_id IS NULL OR $2 THEN $1 ELSE default_version_id END,
    revision = revision + 1,
    updated_by_member_id = $3,
    updated_by_actor_type = $4,
    updated_by_actor_id = $5,
    updated_at = $6
WHERE workspace_id = $7 AND id = $8 AND revision = $9
`, versionID, request.MakeDefault,
		request.Actor.MemberID, request.Actor.Type, request.Actor.ID, now,
		request.WorkspaceID, request.PlanID, request.ExpectedPlanRevision)
	if err != nil {
		return PublishResult{}, fmt.Errorf("advance workflow plan version pointers: %w", mapWriteError(err))
	}
	if update.RowsAffected() == 0 {
		return PublishResult{}, RevisionConflictError{Resource: "plan", Expected: request.ExpectedPlanRevision, Actual: plan.Revision}
	}
	version, err := loadVersionByID(ctx, tx, request.WorkspaceID, request.PlanID, versionID)
	if err != nil {
		return PublishResult{}, err
	}
	updatedPlan, err := loadPlan(ctx, tx, request.WorkspaceID, request.PlanID, false)
	if err != nil {
		return PublishResult{}, err
	}
	return PublishResult{Plan: updatedPlan, Version: version}, nil
}

func (store *Store) LoadVersionByID(ctx context.Context, workspaceID string, planID string, versionID string) (PlanVersion, error) {
	if err := store.Validate(); err != nil {
		return PlanVersion{}, err
	}
	workspaceID, planID, versionID = trim(workspaceID), trim(planID), trim(versionID)
	if workspaceID == "" || planID == "" || versionID == "" {
		return PlanVersion{}, storeerr.BadRequest("workspaceId, planId, and versionId are required")
	}
	return loadVersionByID(ctx, store.db, workspaceID, planID, versionID)
}

// ResolveVersionForBinding resolves one exact immutable PlanVersion for a new
// Binding deployment. It deliberately does not follow current/default
// pointers, rejects archived Plans, and verifies artifact envelope, compiler
// metadata, and semantic digest before returning executable authority.
func (store *Store) ResolveVersionForBinding(ctx context.Context, workspaceID string, planID string, versionID string) (PlanVersion, error) {
	inspection, err := store.InspectVersionForBinding(ctx, workspaceID, planID, versionID)
	if err != nil {
		return PlanVersion{}, err
	}
	if inspection.PlanState != PlanStateActive {
		return PlanVersion{}, ErrArchived
	}
	return inspection.Version, nil
}

// InspectVersionForBinding verifies an exact immutable PlanVersion even when
// its Plan is archived. Binding management uses this for deterministic
// validation and idempotent replay; new-deployment commit authority must still
// recheck Plan active state in the Binding write transaction.
func (store *Store) InspectVersionForBinding(ctx context.Context, workspaceID string, planID string, versionID string) (BindingVersionInspection, error) {
	if err := store.Validate(); err != nil {
		return BindingVersionInspection{}, err
	}
	workspaceID, planID, versionID = trim(workspaceID), trim(planID), trim(versionID)
	if workspaceID == "" || planID == "" || versionID == "" {
		return BindingVersionInspection{}, storeerr.BadRequest("workspaceId, planId, and versionId are required")
	}
	plan, err := loadPlan(ctx, store.db, workspaceID, planID, false)
	if err != nil {
		return BindingVersionInspection{}, err
	}
	version, err := loadVersionByID(ctx, store.db, workspaceID, planID, versionID)
	if err != nil {
		return BindingVersionInspection{}, err
	}
	if _, err := validateVersionArtifactForBinding(version); err != nil {
		return BindingVersionInspection{}, err
	}
	return BindingVersionInspection{PlanState: plan.State, Version: version}, nil
}

func validateVersionArtifactForBinding(version PlanVersion) (*planlang.Artifact, error) {
	artifact, err := planlang.DecodeArtifact(version.Artifact)
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidVersionArtifact, err)
	}
	if trim(version.CompilerVersion) == "" || artifact.CompilerVersion != trim(version.CompilerVersion) {
		return nil, fmt.Errorf("%w: compiler version metadata does not match envelope", ErrInvalidVersionArtifact)
	}
	if trim(version.Digest) == "" || artifact.Digest != trim(version.Digest) {
		return nil, fmt.Errorf("%w: digest metadata does not match envelope", ErrInvalidVersionArtifact)
	}
	return artifact, nil
}

// LoadVersionByPublishIdempotency returns the immutable receipt for a prior
// publish or republish by the same actor on the same Plan. The authoring
// service uses this before consulting mutable draft state, so an exact retry
// remains replayable after later draft saves or compiler upgrades. The store
// still performs the authoritative full request-digest comparison when the
// mutation is retried.
// LoadVersionByPublishCommand resolves a publish receipt inside the
// caller-owned transaction and verifies the immutable command fence before any
// mutable Plan/draft CAS is consulted.
// LoadVersionByPublishCommandParams describes the immutable external command
// fence used by a domain that saves a draft and publishes it in one
// transaction. DraftRevision is the caller's expected/input revision, not
// necessarily the revision recorded on the published snapshot.
type LoadVersionByPublishCommandParams struct {
	WorkspaceID          string
	PlanID               string
	ExpectedPlanRevision int64
	DraftRevision        int64
	MakeDefault          bool
	Actor                Actor
	IdempotencyKey       string
	RequestDigest        string
}

func (store *TxStore) LoadVersionByPublishCommand(
	ctx context.Context,
	params LoadVersionByPublishCommandParams,
) (PlanVersion, bool, error) {
	if err := store.validate(); err != nil {
		return PlanVersion{}, false, err
	}
	if _, err := validateActor(params.Actor); err != nil {
		return PlanVersion{}, false, err
	}
	if params.ExpectedPlanRevision <= 0 || params.DraftRevision <= 0 {
		return PlanVersion{}, false, storeerr.BadRequest("publish command revisions must be positive")
	}
	params.WorkspaceID, params.PlanID = trim(params.WorkspaceID), trim(params.PlanID)
	params.IdempotencyKey, params.RequestDigest = trim(params.IdempotencyKey), trim(params.RequestDigest)
	if params.WorkspaceID == "" || params.PlanID == "" || params.IdempotencyKey == "" || params.RequestDigest == "" {
		return PlanVersion{}, false, storeerr.BadRequest("workspaceId, planId, idempotencyKey, and requestDigest are required")
	}
	// Team direct publication derives an actor-independent key from the
	// external client request. Resolve that receipt before consulting mutable
	// Plan/draft rows, then let the immutable command digest fence actor and
	// every other external field.
	version, found, err := loadVersionByPublishCommandKeyForUpdate(ctx, store.tx, params.WorkspaceID, params.PlanID, params.IdempotencyKey)
	if err != nil || !found {
		return version, found, err
	}
	if version.PublishRequestDigest != params.RequestDigest {
		return PlanVersion{}, false, IdempotencyConflictError{Operation: "publish"}
	}
	return version, true, nil
}

// LoadVersionByPublishIdempotency resolves a publish receipt inside the
// caller-owned transaction for atomic domain replays.
func (store *TxStore) LoadVersionByPublishIdempotency(
	ctx context.Context,
	workspaceID string,
	planID string,
	actor Actor,
	idempotencyKey string,
) (PlanVersion, bool, error) {
	if err := store.validate(); err != nil {
		return PlanVersion{}, false, err
	}
	workspaceID, planID, idempotencyKey = trim(workspaceID), trim(planID), trim(idempotencyKey)
	if workspaceID == "" || planID == "" || idempotencyKey == "" {
		return PlanVersion{}, false, storeerr.BadRequest("workspaceId, planId, and idempotencyKey are required")
	}
	if _, err := validateActor(actor); err != nil {
		return PlanVersion{}, false, err
	}
	return loadVersionByPublishIdempotencyForUpdate(ctx, store.tx, workspaceID, planID, actor, idempotencyKey)
}

func (store *Store) LoadVersionByPublishIdempotency(
	ctx context.Context,
	workspaceID string,
	planID string,
	actor Actor,
	idempotencyKey string,
) (PlanVersion, bool, error) {
	if err := store.Validate(); err != nil {
		return PlanVersion{}, false, err
	}
	workspaceID, planID, idempotencyKey = trim(workspaceID), trim(planID), trim(idempotencyKey)
	if workspaceID == "" || planID == "" || idempotencyKey == "" {
		return PlanVersion{}, false, storeerr.BadRequest("workspaceId, planId, and idempotencyKey are required")
	}
	actor, err := validateActor(actor)
	if err != nil {
		return PlanVersion{}, false, err
	}
	return loadVersionByPublishIdempotency(ctx, store.db, workspaceID, planID, actor, idempotencyKey)
}

func (store *Store) ListVersions(ctx context.Context, params ListVersionsParams) ([]PlanVersionSummary, error) {
	if err := store.Validate(); err != nil {
		return nil, err
	}
	params.WorkspaceID, params.PlanID = trim(params.WorkspaceID), trim(params.PlanID)
	params.BeforeID = trim(params.BeforeID)
	if params.WorkspaceID == "" || params.PlanID == "" || (params.BeforePublishedAt.IsZero() != (params.BeforeID == "")) {
		return nil, storeerr.BadRequest("workspaceId and planId are required and the version cursor must include publishedAt and id")
	}
	query := versionSummarySelectSQL() + "\nWHERE v.workspace_id = $1 AND v.plan_id = $2\n"
	args := []any{params.WorkspaceID, params.PlanID}
	if !params.BeforePublishedAt.IsZero() {
		args = append(args, params.BeforePublishedAt.UTC(), params.BeforeID)
		query += fmt.Sprintf("  AND (v.published_at < $%d OR (v.published_at = $%d AND v.id < $%d))\n", len(args)-1, len(args)-1, len(args))
	}
	args = append(args, normalizeLimit(params.Limit))
	query += fmt.Sprintf("ORDER BY v.published_at DESC, v.id DESC\nLIMIT $%d\n", len(args))
	rows, err := store.db.Query(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("list workflow plan versions: %w", mapWriteError(err))
	}
	defer rows.Close()
	versions := make([]PlanVersionSummary, 0)
	for rows.Next() {
		version, err := scanVersionSummary(rows)
		if err != nil {
			return nil, fmt.Errorf("scan workflow plan version: %w", err)
		}
		versions = append(versions, version)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate workflow plan versions: %w", err)
	}
	return versions, nil
}

func nextVersionPublishedAt(ctx context.Context, tx coredb.QueryRower, workspaceID string, planID string, requested time.Time) (time.Time, error) {
	now := utcOrNow(requested)
	// workflow_plan_versions.published_at is stored at second precision in the
	// current TiDB schema. Multiple authoring operations can legitimately commit
	// inside one wall-clock second, so preserve the durable history order by
	// advancing the new timestamp past the latest stored value at that precision.
	storedNow := now.Truncate(time.Second)
	var latest sql.NullTime
	if err := tx.QueryRow(ctx, `
SELECT MAX(published_at)
FROM workflow_plan_versions
WHERE workspace_id = $1 AND plan_id = $2
`, workspaceID, planID).Scan(&latest); err != nil {
		return time.Time{}, fmt.Errorf("load latest workflow plan version timestamp: %w", mapWriteError(err))
	}
	if latest.Valid {
		latestAt := latest.Time.UTC()
		if !storedNow.After(latestAt) {
			return latestAt.Add(time.Second), nil
		}
	}
	return now, nil
}

func (store *Store) SetDefaultVersion(ctx context.Context, params SetDefaultVersionParams) (Plan, error) {
	if err := store.Validate(); err != nil {
		return Plan{}, err
	}
	params.WorkspaceID, params.PlanID = trim(params.WorkspaceID), trim(params.PlanID)
	params.VersionID = trim(params.VersionID)
	if params.WorkspaceID == "" || params.PlanID == "" || params.ExpectedRevision <= 0 || params.VersionID == "" {
		return Plan{}, storeerr.BadRequest("workspaceId, planId, expectedRevision, and versionId are required")
	}
	actor, err := validateActor(params.Actor)
	if err != nil {
		return Plan{}, err
	}
	var updated Plan
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		plan, err := loadPlan(ctx, tx, params.WorkspaceID, params.PlanID, true)
		if err != nil {
			return err
		}
		if err := requirePlanRevision(plan, params.ExpectedRevision); err != nil {
			return err
		}
		if plan.ArchivedAt != nil {
			return ErrArchived
		}
		version, err := loadVersionByIDForUpdate(ctx, tx, params.WorkspaceID, params.PlanID, params.VersionID)
		if err != nil {
			return err
		}
		if plan.DefaultVersion != nil && plan.DefaultVersion.ID == version.ID {
			updated = plan
			return nil
		}
		result, err := tx.Exec(ctx, `
UPDATE workflow_plans
SET default_version_id = $1,
    revision = revision + 1,
    updated_by_member_id = $2,
    updated_by_actor_type = $3,
    updated_by_actor_id = $4,
    updated_at = $5
WHERE workspace_id = $6 AND id = $7 AND revision = $8
`, version.ID, actor.MemberID, actor.Type, actor.ID, utcOrNow(params.Now),
			params.WorkspaceID, params.PlanID, params.ExpectedRevision)
		if err != nil {
			return fmt.Errorf("set workflow plan default version: %w", mapWriteError(err))
		}
		if result.RowsAffected() == 0 {
			return RevisionConflictError{Resource: "plan", Expected: params.ExpectedRevision, Actual: plan.Revision}
		}
		updated, err = loadPlan(ctx, tx, params.WorkspaceID, params.PlanID, false)
		return err
	})
	return updated, err
}

func normalizePublishRequest(request publishVersionRequest) (publishVersionRequest, error) {
	request.WorkspaceID, request.PlanID = trim(request.WorkspaceID), trim(request.PlanID)
	request.CompilerVersion, request.Digest = trim(request.CompilerVersion), trim(request.Digest)
	request.IdempotencyKey = trim(request.IdempotencyKey)
	if request.WorkspaceID == "" || request.PlanID == "" || request.ExpectedPlanRevision <= 0 || request.CompilerVersion == "" || request.Digest == "" || request.IdempotencyKey == "" {
		return publishVersionRequest{}, storeerr.BadRequest("workspaceId, planId, expectedPlanRevision, compilerVersion, digest, and idempotencyKey are required")
	}
	if (request.DraftRevision == nil) == (request.SourceVersionID == nil) {
		return publishVersionRequest{}, storeerr.BadRequest("exactly one version origin is required")
	}
	originKind, originValue := "draft", ""
	if request.DraftRevision != nil {
		if *request.DraftRevision <= 0 {
			return publishVersionRequest{}, storeerr.BadRequest("origin draft revision must be positive")
		}
		commandDraftRevision := request.DraftRevision
		if request.CommandDraftRevision != nil {
			if *request.CommandDraftRevision <= 0 {
				return publishVersionRequest{}, storeerr.BadRequest("command draft revision must be positive")
			}
			commandDraftRevision = request.CommandDraftRevision
		}
		originValue = intText(*commandDraftRevision)
	} else {
		originKind, originValue = "version", trim(*request.SourceVersionID)
		if originValue == "" {
			return publishVersionRequest{}, storeerr.BadRequest("source version id is required")
		}
		request.SourceVersionID = &originValue
	}
	actor, err := validateActor(request.Actor)
	if err != nil {
		return publishVersionRequest{}, err
	}
	request.Actor = actor
	// Idempotency identifies the caller's logical publish command, not values
	// derived by the compiler handling that attempt. During a rolling deploy,
	// two compiler binaries may race on the same exact command; the first
	// committed immutable artifact is the receipt and every later attempt must
	// replay it rather than conflict because its locally derived bytes differ.
	operation := "publish"
	if request.AllowHistoricalSnapshot {
		operation = "publish-approved-snapshot"
	}
	if request.CommandRequestDigest != "" {
		request.RequestDigest = trim(request.CommandRequestDigest)
		if request.RequestDigest == "" {
			return publishVersionRequest{}, storeerr.BadRequest("command request digest is required when provided")
		}
	} else {
		request.RequestDigest = requestDigest(
			operation, request.WorkspaceID, request.PlanID, intText(request.ExpectedPlanRevision), originKind, originValue,
			boolText(request.MakeDefault), actor.Type, actor.ID,
		)
	}
	return request, nil
}

func bindArtifactToLockedSource(source string, request *publishVersionRequest) error {
	if request == nil {
		return fmt.Errorf("bind workflow Plan artifact: publish request is nil")
	}
	artifact, err := planlang.DecodeArtifact(request.Artifact)
	if err != nil {
		return storeerr.BadRequest("artifact must be a valid normalized Workflow Plan artifact")
	}
	if artifact.CompilerVersion != request.CompilerVersion {
		return storeerr.BadRequest("artifact compilerVersion does not match compilerVersion")
	}
	if artifact.Digest != request.Digest {
		return storeerr.BadRequest("artifact semantic digest does not match digest")
	}
	canonicalArtifact, err := artifact.DeterministicJSON()
	if err != nil {
		return storeerr.BadRequest("artifact cannot be encoded canonically")
	}

	compiled, diagnostics, err := planlang.Compile([]byte(source))
	if err != nil {
		return fmt.Errorf("compile locked workflow Plan source: %w", err)
	}
	if planlang.HasErrors(diagnostics) {
		return storeerr.BadRequest("locked workflow Plan source does not compile")
	}
	if compiled == nil {
		return fmt.Errorf("compile locked workflow Plan source: compiler returned no artifact")
	}
	if err := compiled.ValidateEnvelope(); err != nil {
		return fmt.Errorf("validate artifact compiled from locked workflow Plan source: %w", err)
	}
	expectedArtifact, err := compiled.DeterministicJSON()
	if err != nil {
		return fmt.Errorf("encode artifact compiled from locked workflow Plan source: %w", err)
	}
	if compiled.CompilerVersion != request.CompilerVersion ||
		compiled.Digest != request.Digest ||
		!bytes.Equal(expectedArtifact, canonicalArtifact) {
		return storeerr.BadRequest("artifact does not match the locked workflow Plan source")
	}
	request.Artifact = canonicalArtifact
	return nil
}

func loadVersionByID(ctx context.Context, queryer coredb.QueryRower, workspaceID string, planID string, versionID string) (PlanVersion, error) {
	return loadVersionByIDMode(ctx, queryer, workspaceID, planID, versionID, false)
}

func loadVersionByIDForUpdate(ctx context.Context, queryer coredb.QueryRower, workspaceID string, planID string, versionID string) (PlanVersion, error) {
	return loadVersionByIDMode(ctx, queryer, workspaceID, planID, versionID, true)
}

func loadVersionByIDMode(ctx context.Context, queryer coredb.QueryRower, workspaceID string, planID string, versionID string, forUpdate bool) (PlanVersion, error) {
	query := versionSelectSQL() + `
WHERE workspace_id = $1 AND plan_id = $2 AND id = $3
	`
	if forUpdate {
		query += "FOR UPDATE\n"
	}
	row := queryer.QueryRow(ctx, query, workspaceID, planID, versionID)
	value, err := scanVersion(row)
	if err != nil {
		return PlanVersion{}, mapWriteError(err)
	}
	return value, nil
}

func loadVersionByPublishIdempotency(ctx context.Context, queryer coredb.QueryRower, workspaceID string, planID string, actor Actor, idempotencyKey string) (PlanVersion, bool, error) {
	return loadVersionByPublishIdempotencyMode(ctx, queryer, workspaceID, planID, actor, idempotencyKey, false)
}

func loadVersionByPublishIdempotencyForUpdate(ctx context.Context, queryer coredb.QueryRower, workspaceID string, planID string, actor Actor, idempotencyKey string) (PlanVersion, bool, error) {
	return loadVersionByPublishIdempotencyMode(ctx, queryer, workspaceID, planID, actor, idempotencyKey, true)
}

func loadVersionByPublishCommandKeyForUpdate(ctx context.Context, queryer coredb.QueryRower, workspaceID string, planID string, idempotencyKey string) (PlanVersion, bool, error) {
	query := versionSelectSQL() + `
WHERE workspace_id = $1
  AND plan_id = $2
  AND publish_idempotency_key = $3
ORDER BY published_at DESC, id DESC
LIMIT 1
FOR UPDATE
`
	row := queryer.QueryRow(ctx, query, workspaceID, planID, idempotencyKey)
	value, err := scanVersion(row)
	if coredb.IsNotFound(err) {
		return PlanVersion{}, false, nil
	}
	if err != nil {
		return PlanVersion{}, false, mapWriteError(err)
	}
	return value, true, nil
}

func loadVersionByPublishIdempotencyMode(ctx context.Context, queryer coredb.QueryRower, workspaceID string, planID string, actor Actor, idempotencyKey string, forUpdate bool) (PlanVersion, bool, error) {
	query := versionSelectSQL() + `
WHERE workspace_id = $1
  AND plan_id = $2
  AND published_by_actor_type = $3
  AND published_by_actor_id = $4
  AND publish_idempotency_key = $5
	`
	if forUpdate {
		query += "FOR UPDATE\n"
	}
	row := queryer.QueryRow(ctx, query, workspaceID, planID, actor.Type, actor.ID, idempotencyKey)
	value, err := scanVersion(row)
	if coredb.IsNotFound(err) {
		return PlanVersion{}, false, nil
	}
	if err != nil {
		return PlanVersion{}, false, mapWriteError(err)
	}
	return value, true, nil
}

func versionSelectSQL() string {
	return `
SELECT id, workspace_id, plan_id,
       draft_revision, source_version_id, source, layout, artifact,
       compiler_version, digest, publish_idempotency_key, publish_request_digest,
       published_by_actor_type, published_by_actor_id, published_by_member_id,
       COALESCE((SELECT display_name FROM members
                 WHERE workspace_id = workflow_plan_versions.workspace_id
                   AND id = workflow_plan_versions.published_by_member_id), ''),
       published_at, created_at
FROM workflow_plan_versions
`
}

func versionSummarySelectSQL() string {
	return `
SELECT v.id, v.workspace_id, v.plan_id,
       v.draft_revision, v.source_version_id,
       v.compiler_version, v.digest,
       v.published_by_actor_type, v.published_by_actor_id, v.published_by_member_id,
       COALESCE(m.display_name, ''), v.published_at
FROM workflow_plan_versions v
LEFT JOIN members m
  ON m.workspace_id = v.workspace_id AND m.id = v.published_by_member_id
`
}

func scanVersion(row coredb.Row) (PlanVersion, error) {
	var (
		version         PlanVersion
		draftRevision   sql.NullInt64
		sourceVersionID sql.NullString
		layoutText      string
		artifactText    string
	)
	if err := row.Scan(
		&version.ID, &version.WorkspaceID, &version.PlanID,
		&draftRevision, &sourceVersionID, &version.Source, &layoutText, &artifactText,
		&version.CompilerVersion, &version.Digest, &version.PublishIdempotencyKey, &version.PublishRequestDigest,
		&version.PublishedBy.Type, &version.PublishedBy.ID, &version.PublishedBy.MemberID,
		&version.PublishedByDisplayName,
		&version.PublishedAt, &version.CreatedAt,
	); err != nil {
		return PlanVersion{}, err
	}
	version.DraftRevision = ptrInt64(draftRevision)
	version.SourceVersionID = ptrString(sourceVersionID)
	version.Layout = []byte(layoutText)
	version.Artifact = []byte(artifactText)
	version.PublishedAt = version.PublishedAt.UTC()
	version.CreatedAt = version.CreatedAt.UTC()
	return version, nil
}

func scanVersionSummary(row coredb.Row) (PlanVersionSummary, error) {
	var (
		version         PlanVersionSummary
		draftRevision   sql.NullInt64
		sourceVersionID sql.NullString
	)
	if err := row.Scan(
		&version.ID, &version.WorkspaceID, &version.PlanID,
		&draftRevision, &sourceVersionID,
		&version.CompilerVersion, &version.Digest,
		&version.PublishedBy.Type, &version.PublishedBy.ID, &version.PublishedBy.MemberID,
		&version.PublishedByDisplayName,
		&version.PublishedAt,
	); err != nil {
		return PlanVersionSummary{}, err
	}
	version.DraftRevision = ptrInt64(draftRevision)
	version.SourceVersionID = ptrString(sourceVersionID)
	version.PublishedAt = version.PublishedAt.UTC()
	return version, nil
}
