package teamworkflow

import (
	"bytes"
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/coordinator"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/team"
	storeteamworkflow "github.com/tidbcloud/AgentTeam/internal/store/teamworkflow"
	planstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/plan"
	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
	"github.com/tidbcloud/AgentTeam/internal/workflow/planmanage"
)

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

// Service coordinates Team, Team Workflow, and Workflow Plan stores. Every
// multi-aggregate state transition is committed in one database transaction.
type Service struct {
	db                           coredb.DB
	teams                        *team.Store
	teamWorkflows                *storeteamworkflow.Store
	plans                        *planstore.Store
	compile                      compiler
	lifecycleEvents              LifecycleEventSink
	afterPendingProposalSnapshot func()
}

func New(db coredb.DB, optionValues ...ServiceOptions) *Service {
	var options ServiceOptions
	if len(optionValues) != 0 {
		options = optionValues[0]
	}
	return &Service{
		db: db, teams: team.New(db),
		teamWorkflows: storeteamworkflow.New(db), plans: planstore.New(db),
		compile: plan.Compile, lifecycleEvents: options.LifecycleEvents,
	}
}

func (service *Service) validate() error {
	if service == nil || service.db == nil ||
		service.teams == nil || service.teamWorkflows == nil || service.plans == nil ||
		service.compile == nil {
		return fmt.Errorf("Team Workflow service is not configured")
	}
	return nil
}

func normalizedNow(value time.Time) time.Time {
	if value.IsZero() {
		return time.Now().UTC()
	}
	return value.UTC()
}

func required(value, label string, max int) (string, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return "", storeerr.BadRequest(label + " is required")
	}
	if max > 0 && len(value) > max {
		return "", storeerr.BadRequest(label + " is too long")
	}
	return value, nil
}

func digestStrings(values ...string) string {
	digest := sha256.Sum256([]byte(strings.Join(values, "\x00")))
	return hex.EncodeToString(digest[:])
}

