import type {
  AuditRecordId,
  CorrelationId,
  EventId,
  GoalId,
  MutationContext,
  OutboxMessageId,
  PlanId,
  PlanRevisionId,
  PlanningSessionId,
  PlanningTurnId,
  ProjectId,
  TaskId,
} from "@awp/contracts";
import type {
  EffectiveDeliveryPlan,
  Goal,
  PlanningDeferral,
  PlanningItem,
  PlanningInterviewerResolvedSelection,
  PlanningInterviewerSelection,
  PlanningParticipationMode,
  PlanningTurn,
  PlanningProfile,
  PlanningProfileKind,
  PlanningSession,
  ProjectPlanningDefaults,
  Task,
} from "@awp/domain";
import { recomputePlanningReadiness } from "@awp/domain";
import type { Clock, IdGenerator } from "./ports/runtime.js";
import type { ApplicationTransaction, UnitOfWork } from "./ports/repositories.js";
import type { I1LifecycleService } from "./golive.js";
import type { ExecutionSelection } from "./execution.js";

export interface StartPlanningInput {
  readonly projectId: ProjectId;
  readonly title: string;
  readonly intent: string;
  readonly goalIds?: readonly GoalId[];
  readonly mode?: PlanningParticipationMode;
  readonly context: MutationContext;
}

export interface OnboardProjectInput {
  readonly name: string;
  readonly repositoryUrl: string;
  readonly intent: string;
  readonly vision?: string;
  readonly goalTitle?: string;
  readonly successCriteria?: readonly string[];
  readonly mode?: PlanningParticipationMode;
  readonly context: MutationContext;
}

export interface ResolvePlanningItemInput {
  readonly sessionId: PlanningSessionId;
  readonly itemKey: string;
  readonly answer?: string;
  readonly context: MutationContext;
}

export interface DeferPlanningItemInput {
  readonly sessionId: PlanningSessionId;
  readonly itemKey: string;
  readonly reason: string;
  readonly blockingAt: PlanningDeferral["blockingAt"];
  readonly consequence: string;
  readonly revisit: string;
  readonly context: MutationContext;
}

export interface PlanningLaunchInput {
  readonly sessionId: PlanningSessionId;
  readonly choice: "start" | "schedule" | "park";
  readonly scheduledFor?: string;
  readonly timezone?: string;
  readonly selection?: ExecutionSelection;
  readonly context: MutationContext;
}

export type PlannerStructuredDisposition =
  | {
      readonly kind: "resolve";
      readonly itemKey: string;
      readonly answer: string;
      readonly nextItemKey?: string;
    }
  | {
      readonly kind: "continue" | "none";
      readonly itemKey?: string;
    }
  | {
      readonly kind: "defer";
      readonly itemKey: string;
      readonly reason: string;
      readonly blockingAt: PlanningDeferral["blockingAt"];
      readonly consequence: string;
      readonly revisit: string;
      readonly nextItemKey?: string;
    };

export interface ApplyPlannerTurnInput {
  readonly sessionId: PlanningSessionId;
  readonly expectedRevision: number;
  readonly userTurnId: PlanningTurnId;
  readonly plannerTurnId: PlanningTurnId;
  readonly userMessage: string;
  readonly plannerMessage: string;
  readonly selection: PlanningInterviewerResolvedSelection;
  readonly providerId: string;
  readonly invocationId?: string;
  readonly observedReasoningEffort?: string;
  readonly disposition: PlannerStructuredDisposition;
  readonly context: MutationContext;
}

export interface RecordPlannerFailureInput {
  readonly sessionId: PlanningSessionId;
  readonly expectedRevision: number;
  readonly userTurnId: PlanningTurnId;
  readonly plannerTurnId: PlanningTurnId;
  readonly userMessage: string;
  readonly selection: PlanningInterviewerResolvedSelection;
  readonly providerId: string;
  readonly failureCategory: string;
  readonly safeMessage: string;
  readonly retryable: boolean;
  readonly context: MutationContext;
}

const trimRequired = (value: string, label: string): string => {
  const normalized = value.trim();
  if (!normalized) throw new Error(`${label} is required`);
  return normalized;
};

const defaultDelivery = (): EffectiveDeliveryPlan => ({
  quality: "CUJ-first affected verification plus regression coverage",
  ci: "Repository-required checks with GitHub Actions + ARC where configured",
  delivery: "Review-gated trusted publication; production remains explicitly gated",
  execution: "Dependency-aware K3s execution with durable WIP and independent review",
  connections: [],
});

function inferProfile(repositoryUrl: string, intent: string): PlanningProfile {
  const source = intent.toLowerCase();
  let kind: PlanningProfileKind = "existing-product-feature";
  let confidence: PlanningProfile["confidence"] = "medium";
  if (/infrastructure|platform|cluster|kubernetes|k3s|runtime/u.test(source)) {
    kind = "infrastructure-platform";
    confidence = "high";
  } else if (/sdk|library|package|api\b/u.test(source)) {
    kind = "library-sdk-api";
    confidence = "high";
  } else if (/\bcli\b|command[- ]line/u.test(source)) {
    kind = "cli-developer-tool";
    confidence = "high";
  } else if (/ui|ux|web|frontend|dashboard|application/u.test(source)) {
    kind = "ui-heavy-application";
    confidence = "medium";
  } else if (/prototype|quick|small|spike/u.test(source)) {
    kind = "small-quick-project";
    confidence = "medium";
  }
  const traits = new Set<string>();
  if (/ui|ux|web|frontend|dashboard|application/u.test(source)) traits.add("user-facing-ui");
  if (/deploy|production|release|cloud|worker|cluster|kubernetes|k3s/u.test(source))
    traits.add("production-deployment");
  if (/agent|untrusted|sandbox|execute code|runner/u.test(source)) traits.add("untrusted-code");
  if (/customer|personal data|database|postgres|storage/u.test(source)) traits.add("customer-data");
  if (/sdk|library|api|contract/u.test(source)) traits.add("stable-contracts");
  return {
    kind,
    confidence,
    traits: [...traits],
    basis: [
      `project repository identity: ${repositoryUrl}`,
      "owner intent",
      "project evidence available to AWP",
    ],
  };
}

