/**
 * /api/projects/[projectId]/plan
 *
 * Project-level "thinking" surface — the home of the Plan tab.
 * Stores everything that happens BEFORE building:
 *   - vision      — one-line elevator pitch (mirrored to data.productVision)
 *   - ideas       — unstructured capture (the "park-it" bin)
 *   - tasks       — what needs to happen next (status: open | done)
 *   - decisions   — log of "we chose X over Y because Z"
 *
 * Storage lives under fs_projects.data.plan.{ideas,tasks,decisions}
 * so we don't need a schema migration. Items are appended-and-mutated
 * in place; we never load the full document back to the client without
 * an ownership check.
 *
 * Methods:
 *   GET                                   → full plan snapshot
 *   POST  { kind: "vision",   text }      → set vision (also mirrors to productVision)
 *   POST  { kind: "idea",     text }      → add idea
 *   POST  { kind: "task",     text }      → add task (status="open")
 *   POST  { kind: "decision", title, choice, why? } → add decision
 *   PATCH { kind, id, ... }               → update one item (toggle task, edit text)
 *   DELETE?kind=…&id=…                    → remove one item
 */
import { NextResponse } from "next/server";
import { authSession } from "@/lib/auth/session-server";
import { query } from "@/lib/db-postgres";

interface Idea {
  id: string;
  text: string;
  createdAt: string;
}
type TaskStatus = "open" | "in_progress" | "review" | "done" | "blocked";
interface Task {
  id: string;
  // Short headline. For backward compat we accept incoming `text` and
  // mirror to `title` when no explicit title is given.
  title: string;
  // Markdown body — the spec/plan for the unit of work. Optional on
  // legacy rows (we coerce undefined → empty string at read time).
  description?: string;
  status: TaskStatus;
  // Reserved for Phase 2 (background agent delegation). Set when an
  // autonomous agent has been kicked off against this task.
  agent?: {
    runId: string;
    startedAt: string;
    finishedAt?: string;
    status: "queued" | "running" | "succeeded" | "failed";
  } | null;
  createdAt: string;
  startedAt?: string;
  doneAt?: string;
  // Legacy: original single-line text from the v1 task model. Only
  // present on rows created before the markdown migration.
  text?: string;
}
interface Decision {
  id: string;
  title: string;
  choice: string;
  why?: string;
  createdAt: string;
}

export interface BlueprintDocs {
  stories?: string;
  acceptance?: string;
  success?: string;
  ui_design?: string;
  tech_context?: string;
  data_model?: string;
  file_structure?: string;
  tasks?: string;
  checklist?: string;
}

interface PlanShape {
  vision?: string;
  blueprint?: BlueprintDocs;
  ideas: Idea[];
  tasks: Task[];
  decisions: Decision[];
}

function emptyPlan(): PlanShape {
  return { ideas: [], tasks: [], decisions: [], blueprint: {} };
}

function newId(): string {
  return Math.random().toString(36).slice(2, 11);
}

async function loadOwnedProject(projectId: string, email: string) {
  const rows = await query<{ id: string; data: any }>(
    `SELECT p.id, p.data FROM fs_projects p
       JOIN fs_users u ON u.id = p.user_id
      WHERE p.id = $1 AND u.data->>'email' = $2 LIMIT 1`,
    [projectId, email],
  );
  return rows[0] ?? null;
}

