/**
 * Fire-and-forget plan extraction from chat conversations.
 *
 * After each chat turn, we call a cheap Gemini model (Flash) to scan the
 * conversation for plan-worthy content — new tasks, decisions, vision updates —
 * and auto-persist them via the same `fs_projects.data->plan` path used by
 * the Plan tab MCP tools.
 *
 * The cheap model is configured via VIBN_CHEAP_MODEL (default: gemini-2.0-flash-001).
 */

import { query } from "@/lib/db-postgres";

const GEMINI_API_KEY = process.env.GOOGLE_API_KEY || "";
const CHEAP_MODEL =
  process.env.VIBN_CHEAP_MODEL || "gemini-2.0-flash-001";
const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";

interface PlanExtraction {
  tasks: Array<{ title: string; description?: string }>;
  decisions: Array<{ title: string; choice: string; why?: string }>;
  visionUpdate?: string;
}

/**
 * Call the cheap Gemini model to extract plan updates from the transcript.
 */
async function extractPlanFromTranscript(
  transcript: string,
): Promise<PlanExtraction | null> {
  const url = `${GEMINI_BASE_URL}/models/${CHEAP_MODEL}:generateContent?key=${GEMINI_API_KEY}`;

  const body = {
    contents: [
      {
        role: "user",
        parts: [
          {
            text:
              "Extract any plan-worthy content from this AI coding conversation. " +
              "Return ONLY valid JSON with this schema:\n" +
              '{\n  "tasks": [{"title": "short task name", "description": "optional details"}],\n' +
              '  "decisions": [{"title": "what was decided", "choice": "the chosen option", "why": "reasoning"}],\n' +
              '  "visionUpdate": "updated product vision (only if the conversation meaningfully changes or clarifies it)"\n' +
              "}\n\n" +
              "Rules:\n" +
              "- Only extract CLEAR tasks the AI committed to do or the user explicitly requested.\n" +
              "- Only extract NON-TRIVIAL decisions (not 'I'll read that file').\n" +
              "- visionUpdate: set ONLY when the user articulates or refines their product vision. Omit entirely if not.\n" +
              "- Return empty arrays if nothing worthy found.\n" +
              "- Do NOT wrap in markdown code fences. Just the raw JSON.\n\n" +
              "Conversation:\n" +
              transcript.slice(0, 12000),
          },
        ],
      },
    ],
    generationConfig: { temperature: 0.1, maxOutputTokens: 1024 },
  };

  let res: Response;
  try {
    res = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
  } catch {
    return null;
  }

  const data = await res.json().catch(() => ({}));
  if (!res.ok) return null;

  const text = data?.candidates?.[0]?.content?.parts?.[0]?.text || "";
  if (!text.trim()) return null;

  try {
    return JSON.parse(text.trim()) as PlanExtraction;
  } catch {
    // Strip markdown code fences if present
    const cleaned = text.replace(/```(?:json)?\s*/g, "").trim();
    try {
      return JSON.parse(cleaned) as PlanExtraction;
    } catch {
      return null;
    }
  }
}

function planNewId(): string {
  return `plan_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
}

interface PlanProject {
  id: string;
  data: Record<string, unknown>;
}

async function loadPlanProject(
  projectId: string,
): Promise<PlanProject | null> {
  const rows = await query<PlanProject>(
    `SELECT id, data FROM fs_projects WHERE id = $1 LIMIT 1`,
    [projectId],
  );
  return rows[0] ?? null;
}

interface PlanTask {
  id: string;
  title: string;
  description?: string;
  status: "open" | "in_progress" | "review" | "done" | "blocked";
  text?: string;
  createdAt: string;
}

interface PlanDecision {
  id: string;
  title: string;
  choice: string;
  why?: string;
  createdAt: string;
}

interface PlanShape {
  vision?: string;
  ideas: Array<{ id: string; text: string; createdAt: string }>;
  tasks: PlanTask[];
  decisions: PlanDecision[];
}

function readPlanFromData(data: Record<string, unknown>): PlanShape {
  const raw = (data?.plan as Record<string, unknown>) ?? {};
  const ideas = Array.isArray(raw.ideas) ? raw.ideas : [];
  const tasks = Array.isArray(raw.tasks)
    ? (raw.tasks as PlanTask[]).map((t) => ({
        ...t,
        id: String(t.id ?? planNewId()),
        title: String(t.title ?? t.text ?? "").trim(),
        status: t.status ?? "open",
        createdAt: String(t.createdAt ?? new Date().toISOString()),
      }))
    : [];
  const decisions = Array.isArray(raw.decisions)
    ? (raw.decisions as PlanDecision[]).map((d) => ({
        ...d,
        id: String(d.id ?? planNewId()),
        title: String(d.title ?? "").trim(),
        choice: String(d.choice ?? "").trim(),
        createdAt: String(d.createdAt ?? new Date().toISOString()),
      }))
    : [];
  return {
    vision: typeof raw.vision === "string" ? raw.vision : undefined,
    ideas,
    tasks,
    decisions,
  };
}

async function writePlan(
  projectId: string,
  plan: PlanShape,
  alsoVision?: string,
): Promise<void> {
  const serialized = {
    vision: plan.vision,
    ideas: plan.ideas,
    tasks: plan.tasks,
    decisions: plan.decisions,
  };
  if (alsoVision !== undefined) {
    await query(
      `UPDATE fs_projects
          SET data = data || jsonb_build_object('plan', $2::jsonb, 'productVision', $3::text),
              updated_at = NOW()
        WHERE id = $1`,
      [projectId, JSON.stringify(serialized), alsoVision],
    );
  } else {
    await query(
      `UPDATE fs_projects
          SET data = data || jsonb_build_object('plan', $2::jsonb),
              updated_at = NOW()
        WHERE id = $1`,
      [projectId, JSON.stringify(serialized)],
    );
  }
}

/**
 * Main entry point: scan the conversation transcript and auto-update the
 * project plan with any extracted tasks/decisions/vision.
 *
 * Called fire-and-forget after each chat turn. Never throws.
 */
export async function autoExtractPlanUpdates(
  projectId: string,
  transcript: string,
): Promise<{ tasks: number; decisions: number; vision: boolean } | null> {
  if (!projectId || transcript.length < 20) return null;

  try {
    const extraction = await extractPlanFromTranscript(transcript);
    if (!extraction) return null;

    const hasTasks = extraction.tasks?.length > 0;
    const hasDecisions = extraction.decisions?.length > 0;
    const hasVision =
      typeof extraction.visionUpdate === "string" &&
      extraction.visionUpdate.trim().length > 0;

    if (!hasTasks && !hasDecisions && !hasVision) return null;

    const project = await loadPlanProject(projectId);
    if (!project) return null;

    const plan = readPlanFromData(project.data);
    const now = new Date().toISOString();
    let taskCount = 0;
    let decisionCount = 0;

    for (const t of extraction.tasks ?? []) {
      const exists = plan.tasks.some(
        (existing) => existing.title.toLowerCase() === t.title.toLowerCase(),
      );
      if (exists) continue;
      plan.tasks.unshift({
        id: planNewId(),
        title: t.title.trim(),
        description: t.description?.trim(),
        status: "open",
        createdAt: now,
      });
      taskCount++;
    }

    for (const d of extraction.decisions ?? []) {
      const exists = plan.decisions.some(
        (existing) => existing.title.toLowerCase() === d.title.toLowerCase(),
      );
      if (exists) continue;
      plan.decisions.unshift({
        id: planNewId(),
        title: d.title.trim(),
        choice: d.choice.trim(),
        why: d.why?.trim(),
        createdAt: now,
      });
      decisionCount++;
    }

    if (hasVision && extraction.visionUpdate) {
      plan.vision = extraction.visionUpdate.trim();
    }

    if (taskCount === 0 && decisionCount === 0 && !hasVision) return null;

    await writePlan(projectId, plan, hasVision ? extraction.visionUpdate : undefined);
    return { tasks: taskCount, decisions: decisionCount, vision: hasVision };
  } catch {
    return null;
  }
}