function planningItems(
  profile: PlanningProfile,
  mode: PlanningParticipationMode,
  defaults: ProjectPlanningDefaults | undefined,
  visionSummary?: string,
): readonly PlanningItem[] {
  const now = new Date().toISOString();
  const expert = (input: Omit<PlanningItem, "status">): PlanningItem => {
    const inherited = defaults?.acceptedDecisionAnswers[input.key];
    if (inherited) {
      return { ...input, status: "delegated", answer: inherited, updatedAt: now };
    }
    if (mode === "simple") {
      return {
        ...input,
        status: "delegated",
        ...(input.recommendation ? { answer: input.recommendation } : {}),
        updatedAt: now,
      };
    }
    return { ...input, status: "pending" };
  };
  const items: PlanningItem[] = [
    {
      key: "intent",
      stage: "DEFINE",
      title: "Outcome and intent",
      summary: "Confirm the bounded outcome this Plan must achieve.",
      participation: "owner-required",
      status: "active",
    },
    ...(visionSummary
      ? [
          {
            key: "vision-alignment",
            stage: "DEFINE" as const,
            title: "ProjectVision alignment",
            summary:
              "Reconcile this Plan outcome with the durable ProjectVision before downstream design hardens around the wrong direction.",
            participation: "owner-required" as const,
            status: "pending" as const,
            recommendation: `Keep the current ProjectVision as the governing direction: ${visionSummary}`,
            confidence: "high" as const,
            basis: ["current durable ProjectVision", "current Plan intent"],
            consequences: [
              "If this Plan intentionally changes project direction, revise ProjectVision explicitly rather than silently drifting.",
            ],
            alternatives: [
              {
                id: "align-plan",
                label: "Align this Plan to ProjectVision",
                consequence: "Preserves the current durable project direction.",
              },
              {
                id: "revise-vision",
                label: "Revise ProjectVision first",
                consequence: "Makes the direction change explicit before planning continues.",
              },
            ],
          },
        ]
      : []),
    {
      key: "scope",
      stage: "DEFINE",
      title: "Scope and non-goals",
      summary: "State what is in scope and what must not be pulled into this Plan.",
      participation: "owner-required",
      status: "pending",
    },
  ];
  if (profile.traits.includes("user-facing-ui")) {
    items.push({
      key: "experience",
      stage: "DESIGN",
      title: "Product experience",
      summary: "Confirm the critical user journey and visible acceptance bar.",
      participation: "owner-required",
      status: "pending",
    });
  }
  items.push(
    expert({
      key: "architecture",
      stage: "DESIGN",
      title: "Architecture",
      summary: "Choose the smallest architecture change consistent with existing boundaries.",
      participation: "delegable-expert",
      recommendation:
        "Deepen the existing modular-monolith boundaries; do not add a new service unless the current dependency law requires it.",
      confidence: "high",
      basis: ["existing AWP architecture", "incremental delivery rule"],
      consequences: ["lower migration risk", "preserves existing domain identities"],
      alternatives: [
        {
          id: "new-service",
          label: "Add a service",
          consequence: "More operational surface and a new trust boundary.",
        },
        {
          id: "in-place",
          label: "Deepen current modules",
          consequence: "Keeps the current deployment and ownership model.",
        },
      ],
    }),
  );
  if (
    profile.traits.some((trait) =>
      ["untrusted-code", "customer-data", "production-deployment"].includes(trait),
    )
  ) {
    items.push(
      expert({
        key: "security",
        stage: "DESIGN",
        title: "Security and trust",
        summary: "Preserve current authority, credential and isolation boundaries.",
        participation: "delegable-expert",
        recommendation:
          "Keep browser-to-control auth, scoped credentials, K3s isolation and trusted publication boundaries fail-closed.",
        confidence: "high",
        basis: ["AWP security baseline", "project traits"],
      }),
    );
  }
  items.push(
    {
      key: "acceptance",
      stage: "SPECIFY",
      title: "Acceptance criteria",
      summary: "Define observable completion criteria for this Plan.",
      participation: "owner-required",
      status: "pending",
    },
    expert({
      key: "quality",
      stage: "DELIVER",
      title: "Quality strategy",
      summary: "Plan affected verification and regression coverage.",
      participation: "delegable-expert",
      recommendation:
        "CUJ-first affected verification plus regression coverage on allowed build hosts.",
      confidence: "high",
      basis: ["AWP quality policy", "current CI topology"],
    }),
    expert({
      key: "ci",
      stage: "DELIVER",
      title: "CI plan",
      summary: "Define repository-required checks and runner placement.",
      participation: "delegable-expert",
      recommendation:
        "Use repository-required checks; run CI on GitHub Actions + ARC where configured, never on the owner workstation.",
      confidence: "high",
      basis: ["repository required checks", "AWP execution policy"],
    }),
    expert({
      key: "delivery",
      stage: "DELIVER",
      title: "Delivery strategy",
      summary: "Plan review, publication and release boundaries.",
      participation: "delegable-expert",
      recommendation:
        "Independent review, exact-candidate verification, then trusted publication; keep production promotion explicitly gated.",
      confidence: "high",
      basis: ["trusted merge architecture", "delivery policy"],
    }),
    expert({
      key: "execution",
      stage: "DELIVER",
      title: "Execution strategy",
      summary: "Plan work decomposition and execution topology.",
      participation: "delegable-expert",
      recommendation:
        "Dependency-aware K3s execution with durable WIP, retryable Attempts and independent review.",
      confidence: "high",
      basis: ["AWP execution substrate", "current project defaults"],
    }),
    expert({
      key: "work-breakdown",
      stage: "DELIVER",
      title: "Execution work breakdown",
      summary: "Define the dependency-ready work that Factory will execute.",
      participation: "delegable-expert",
      recommendation:
        "Implement the approved Plan intent\nVerify affected behavior and acceptance criteria\nPrepare reviewed ChangeSet for delivery",
      confidence: "medium",
      basis: ["plan intent", "minimum complete execution chain"],
    }),
  );
  if (profile.traits.includes("production-deployment")) {
    items.push({
      key: "connection",
      stage: "DELIVER",
      title: "Required execution connection",
      summary: "Ensure a usable provider account/connection exists before execution.",
      participation: "policy-required",
      status: "pending",
      consequences: ["Start now remains blocked until execution authority is available."],
    });
  }
  return items;
}

