package planmanage

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

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

// Repository is the persistence boundary required by the Plan authoring
// service. Keeping it narrow lets lifecycle tests prove orchestration and CAS
// inputs without a database while the concrete store owns transactionality.
type Repository interface {
	CreatePlan(context.Context, plan.CreatePlanParams) (plan.CreateResult, error)
	LoadPlan(context.Context, string, string) (plan.Plan, error)
	ListPlans(context.Context, plan.ListPlansParams) ([]plan.Plan, error)
	UpdatePlanMetadata(context.Context, plan.UpdatePlanMetadataParams) (plan.Plan, error)
	LoadDraft(context.Context, string, string) (plan.PlanDraft, error)
	SaveDraft(context.Context, plan.SaveDraftParams) (plan.PlanDraft, error)
	LoadVersionByPublishIdempotency(context.Context, string, string, plan.Actor, string) (plan.PlanVersion, bool, error)
	PublishDraft(context.Context, plan.PublishDraftParams) (plan.PublishResult, error)
	LoadVersionByID(context.Context, string, string, string) (plan.PlanVersion, error)
	ResolveVersionForBinding(context.Context, string, string, string) (plan.PlanVersion, error)
	InspectVersionForBinding(context.Context, string, string, string) (plan.BindingVersionInspection, error)
	ListVersions(context.Context, plan.ListVersionsParams) ([]plan.PlanVersionSummary, error)
	RepublishVersion(context.Context, plan.RepublishVersionParams) (plan.PublishResult, error)
	SetDefaultVersion(context.Context, plan.SetDefaultVersionParams) (plan.Plan, error)
	ArchivePlan(context.Context, plan.ArchivePlanParams) (plan.Plan, error)
	RestorePlan(context.Context, plan.RestorePlanParams) (plan.Plan, error)
}

type authoringAuthorityRepository interface {
	ResolvePlanAuthoringMember(context.Context, string, string) (string, error)
}

type compiler func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error)

// Service coordinates authoring validation with transactional persistence.
// It never creates an Invocation or executes an artifact.
type Service struct {
	repository   Repository
	clock        Clock
	compile      compiler
	draftUpdated func(Draft)
}

// Option customizes deterministic dependencies for tests.
type Option func(*Service)

func WithClock(clock Clock) Option {
	return func(service *Service) {
		if clock != nil {
			service.clock = clock
		}
	}
}

func withCompiler(value compiler) Option {
	return func(service *Service) {
		if value != nil {
			service.compile = value
		}
	}
}

func WithDraftUpdated(callback func(Draft)) Option {
	return func(service *Service) {
		service.draftUpdated = callback
	}
}

func New(repository Repository, options ...Option) *Service {
	service := &Service{repository: repository, clock: systemClock{}, compile: planlang.Compile}
	for _, option := range options {
		if option != nil {
			option(service)
		}
	}
	return service
}

func (service *Service) validate() error {
	if service == nil || service.repository == nil {
		return fmt.Errorf("workflow Plan repository is not configured")
	}
	if service.clock == nil || service.compile == nil {
		return fmt.Errorf("workflow Plan service dependencies are not configured")
	}
	return nil
}

func (service *Service) Create(ctx context.Context, input CreateInput) (Plan, Draft, error) {
	if err := service.validate(); err != nil {
		return Plan{}, Draft{}, err
	}
	normalized, err := normalizeCreateInput(input)
	if err != nil {
		return Plan{}, Draft{}, err
	}
	if err := service.requireCompilableSource(normalized.Source); err != nil {
		return Plan{}, Draft{}, err
	}
	result, err := service.repository.CreatePlan(ctx, plan.CreatePlanParams{
		WorkspaceID:    normalized.WorkspaceID,
		PlanKey:        normalized.Key,
		DisplayName:    normalized.DisplayName,
		DescriptionMD:  normalized.DescriptionMD,
		Source:         normalized.Source,
		Layout:         normalized.Layout,
		Actor:          storeActor(normalized.Actor),
		IdempotencyKey: normalized.IdempotencyKey,
		Now:            service.clock.Now(),
	})
	if err != nil {
		return Plan{}, Draft{}, mapRepositoryError(err)
	}
	return planFromStore(result.Plan), draftFromStore(result.Draft), nil
}