function readPlan(data: any): PlanShape {
  const raw = (data?.plan ?? {}) as Partial<PlanShape>;
  // Migrate legacy task shape ({ text }) → new shape ({ title, description })
  // on read, so old rows still render correctly without a one-shot
  // migration script. We DON'T persist back here; the next write will
  // serialize the new shape.
  const tasksIn = Array.isArray(raw.tasks)
    ? (raw.tasks as Array<Partial<Task>>)
    : [];
  const tasks: Task[] = tasksIn.map((t) => ({
    id: String(t.id ?? newId()),
    title: String(t.title ?? t.text ?? "").trim(),
    description: typeof t.description === "string" ? t.description : "",
    status: (t.status === "in_progress" ||
    t.status === "done" ||
    t.status === "blocked"
      ? t.status
      : "open") as TaskStatus,
    agent: t.agent ?? null,
    createdAt: String(t.createdAt ?? new Date().toISOString()),
    startedAt: t.startedAt,
    doneAt: t.doneAt,
    text: t.text, // keep so legacy code paths (older API consumers) still see it
  }));
  return {
    vision: data?.productVision ?? raw.vision,
    brief: raw.brief,
    brief_filename: raw.brief_filename,
    blueprint: raw.blueprint ?? {},
    ideas: Array.isArray(raw.ideas) ? raw.ideas : [],
    tasks,
    decisions: Array.isArray(raw.decisions) ? raw.decisions : [],
  };
}

async function writePlan(
  projectId: string,
  plan: PlanShape,
  alsoVision?: string,
) {
  // Use a single jsonb_set call so we don't race with other writers.
  // When the vision changes we also mirror it to productVision since
  // it's the canonical field elsewhere in the app.
  if (alsoVision !== undefined) {
    await query(
      `UPDATE fs_projects
          SET data = jsonb_set(
                       jsonb_set(data, '{plan}', $2::jsonb, true),
                       '{productVision}', to_jsonb($3::text), true
                     )
        WHERE id = $1`,
      [projectId, JSON.stringify({ ...plan, vision: undefined }), alsoVision],
    );
  } else {
    await query(
      `UPDATE fs_projects
          SET data = jsonb_set(data, '{plan}', $2::jsonb, true)
        WHERE id = $1`,
      [projectId, JSON.stringify({ ...plan, vision: undefined })],
    );
  }
  // Immediately notify the SSE connections that this project has changed!
  try {
    // Note: Postgres NOTIFY does not support parameterized queries ($1) for the payload.
    // It requires the payload string to be directly injected or sent as a literal.
    await query(`NOTIFY project_updates, '${projectId}'`);
  } catch (e) {
    console.error("Failed to notify project updates:", e);
  }
}

export async function GET(
  _req: Request,
  ctx: { params: Promise<{ projectId: string }> },
) {
  const { projectId } = await ctx.params;
  const session = await authSession();
  if (!session?.user?.email)
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const project = await loadOwnedProject(projectId, session.user.email);
  if (!project)
    return NextResponse.json({ error: "Not found" }, { status: 404 });

  return NextResponse.json({ plan: readPlan(project.data) });
}

export async function POST(
  req: Request,
  ctx: { params: Promise<{ projectId: string }> },
) {
  const { projectId } = await ctx.params;
  const session = await authSession();
  if (!session?.user?.email)
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const project = await loadOwnedProject(projectId, session.user.email);
  if (!project)
    return NextResponse.json({ error: "Not found" }, { status: 404 });

  const body = await req.json().catch(() => ({}));
  const kind = String(body.kind ?? "");
  const plan = readPlan(project.data);
  const now = new Date().toISOString();

  if (kind === "vision") {
    const text = String(body.text ?? "").trim();
    plan.vision = text;
    await writePlan(projectId, plan, text);
    return NextResponse.json({ plan });
  }
  if (kind === "idea") {
    const text = String(body.text ?? "").trim();
    if (!text)
      return NextResponse.json({ error: "text required" }, { status: 400 });
    plan.ideas.unshift({ id: newId(), text, createdAt: now });
    await writePlan(projectId, plan);
    return NextResponse.json({ plan });
  }
  if (kind === "task") {
    // New richer shape: { title, description? }. Legacy callers that
    // still send `text` are mapped to `title` so older AI tool versions
    // and any open clients keep working.
    const title = String(body.title ?? body.text ?? "").trim();
    const description =
      typeof body.description === "string" ? body.description : "";
    if (!title)
      return NextResponse.json({ error: "title required" }, { status: 400 });
    plan.tasks.unshift({
      id: newId(),
      title,
      description,
      status: "open",
      createdAt: now,
    });
    await writePlan(projectId, plan);
    return NextResponse.json({ plan });
  }
  if (kind === "decision") {
    const title = String(body.title ?? "").trim();
    const choice = String(body.choice ?? "").trim();
    const why = body.why ? String(body.why).trim() : undefined;
    if (!title || !choice)
      return NextResponse.json(
        { error: "title and choice required" },
        { status: 400 },
      );
    plan.decisions.unshift({ id: newId(), title, choice, why, createdAt: now });
    await writePlan(projectId, plan);
    return NextResponse.json({ plan });
  }
  return NextResponse.json({ error: "unknown kind" }, { status: 400 });
}