function nextActive(items: readonly PlanningItem[]): string | undefined {
  return items.find((item) => ["active", "pending", "blocked"].includes(item.status))?.key;
}

function normalizeItems(
  items: readonly PlanningItem[],
  activeItemKey?: string,
): readonly PlanningItem[] {
  const active = activeItemKey ?? nextActive(items);
  return items.map((item) => {
    if (item.key === active && ["pending", "blocked"].includes(item.status))
      return { ...item, status: "active" as const };
    if (item.key !== active && item.status === "active")
      return { ...item, status: "pending" as const };
    return item;
  });
}

function parseWorkBreakdown(session: PlanningSession): readonly string[] {
  const item = session.items.find((candidate) => candidate.key === "work-breakdown");
  const source = item?.answer ?? item?.recommendation ?? "";
  const tasks = source
    .split(/\n+/u)
    .map((value) => value.trim())
    .filter(Boolean);
  if (tasks.length < 3)
    throw new Error("Planning requires at least three execution work items before launch");
  return tasks;
}

export class I2PlanningService {
  constructor(
    private readonly uow: UnitOfWork,
    private readonly ids: IdGenerator,
    private readonly clock: Clock,
    private readonly lifecycle: I1LifecycleService,
  ) {}

  async onboardProject(
    input: OnboardProjectInput,
  ): Promise<{ session: PlanningSession; projectId: ProjectId; goalId: GoalId }> {
    const project = await this.lifecycle.createProject({
      name: input.name,
      repositoryUrl: input.repositoryUrl,
      context: input.context,
    });
    const vision = await this.lifecycle.setProjectVision(
      project.id,
      input.vision?.trim() ||
        `Build and evolve ${input.name.trim()} toward: ${trimRequired(input.intent, "Intent")}`,
      input.context,
    );
    const criteria = input.successCriteria?.filter((value) => value.trim()) ?? [
      "The requested outcome is usable and verified end to end",
    ];
    const goal = await this.lifecycle.createGoal({
      projectId: project.id,
      title: input.goalTitle?.trim() || trimRequired(input.intent, "Intent"),
      successCriteria: criteria,
      context: input.context,
    });
    const session = await this.startPlanning({
      projectId: project.id,
      title: input.goalTitle?.trim() || "First Plan",
      intent: input.intent,
      goalIds: [goal.id],
      ...(input.mode ? { mode: input.mode } : {}),
      context: {
        ...input.context,
        authority: { ...input.context.authority, projectId: project.id },
      },
    });
    return {
      session: { ...session, projectVisionVersionId: vision.id },
      projectId: project.id,
      goalId: goal.id,
    };
  }

  async startPlanning(input: StartPlanningInput): Promise<PlanningSession> {
    return this.uow.transaction(async (tx) => {
      const project = await tx.projects.getById(input.projectId);
      if (!project) throw new Error(`Unknown Project ${input.projectId}`);
      const defaults = await tx.projectPlanningDefaults.getByProjectId(input.projectId);
      const mode = input.mode ?? defaults?.mode ?? "simple";
      const profile = inferProfile(project.repositoryUrl, input.intent);
      const vision = await tx.projectVisions.getCurrent(input.projectId);
      const goals = await tx.goals.listByProject(input.projectId);
      const goalIds = input.goalIds?.length
        ? input.goalIds
        : goals.filter((goal) => goal.status === "active").map((goal) => goal.id);
      for (const goalId of goalIds) {
        if (!goals.some((goal) => goal.id === goalId))
          throw new Error(`Goal ${goalId} is outside Project`);
      }
      const planId = this.ids.next<PlanId>();
      await tx.plans.insert({
        id: planId,
        projectId: input.projectId,
        title: trimRequired(input.title, "Plan title"),
        status: "draft",
        revision: 1,
      });
      const createdAt = this.clock.now().toISOString();
      const initialItems = planningItems(profile, mode, defaults, vision?.summary);
      const session: PlanningSession = {
        id: this.ids.next<PlanningSessionId>(),
        projectId: input.projectId,
        planId,
        ...(vision ? { projectVisionVersionId: vision.id } : {}),
        goalIds,
        intent: trimRequired(input.intent, "Intent"),
        mode,
        profile,
        status: "active",
        readiness: recomputePlanningReadiness(initialItems, []),
        activeItemKey: nextActive(initialItems),
        draft: "",
        turns: [],
        items: normalizeItems(initialItems),
        deferrals: [],
        delivery: defaults?.delivery ?? defaultDelivery(),
        createdAt,
        updatedAt: createdAt,
        revision: 1,
      };
      await tx.planningSessions.insert(session);
      await this.reconcileGoalReadiness(tx, input.projectId, goalIds);
      await this.record(
        tx,
        input.context,
        "PlanningStarted",
        "PlanningSession",
        session.id,
        session.revision,
        session.projectId,
        {
          planId: session.planId,
          goalIds: session.goalIds,
          mode: session.mode,
          profile: session.profile.kind,
        },
      );
      return session;
    });
  }