// NormalizeCreateInput is shared by the ordinary Plan authoring service and
// Team Workflow orchestration, which must insert Plan + Team relation in one
// caller-owned transaction. Keeping one normalization boundary prevents the
// transactional path from accepting inputs the ordinary path would reject.
func NormalizeCreateInput(input CreateInput) (CreateInput, error) {
	normalized, err := normalizeCreateInput(input)
	if err != nil {
		return CreateInput{}, err
	}
	validation := validateSourceWithCompiler(normalized.Source, planlang.Compile)
	if planlang.HasErrors(validation.Diagnostics) {
		return CreateInput{}, compileFailed(validation.Diagnostics)
	}
	return normalized, nil
}

func normalizeCreateInput(input CreateInput) (CreateInput, error) {
	workspaceID, err := requiredID("workspace id", input.WorkspaceID)
	if err != nil {
		return CreateInput{}, err
	}
	key, err := normalizePlanKey(input.Key)
	if err != nil {
		return CreateInput{}, err
	}
	displayName, err := normalizeDisplayName(input.DisplayName)
	if err != nil {
		return CreateInput{}, err
	}
	description, err := normalizeDescription(input.DescriptionMD)
	if err != nil {
		return CreateInput{}, err
	}
	source, err := normalizeSource(input.Source)
	if err != nil {
		return CreateInput{}, err
	}
	actor, err := normalizeActor(input.Actor)
	if err != nil {
		return CreateInput{}, err
	}
	idempotencyKey, err := normalizeIdempotencyKey(input.IdempotencyKey)
	if err != nil {
		return CreateInput{}, err
	}
	layout, err := normalizeLayout(input.Layout)
	if err != nil {
		return CreateInput{}, err
	}
	input.WorkspaceID = workspaceID
	input.Key = key
	input.DisplayName = displayName
	input.DescriptionMD = description
	input.Source = source
	input.Actor = actor
	input.IdempotencyKey = idempotencyKey
	input.Layout = layout
	return input, nil
}

func (service *Service) List(ctx context.Context, input ListInput) (PlanPage, error) {
	if err := service.validate(); err != nil {
		return PlanPage{}, err
	}
	normalized, err := normalizeList(input)
	if err != nil {
		return PlanPage{}, err
	}
	cursor, err := decodePlanCursor(normalized.Cursor)
	if err != nil {
		return PlanPage{}, err
	}
	values, err := service.repository.ListPlans(ctx, plan.ListPlansParams{
		WorkspaceID:     normalized.WorkspaceID,
		IncludeArchived: normalized.IncludeArchived,
		State:           normalized.State,
		Search:          normalized.Search,
		Limit:           normalized.Limit + 1,
		BeforeUpdatedAt: cursor.updatedAt,
		BeforeID:        cursor.id,
	})
	if err != nil {
		return PlanPage{}, mapRepositoryError(err)
	}
	hasMore := len(values) > normalized.Limit
	if hasMore {
		values = values[:normalized.Limit]
	}
	page := PlanPage{Items: make([]Plan, 0, len(values))}
	for _, value := range values {
		page.Items = append(page.Items, planFromStore(value))
	}
	if hasMore {
		last := values[len(values)-1]
		page.NextCursor, err = encodePlanCursor(last.UpdatedAt, last.ID)
		if err != nil {
			return PlanPage{}, err
		}
	}
	return page, nil
}

func (service *Service) Get(ctx context.Context, workspaceID string, planID string) (Plan, error) {
	if err := service.validate(); err != nil {
		return Plan{}, err
	}
	workspaceID, planID, err := requiredScope(workspaceID, planID)
	if err != nil {
		return Plan{}, err
	}
	value, err := service.repository.LoadPlan(ctx, workspaceID, planID)
	if err != nil {
		return Plan{}, mapRepositoryError(err)
	}
	return planFromStore(value), nil
}

// CanAuthor reports whether a Human workspace member owns direct authoring
// authority for one ordinary Plan.
func (service *Service) CanAuthor(
	ctx context.Context,
	workspaceID string,
	planID string,
	memberID string,
) (bool, error) {
	if err := service.validate(); err != nil {
		return false, err
	}
	workspaceID, planID, err := requiredScope(workspaceID, planID)
	if err != nil {
		return false, err
	}
	memberID, err = requiredID("member id", memberID)
	if err != nil {
		return false, err
	}
	repository, ok := service.repository.(authoringAuthorityRepository)
	if !ok {
		return false, fmt.Errorf("workflow Plan authoring authority repository is not configured")
	}
	authoringMemberID, err := repository.ResolvePlanAuthoringMember(ctx, workspaceID, planID)
	if err != nil {
		return false, mapRepositoryError(err)
	}
	return authoringMemberID == memberID, nil
}