func (service *Service) Create(ctx context.Context, authenticated agent.AuthenticatedAgent, input CreateInput) (CreateResult, error) {
	if err := service.validate(); err != nil {
		return CreateResult{}, err
	}
	prepared, err := prepareCreate(authenticated, input)
	if err != nil {
		return CreateResult{}, err
	}
	var result CreateResult
	changed := false
	err = coordinator.New(service.db).Run(ctx, "create Team Workflow", func(tx coredb.Tx) error {
		changed = false
		team, err := requireCurrentLeader(ctx, tx, authenticated, prepared.teamID)
		if err != nil {
			return err
		}
		relations := service.teamWorkflows.InTx(tx)
		current, found, err := relations.LoadCurrentForTeam(
			ctx,
			prepared.workspaceID,
			prepared.teamID,
		)
		if err != nil {
			return err
		}
		if found && current.SourceMessageID != prepared.sourceMessageID {
			return storeerr.Conflict("Team already has a Workflow")
		}
		relationResult, err := relations.CreateGenerating(ctx, storeteamworkflow.CreateGeneratingParams{
			WorkspaceID: prepared.workspaceID, TeamID: prepared.teamID,
			SourceScopeID: team.ChannelID, SourceMessageID: prepared.sourceMessageID,
			RequestedByMemberID: prepared.memberID, Now: prepared.now,
		})
		if err != nil {
			return err
		}
		changed = !relationResult.Replayed
		actor := planstore.Actor{Type: planstore.ActorTypeAgent, ID: prepared.agentID, MemberID: prepared.memberID}
		created, err := service.plans.InTx(tx).CreatePlan(ctx, planstore.CreatePlanParams{
			WorkspaceID: prepared.workspaceID, PlanKey: prepared.planKey,
			DisplayName: prepared.displayName, DescriptionMD: prepared.descriptionMD,
			Source: prepared.source, Layout: prepared.layout,
			Actor: actor, IdempotencyKey: prepared.idempotencyKey, Now: prepared.now,
		})
		if err != nil {
			return err
		}
		artifact, compilerVersion, digest, err := service.compileSnapshot(created.Draft.Source)
		if err != nil {
			return err
		}
		publishKey := digestStrings("team-workflow-create-publish-v3", prepared.workspaceID, created.Plan.ID, prepared.idempotencyKey)
		layoutIdentity := "omitted"
		if len(prepared.layout) != 0 {
			layoutIdentity = "provided:" + base64.RawStdEncoding.EncodeToString(prepared.layout)
		}
		commandDigest := digestStrings(
			"team-workflow-publish-command-v3", prepared.workspaceID, created.Plan.ID,
			"1", "1", prepared.source, layoutIdentity, "true",
			actor.Type, actor.ID, actor.MemberID,
		)
		published, err := service.plans.InTx(tx).PublishFrozenDraft(ctx, planstore.PublishFrozenDraftParams{
			WorkspaceID: prepared.workspaceID, PlanID: created.Plan.ID,
			ExpectedPlanRevision: created.Plan.Revision, DraftRevision: created.Draft.Revision,
			CommandDraftRevision: created.Draft.Revision,
			Source:               created.Draft.Source, Layout: append([]byte(nil), created.Draft.Layout...),
			Artifact: artifact, CompilerVersion: compilerVersion, Digest: digest,
			MakeDefault: true, Actor: actor, CommandRequestDigest: commandDigest,
			IdempotencyKey: publishKey, Now: prepared.now,
		})
		if err != nil {
			return err
		}
		relation := relationResult.TeamWorkflow
		if relation.PlanID == nil {
			relation, err = relations.BindPlan(ctx, storeteamworkflow.BindPlanParams{
				WorkspaceID: prepared.workspaceID, TeamID: prepared.teamID,
				TeamWorkflowID: relation.ID, PlanID: created.Plan.ID,
				ExpectedRevision: relation.Revision, Now: prepared.now,
			})
			if err != nil {
				return err
			}
			changed = true
		} else if *relation.PlanID != created.Plan.ID {
			return storeerr.Conflict("Team Workflow command is already bound to another Plan")
		}
		if relation.AuthoringState != storeteamworkflow.AuthoringStateIdle {
			relation, err = relations.MarkPublished(ctx, storeteamworkflow.MarkPublishedParams{
				WorkspaceID: prepared.workspaceID, TeamID: prepared.teamID,
				TeamWorkflowID: relation.ID, ExpectedRevision: relation.Revision, Now: prepared.now,
			})
			if err != nil {
				return err
			}
			changed = true
		}
		result = CreateResult{
			TeamWorkflow: relation, Plan: published.Plan, Draft: created.Draft,
			Replayed: relationResult.Replayed || created.Replayed || published.Replayed,
		}
		return nil
	})
	if err != nil {
		return CreateResult{}, err
	}
	if changed {
		service.emitLifecycleUpdated(result.TeamWorkflow)
	}
	return result, nil
}

type preparedCreate struct {
	workspaceID, agentID, memberID, teamID string
	sourceMessageID                        string
	planKey, displayName, descriptionMD    string
	source, idempotencyKey                 string
	layout                                 []byte
	now                                    time.Time
}

func prepareCreate(authenticated agent.AuthenticatedAgent, input CreateInput) (preparedCreate, error) {
	normalized, err := planmanage.NormalizeCreateInput(planmanage.CreateInput{
		WorkspaceID: authenticated.WorkspaceID, Key: input.PlanKey,
		DisplayName: input.DisplayName, DescriptionMD: input.DescriptionMD,
		Source: input.Source, Layout: append(json.RawMessage(nil), input.Layout...),
		Actor:          planmanage.Actor{Type: planmanage.ActorTypeAgent, ID: authenticated.AgentID, MemberID: authenticated.MemberID},
		IdempotencyKey: input.IdempotencyKey,
	})
	if err != nil {
		return preparedCreate{}, err
	}
	p := preparedCreate{
		workspaceID: strings.TrimSpace(authenticated.WorkspaceID),
		agentID:     strings.TrimSpace(authenticated.AgentID), memberID: strings.TrimSpace(authenticated.MemberID),
		teamID: strings.TrimSpace(input.TeamID), sourceMessageID: strings.TrimSpace(input.SourceMessageID),
		planKey: normalized.Key, displayName: normalized.DisplayName,
		descriptionMD: normalized.DescriptionMD, source: normalized.Source,
		layout: append([]byte(nil), normalized.Layout...), idempotencyKey: normalized.IdempotencyKey,
		now: normalizedNow(input.Now),
	}
	if p.workspaceID == "" || p.agentID == "" || p.memberID == "" || p.teamID == "" ||
		p.sourceMessageID == "" || p.planKey == "" || p.displayName == "" || p.source == "" || p.idempotencyKey == "" {
		return preparedCreate{}, storeerr.BadRequest("authenticated Agent, teamId, source command, and Plan fields are required")
	}
	return p, nil
}