  async get(sessionId: PlanningSessionId): Promise<PlanningSession | undefined> {
    return this.uow.transaction((tx) => tx.planningSessions.getById(sessionId));
  }

  async listByProject(projectId: ProjectId): Promise<readonly PlanningSession[]> {
    return this.uow.transaction((tx) => tx.planningSessions.listByProject(projectId));
  }

  private async record(
    tx: ApplicationTransaction,
    context: MutationContext,
    type: string,
    aggregateType: string,
    aggregateId: string,
    aggregateRevision: number,
    projectId: ProjectId,
    payload: unknown,
  ): Promise<void> {
    const occurredAt = this.clock.now().toISOString();
    await tx.events.append({
      id: this.ids.next<EventId>(),
      type,
      schemaVersion: 1,
      occurredAt,
      aggregateType,
      aggregateId,
      aggregateRevision,
      projectId,
      principalId: context.authority.principal.id,
      correlationId: context.correlationId as CorrelationId,
      payload,
    });
    await tx.audit.append({
      id: this.ids.next<AuditRecordId>(),
      occurredAt,
      principalId: context.authority.principal.id,
      action: type,
      targetType: aggregateType,
      targetId: aggregateId,
      projectId,
      disposition: "allowed",
      correlationId: context.correlationId,
      safeMetadata: { operationId: context.operationId },
    });
    await tx.outbox.append({
      id: this.ids.next<OutboxMessageId>(),
      topic: type,
      payload: { aggregateType, aggregateId, aggregateRevision, projectId, payload },
      occurredAt,
    });
  }

  private async reconcileGoalReadiness(
    tx: ApplicationTransaction,
    projectId: ProjectId,
    goalIds: readonly GoalId[],
  ): Promise<void> {
    if (goalIds.length === 0) return;
    const sessions = await tx.planningSessions.listByProject(projectId);
    const goals = await tx.goals.listByProject(projectId);
    for (const goalId of goalIds) {
      const goal = goals.find((candidate) => candidate.id === goalId);
      if (!goal) continue;
      const linked = sessions.filter((session) => session.goalIds.includes(goalId));
      const blocked = linked.some((session) => session.readiness === "blocked");
      const ready = linked.length > 0 && linked.every((session) => session.readiness !== "blocked");
      const readiness: NonNullable<Goal["readiness"]> = blocked
        ? "blocked"
        : ready
          ? "ready"
          : "not-ready";
      const readinessReason = blocked
        ? "Linked Planning still has unresolved or currently-blocking requirements."
        : ready
          ? linked.some((session) => session.readiness === "ready-with-deferred-gaps")
            ? "Planning can proceed; deferred gaps remain explicit before their declared later gate."
            : "Linked Planning is ready with no unresolved required planning items."
          : "No linked PlanningSession has established readiness yet.";
      if (goal.readiness === readiness && goal.readinessReason === readinessReason) continue;
      await tx.goals.update({
        ...goal,
        readiness,
        readinessReason,
        revision: goal.revision + 1,
      });
    }
  }

  private async mutate(
    sessionId: PlanningSessionId,
    context: MutationContext,
    eventType: string,
    work: (
      session: PlanningSession,
      tx: ApplicationTransaction,
      now: string,
    ) => Promise<PlanningSession> | PlanningSession,
  ): Promise<PlanningSession> {
    return this.uow.transaction(async (tx) => {
      const current = await tx.planningSessions.getById(sessionId);
      if (!current) throw new Error(`Unknown PlanningSession ${sessionId}`);
      const now = this.clock.now().toISOString();
      const candidate = await work(current, tx, now);
      const next: PlanningSession = {
        ...candidate,
        planRevisionId: undefined,
        launch: undefined,
        status:
          candidate.status === "launched"
            ? "launched"
            : candidate.readiness === "blocked"
              ? "active"
              : "ready",
        updatedAt: now,
        revision: current.revision + 1,
      };
      const plan = await tx.plans.getById(current.planId);
      if (plan && plan.status !== "draft" && plan.status !== "active") {
        await tx.plans.update({ ...plan, status: "draft", revision: plan.revision + 1 });
      }
      await tx.planningSessions.update(next);
      await this.reconcileGoalReadiness(tx, next.projectId, next.goalIds);
      await this.record(
        tx,
        context,
        eventType,
        "PlanningSession",
        next.id,
        next.revision,
        next.projectId,
        {
          readiness: next.readiness,
          activeItemKey: next.activeItemKey,
          mode: next.mode,
          profile: next.profile.kind,
        },
      );
      return next;
    });
  }

  async setPlannerOverride(
    sessionId: PlanningSessionId,
    selection: PlanningInterviewerSelection | undefined,
    context: MutationContext,
  ): Promise<PlanningSession> {
    return this.mutate(sessionId, context, "PlanningInterviewerOverrideChanged", (session) => {
      if (["scheduled", "parked", "launched"].includes(session.status)) {
        throw new Error("Planner interviewer cannot change after launch disposition is selected");
      }
      if (selection !== undefined) {
        const providerId = trimRequired(selection.providerId, "Planner provider");
        const accountId = trimRequired(selection.accountId, "Planner account");
        const model = trimRequired(selection.model, "Planner model");
        const reasoningEffort = trimRequired(selection.reasoningEffort, "Planner reasoning effort");
        return { ...session, plannerOverride: { providerId, accountId, model, reasoningEffort } };
      }
      return { ...session, plannerOverride: undefined };
    });
  }