func (service *Service) UpdateMetadata(ctx context.Context, input UpdateMetadataInput) (Plan, error) {
	if err := service.validate(); err != nil {
		return Plan{}, err
	}
	workspaceID, planID, err := requiredScope(input.WorkspaceID, input.PlanID)
	if err != nil {
		return Plan{}, err
	}
	if err := positiveRevision("expected Plan revision", input.ExpectedRevision); err != nil {
		return Plan{}, err
	}
	if input.DisplayName == nil && input.DescriptionMD == nil {
		return Plan{}, invalidRequest("display name or description is required")
	}
	var displayName *string
	if input.DisplayName != nil {
		value, err := normalizeDisplayName(*input.DisplayName)
		if err != nil {
			return Plan{}, err
		}
		displayName = &value
	}
	var description *string
	if input.DescriptionMD != nil {
		value, err := normalizeDescription(*input.DescriptionMD)
		if err != nil {
			return Plan{}, err
		}
		description = &value
	}
	actor, err := normalizeActor(input.Actor)
	if err != nil {
		return Plan{}, err
	}
	value, err := service.repository.UpdatePlanMetadata(ctx, plan.UpdatePlanMetadataParams{
		WorkspaceID: workspaceID, PlanID: planID, ExpectedRevision: input.ExpectedRevision,
		DisplayName: displayName, DescriptionMD: description, Actor: storeActor(actor), Now: service.clock.Now(),
	})
	if err != nil {
		return Plan{}, mapRepositoryError(err)
	}
	return planFromStore(value), nil
}

func (service *Service) GetDraft(ctx context.Context, workspaceID string, planID string) (Draft, error) {
	if err := service.validate(); err != nil {
		return Draft{}, err
	}
	workspaceID, planID, err := requiredScope(workspaceID, planID)
	if err != nil {
		return Draft{}, err
	}
	value, err := service.repository.LoadDraft(ctx, workspaceID, planID)
	if err != nil {
		return Draft{}, mapRepositoryError(err)
	}
	return draftFromStore(value), nil
}

func (service *Service) SaveDraft(ctx context.Context, input SaveDraftInput) (Draft, error) {
	if err := service.validate(); err != nil {
		return Draft{}, err
	}
	workspaceID, planID, err := requiredScope(input.WorkspaceID, input.PlanID)
	if err != nil {
		return Draft{}, err
	}
	if err := positiveRevision("expected draft revision", input.ExpectedRevision); err != nil {
		return Draft{}, err
	}
	source, err := normalizeSource(input.Source)
	if err != nil {
		return Draft{}, err
	}
	preserveLayout := len(input.Layout) == 0
	var layout json.RawMessage
	if !preserveLayout {
		layout, err = normalizeLayout(input.Layout)
		if err != nil {
			return Draft{}, err
		}
	}
	actor, err := normalizeActor(input.Actor)
	if err != nil {
		return Draft{}, err
	}
	if err := service.requireCompilableSource(source); err != nil {
		return Draft{}, err
	}
	value, err := service.repository.SaveDraft(ctx, plan.SaveDraftParams{
		WorkspaceID: workspaceID, PlanID: planID, ExpectedRevision: input.ExpectedRevision,
		Source: source, Layout: layout, PreserveLayout: preserveLayout,
		Actor: storeActor(actor), Now: service.clock.Now(),
	})
	if err != nil {
		return Draft{}, mapRepositoryError(err)
	}
	draft := draftFromStore(value)
	if draft.Revision > input.ExpectedRevision && service.draftUpdated != nil {
		service.draftUpdated(draft)
	}
	return draft, nil
}

func (service *Service) Validate(_ context.Context, source string) Validation {
	return service.validateSource(source)
}

// ValidateDraft compiles the exact stored draft revision requested by the
// caller without mutating the Plan aggregate. Archived Plans remain readable,
// so their preserved drafts may be inspected and validated as well.
func (service *Service) ValidateDraft(ctx context.Context, workspaceID string, planID string, draftRevision int64) (Validation, error) {
	if err := service.validate(); err != nil {
		return Validation{}, err
	}
	workspaceID, planID, err := requiredScope(workspaceID, planID)
	if err != nil {
		return Validation{}, err
	}
	if err := positiveRevision("draft revision", draftRevision); err != nil {
		return Validation{}, err
	}
	draft, err := service.repository.LoadDraft(ctx, workspaceID, planID)
	if err != nil {
		return Validation{}, mapRepositoryError(err)
	}
	if draft.Revision != draftRevision {
		return Validation{}, draftConflict(
			fmt.Sprintf("draft revision conflict: expected %d, actual %d", draftRevision, draft.Revision),
			draftRevision,
			draft.Revision,
			nil,
		)
	}
	return service.validateSource(draft.Source), nil
}