export async function PATCH(
  req: Request,
  ctx: { params: Promise<{ projectId: string }> },
) {
  const { projectId } = await ctx.params;
  const session = await authSession();
  if (!session?.user?.email)
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const project = await loadOwnedProject(projectId, session.user.email);
  if (!project)
    return NextResponse.json({ error: "Not found" }, { status: 404 });

  const body = await req.json().catch(() => ({}));
  const { kind, id } = body;
  const plan = readPlan(project.data);

  if (kind === "task" && id) {
    const t = plan.tasks.find((x) => x.id === id);
    if (!t)
      return NextResponse.json({ error: "task not found" }, { status: 404 });
    if (typeof body.title === "string") t.title = body.title.trim();
    if (typeof body.description === "string") t.description = body.description;
    // Backward compat: PATCH body { text } updates the title.
    if (typeof body.text === "string" && typeof body.title !== "string")
      t.title = body.text.trim();
    const validStatuses: TaskStatus[] = [
      "open",
      "in_progress",
      "done",
      "blocked",
    ];
    if (validStatuses.includes(body.status)) {
      const next = body.status as TaskStatus;
      const now = new Date().toISOString();
      t.status = next;
      if (next === "in_progress" && !t.startedAt) t.startedAt = now;
      if (next === "done") t.doneAt = now;
      if (next === "open") {
        t.doneAt = undefined;
        t.startedAt = undefined;
      }
    }
    await writePlan(projectId, plan);
    return NextResponse.json({ plan });
  }
  if (kind === "idea" && id) {
    const i = plan.ideas.find((x) => x.id === id);
    if (!i)
      return NextResponse.json({ error: "idea not found" }, { status: 404 });
    if (typeof body.text === "string") i.text = body.text.trim();
    await writePlan(projectId, plan);
    return NextResponse.json({ plan });
  }
  if (kind === "decision" && id) {
    const d = plan.decisions.find((x) => x.id === id);
    if (!d)
      return NextResponse.json(
        { error: "decision not found" },
        { status: 404 },
      );
    if (typeof body.title === "string") d.title = body.title.trim();
    if (typeof body.choice === "string") d.choice = body.choice.trim();
    if (typeof body.why === "string") d.why = body.why.trim();
    await writePlan(projectId, plan);
    return NextResponse.json({ plan });
  }
  return NextResponse.json({ error: "unsupported patch" }, { status: 400 });
}

export async function DELETE(
  req: Request,
  ctx: { params: Promise<{ projectId: string }> },
) {
  const { projectId } = await ctx.params;
  const session = await authSession();
  if (!session?.user?.email)
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const project = await loadOwnedProject(projectId, session.user.email);
  if (!project)
    return NextResponse.json({ error: "Not found" }, { status: 404 });

  const { searchParams } = new URL(req.url);
  const kind = searchParams.get("kind") || "";
  const id = searchParams.get("id") || "";
  const plan = readPlan(project.data);

  if (kind === "task") plan.tasks = plan.tasks.filter((x) => x.id !== id);
  if (kind === "idea") plan.ideas = plan.ideas.filter((x) => x.id !== id);
  if (kind === "decision")
    plan.decisions = plan.decisions.filter((x) => x.id !== id);

  await writePlan(projectId, plan);
  return NextResponse.json({ plan });
}