  async applyPlannerTurn(input: ApplyPlannerTurnInput): Promise<PlanningSession> {
    return this.mutate(
      input.sessionId,
      input.context,
      "PlanningTurnCompleted",
      (session, _tx, now) => {
        if (session.revision !== input.expectedRevision) {
          throw new Error(
            `PlanningSession revision changed during Planner turn: expected ${input.expectedRevision}, got ${session.revision}`,
          );
        }
        if (["scheduled", "parked", "launched"].includes(session.status)) {
          throw new Error("Planner conversation is closed after launch disposition is selected");
        }
        const userMessage = trimRequired(input.userMessage, "Planner user message");
        const plannerMessage = trimRequired(input.plannerMessage, "Planner response");
        let items = [...session.items];
        let deferrals = [...session.deferrals];
        let activeItemKey = session.activeItemKey ?? nextActive(items);
        const inputItemKey = activeItemKey;
        const disposition = input.disposition;
        if (disposition.kind === "resolve") {
          const target = items.find((item) => item.key === disposition.itemKey);
          if (!target || target.key !== activeItemKey) {
            throw new Error("Planner may resolve only the current active Planning item");
          }
          const answer = trimRequired(disposition.answer, "Planner interpreted answer");
          if (target.participation === "policy-required" && !answer) {
            throw new Error("Policy-required Planning item requires explicit owner resolution");
          }
          items = items.map((item) =>
            item.key === target.key
              ? { ...item, status: "accepted" as const, answer, updatedAt: now }
              : item,
          );
          deferrals = deferrals.map((deferral) =>
            deferral.itemKey === target.key
              ? { ...deferral, status: "resolved" as const }
              : deferral,
          );
          const proposedNext = disposition.nextItemKey
            ? items.find(
                (item) =>
                  item.key === disposition.nextItemKey &&
                  ["pending", "active", "blocked"].includes(item.status),
              )
            : undefined;
          activeItemKey = proposedNext?.key ?? nextActive(items);
          items = [...normalizeItems(items, activeItemKey)];
        } else if (disposition.kind === "defer") {
          const target = items.find((item) => item.key === disposition.itemKey);
          if (!target || target.key !== activeItemKey) {
            throw new Error("Planner may defer only the current active Planning item");
          }
          const deferral: PlanningDeferral = {
            itemKey: target.key,
            reason: trimRequired(disposition.reason, "Deferral reason"),
            blockingAt: disposition.blockingAt,
            owner: String(input.context.authority.principal.id),
            consequence: trimRequired(disposition.consequence, "Deferral consequence"),
            revisit: trimRequired(disposition.revisit, "Deferral revisit trigger"),
            status: "open",
          };
          items = items.map((item) =>
            item.key === target.key ? { ...item, status: "deferred" as const } : item,
          );
          deferrals = [...deferrals.filter((value) => value.itemKey !== target.key), deferral];
          const proposedNext = disposition.nextItemKey
            ? items.find(
                (item) =>
                  item.key === disposition.nextItemKey &&
                  ["pending", "active", "blocked"].includes(item.status),
              )
            : undefined;
          activeItemKey = proposedNext?.key ?? nextActive(items);
          items = [...normalizeItems(items, activeItemKey)];
        }
        const sequence = session.turns.length;
        const plannerDisposition: NonNullable<PlanningTurn["disposition"]> =
          disposition.kind === "resolve"
            ? "resolved"
            : disposition.kind === "defer"
              ? "deferred"
              : disposition.kind === "continue"
                ? "continued"
                : "none";
        const turns: PlanningSession["turns"] = [
          ...session.turns,
          {
            id: input.userTurnId,
            sequence: sequence + 1,
            role: "owner" as const,
            content: userMessage,
            status: "completed" as const,
            createdAt: now,
            inputRevision: input.expectedRevision,
            ...(inputItemKey ? { itemKey: inputItemKey } : {}),
          },
          {
            id: input.plannerTurnId,
            sequence: sequence + 2,
            role: "planner" as const,
            content: plannerMessage,
            status: "completed" as const,
            createdAt: now,
            inputRevision: input.expectedRevision,
            providerId: input.selection.providerId || input.providerId,
            accountId: input.selection.accountId,
            model: input.selection.model,
            reasoningEffort: input.selection.reasoningEffort,
            ...(input.observedReasoningEffort
              ? { observedReasoningEffort: input.observedReasoningEffort }
              : {}),
            ...(input.invocationId ? { invocationId: input.invocationId } : {}),
            disposition: plannerDisposition,
            ...(disposition.itemKey ? { itemKey: disposition.itemKey } : {}),
          },
        ];
        return {
          ...session,
          turns,
          items,
          deferrals,
          activeItemKey,
          draft: "",
          readiness: recomputePlanningReadiness(items, deferrals),
        };
      },
    );
  }

  async recordPlannerFailure(input: RecordPlannerFailureInput): Promise<PlanningSession> {
    return this.mutate(
      input.sessionId,
      input.context,
      "PlanningTurnFailed",
      (session, _tx, now) => {
        if (session.revision !== input.expectedRevision) {
          throw new Error(
            `PlanningSession revision changed during failed Planner turn: expected ${input.expectedRevision}, got ${session.revision}`,
          );
        }
        const sequence = session.turns.length;
        const turns: PlanningSession["turns"] = [
          ...session.turns,
          {
            id: input.userTurnId,
            sequence: sequence + 1,
            role: "owner",
            content: trimRequired(input.userMessage, "Planner user message"),
            status: "completed",
            createdAt: now,
            inputRevision: input.expectedRevision,
            ...(session.activeItemKey ? { itemKey: session.activeItemKey } : {}),
          },
          {
            id: input.plannerTurnId,
            sequence: sequence + 2,
            role: "planner",
            content: trimRequired(input.safeMessage, "Planner failure message"),
            status: "failed",
            createdAt: now,
            inputRevision: input.expectedRevision,
            providerId: input.selection.providerId || input.providerId,
            accountId: input.selection.accountId,
            model: input.selection.model,
            reasoningEffort: input.selection.reasoningEffort,
            failureCategory: input.failureCategory,
            retryable: input.retryable,
            ...(session.activeItemKey ? { itemKey: session.activeItemKey } : {}),
          },
        ];
        return { ...session, turns };
      },
    );
  }