func (service *Service) compileSnapshot(source string, draftRevisions ...int64) ([]byte, string, string, error) {
	artifact, diagnostics, err := service.compile([]byte(source))
	if err != nil {
		return nil, "", "", fmt.Errorf("compile Team Workflow Plan draft: %w", err)
	}
	if plan.HasErrors(diagnostics) {
		draftRevision := int64(1)
		if len(draftRevisions) > 0 && draftRevisions[0] > 0 {
			draftRevision = draftRevisions[0]
		}
		return nil, "", "", planmanage.RequestError{
			Kind: storeerr.ErrUnprocessable, Code: planmanage.ErrorCodeCompileFailed,
			Message: "Team Workflow Plan draft did not compile",
			Details: map[string]any{"draftRevision": draftRevision, "diagnostics": diagnostics},
		}
	}
	if artifact == nil {
		return nil, "", "", fmt.Errorf("compile Team Workflow Plan draft: compiler returned no artifact")
	}
	artifactJSON, err := artifact.DeterministicJSON()
	if err != nil {
		return nil, "", "", fmt.Errorf("encode Team Workflow Plan artifact deterministically: %w", err)
	}
	return artifactJSON, artifact.CompilerVersion, artifact.Digest, nil
}

// Update directly compiles and publishes a Team-linked Plan revision. The
// Team relation, mutable draft, immutable version, current/default pointers,
// and optimistic-concurrency fences all commit in one transaction.
func (service *Service) Update(ctx context.Context, authenticated agent.AuthenticatedAgent, input UpdateInput) (UpdateResult, error) {
	if err := service.validate(); err != nil {
		return UpdateResult{}, err
	}
	teamID, err := required(input.TeamID, "teamId", 255)
	if err != nil {
		return UpdateResult{}, err
	}
	planID, err := required(input.PlanID, "planId", 64)
	if err != nil {
		return UpdateResult{}, err
	}
	clientRequestID, err := required(input.ClientRequestID, "clientRequestId", 128)
	if err != nil {
		return UpdateResult{}, err
	}
	if input.ExpectedPlanRevision <= 0 || input.DraftRevision <= 0 {
		return UpdateResult{}, storeerr.BadRequest("Team Workflow Plan and draft revisions must be positive")
	}
	if strings.TrimSpace(input.Source) == "" {
		return UpdateResult{}, storeerr.BadRequest("source is required")
	}
	now := normalizedNow(input.Now)
	workspaceID := strings.TrimSpace(authenticated.WorkspaceID)
	actor := planstore.Actor{Type: planstore.ActorTypeAgent, ID: strings.TrimSpace(authenticated.AgentID), MemberID: strings.TrimSpace(authenticated.MemberID)}
	if workspaceID == "" || actor.ID == "" || actor.MemberID == "" {
		return UpdateResult{}, storeerr.BadRequest("authenticated Agent is required")
	}
	var result UpdateResult
	err = coredb.WithTx(ctx, service.db, func(tx coredb.Tx) error {
		if _, err := requireCurrentLeader(ctx, tx, authenticated, teamID); err != nil {
			return err
		}
		relations := service.teamWorkflows.InTx(tx)
		relation, err := relations.LoadByPlan(ctx, workspaceID, teamID, planID, false)
		if err != nil {
			return err
		}
		planStore := service.plans.InTx(tx)
		publishKey := digestStrings("team-workflow-update-publish-v3", workspaceID, relation.ID, clientRequestID)
		layoutIdentity := "omitted"
		if len(input.Layout) != 0 {
			layoutIdentity = "provided:" + base64.RawStdEncoding.EncodeToString(input.Layout)
		}
		commandDigest := digestStrings("team-workflow-publish-command-v3", workspaceID, planID, strconv.FormatInt(input.ExpectedPlanRevision, 10), strconv.FormatInt(input.DraftRevision, 10), strings.TrimSpace(input.Source), layoutIdentity, strconv.FormatBool(input.MakeDefault), actor.Type, actor.ID, actor.MemberID)
		if existing, found, err := planStore.LoadVersionByPublishCommand(ctx, planstore.LoadVersionByPublishCommandParams{
			WorkspaceID: workspaceID, PlanID: planID,
			ExpectedPlanRevision: input.ExpectedPlanRevision, DraftRevision: input.DraftRevision,
			MakeDefault: input.MakeDefault, Actor: actor, IdempotencyKey: publishKey,
			RequestDigest: commandDigest,
		}); err != nil {
			return err
		} else if found {
			result, err = service.replayUpdateInTx(ctx, planStore, relation, existing, workspaceID, planID)
			return err
		}
		plan, err := planStore.LoadPlanForUpdate(ctx, workspaceID, planID)
		if err != nil {
			return err
		}
		if plan.Revision != input.ExpectedPlanRevision {
			return planstore.RevisionConflictError{Resource: "plan", Expected: input.ExpectedPlanRevision, Actual: plan.Revision}
		}
		if plan.ArchivedAt != nil {
			return planstore.ErrArchived
		}
		if err := planStore.RequireNoMatchingActiveAgentEditWake(ctx, plan, actor); err != nil {
			return err
		}
		layout := append([]byte(nil), input.Layout...)
		if len(layout) == 0 {
			currentDraft, err := planStore.LoadDraftForUpdate(ctx, workspaceID, planID)
			if err != nil {
				return err
			}
			layout = append([]byte(nil), currentDraft.Layout...)
		}
		draft, err := planStore.SaveDraft(ctx, planstore.SaveDraftParams{
			WorkspaceID: workspaceID, PlanID: planID, ExpectedRevision: input.DraftRevision,
			Source: strings.TrimSpace(input.Source), Layout: layout, Actor: actor, Now: now,
		})
		if err != nil {
			return err
		}
		artifact, compilerVersion, digest, err := service.compileSnapshot(draft.Source, draft.Revision)
		if err != nil {
			return err
		}
		published, err := planStore.PublishFrozenDraft(ctx, planstore.PublishFrozenDraftParams{
			WorkspaceID: workspaceID, PlanID: planID, ExpectedPlanRevision: plan.Revision,
			DraftRevision: draft.Revision, CommandDraftRevision: input.DraftRevision,
			Source: draft.Source, Layout: append([]byte(nil), draft.Layout...),
			Artifact: artifact, CompilerVersion: compilerVersion, Digest: digest,
			MakeDefault: input.MakeDefault, Actor: actor, CommandRequestDigest: commandDigest,
			IdempotencyKey: publishKey, Now: now,
		})
		if err != nil {
			return err
		}
		relation, err = relations.MarkPublished(ctx, storeteamworkflow.MarkPublishedParams{
			WorkspaceID: workspaceID, TeamID: teamID, TeamWorkflowID: relation.ID,
			ExpectedRevision: relation.Revision, Now: now,
		})
		if err != nil {
			return err
		}
		result = UpdateResult{TeamWorkflow: relation, Plan: published.Plan, Draft: draft, Version: published.Version}
		return nil
	})
	if err != nil {
		return UpdateResult{}, err
	}
	if !result.Replayed {
		service.emitLifecycleUpdated(result.TeamWorkflow)
	}
	return result, nil
}