func (service *Service) validateSource(source string) Validation {
	if service == nil {
		return internalCompilerValidation()
	}
	return validateSourceWithCompiler(source, service.compile)
}

func validateSourceWithCompiler(source string, compile compiler) Validation {
	if compile == nil {
		return internalCompilerValidation()
	}
	if diagnostics := sourceBudgetDiagnostics(source); len(diagnostics) != 0 {
		return Validation{Diagnostics: diagnostics}
	}
	artifact, diagnostics, err := compile([]byte(source))
	if err != nil {
		return internalCompilerValidation()
	}
	result := Validation{Diagnostics: diagnostics}
	if planlang.HasErrors(diagnostics) {
		return result
	}
	if artifact == nil {
		return internalCompilerValidation()
	}
	payload, err := normalizedArtifactPayload(artifact)
	if err != nil {
		return internalCompilerValidation()
	}
	result.Artifact = payload
	result.Digest = artifact.Digest
	return result
}

func (service *Service) requireCompilableSource(source string) error {
	validation := service.validateSource(source)
	if planlang.HasErrors(validation.Diagnostics) {
		return compileFailed(validation.Diagnostics)
	}
	return nil
}

func (service *Service) Publish(ctx context.Context, input PublishInput) (Plan, Version, error) {
	if err := service.validate(); err != nil {
		return Plan{}, Version{}, err
	}
	if err := positiveRevision("draft revision", input.DraftRevision); err != nil {
		return Plan{}, Version{}, err
	}
	workspaceID, planID, actor, idempotencyKey, err := normalizePublishIdentity(
		input.WorkspaceID, input.PlanID, input.ExpectedPlanRevision, input.Actor, input.IdempotencyKey,
	)
	if err != nil {
		return Plan{}, Version{}, err
	}
	// A transport retry must not depend on the mutable draft or the compiler
	// binary currently deployed. Reuse the first committed immutable artifact;
	// PublishDraft still compares the caller-owned logical request digest and
	// rejects the same key when revision/origin/makeDefault differs.
	existing, found, err := service.repository.LoadVersionByPublishIdempotency(ctx, workspaceID, planID, storeActor(actor), idempotencyKey)
	if err != nil {
		return Plan{}, Version{}, mapRepositoryError(err)
	}
	if found {
		result, err := service.repository.PublishDraft(ctx, plan.PublishDraftParams{
			WorkspaceID: workspaceID, PlanID: planID, ExpectedPlanRevision: input.ExpectedPlanRevision,
			DraftRevision: input.DraftRevision, Artifact: existing.Artifact,
			CompilerVersion: existing.CompilerVersion, Digest: existing.Digest,
			MakeDefault: input.MakeDefault, Actor: storeActor(actor),
			IdempotencyKey:               idempotencyKey,
			RequireNoActiveAgentEditWake: actor.Type == ActorTypeAgent,
			Now:                          service.clock.Now(),
		})
		if err != nil {
			return Plan{}, Version{}, mapRepositoryError(err)
		}
		return planFromStore(result.Plan), versionFromStore(result.Version), nil
	}
	if err := service.requireMutablePlanRevision(ctx, workspaceID, planID, input.ExpectedPlanRevision); err != nil {
		return Plan{}, Version{}, err
	}
	draft, err := service.repository.LoadDraft(ctx, workspaceID, planID)
	if err != nil {
		return Plan{}, Version{}, mapRepositoryError(err)
	}
	if draft.Revision != input.DraftRevision {
		return Plan{}, Version{}, draftConflict(
			fmt.Sprintf("draft revision conflict: expected %d, actual %d", input.DraftRevision, draft.Revision),
			input.DraftRevision,
			draft.Revision,
			nil,
		)
	}
	if diagnostics := sourceBudgetDiagnostics(draft.Source); len(diagnostics) != 0 {
		return Plan{}, Version{}, compileFailed(diagnostics)
	}
	artifact, diagnostics, err := service.compile([]byte(draft.Source))
	if err != nil {
		return Plan{}, Version{}, fmt.Errorf("compile workflow Plan draft: %w", err)
	}
	if planlang.HasErrors(diagnostics) {
		return Plan{}, Version{}, compileFailed(diagnostics)
	}
	if artifact == nil {
		return Plan{}, Version{}, fmt.Errorf("compile workflow Plan draft: compiler returned no artifact")
	}
	payload, err := normalizedArtifactPayload(artifact)
	if err != nil {
		return Plan{}, Version{}, fmt.Errorf("verify workflow Plan artifact: %w", err)
	}
	result, err := service.repository.PublishDraft(ctx, plan.PublishDraftParams{
		WorkspaceID: workspaceID, PlanID: planID, ExpectedPlanRevision: input.ExpectedPlanRevision,
		DraftRevision: input.DraftRevision, Artifact: payload, CompilerVersion: artifact.CompilerVersion,
		Digest: artifact.Digest, MakeDefault: input.MakeDefault, Actor: storeActor(actor),
		IdempotencyKey:               idempotencyKey,
		RequireNoActiveAgentEditWake: actor.Type == ActorTypeAgent,
		Now:                          service.clock.Now(),
	})
	if err != nil {
		return Plan{}, Version{}, mapRepositoryError(err)
	}
	return planFromStore(result.Plan), versionFromStore(result.Version), nil
}