  async saveDraft(
    sessionId: PlanningSessionId,
    draft: string,
    _context: MutationContext,
  ): Promise<PlanningSession> {
    void _context;
    return this.uow.transaction(async (tx) => {
      const session = await tx.planningSessions.getById(sessionId);
      if (!session) throw new Error(`Unknown PlanningSession ${sessionId}`);
      if (session.draft === draft) return session;
      const next: PlanningSession = {
        ...session,
        draft,
        updatedAt: this.clock.now().toISOString(),
        revision: session.revision + 1,
      };
      await tx.planningSessions.update(next);
      return next;
    });
  }

  async setMode(
    sessionId: PlanningSessionId,
    mode: PlanningParticipationMode,
    _context: MutationContext,
  ): Promise<PlanningSession> {
    return this.mutate(sessionId, _context, "PlanningModeChanged", (session) => {
      const items = session.items.map((item) => {
        if (
          mode === "simple" &&
          item.participation === "delegable-expert" &&
          ["pending", "active"].includes(item.status)
        ) {
          return {
            ...item,
            status: "delegated" as const,
            ...(item.answer || !item.recommendation ? {} : { answer: item.recommendation }),
          };
        }
        return item;
      });
      const activeItemKey = nextActive(items);
      const normalized = normalizeItems(items, activeItemKey);
      return {
        ...session,
        mode,
        items: normalized,
        activeItemKey,
        readiness: recomputePlanningReadiness(normalized, session.deferrals),
      };
    });
  }

  async setProfileKind(
    sessionId: PlanningSessionId,
    kind: PlanningProfileKind,
    _context: MutationContext,
  ): Promise<PlanningSession> {
    return this.mutate(sessionId, _context, "PlanningProfileChanged", async (session, tx) => {
      const defaults = await tx.projectPlanningDefaults.getByProjectId(session.projectId);
      const vision = await tx.projectVisions.getCurrent(session.projectId);
      const profile: PlanningProfile = {
        ...session.profile,
        kind,
        confidence: "high",
        basis: [...session.profile.basis, "explicit owner profile override"],
      };
      const proposed = planningItems(profile, session.mode, defaults, vision?.summary);
      const proposedKeys = new Set(proposed.map((item) => item.key));
      const retained = session.items
        .filter((item) => !proposedKeys.has(item.key))
        .map((item) => ({ ...item, status: "not-required" as const }));
      const merged = proposed.map((item) => {
        const previous = session.items.find((candidate) => candidate.key === item.key);
        if (!previous) return item;
        if (["accepted", "delegated", "deferred"].includes(previous.status)) {
          return {
            ...item,
            status: previous.status,
            ...(previous.answer ? { answer: previous.answer } : {}),
            ...(previous.updatedAt ? { updatedAt: previous.updatedAt } : {}),
          };
        }
        return item;
      });
      const activeItemKey = nextActive(merged);
      const normalized = [...normalizeItems([...merged, ...retained], activeItemKey)];
      return {
        ...session,
        profile,
        items: normalized,
        activeItemKey,
        readiness: recomputePlanningReadiness(normalized, session.deferrals),
      };
    });
  }

  async resolveItem(input: ResolvePlanningItemInput): Promise<PlanningSession> {
    return this.mutate(
      input.sessionId,
      input.context,
      "PlanningItemResolved",
      (session, _tx, now) => {
        const target = session.items.find((item) => item.key === input.itemKey);
        if (!target) throw new Error(`Unknown planning item ${input.itemKey}`);
        if (target.participation === "policy-required" && !input.answer?.trim()) {
          throw new Error("Policy-required planning item requires an explicit recorded answer");
        }
        const answer = input.answer?.trim() || target.answer || target.recommendation;
        if (!answer) throw new Error("Planning answer is required");
        let items = session.items.map((item) =>
          item.key === input.itemKey
            ? { ...item, status: "accepted" as const, answer, updatedAt: now }
            : item,
        );
        const activeItemKey = nextActive(items);
        items = [...normalizeItems(items, activeItemKey)];
        const deferrals = session.deferrals.map((deferral) =>
          deferral.itemKey === input.itemKey
            ? { ...deferral, status: "resolved" as const }
            : deferral,
        );
        const readiness = recomputePlanningReadiness(items, deferrals);
        return {
          ...session,
          items,
          deferrals,
          activeItemKey,
          readiness,
          status: readiness === "blocked" ? "active" : "ready",
        };
      },
    );
  }

  async deferItem(input: DeferPlanningItemInput): Promise<PlanningSession> {
    return this.mutate(input.sessionId, input.context, "PlanningItemDeferred", (session) => {
      const target = session.items.find((item) => item.key === input.itemKey);
      if (!target) throw new Error(`Unknown planning item ${input.itemKey}`);
      const deferral: PlanningDeferral = {
        itemKey: target.key,
        reason: trimRequired(input.reason, "Deferral reason"),
        blockingAt: input.blockingAt,
        owner: String(input.context.authority.principal.id),
        consequence: trimRequired(input.consequence, "Deferral consequence"),
        revisit: trimRequired(input.revisit, "Deferral revisit trigger"),
        status: "open",
      };
      let items = session.items.map((item) =>
        item.key === target.key ? { ...item, status: "deferred" as const } : item,
      );
      const activeItemKey = nextActive(items);
      items = [...normalizeItems(items, activeItemKey)];
      const deferrals = [
        ...session.deferrals.filter((value) => value.itemKey !== target.key),
        deferral,
      ];
      return {
        ...session,
        items,
        deferrals,
        activeItemKey,
        readiness: recomputePlanningReadiness(items, deferrals),
      };
    });
  }

