import { promises as fs } from "fs";
import { execSync } from "child_process";
import path from "path";
import type {
  Initiative,
  Task,
  Project,
  ProjectConfig,
  ProjectStatus,
  ProcessInfo,
  Screenshot,
  DashboardConfig,
} from "./types";

const CONFIG_PATH = path.join(process.cwd(), "..", "config.json");

async function loadConfig(): Promise<DashboardConfig> {
  try {
    const raw = await fs.readFile(CONFIG_PATH, "utf-8");
    return JSON.parse(raw);
  } catch {
    return { projects: [] };
  }
}

function parseFrontmatter(content: string): Record<string, string> {
  const lines = content.split("\n");
  if (!lines[0]?.trim().startsWith("---")) return {};
  const fm: Record<string, string> = {};
  for (let i = 1; i < lines.length; i++) {
    if (lines[i].trim() === "---") break;
    const match = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
    if (match) fm[match[1]] = match[2];
  }
  return fm;
}

function parseBool(v: string | undefined): boolean {
  if (!v) return false;
  return ["true", "yes", "1"].includes(v.trim().toLowerCase());
}

function stripQuotes(s: string): string {
  if (s.length >= 2 && s[0] === s[s.length - 1] && (s[0] === '"' || s[0] === "'"))
    return s.slice(1, -1);
  return s;
}

function parseDeps(val: string): string[] {
  val = val.trim();
  if (!val || val === "[]" || val === "null" || val === "~") return [];
  if (val.startsWith("[") && val.endsWith("]")) val = val.slice(1, -1);
  return val.split(",").map((x) => x.trim()).filter(Boolean);
}

async function dirExists(p: string): Promise<boolean> {
  try { return (await fs.stat(p)).isDirectory(); } catch { return false; }
}

async function fileExists(p: string): Promise<boolean> {
  try { return (await fs.stat(p)).isFile(); } catch { return false; }
}

async function safeReadFile(p: string): Promise<string> {
  try { return await fs.readFile(p, "utf-8"); } catch { return ""; }
}

function isProcessRunning(pid: number): boolean {
  try { process.kill(pid, 0); return true; } catch { return false; }
}

function getProcessUptime(pid: number): string | null {
  try {
    return execSync(`ps -p ${pid} -o etime=`, { encoding: "utf-8", timeout: 5000 }).trim() || null;
  } catch { return null; }
}

function safeExec(cmd: string, cwd: string): string {
  try { return execSync(cmd, { encoding: "utf-8", timeout: 5000, cwd }).trim(); } catch { return ""; }
}

export async function getProjectConfigs(): Promise<ProjectConfig[]> {
  const config = await loadConfig();
  return config.projects;
}

export async function resolveAutobuilderDir(projectName: string): Promise<string | null> {
  const configs = await getProjectConfigs();
  const cfg = configs.find((c) => c.name === projectName);
  return cfg?.autobuilderDir || null;
}