func (service *Service) ListVersions(ctx context.Context, workspaceID string, planID string, limit int, rawCursor string) (VersionPage, error) {
	if err := service.validate(); err != nil {
		return VersionPage{}, err
	}
	workspaceID, planID, err := requiredScope(workspaceID, planID)
	if err != nil {
		return VersionPage{}, err
	}
	if limit == 0 {
		limit = defaultPageLimit
	}
	if limit < 1 || limit > maxPageLimit {
		return VersionPage{}, invalidRequest(fmt.Sprintf("limit must be between 1 and %d", maxPageLimit))
	}
	before, err := decodeVersionCursor(strings.TrimSpace(rawCursor))
	if err != nil {
		return VersionPage{}, err
	}
	// A version collection is a nested Plan resource. Verify the parent first
	// so a missing or cross-workspace Plan is not silently represented as the
	// same empty collection as an existing Plan with no published versions.
	if _, err := service.repository.LoadPlan(ctx, workspaceID, planID); err != nil {
		return VersionPage{}, mapRepositoryError(err)
	}
	values, err := service.repository.ListVersions(ctx, plan.ListVersionsParams{
		WorkspaceID: workspaceID, PlanID: planID, Limit: limit + 1,
		BeforePublishedAt: before.publishedAt, BeforeID: before.id,
	})
	if err != nil {
		return VersionPage{}, mapRepositoryError(err)
	}
	hasMore := len(values) > limit
	if hasMore {
		values = values[:limit]
	}
	page := VersionPage{Items: make([]VersionSummary, 0, len(values))}
	for _, value := range values {
		page.Items = append(page.Items, versionSummaryFromStore(value))
	}
	if hasMore {
		last := values[len(values)-1]
		page.NextCursor, err = encodeVersionCursor(last.PublishedAt, last.ID)
		if err != nil {
			return VersionPage{}, err
		}
	}
	return page, nil
}

func (service *Service) GetVersion(ctx context.Context, workspaceID string, planID string, versionID string) (Version, error) {
	if err := service.validate(); err != nil {
		return Version{}, err
	}
	workspaceID, planID, err := requiredScope(workspaceID, planID)
	if err != nil {
		return Version{}, err
	}
	versionID, err = requiredID("Plan version id", versionID)
	if err != nil {
		return Version{}, err
	}
	value, err := service.repository.LoadVersionByID(ctx, workspaceID, planID, versionID)
	if err != nil {
		return Version{}, mapRepositoryError(err)
	}
	return versionFromStore(value), nil
}

// ResolveVersionForBinding selects one exact immutable PlanVersion ID for a
// new deployment. Unlike authoring reads it rejects archived Plans and repeats
// the artifact trust-boundary checks performed by the concrete store.
func (service *Service) ResolveVersionForBinding(ctx context.Context, workspaceID string, planID string, versionID string) (ResolvedVersionForBinding, error) {
	if err := service.validate(); err != nil {
		return ResolvedVersionForBinding{}, err
	}
	workspaceID, planID, err := requiredScope(workspaceID, planID)
	if err != nil {
		return ResolvedVersionForBinding{}, err
	}
	versionID, err = requiredID("Plan version id", versionID)
	if err != nil {
		return ResolvedVersionForBinding{}, err
	}
	value, err := service.repository.ResolveVersionForBinding(ctx, workspaceID, planID, versionID)
	if err != nil {
		return ResolvedVersionForBinding{}, mapRepositoryError(err)
	}
	artifact, err := validateBindingVersionResource(workspaceID, planID, versionID, value)
	if err != nil {
		return ResolvedVersionForBinding{}, err
	}
	return ResolvedVersionForBinding{Version: versionFromStore(value), Artifact: artifact}, nil
}