  async saveDefaults(
    sessionId: PlanningSessionId,
    _context: MutationContext,
  ): Promise<ProjectPlanningDefaults> {
    return this.uow.transaction(async (tx) => {
      const session = await tx.planningSessions.getById(sessionId);
      if (!session) throw new Error(`Unknown PlanningSession ${sessionId}`);
      const previous = await tx.projectPlanningDefaults.getByProjectId(session.projectId);
      const acceptedDecisionAnswers = Object.fromEntries(
        session.items
          .filter((item) => item.participation === "delegable-expert" && item.answer)
          .map((item) => [item.key, item.answer!]),
      );
      const defaults: ProjectPlanningDefaults = {
        projectId: session.projectId,
        mode: session.mode,
        profileKind: session.profile.kind,
        delivery: session.delivery,
        acceptedDecisionAnswers,
        revision: (previous?.revision ?? 0) + 1,
      };
      await tx.projectPlanningDefaults.upsert(defaults);
      await this.record(
        tx,
        _context,
        "ProjectPlanningDefaultsUpdated",
        "ProjectPlanningDefaults",
        String(defaults.projectId),
        defaults.revision,
        defaults.projectId,
        {
          mode: defaults.mode,
          profileKind: defaults.profileKind,
          decisionKeys: Object.keys(defaults.acceptedDecisionAnswers),
        },
      );
      return defaults;
    });
  }

  async updateGoal(input: {
    readonly projectId: ProjectId;
    readonly goalId: GoalId;
    readonly title?: string;
    readonly description?: string | null;
    readonly status?: Goal["status"];
    readonly priority?: number | null | undefined;
    readonly targetDate?: string | null | undefined;
    readonly successCriteria?: readonly string[];
    readonly context: MutationContext;
  }): Promise<Goal> {
    return this.uow.transaction(async (tx) => {
      const goal = await tx.goals.getById(input.goalId);
      if (!goal || goal.projectId !== input.projectId)
        throw new Error(`Unknown Goal ${input.goalId}`);
      const {
        description: previousDescription,
        priority: previousPriority,
        targetDate: previousTargetDate,
        ...goalBase
      } = goal;
      const next: Goal = {
        ...goalBase,
        ...(input.title === undefined ? {} : { title: trimRequired(input.title, "Goal title") }),
        ...(input.description === undefined
          ? previousDescription === undefined
            ? {}
            : { description: previousDescription }
          : input.description?.trim()
            ? { description: input.description.trim() }
            : {}),
        ...(input.status === undefined ? {} : { status: input.status }),
        ...(input.priority === undefined
          ? previousPriority === undefined
            ? {}
            : { priority: previousPriority }
          : input.priority === null
            ? {}
            : { priority: input.priority }),
        ...(input.targetDate === undefined
          ? previousTargetDate === undefined
            ? {}
            : { targetDate: previousTargetDate }
          : input.targetDate?.trim()
            ? { targetDate: input.targetDate.trim() }
            : {}),
        ...(input.successCriteria === undefined
          ? {}
          : {
              successCriteria: (() => {
                const values = input.successCriteria.map((value) =>
                  trimRequired(value, "Success criterion"),
                );
                if (values.length === 0)
                  throw new Error("At least one Goal success criterion is required");
                return values;
              })(),
            }),
        revision: goal.revision + 1,
      };
      await tx.goals.update(next);
      await this.record(
        tx,
        input.context,
        "GoalUpdated",
        "Goal",
        next.id,
        next.revision,
        next.projectId,
        {
          status: next.status,
          priority: next.priority,
          targetDate: next.targetDate,
          successCriteriaCount: next.successCriteria.length,
        },
      );
      return next;
    });
  }

  private async finalizeRevision(
    sessionId: PlanningSessionId,
    context: MutationContext,
  ): Promise<PlanningSession> {
    return this.uow.transaction(async (tx) => {
      const session = await tx.planningSessions.getById(sessionId);
      if (!session) throw new Error(`Unknown PlanningSession ${sessionId}`);
      if (session.readiness === "blocked") throw new Error("Plan is not ready to launch");
      if (session.planRevisionId) return session;
      const plan = await tx.plans.getById(session.planId);
      if (!plan) throw new Error(`Unknown Plan ${session.planId}`);
      const current = await tx.planRevisions.getCurrent(plan.id);
      const revisionId = this.ids.next<PlanRevisionId>();
      await tx.planRevisions.insert({
        id: revisionId,
        planId: plan.id,
        projectId: plan.projectId,
        sequence: (current?.sequence ?? 0) + 1,
        title: plan.title,
        goalIds: session.goalIds,
        ...(session.projectVisionVersionId
          ? { projectVisionVersionId: session.projectVisionVersionId }
          : {}),
      });
      const taskTitles = parseWorkBreakdown(session);
      const taskIds = taskTitles.map(() => this.ids.next<TaskId>());
      const tasks: Task[] = taskTitles.map((title, position) => ({
        id: taskIds[position]!,
        projectId: plan.projectId,
        planRevisionId: revisionId,
        title,
        position,
        status: position === 0 ? "ready" : "blocked",
        dependencyIds: position === 0 ? [] : [taskIds[position - 1]!],
        goalIds: session.goalIds,
        revision: 1,
      }));
      for (const task of tasks) await tx.tasks.insert(task);
      const nextPlan = { ...plan, status: "ready" as const, revision: plan.revision + 1 };
      await tx.plans.update(nextPlan);
      const now = this.clock.now().toISOString();
      const next: PlanningSession = {
        ...session,
        planRevisionId: revisionId,
        status: "ready",
        updatedAt: now,
        revision: session.revision + 1,
      };
      await tx.planningSessions.update(next);
      await this.record(
        tx,
        context,
        "PlanRevisionAccepted",
        "PlanRevision",
        revisionId,
        (current?.sequence ?? 0) + 1,
        plan.projectId,
        {
          planningSessionId: session.id,
          planId: plan.id,
          goalIds: session.goalIds,
          taskCount: tasks.length,
        },
      );
      return next;
    });
  }