export async function parseStatus(abDir: string): Promise<ProjectStatus | null> {
  const raw = await safeReadFile(path.join(abDir, "status.md"));
  if (!raw) return null;

  let projectId = "", description = "", updated = "", phase = "";
  let iteration = 0;
  const detailLines: string[] = [];
  let pastHeader = false;

  for (const line of raw.split("\n")) {
    const m1 = line.match(/^#\s+Loop Status\s+—\s+(.+)/);
    if (m1) { projectId = m1[1].trim(); continue; }
    const m2 = line.match(/^\*\*Description:\*\*\s*(.+)/);
    if (m2) { description = m2[1].trim(); continue; }
    const m3 = line.match(/^\*\*Updated:\*\*\s*(.+)/);
    if (m3) { updated = m3[1].trim(); continue; }
    const m4 = line.match(/^\*\*Iteration:\*\*\s*(\d+)/);
    if (m4) { iteration = parseInt(m4[1], 10); continue; }
    const m5 = line.match(/^\*\*Phase:\*\*\s*(.+)/);
    if (m5) { phase = m5[1].trim(); pastHeader = true; continue; }
    if (pastHeader && line.trim()) detailLines.push(line);
  }

  return { projectId, description, updated, iteration, phase, details: detailLines.join("\n"), raw };
}

export async function getProcessInfo(abDir: string): Promise<ProcessInfo> {
  const pidContent = await safeReadFile(path.join(abDir, "daemon.pid"));
  const pid = pidContent ? parseInt(pidContent.trim(), 10) : null;
  if (!pid || isNaN(pid)) return { pid: null, running: false, stopping: false, uptime: null, command: null };

  const running = isProcessRunning(pid);
  const stopping = running && await fileExists(path.join(abDir, "stop"));
  const uptime = running ? getProcessUptime(pid) : null;
  let command: string | null = null;
  if (running) {
    try { command = execSync(`ps -p ${pid} -o command=`, { encoding: "utf-8", timeout: 5000 }).trim(); } catch {}
  }
  return { pid, running, stopping, uptime, command };
}

async function loadDirectoryInitiative(initDir: string, dirName: string): Promise<Initiative> {
  const specContent = await safeReadFile(path.join(initDir, "spec.md"));
  const statusContent = await safeReadFile(path.join(initDir, "status.md"));
  const progressContent = await safeReadFile(path.join(initDir, "progress.md"));

  let taskCount = 0, tasksExecuted = 0;
  const tasksDir = path.join(initDir, "tasks");
  if (await dirExists(tasksDir)) {
    const taskFiles = (await fs.readdir(tasksDir)).filter((f) => f.endsWith(".md"));
    taskCount = taskFiles.length;
    for (const tf of taskFiles) {
      const fm = parseFrontmatter(await safeReadFile(path.join(tasksDir, tf)));
      if (parseBool(fm.executed)) tasksExecuted++;
    }
  }

  let screenshotCount = 0;
  const ssDir = path.join(initDir, "review-screenshots");
  if (await dirExists(ssDir)) {
    screenshotCount = (await fs.readdir(ssDir)).filter((f) => /\.(png|jpg|jpeg|gif|webp)$/i.test(f)).length;
  }

  const specFm = parseFrontmatter(specContent);
  const phaseMatch = statusContent.match(/\*\*Phase:\*\*\s*(.+)/);
  const iterMatch = statusContent.match(/\*\*Iteration:\*\*\s*(\d+)/);
  const iterPhase = phaseMatch ? phaseMatch[1].trim() : "";
  const iterNum = iterMatch ? `#${iterMatch[1]}` : "";

  return {
    id: dirName,
    title: stripQuotes(specFm.title || dirName.replace(/-/g, " ")),
    parent: specFm.parent || null,
    deps: parseDeps(specFm.deps || "[]"),
    split: false,
    planned: taskCount > 0,
    executed: (taskCount > 0 && tasksExecuted === taskCount) || iterPhase === "production-ready",
    depth: 0,
    filename: dirName,
    content: specContent,
    format: "directory",
    priority: parseInt(specFm.priority || "0", 10) || 0,
    author: specFm.author ? stripQuotes(specFm.author) : undefined,
    taskCount,
    tasksExecuted,
    screenshotCount,
    progressSnippet: progressContent.split("\n").slice(0, 5).join("\n"),
    iterationStatus: iterPhase ? `${iterPhase}${iterNum ? ` (iter ${iterNum})` : ""}` : undefined,
  };
}

export async function loadInitiatives(abDir: string): Promise<Initiative[]> {
  const dir = path.join(abDir, "initiatives");
  if (!(await dirExists(dir))) return [];

  const entries = await fs.readdir(dir, { withFileTypes: true });
  const results: Initiative[] = [];

  for (const e of entries.filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
    const content = await safeReadFile(path.join(dir, e.name));
    const fm = parseFrontmatter(content);
    results.push({
      id: fm.id || e.name.replace(".md", ""),
      title: stripQuotes(fm.title || fm.id || e.name.replace(".md", "")),
      parent: fm.parent || null,
      deps: parseDeps(fm.deps || "[]"),
      split: parseBool(fm.split),
      planned: parseBool(fm.planned),
      executed: parseBool(fm.executed),
      depth: parseInt(fm.depth || "0", 10),
      filename: e.name,
      content,
      format: "flat",
      priority: parseInt(fm.priority || "0", 10) || 0,
      author: fm.author ? stripQuotes(fm.author) : undefined,
    });
  }

  for (const d of entries.filter((e) => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
    results.push(await loadDirectoryInitiative(path.join(dir, d.name), d.name));
  }

  return results;
}

function statToIso(st: { birthtime: Date; mtime: Date; birthtimeMs: number }, kind: "birth" | "mtime"): string {
  if (kind === "birth") {
    const t = st.birthtimeMs > 0 ? st.birthtime : st.mtime;
    return new Date(t).toISOString();
  }
  return new Date(st.mtime).toISOString();
}

/** Basename only; must be a single .md segment (no path traversal). */
export function isSafeTaskFilename(name: string): boolean {
  const base = path.basename(name);
  if (base !== name || name.includes("..") || !name.endsWith(".md")) return false;
  return /^[a-zA-Z0-9._-]+\.md$/.test(name);
}

export async function loadTasks(abDir: string, initiativeDir: string): Promise<Task[]> {
  const tasksPath = path.join(abDir, "initiatives", initiativeDir, "tasks");
  if (!(await dirExists(tasksPath))) return [];

  const files = (await fs.readdir(tasksPath)).filter((f) => f.endsWith(".md")).sort();
  const results: Task[] = [];
  for (const file of files) {
    const fp = path.join(tasksPath, file);
    const fm = parseFrontmatter(await safeReadFile(fp));
    let st: Awaited<ReturnType<typeof fs.stat>>;
    try {
      st = await fs.stat(fp);
    } catch {
      continue;
    }
    results.push({
      id: fm.id || file.replace(".md", ""),
      title: stripQuotes(fm.title || file.replace(".md", "")),
      planned: parseBool(fm.planned),
      executed: parseBool(fm.executed),
      filename: file,
      createdAt: statToIso(st, "birth"),
      modifiedAt: statToIso(st, "mtime"),
    });
  }
  return results;
}

/** Read raw task markdown; returns null if not found or unsafe path. */
export async function loadTaskMarkdown(
  abDir: string,
  initiativeDir: string,
  filename: string
): Promise<{ content: string; createdAt: string; modifiedAt: string } | null> {
  if (!isSafeTaskFilename(filename)) return null;
  const tasksPath = path.resolve(path.join(abDir, "initiatives", initiativeDir, "tasks"));
  const fp = path.resolve(tasksPath, filename);
  if (!fp.startsWith(tasksPath + path.sep) && fp !== tasksPath) return null;
  if (!(await fileExists(fp))) return null;
  let st: Awaited<ReturnType<typeof fs.stat>>;
  try {
    st = await fs.stat(fp);
  } catch {
    return null;
  }
  const content = await safeReadFile(fp);
  return {
    content,
    createdAt: statToIso(st, "birth"),
    modifiedAt: statToIso(st, "mtime"),
  };
}

export async function getLogContent(
  abDir: string,
  lines = 500,
  offset = 0
): Promise<{ content: string; totalSize: number; totalLines: number }> {
  const logPath = path.join(abDir, "loop.log");
  if (!(await fileExists(logPath))) return { content: "", totalSize: 0, totalLines: 0 };

  const stat = await fs.stat(logPath);
  const raw = await safeReadFile(logPath);
  const allLines = raw.split("\n");

  if (offset > 0) {
    return { content: allLines.slice(offset, offset + lines).join("\n"), totalSize: stat.size, totalLines: allLines.length };
  }
  return { content: allLines.slice(-lines).join("\n"), totalSize: stat.size, totalLines: allLines.length };
}

export async function getScreenshots(abDir: string, projectDir: string): Promise<Screenshot[]> {
  const results: Screenshot[] = [];
  const dirs = [
    path.join(abDir, "screenshots"),
    path.join(projectDir, "dogfood-output", "screenshots"),
    path.join(projectDir, "dogfood-output"),
  ];

  const initDir = path.join(abDir, "initiatives");
  if (await dirExists(initDir)) {
    try {
      for (const entry of (await fs.readdir(initDir, { withFileTypes: true })).filter((e) => e.isDirectory())) {
        const reviewDir = path.join(initDir, entry.name, "review-screenshots");
        if (await dirExists(reviewDir)) dirs.push(reviewDir);
      }
    } catch {}
  }

  for (const dir of dirs) {
    if (!(await dirExists(dir))) continue;
    for (const file of await fs.readdir(dir)) {
      if (!/\.(png|jpg|jpeg|gif|webp)$/i.test(file)) continue;
      try {
        const fp = path.join(dir, file);
        const stat = await fs.stat(fp);
        results.push({ filename: file, path: fp, modified: stat.mtimeMs, size: stat.size });
      } catch {}
    }
  }

  return results.sort((a, b) => b.modified - a.modified);
}

function getProductSnippet(content: string): string {
  const sectionHeadings = ["## what", "## vision", "## objectives", "## overview"];
  let inSection = false;
  for (const line of content.split("\n")) {
    const s = line.trim();
    if (sectionHeadings.includes(s.toLowerCase())) { inSection = true; continue; }
    if (inSection) {
      if (s.startsWith("#")) break;
      if (s) return s.slice(0, 200);
    }
  }
  const first = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("---") && !l.startsWith("**"));
  return first?.slice(0, 200) || "";
}

export async function getActiveInitiative(abDir: string): Promise<string | null> {
  const content = await safeReadFile(path.join(abDir, "active-initiative"));
  return content.trim() || null;
}

/** Roll up tasks across initiatives: directory layout uses task files; legacy flat = one task per file. */
function aggregateTaskStats(initiatives: Initiative[]): {
  initiativeCount: number;
  tasksTotal: number;
  tasksExecuted: number;
} {
  let tasksTotal = 0;
  let tasksExecuted = 0;
  const initiativeCount = initiatives.length;

  for (const i of initiatives) {
    if (i.format === "directory") {
      tasksTotal += i.taskCount ?? 0;
      tasksExecuted += i.tasksExecuted ?? 0;
    } else {
      tasksTotal += 1;
      if (i.executed) tasksExecuted += 1;
    }
  }
  return { initiativeCount, tasksTotal, tasksExecuted };
}

export async function getProject(cfg: ProjectConfig): Promise<Project> {
  const abDir = cfg.autobuilderDir;
  const projectDir = cfg.dir;

  const [status, processInfo, initiatives, screenshots, activeInit] = await Promise.all([
    parseStatus(abDir),
    getProcessInfo(abDir),
    loadInitiatives(abDir),
    getScreenshots(abDir, projectDir),
    getActiveInitiative(abDir),
  ]);

  let snippet = "";
  const productContent = await safeReadFile(path.join(abDir, "PRODUCT.md"));
  if (productContent) {
    snippet = getProductSnippet(productContent);
  } else {
    const scopeContent = await safeReadFile(path.join(abDir, "scope.md"));
    if (scopeContent) {
      snippet = getProductSnippet(scopeContent);
    } else if (activeInit) {
      const specContent = await safeReadFile(
        path.join(abDir, "initiatives", activeInit, "spec.md")
      );
      if (specContent) snippet = getProductSnippet(specContent);
    }
  }

  const logPath = path.join(abDir, "loop.log");
  let hasLog = false, logSize = 0;
  try { const s = await fs.stat(logPath); hasLog = true; logSize = s.size; } catch {}

  const gitBranch = safeExec("git rev-parse --abbrev-ref HEAD", projectDir);
  const lastCommit = safeExec('git log -1 --format="%h %s" --no-walk 2>/dev/null', projectDir);

  const counts = aggregateTaskStats(initiatives);

  return {
    id: cfg.name,
    name: cfg.name,
    projectDir,
    autobuilderDir: abDir,
    status,
    process: processInfo,
    initiativeCounts: counts,
    hasLog,
    logSize,
    screenshotCount: screenshots.length,
    scopeSnippet: status?.description || snippet,
    activeInitiative: activeInit,
    gitBranch,
    lastCommit,
  };
}

export async function getAllProjects(): Promise<Project[]> {
  const configs = await getProjectConfigs();
  return Promise.all(configs.map(getProject));
}