func (service *Service) replayUpdateInTx(
	ctx context.Context,
	planStore *planstore.TxStore,
	relation storeteamworkflow.TeamWorkflow,
	existing planstore.PlanVersion,
	workspaceID, planID string,
) (UpdateResult, error) {
	plan, err := planStore.LoadPlanForUpdate(ctx, workspaceID, planID)
	if err != nil {
		return UpdateResult{}, err
	}
	draft, err := planStore.LoadDraftForUpdate(ctx, workspaceID, planID)
	if err != nil {
		return UpdateResult{}, err
	}
	return UpdateResult{TeamWorkflow: relation, Plan: plan, Draft: draft, Version: existing, Replayed: true}, nil
}

func workflowplanJSONEqual(left, right []byte) bool {
	var leftValue, rightValue any
	if json.Unmarshal(left, &leftValue) != nil || json.Unmarshal(right, &rightValue) != nil {
		return bytes.Equal(left, right)
	}
	leftJSON, _ := json.Marshal(leftValue)
	rightJSON, _ := json.Marshal(rightValue)
	return bytes.Equal(leftJSON, rightJSON)
}

type availableCursor struct {
	UpdatedAt time.Time `json:"updatedAt"`
	ID        string    `json:"id"`
}

// ListAvailable returns only already-published, executable Team Plans.
func (service *Service) ListAvailable(ctx context.Context, authenticated agent.AuthenticatedAgent, teamID string, limit int, rawCursor string) (AvailablePage, error) {
	if err := service.validate(); err != nil {
		return AvailablePage{}, err
	}
	teamID = strings.TrimSpace(teamID)
	if teamID == "" {
		return AvailablePage{}, storeerr.BadRequest("teamId is required")
	}
	if limit <= 0 {
		limit = 50
	}
	if limit > 100 {
		return AvailablePage{}, storeerr.BadRequest("limit must not exceed 100")
	}
	if _, _, err := service.teams.GetTeamForAgent(ctx, authenticated, teamID); err != nil {
		return AvailablePage{}, err
	}
	cursor, err := decodeAvailableCursor(rawCursor)
	if err != nil {
		return AvailablePage{}, err
	}
	var beforeUpdatedAt *time.Time
	if !cursor.UpdatedAt.IsZero() {
		value := cursor.UpdatedAt
		beforeUpdatedAt = &value
	}
	views, err := service.teamWorkflows.List(ctx, storeteamworkflow.ListParams{
		WorkspaceID: authenticated.WorkspaceID, TeamID: teamID,
		State: storeteamworkflow.StateFilterAvailable, Limit: limit + 1,
		BeforeUpdatedAt: beforeUpdatedAt, BeforeID: cursor.ID,
	})
	if err != nil {
		return AvailablePage{}, err
	}
	hasMore := len(views) > limit
	if hasMore {
		views = views[:limit]
	}
	page := AvailablePage{Items: make([]planstore.Plan, 0, len(views))}
	for _, view := range views {
		if view.Plan == nil || view.Plan.CurrentVersion == nil || view.Plan.ArchivedAt != nil {
			return AvailablePage{}, fmt.Errorf("available Team Workflow query returned an unusable Plan")
		}
		page.Items = append(page.Items, *view.Plan)
	}
	if hasMore {
		last := views[len(views)-1].TeamWorkflow
		page.NextCursor, err = encodeAvailableCursor(availableCursor{UpdatedAt: last.UpdatedAt, ID: last.ID})
		if err != nil {
			return AvailablePage{}, err
		}
	}
	return page, nil
}