// InspectVersionForBinding verifies an exact version while preserving archived
// Plan visibility for idempotent Binding request replay. New Binding writes
// must independently fence active Plan state in their store transaction.
func (service *Service) InspectVersionForBinding(ctx context.Context, workspaceID string, planID string, versionID string) (InspectedVersionForBinding, error) {
	if err := service.validate(); err != nil {
		return InspectedVersionForBinding{}, err
	}
	workspaceID, planID, err := requiredScope(workspaceID, planID)
	if err != nil {
		return InspectedVersionForBinding{}, err
	}
	versionID, err = requiredID("Plan version id", versionID)
	if err != nil {
		return InspectedVersionForBinding{}, err
	}
	inspection, err := service.repository.InspectVersionForBinding(ctx, workspaceID, planID, versionID)
	if err != nil {
		return InspectedVersionForBinding{}, mapRepositoryError(err)
	}
	artifact, err := validateBindingVersionResource(workspaceID, planID, versionID, inspection.Version)
	if err != nil {
		return InspectedVersionForBinding{}, err
	}
	state := PlanState(strings.TrimSpace(inspection.PlanState))
	if state != PlanStateActive && state != PlanStateArchived {
		return InspectedVersionForBinding{}, fmt.Errorf("inspect workflow Plan version for Binding: repository returned invalid Plan state %q", inspection.PlanState)
	}
	return InspectedVersionForBinding{PlanState: state, Version: versionFromStore(inspection.Version), Artifact: artifact}, nil
}

func validateBindingVersionResource(workspaceID string, planID string, versionID string, value plan.PlanVersion) (*planlang.Artifact, error) {
	if strings.TrimSpace(value.WorkspaceID) != workspaceID || strings.TrimSpace(value.PlanID) != planID || strings.TrimSpace(value.ID) != versionID {
		return nil, fmt.Errorf("resolve workflow Plan version for Binding: repository returned a different resource")
	}
	artifact, err := planlang.DecodeArtifact(value.Artifact)
	if err != nil {
		return nil, fmt.Errorf("resolve workflow Plan version for Binding: %w", err)
	}
	if strings.TrimSpace(value.CompilerVersion) == "" || artifact.CompilerVersion != strings.TrimSpace(value.CompilerVersion) {
		return nil, fmt.Errorf("resolve workflow Plan version for Binding: compiler version metadata does not match artifact")
	}
	if strings.TrimSpace(value.Digest) == "" || artifact.Digest != strings.TrimSpace(value.Digest) {
		return nil, fmt.Errorf("resolve workflow Plan version for Binding: digest metadata does not match artifact")
	}
	return artifact, nil
}

func (service *Service) Republish(ctx context.Context, input RepublishInput) (Plan, Version, error) {
	if err := service.validate(); err != nil {
		return Plan{}, Version{}, err
	}
	sourceVersionID, err := requiredID("Plan version id", input.SourceVersionID)
	if err != nil {
		return Plan{}, Version{}, err
	}
	workspaceID, planID, actor, idempotencyKey, err := normalizePublishIdentity(
		input.WorkspaceID, input.PlanID, input.ExpectedPlanRevision, input.Actor, input.IdempotencyKey,
	)
	if err != nil {
		return Plan{}, Version{}, err
	}
	existing, found, err := service.repository.LoadVersionByPublishIdempotency(ctx, workspaceID, planID, storeActor(actor), idempotencyKey)
	if err != nil {
		return Plan{}, Version{}, mapRepositoryError(err)
	}
	if found {
		result, err := service.repository.RepublishVersion(ctx, plan.RepublishVersionParams{
			WorkspaceID: workspaceID, PlanID: planID, SourceVersionID: sourceVersionID,
			ExpectedPlanRevision: input.ExpectedPlanRevision, Artifact: existing.Artifact,
			CompilerVersion: existing.CompilerVersion, Digest: existing.Digest,
			MakeDefault: input.MakeDefault, Actor: storeActor(actor),
			IdempotencyKey: idempotencyKey, Now: service.clock.Now(),
		})
		if err != nil {
			return Plan{}, Version{}, mapRepositoryError(err)
		}
		return planFromStore(result.Plan), versionFromStore(result.Version), nil
	}
	if err := service.requireMutablePlanRevision(ctx, workspaceID, planID, input.ExpectedPlanRevision); err != nil {
		return Plan{}, Version{}, err
	}
	origin, err := service.repository.LoadVersionByID(ctx, workspaceID, planID, sourceVersionID)
	if err != nil {
		return Plan{}, Version{}, mapRepositoryError(err)
	}
	if diagnostics := sourceBudgetDiagnostics(origin.Source); len(diagnostics) != 0 {
		return Plan{}, Version{}, compileFailed(diagnostics)
	}
	artifact, diagnostics, err := service.compile([]byte(origin.Source))
	if err != nil {
		return Plan{}, Version{}, fmt.Errorf("compile historical workflow Plan version: %w", err)
	}
	if planlang.HasErrors(diagnostics) {
		return Plan{}, Version{}, compileFailed(diagnostics)
	}
	if artifact == nil {
		return Plan{}, Version{}, fmt.Errorf("compile historical workflow Plan version: compiler returned no artifact")
	}
	payload, err := normalizedArtifactPayload(artifact)
	if err != nil {
		return Plan{}, Version{}, fmt.Errorf("verify workflow Plan artifact: %w", err)
	}
	result, err := service.repository.RepublishVersion(ctx, plan.RepublishVersionParams{
		WorkspaceID: workspaceID, PlanID: planID, SourceVersionID: sourceVersionID,
		ExpectedPlanRevision: input.ExpectedPlanRevision, Artifact: payload,
		CompilerVersion: artifact.CompilerVersion, Digest: artifact.Digest, MakeDefault: input.MakeDefault,
		Actor: storeActor(actor), IdempotencyKey: idempotencyKey, Now: service.clock.Now(),
	})
	if err != nil {
		return Plan{}, Version{}, mapRepositoryError(err)
	}
	return planFromStore(result.Plan), versionFromStore(result.Version), nil
}