  async launch(input: PlanningLaunchInput): Promise<PlanningSession> {
    const existing = await this.get(input.sessionId);
    if (!existing) throw new Error(`Unknown PlanningSession ${input.sessionId}`);
    if (existing.status === "launched") {
      if (input.choice !== "start")
        throw new Error("Launched PlanningSession cannot be rescheduled or parked");
      const accountMatches =
        !input.selection?.accountId || input.selection.accountId === existing.launch?.accountId;
      const modelMatches =
        !input.selection?.model || input.selection.model === existing.launch?.model;
      if (!accountMatches || !modelMatches)
        throw new Error("Launched PlanningSession replay cannot change execution selection");
      return existing;
    }
    const finalized = await this.finalizeRevision(input.sessionId, input.context);
    const selectedAt = this.clock.now().toISOString();
    if (input.choice === "schedule") {
      const scheduledFor = trimRequired(input.scheduledFor ?? "", "Scheduled time");
      if (
        !Number.isFinite(Date.parse(scheduledFor)) ||
        Date.parse(scheduledFor) <= this.clock.now().getTime()
      )
        throw new Error("Scheduled time must be in the future");
      if (!input.timezone?.trim()) throw new Error("Schedule timezone is required");
    }
    const updated = await this.uow.transaction(async (tx) => {
      const session = await tx.planningSessions.getById(finalized.id);
      if (!session) throw new Error(`Unknown PlanningSession ${finalized.id}`);
      const next: PlanningSession = {
        ...session,
        status:
          input.choice === "schedule" ? "scheduled" : input.choice === "park" ? "parked" : "ready",
        launch: {
          choice: input.choice,
          selectedAt,
          ...(input.scheduledFor ? { scheduledFor: input.scheduledFor } : {}),
          ...(input.timezone ? { timezone: input.timezone } : {}),
          ...(input.selection?.accountId ? { accountId: input.selection.accountId } : {}),
          ...(input.selection?.model ? { model: input.selection.model } : {}),
        },
        updatedAt: selectedAt,
        revision: session.revision + 1,
      };
      await tx.planningSessions.update(next);
      const plan = await tx.plans.getById(session.planId);
      if (plan)
        await tx.plans.update({
          ...plan,
          status: input.choice === "park" ? "paused" : "ready",
          revision: plan.revision + 1,
        });
      await this.record(
        tx,
        input.context,
        "PlanningLaunchDispositionSelected",
        "PlanningSession",
        next.id,
        next.revision,
        next.projectId,
        {
          choice: input.choice,
          scheduledFor: next.launch?.scheduledFor,
          timezone: next.launch?.timezone,
          hasExecutionSelection: Boolean(next.launch?.accountId),
        },
      );
      return next;
    });
    if (input.choice !== "start") return updated;
    await this.lifecycle.approvePlan(
      updated.projectId,
      updated.planId,
      input.context,
      input.selection,
    );
    return this.uow.transaction(async (tx) => {
      const current = await tx.planningSessions.getById(updated.id);
      if (!current) throw new Error(`Unknown PlanningSession ${updated.id}`);
      const now = this.clock.now().toISOString();
      const next: PlanningSession = {
        ...current,
        status: "launched",
        launch: current.launch ? { ...current.launch, confirmedAt: now } : undefined,
        updatedAt: now,
        revision: current.revision + 1,
      };
      await tx.planningSessions.update(next);
      await this.record(
        tx,
        input.context,
        "PlanningLaunched",
        "PlanningSession",
        next.id,
        next.revision,
        next.projectId,
        {
          planId: next.planId,
          planRevisionId: next.planRevisionId,
        },
      );
      return next;
    });
  }

  async activateDueSchedules(
    contextFactory: (projectId: ProjectId) => MutationContext,
  ): Promise<number> {
    const projects = await this.lifecycle.listProjects();
    let activated = 0;
    for (const project of projects) {
      const sessions = await this.listByProject(project.id);
      for (const session of sessions) {
        if (
          session.status !== "scheduled" ||
          session.launch?.choice !== "schedule" ||
          !session.launch.scheduledFor
        )
          continue;
        if (Date.parse(session.launch.scheduledFor) > this.clock.now().getTime()) continue;
        await this.lifecycle.approvePlan(
          project.id,
          session.planId,
          contextFactory(project.id),
          session.launch.accountId
            ? {
                accountId: session.launch.accountId,
                ...(session.launch.model ? { model: session.launch.model } : {}),
              }
            : undefined,
        );
        await this.uow.transaction(async (tx) => {
          const current = await tx.planningSessions.getById(session.id);
          if (!current) return;
          const now = this.clock.now().toISOString();
          await tx.planningSessions.update({
            ...current,
            status: "launched",
            launch: current.launch ? { ...current.launch, confirmedAt: now } : undefined,
            updatedAt: now,
            revision: current.revision + 1,
          });
        });
        activated += 1;
      }
    }
    return activated;
  }
}