// InspectPlan returns the Team ownership and direct-publication authoring
// state needed by the Agent Plan surface. Unlinked Plans return found=false.
func (service *Service) InspectPlan(ctx context.Context, authenticated agent.AuthenticatedAgent, planID string) (PlanInspection, bool, error) {
	if err := service.validate(); err != nil {
		return PlanInspection{}, false, err
	}
	workspaceID := strings.TrimSpace(authenticated.WorkspaceID)
	planID = strings.TrimSpace(planID)
	if workspaceID == "" || planID == "" {
		return PlanInspection{}, false, storeerr.BadRequest("workspaceId and planId are required")
	}
	teamID, teamWorkflowID, deleted, linked, err := service.ResolvePlanManagement(ctx, workspaceID, planID)
	if err != nil || !linked {
		return PlanInspection{}, false, err
	}
	if deleted {
		return PlanInspection{}, false, storeerr.Conflict("Team Workflow is deleted")
	}
	if _, _, err := service.teams.GetTeamForAgent(ctx, authenticated, teamID); err != nil {
		return PlanInspection{}, false, err
	}
	view, err := service.teamWorkflows.Load(ctx, workspaceID, teamID, teamWorkflowID, true)
	if err != nil {
		return PlanInspection{}, false, err
	}
	return PlanInspection{
		TeamWorkflowID: view.TeamWorkflow.ID,
		TeamID:         view.TeamWorkflow.TeamID,
		Revision:       view.TeamWorkflow.Revision,
		AuthoringState: view.AuthoringStatus(),
	}, true, nil
}