// normalizedArtifactPayload makes the compiler/store handoff an explicit
// trust boundary. A compiler bug must not turn a malformed or digest-mismatched
// artifact into an immutable PlanVersion; the concrete store repeats this
// verification to protect non-service callers and transaction integrity.
func normalizedArtifactPayload(artifact *planlang.Artifact) ([]byte, error) {
	if artifact == nil {
		return nil, fmt.Errorf("artifact is nil")
	}
	if err := artifact.ValidateEnvelope(); err != nil {
		return nil, err
	}
	return artifact.DeterministicJSON()
}

func (service *Service) SetDefault(ctx context.Context, input PromoteDefaultInput) (Plan, error) {
	if err := service.validate(); err != nil {
		return Plan{}, err
	}
	workspaceID, planID, err := requiredScope(input.WorkspaceID, input.PlanID)
	if err != nil {
		return Plan{}, err
	}
	if err := positiveRevision("expected Plan revision", input.ExpectedRevision); err != nil {
		return Plan{}, err
	}
	versionID, err := requiredID("Plan version id", input.VersionID)
	if err != nil {
		return Plan{}, err
	}
	actor, err := normalizeActor(input.Actor)
	if err != nil {
		return Plan{}, err
	}
	value, err := service.repository.SetDefaultVersion(ctx, plan.SetDefaultVersionParams{
		WorkspaceID: workspaceID, PlanID: planID, ExpectedRevision: input.ExpectedRevision,
		VersionID: versionID, Actor: storeActor(actor), Now: service.clock.Now(),
	})
	if err != nil {
		return Plan{}, mapRepositoryError(err)
	}
	return planFromStore(value), nil
}

func (service *Service) Archive(ctx context.Context, input LifecycleInput) (Plan, error) {
	if err := service.validate(); err != nil {
		return Plan{}, err
	}
	workspaceID, planID, actor, err := normalizeLifecycle(input)
	if err != nil {
		return Plan{}, err
	}
	value, err := service.repository.ArchivePlan(ctx, plan.ArchivePlanParams{
		WorkspaceID: workspaceID, PlanID: planID, ExpectedRevision: input.ExpectedRevision,
		Actor:                        storeActor(actor),
		RequireNoActiveAgentEditWake: actor.Type == ActorTypeAgent,
		Now:                          service.clock.Now(),
	})
	if err != nil {
		return Plan{}, mapRepositoryError(err)
	}
	return planFromStore(value), nil
}

func (service *Service) Restore(ctx context.Context, input LifecycleInput) (Plan, error) {
	if err := service.validate(); err != nil {
		return Plan{}, err
	}
	workspaceID, planID, actor, err := normalizeLifecycle(input)
	if err != nil {
		return Plan{}, err
	}
	value, err := service.repository.RestorePlan(ctx, plan.RestorePlanParams{
		WorkspaceID: workspaceID, PlanID: planID, ExpectedRevision: input.ExpectedRevision,
		Actor: storeActor(actor), Now: service.clock.Now(),
	})
	if err != nil {
		return Plan{}, mapRepositoryError(err)
	}
	return planFromStore(value), nil
}

