/**
 * Boulder state — cross-session plan persistence for BooCode.
 *
 * Plans live above flow_runs: a plan tracks a user's work goal and can link to
 * a flow run for automatic progress tracking. When the linked flow run reaches
 * a terminal state (completed/failed/cancelled), the plan is auto-updated.
 *
 * Auto-resumption: on startup, plans with a linked in-flight flow_run are
 * surfaced via the GET endpoint so the UI can show a resume prompt. The
 * flow-runner's initResume() re-advances the actual run; this store surfaces
 * the plan-level view.
 */
import type { Sql } from '../db.js';

export interface Plan {
  id: string;
  project_id: string;
  title: string;
  description: string | null;
  status: string;
  flow_run_id: string | null;
  progress_pct: number;
  items_total: number;
  items_completed: number;
  metadata: Record<string, unknown> | null;
  created_at: Date;
  updated_at: Date;
}

export interface CreatePlanOpts {
  projectId: string;
  title: string;
  description?: string;
  flowRunId?: string;
  metadata?: Record<string, unknown>;
}

export interface UpdatePlanOpts {
  title?: string;
  description?: string | null;
  status?: 'active' | 'completed' | 'cancelled' | 'failed';
  progressPct?: number;
  itemsTotal?: number;
  itemsCompleted?: number;
  metadata?: Record<string, unknown> | null;
}

export function createPlan(sql: Sql, opts: CreatePlanOpts): Promise<Plan> {
  return sql`
    INSERT INTO plans (project_id, title, description, flow_run_id, metadata)
    VALUES (
      ${opts.projectId},
      ${opts.title},
      ${opts.description ?? null},
      ${opts.flowRunId ?? null},
      ${opts.metadata ? sql.json(opts.metadata as never) : null}
    )
    RETURNING *
  `.then((rows) => rows[0] as unknown as Plan);
}

export function getPlan(sql: Sql, planId: string): Promise<Plan | null> {
  return sql`
    SELECT * FROM plans WHERE id = ${planId}
  `.then((rows) => (rows[0] as unknown as Plan) ?? null);
}

export function listPlans(sql: Sql, projectId: string): Promise<Plan[]> {
  return sql`
    SELECT * FROM plans
    WHERE project_id = ${projectId}
    ORDER BY created_at DESC
    LIMIT 100
  ` as Promise<Plan[]>;
}

export function listActivePlans(sql: Sql, projectId: string): Promise<Plan[]> {
  return sql`
    SELECT * FROM plans
    WHERE project_id = ${projectId} AND status = 'active'
    ORDER BY created_at DESC
  ` as Promise<Plan[]>;
}

export async function updatePlan(
  sql: Sql,
  planId: string,
  opts: UpdatePlanOpts,
): Promise<Plan | null> {
  const sets: string[] = [];
  const values: unknown[] = [];

  if (opts.title !== undefined) {
    sets.push(`title = $${values.length + 1}`);
    values.push(opts.title);
  }
  if (opts.description !== undefined) {
    sets.push(`description = $${values.length + 1}`);
    values.push(opts.description);
  }
  if (opts.status !== undefined) {
    sets.push(`status = $${values.length + 1}`);
    values.push(opts.status);
  }
  if (opts.progressPct !== undefined) {
    sets.push(`progress_pct = $${values.length + 1}`);
    values.push(opts.progressPct);
  }
  if (opts.itemsTotal !== undefined) {
    sets.push(`items_total = $${values.length + 1}`);
    values.push(opts.itemsTotal);
  }
  if (opts.itemsCompleted !== undefined) {
    sets.push(`items_completed = $${values.length + 1}`);
    values.push(opts.itemsCompleted);
  }
  if (opts.metadata !== undefined) {
    sets.push(`metadata = $${values.length + 1}::jsonb`);
    values.push(opts.metadata !== null ? JSON.stringify(opts.metadata) : null);
  }

  if (sets.length === 0) return getPlan(sql, planId);

  sets.push(`updated_at = clock_timestamp()`);

  const query = `
    UPDATE plans SET ${sets.join(', ')}
    WHERE id = $${values.length + 1}
    RETURNING *
  `;
  values.push(planId);

  const result = await sql.unsafe(query, values as never[]);
  return (result[0] as unknown as Plan) ?? null;
}

/**
 * Called when a flow run reaches a terminal state. Updates the linked plan's
 * status based on the run outcome:
 *  - completed → plan completed
 *  - failed    → plan failed
 *  - cancelled → plan cancelled
 * Returns true when a plan was updated, false when no plan is linked to the run.
 */
export async function updatePlanFromRun(
  sql: Sql,
  runId: string,
  runStatus: 'completed' | 'failed' | 'cancelled',
): Promise<boolean> {
  const planStatus = planStatusFromRun(runStatus);
  const updated = await sql`
    UPDATE plans
    SET status = ${planStatus}, progress_pct = 100,
        items_completed = items_total, updated_at = clock_timestamp()
    WHERE flow_run_id = ${runId} AND status = 'active'
  `;
  return updated.count > 0;
}

/** Map a flow-run terminal status to its corresponding plan status. Pure. */
export function planStatusFromRun(runStatus: 'completed' | 'failed' | 'cancelled'): string {
  return runStatus === 'completed' ? 'completed' : runStatus;
}

export async function findPlanWithRunningRun(
  sql: Sql,
  projectId: string,
): Promise<(Plan & { run_status: string }) | null> {
  const [row] = await sql`
    SELECT p.*, fr.status AS run_status
    FROM plans p
    JOIN flow_runs fr ON fr.id = p.flow_run_id
    WHERE p.project_id = ${projectId}
      AND p.status = 'active'
      AND fr.status = 'running'
    ORDER BY p.created_at DESC
    LIMIT 1
  `;
  return (row as unknown as Plan & { run_status: string }) ?? null;
}