// ArchivePlan retires a Team-linked Plan through the Team ownership boundary.
// The Agent supplies the business Plan revision; relation revision and Human
// owner identity remain internal lifecycle details.
func (service *Service) ArchivePlan(ctx context.Context, authenticated agent.AuthenticatedAgent, planID string, expectedPlanRevision int64) (planstore.Plan, bool, error) {
	if expectedPlanRevision <= 0 {
		return planstore.Plan{}, false, storeerr.BadRequest("expectedRevision must be positive")
	}
	inspection, linked, err := service.InspectPlan(ctx, authenticated, planID)
	if err != nil || !linked {
		return planstore.Plan{}, linked, err
	}
	team, err := requireCurrentLeader(ctx, service.db, authenticated, inspection.TeamID)
	if err != nil {
		return planstore.Plan{}, true, err
	}
	result, err := service.SetAvailability(ctx, LifecycleInput{
		WorkspaceID: authenticated.WorkspaceID, TeamID: inspection.TeamID,
		TeamWorkflowID: inspection.TeamWorkflowID, ActorMemberID: team.CreatedByMemberID,
		ExpectedRevision: inspection.Revision, ExpectedPlanRevision: expectedPlanRevision, Archived: true,
		AgentActor: &planstore.Actor{
			Type:     planstore.ActorTypeAgent,
			ID:       authenticated.AgentID,
			MemberID: authenticated.MemberID,
		},
	})
	return result.Plan, true, err
}

func decodeAvailableCursor(value string) (availableCursor, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return availableCursor{}, nil
	}
	raw, err := base64.RawURLEncoding.DecodeString(value)
	if err != nil {
		return availableCursor{}, storeerr.BadRequest("Team Workflow cursor is invalid")
	}
	var cursor availableCursor
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&cursor); err != nil || cursor.UpdatedAt.IsZero() || strings.TrimSpace(cursor.ID) == "" {
		return availableCursor{}, storeerr.BadRequest("Team Workflow cursor is invalid")
	}
	if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
		return availableCursor{}, storeerr.BadRequest("Team Workflow cursor is invalid")
	}
	cursor.UpdatedAt = cursor.UpdatedAt.UTC()
	cursor.ID = strings.TrimSpace(cursor.ID)
	return cursor, nil
}

func encodeAvailableCursor(cursor availableCursor) (string, error) {
	raw, err := json.Marshal(cursor)
	if err != nil {
		return "", fmt.Errorf("encode Team Workflow cursor: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(raw), nil
}

// RequireUnlinkedPlan protects workspace-wide lifecycle and publication
// surfaces. A durable Team relation, including its deletion tombstone, keeps
// ownership authoritative forever; only Team Workflow domain operations may
// archive or restore a live linked Plan.
func (service *Service) RequireUnlinkedPlan(ctx context.Context, workspaceID, planID string) error {
	if err := service.validate(); err != nil {
		return err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	planID = strings.TrimSpace(planID)
	if workspaceID == "" || planID == "" {
		return storeerr.BadRequest("workspaceId and planId are required")
	}
	var count int
	if err := service.db.QueryRow(ctx, `
SELECT COUNT(*)
FROM workspace_team_workflows
WHERE workspace_id=$1 AND plan_id=$2
`, workspaceID, planID).Scan(&count); err != nil {
		return fmt.Errorf("inspect Team-linked Workflow Plan: %w", err)
	}
	if count != 0 {
		return storeerr.Conflict("Team-linked Workflow Plans must be updated through the Team Workflow owner operation")
	}
	return nil
}

// ResolvePlanManagement exposes the durable Team ownership marker needed by
// the workspace Workflow Plan detail surface. Deletion tombstones remain
// resolvable because they continue fencing direct Plan publication and
// lifecycle mutations.
func (service *Service) ResolvePlanManagement(ctx context.Context, workspaceID, planID string) (
	teamID string,
	teamWorkflowID string,
	deleted bool,
	linked bool,
	err error,
) {
	if err := service.validate(); err != nil {
		return "", "", false, false, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	planID = strings.TrimSpace(planID)
	if workspaceID == "" || planID == "" {
		return "", "", false, false, storeerr.BadRequest("workspaceId and planId are required")
	}
	var deletedAt sql.NullTime
	err = service.db.QueryRow(ctx, `
SELECT team_id, id, deleted_at
FROM workspace_team_workflows
WHERE workspace_id=$1 AND plan_id=$2
`, workspaceID, planID).Scan(&teamID, &teamWorkflowID, &deletedAt)
	if errors.Is(err, sql.ErrNoRows) || coredb.IsNotFound(err) {
		return "", "", false, false, nil
	}
	if err != nil {
		return "", "", false, false, fmt.Errorf("resolve Workflow Plan management: %w", err)
	}
	return teamID, teamWorkflowID, deletedAt.Valid, true, nil
}