func normalizePublishIdentity(workspaceID string, planID string, planRevision int64, actor Actor, idempotencyKey string) (string, string, Actor, string, error) {
	workspaceID, planID, err := requiredScope(workspaceID, planID)
	if err != nil {
		return "", "", Actor{}, "", err
	}
	if err := positiveRevision("expected Plan revision", planRevision); err != nil {
		return "", "", Actor{}, "", err
	}
	actor, err = normalizeActor(actor)
	if err != nil {
		return "", "", Actor{}, "", err
	}
	idempotencyKey, err = normalizeIdempotencyKey(idempotencyKey)
	if err != nil {
		return "", "", Actor{}, "", err
	}
	return workspaceID, planID, actor, idempotencyKey, nil
}

func normalizeLifecycle(input LifecycleInput) (string, string, Actor, error) {
	workspaceID, planID, err := requiredScope(input.WorkspaceID, input.PlanID)
	if err != nil {
		return "", "", Actor{}, err
	}
	if err := positiveRevision("expected Plan revision", input.ExpectedRevision); err != nil {
		return "", "", Actor{}, err
	}
	actor, err := normalizeActor(input.Actor)
	if err != nil {
		return "", "", Actor{}, err
	}
	return workspaceID, planID, actor, nil
}

func (service *Service) requireMutablePlanRevision(ctx context.Context, workspaceID string, planID string, expected int64) error {
	value, err := service.repository.LoadPlan(ctx, workspaceID, planID)
	if err != nil {
		return mapRepositoryError(err)
	}
	if value.Revision != expected {
		return revisionConflict(
			fmt.Sprintf("plan revision conflict: expected %d, actual %d", expected, value.Revision),
			expected,
			value.Revision,
			nil,
		)
	}
	if value.State == plan.PlanStateArchived || value.ArchivedAt != nil {
		return mapRepositoryError(plan.ErrArchived)
	}
	return nil
}

func requiredScope(workspaceID string, planID string) (string, string, error) {
	workspaceID, err := requiredID("workspace id", workspaceID)
	if err != nil {
		return "", "", err
	}
	planID, err = requiredID("Plan id", planID)
	if err != nil {
		return "", "", err
	}
	return workspaceID, planID, nil
}

func requiredID(label string, value string) (string, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return "", invalidRequest(label + " is required")
	}
	return value, nil
}

func positiveRevision(label string, value int64) error {
	if value <= 0 {
		return invalidRequest(label + " must be positive")
	}
	return nil
}

func internalCompilerValidation() Validation {
	return Validation{Diagnostics: []planlang.Diagnostic{{
		Severity: planlang.SeverityError,
		Code:     "compiler.internal",
		Path:     "$",
		Message:  "Plan validation could not be completed",
	}}}
}

func mapRepositoryError(err error) error {
	if err == nil {
		return nil
	}
	var requestError RequestError
	if errors.As(err, &requestError) {
		return err
	}
	var revision plan.RevisionConflictError
	if errors.As(err, &revision) {
		if revision.Resource == "plan draft" {
			return draftConflict("The Plan draft changed while the request was being processed; reload the current draft and retry", revision.Expected, revision.Actual, revision)
		}
		return revisionConflict("The Plan changed while the request was being processed; reload the current revision and retry", revision.Expected, revision.Actual, revision)
	}
	var idempotency plan.IdempotencyConflictError
	if errors.As(err, &idempotency) {
		return RequestError{Kind: storeerr.ErrConflict, Code: ErrorCodeIdempotencyConflict, Message: "The same idempotency key was already used for a different request", Cause: idempotency}
	}
	switch {
	case errors.Is(err, plan.ErrArchived):
		return RequestError{Kind: storeerr.ErrConflict, Code: ErrorCodeArchived, Message: "Plan is archived", Cause: err}
	case errors.Is(err, plan.ErrAgentEditWakeActive):
		return fmt.Errorf(
			"%w: %w",
			ErrAgentWakeActive,
			storeerr.Conflict(
				"Plan lifecycle actions are unavailable during this Builder edit",
			),
		)
	case errors.Is(err, storeerr.ErrNotFound):
		return RequestError{Kind: storeerr.ErrNotFound, Code: ErrorCodeNotFound, Message: "Plan resource not found", Cause: err}
	case errors.Is(err, storeerr.ErrBadRequest):
		return RequestError{Kind: storeerr.ErrBadRequest, Code: ErrorCodeInvalidRequest, Message: "Plan request is invalid", Cause: err}
	case errors.Is(err, storeerr.ErrConflict):
		return RequestError{Kind: storeerr.ErrConflict, Code: ErrorCodeConflict, Message: "Plan mutation conflicts with the current state", Cause: err}
	default:
		return err
	}
}
