// cli.js — command-line interface for the todo list.
// Parses argv into commands, executes them against the domain + store,
// and prints results with ANSI colours and a table layout.

import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { createStore } from "./store.js";
import {
  addTodo,
  editTodo,
  deleteTodo,
  toggleTodo,
  completeTodo,
  completeAll,
  listTodos,
  clearTodos,
  searchTodos,
  getStats,
  archiveCompleted,
  exportTodos,
  dueStatus,
  PRIORITIES,
  migrateAll,
  getTodo,
  // Subtasks
  addSubtask,
  toggleSubtask,
  deleteSubtask,
  editSubtask,
  // Recurring
  setRecurring,
  removeRecurring,
  // Dependencies
  addDependency,
  removeDependency,
  getBlockedBy,
  // Time tracking
  startTimer,
  stopTimer,
  logTime,
  getActiveTimer,
  // Pinning
  togglePin,
  // Duplicate
  duplicateTodo,
  // Bulk
  bulkComplete,
  bulkDelete,
  bulkPriority,
  bulkCategory,
  bulkAssign,
  // Utility
  formatTime,
} from "./todo.js";

// --- version & repo (read from package.json so they stay in sync) -----------
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const __pkg = JSON.parse(
  fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
);
const VERSION = __pkg.version;
const REPO_URL = "https://gitcode.com/hw_aishell_projects/todo_cli.git";

// --- ANSI colours (disabled when not a TTY) -------------------------------
const useColor = process.stdout.isTTY;
function c(code, str) {
  return useColor ? `\x1b[${code}m${str}\x1b[0m` : str;
}
const green = (s) => c("32", s);
const red = (s) => c("31", s);
const yellow = (s) => c("33", s);
const cyan = (s) => c("36", s);
const magenta = (s) => c("35", s);
const blue = (s) => c("34", s);
const bold = (s) => c("1", s);
const dim = (s) => c("2", s);

const PRIORITY_COLOR = { high: red, medium: yellow, low: green };

// --- argument parsing ------------------------------------------------------
function parseArgs(argv) {
  const positional = [];
  const flags = {};
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === "-p" || a === "--priority") flags.priority = argv[++i];
    else if (a === "-t" || a === "--tags") flags.tags = argv[++i];
    else if (a === "-c" || a === "--category") flags.category = argv[++i];
    else if (a === "-d" || a === "--due") flags.due = argv[++i];
    else if (a === "--note") flags.note = argv[++i];
    else if (a === "--title") flags.title = argv[++i];
    else if (a === "--status") flags.status = argv[++i];
    else if (a === "--tag") flags.tag = argv[++i];
    else if (a === "--sort") flags.sort = argv[++i];
    else if (a === "--format") flags.format = argv[++i];
    else if (a === "--assignee") flags.assignee = argv[++i];
    else if (a === "--estimate") flags.estimate = argv[++i];
    else if (a === "--color") flags.color = argv[++i];
    else if (a === "--all") flags.all = true;
    else if (a === "--check") flags.check = true;
    else if (a === "--pinned") flags.pinned = true;
    else if (a === "--overdue") flags.overdue = true;
    else if (a === "--due-today") flags.dueToday = true;
    else if (a === "-h" || a === "--help") flags.help = true;
    else if (a.startsWith("-")) {
      // unknown flag — ignore but don't swallow a value
    } else {
      positional.push(a);
    }
  }
  return { positional, flags };
}

// --- table rendering -------------------------------------------------------
function pad(str, width) {
  str = String(str);
  return str.length >= width ? str : str + " ".repeat(width - str.length);
}

function renderTable(todos) {
  if (todos.length === 0) {
    console.log(dim("  (no todos)"));
    return;
  }
  const hasDue = todos.some((t) => t.due);
  const hasAssignee = todos.some((t) => t.assignee);
  const cols = ["ID", "Status", "Priority"];
  if (hasDue) cols.push("Due");
  cols.push("Category", "Tags");
  if (hasAssignee) cols.push("Assignee");
  cols.push("Title");

  function dueCell(t) {
    if (!t.due) return "-";
    const ds = dueStatus(t.due);
    if (t.done) return dim(t.due);
    if (ds === "overdue") return red(t.due);
    if (ds === "today") return yellow(t.due);
    if (ds === "soon") return yellow(t.due);
    return t.due;
  }

  const rows = todos.map((t) => {
    const base = [
      String(t.id),
      t.done ? green("✓") : dim("○"),
      PRIORITY_COLOR[t.priority](t.priority),
    ];
    if (hasDue) base.push(dueCell(t));
    base.push(
      t.category || "-",
      t.tags.length ? cyan(t.tags.join(", ")) : "-"
    );
    if (hasAssignee) base.push(t.assignee || "-");
    const titleParts = [];
    if (t.pinned) titleParts.push(magenta("📌"));
    if (t.recurring) titleParts.push(blue("⟳"));
    if (t.timerStartedAt) titleParts.push(green("⏱"));
    if (t.subtasks && t.subtasks.length) {
      const done = t.subtasks.filter((s) => s.done).length;
      titleParts.push(dim(`[${done}/${t.subtasks.length}]`));
    }
    const blocked = getBlockedBy(todos, t.id);
    if (blocked.length) titleParts.push(red(`⛔${blocked.length}`));
    titleParts.push(t.done ? dim(t.title) : t.title);
    base.push(titleParts.join(" "));
    return base;
  });

  const widths = cols.map((_, ci) =>
    Math.max(cols[ci].length, ...rows.map((r) => r[ci].length))
  );

  const header = "  " + cols.map((h, i) => bold(pad(h, widths[i]))).join("  ");
  console.log(header);
  console.log("  " + dim("-".repeat(widths.reduce((a, w) => a + w + 2, -2))));
  for (const row of rows) {
    console.log("  " + row.map((cell, i) => pad(cell, widths[i])).join("  "));
  }
  console.log(dim(`\n  ${todos.length} item(s)`));
}

function showTodoDetail(todos, t) {
  console.log(bold(`  #${t.id}  ${t.title}`));
  console.log(dim("  " + "─".repeat(50)));
  console.log(`  Status:     ${t.done ? green("✓ done") : yellow("○ pending")}`);
  console.log(`  Priority:   ${PRIORITY_COLOR[t.priority](t.priority)}`);
  if (t.category) console.log(`  Category:   ${t.category}`);
  if (t.tags.length) console.log(`  Tags:       ${cyan(t.tags.join(", "))}`);
  if (t.due) {
    const ds = dueStatus(t.due);
    const label = ds === "overdue" ? red(" (overdue)") : ds === "today" ? yellow(" (today)") : ds === "soon" ? yellow(" (soon)") : "";
    console.log(`  Due:        ${t.due}${label}`);
  }
  if (t.note) console.log(`  Note:       ${t.note}`);
  if (t.assignee) console.log(`  Assignee:   ${t.assignee}`);
  if (t.estimate) console.log(`  Estimate:   ${formatTime(t.estimate)}`);
  if (t.timeLogged) console.log(`  Time logged:${formatTime(t.timeLogged)}`);
  if (t.pinned) console.log(`  Pinned:     ${magenta("yes")}`);
  if (t.recurring) console.log(`  Recurring:  ${blue(`every ${t.recurring.interval} ${t.recurring.freq}(s)`)}`);
  if (t.timerStartedAt) {
    const elapsed = Math.round((Date.now() - new Date(t.timerStartedAt).getTime()) / 60000);
    console.log(`  Timer:      ${green("running")} (${formatTime(elapsed)} elapsed)`);
  }
  if (t.subtasks && t.subtasks.length) {
    console.log("  Subtasks:");
    for (const s of t.subtasks) {
      console.log(`    ${s.done ? green("✓") : dim("○")} #${s.id} ${s.done ? dim(s.title) : s.title}`);
    }
  }
  if (t.dependsOn && t.dependsOn.length) {
    console.log("  Depends on:");
    for (const d of t.dependsOn) {
      const dep = getTodo(todos, d);
      const status = dep ? (dep.done ? green("✓") : red("✗")) : dim("?");
      console.log(`    ${status} #${d} ${dep ? dep.title : "(deleted)"}`);
    }
  }
  console.log(dim(`  Created:    ${t.createdAt}`));
  if (t.updatedAt) console.log(dim(`  Updated:    ${t.updatedAt}`));
  if (t.completedAt) console.log(dim(`  Completed:  ${t.completedAt}`));
}

// --- commands --------------------------------------------------------------
function cmdAdd(todos, { positional, flags }) {
  const title = positional.join(" ");
  const todo = addTodo(todos, {
    title,
    priority: flags.priority,
    tags: flags.tags,
    category: flags.category,
    due: flags.due,
    note: flags.note,
    estimate: flags.estimate,
    assignee: flags.assignee,
    color: flags.color,
    pinned: flags.pinned,
  });
  let msg = green("✓ Added:") + ` #${todo.id} ${todo.title}`;
  if (todo.due) msg += dim(`  (due ${todo.due})`);
  console.log(msg);
}

function cmdList(todos, { flags }) {
  const result = listTodos(todos, {
    status: flags.status,
    category: flags.category,
    tag: flags.tag,
    priority: flags.priority,
    assignee: flags.assignee,
    sort: flags.sort,
    overdue: flags.overdue,
    dueToday: flags.dueToday,
    pinnedFirst: true,
  });
  renderTable(result);
}

function cmdShow(todos, { positional }) {
  const id = positional[0];
  const t = getTodo(todos, id);
  if (!t) { console.log(red(`✗ No todo with id ${id}`)); return; }
  showTodoDetail(todos, t);
}

function cmdDone(todos, { positional, flags }) {
  if (flags.all) {
    const n = completeAll(todos);
    console.log(green(`✓ Marked ${n} pending item(s) as done`));
    return;
  }
  const id = positional[0];
  if (!id) {
    console.log(red("✗ Usage: todo done <id>  (or todo done --all)"));
    return;
  }
  const todo = completeTodo(todos, id);
  if (!todo) console.log(red(`✗ No todo with id ${id}`));
  else console.log(green("✓ Done:") + ` #${todo.id} ${todo.title}`);
}

function cmdToggle(todos, { positional }) {
  const id = positional[0];
  const todo = toggleTodo(todos, id);
  if (!todo) console.log(red(`✗ No todo with id ${id}`));
  else
    console.log(
      (todo.done ? green("✓ Done:") : yellow("◌ Reopened:")) +
        ` #${todo.id} ${todo.title}`
    );
}

function cmdEdit(todos, { positional, flags }) {
  const id = positional[0];
  const todo = editTodo(todos, id, {
    title: flags.title,
    priority: flags.priority,
    tags: flags.tags,
    category: flags.category,
    due: flags.due,
    note: flags.note,
    estimate: flags.estimate,
    assignee: flags.assignee,
    color: flags.color,
  });
  if (!todo) console.log(red(`✗ No todo with id ${id}`));
  else console.log(green("✓ Edited:") + ` #${todo.id} ${todo.title}`);
}

function cmdDelete(todos, { positional }) {
  const id = positional[0];
  const ok = deleteTodo(todos, id);
  if (!ok) console.log(red(`✗ No todo with id ${id}`));
  else console.log(green("✓ Deleted:") + ` #${id}`);
}

function cmdClear(todos) {
  const n = clearTodos(todos);
  console.log(green(`✓ Cleared ${n} item(s)`));
}

function cmdSearch(todos, { positional }) {
  const keyword = positional.join(" ");
  const results = searchTodos(todos, keyword);
  console.log(bold(`Search:`) + ` "${keyword}" — ${results.length} match(es)`);
  renderTable(results);
}

function cmdStats(todos) {
  const s = getStats(todos);
  console.log(bold("Todo Statistics"));
  console.log(dim("─────────────────"));
  console.log(`  Total:      ${s.total}`);
  console.log(`  ${green("Done:")}      ${s.done}`);
  console.log(`  ${yellow("Pending:")}   ${s.pending}`);
  console.log(`  Completion: ${s.completionRate}%`);
  if (s.overdue > 0) console.log(`  ${red("Overdue:")}    ${s.overdue}`);
  if (s.dueToday > 0) console.log(`  ${yellow("Due today:")} ${s.dueToday}`);
  if (s.pinned > 0) console.log(`  ${magenta("Pinned:")}    ${s.pinned}`);
  if (s.totalTimeLogged > 0) console.log(`  Time logged: ${formatTime(s.totalTimeLogged)}`);
  console.log(dim("\n  By priority:"));
  console.log(`    ${red("high")}: ${s.byPriority.high}  ${yellow("medium")}: ${s.byPriority.medium}  ${green("low")}: ${s.byPriority.low}`);
  const cats = Object.keys(s.byCategory);
  if (cats.length) {
    console.log(dim("  By category:"));
    for (const c of cats.sort()) console.log(`    ${c}: ${s.byCategory[c]}`);
  }
  const assignees = Object.keys(s.byAssignee);
  if (assignees.length) {
    console.log(dim("  By assignee:"));
    for (const a of assignees.sort()) console.log(`    ${a}: ${s.byAssignee[a]}`);
  }
}

function cmdExport(todos, { positional, flags }) {
  const format = (positional[0] || flags.format || "json").toLowerCase();
  const out = exportTodos(todos, format);
  console.log(out);
}

function cmdArchive(todos) {
  const n = archiveCompleted(todos);
  console.log(green(`✓ Archived (removed) ${n} completed item(s)`));
}

// --- Subtask commands ------------------------------------------------------
function cmdSubtask(todos, { positional }) {
  const subcmd = positional[0];
  const id = positional[1];
  if (!subcmd || !id) {
    console.log(red("✗ Usage: todo subtask <add|done|delete|edit> <todoId> [args]"));
    return;
  }
  switch (subcmd) {
    case "add": {
      const title = positional.slice(2).join(" ");
      const st = addSubtask(todos, id, title);
      if (!st) console.log(red(`✗ No todo with id ${id}`));
      else console.log(green("✓ Subtask added:") + ` #${st.id} ${st.title}`);
      break;
    }
    case "done":
    case "toggle": {
      const st = toggleSubtask(todos, id, positional[2]);
      if (!st) console.log(red(`✗ Subtask not found`));
      else console.log(green("✓ Subtask:") + ` #${st.id} ${st.done ? "done" : "pending"}`);
      break;
    }
    case "delete":
    case "rm": {
      const ok = deleteSubtask(todos, id, positional[2]);
      if (!ok) console.log(red("✗ Subtask not found"));
      else console.log(green("✓ Subtask deleted"));
      break;
    }
    case "edit": {
      const title = positional.slice(3).join(" ");
      const st = editSubtask(todos, id, positional[2], title);
      if (!st) console.log(red("✗ Subtask not found"));
      else console.log(green("✓ Subtask edited:") + ` #${st.id} ${st.title}`);
      break;
    }
    default:
      console.log(red(`✗ Unknown subtask command: ${subcmd}`));
  }
}

// --- Pin command -----------------------------------------------------------
function cmdPin(todos, { positional }) {
  const todo = togglePin(todos, positional[0]);
  if (!todo) console.log(red(`✗ No todo with id ${positional[0]}`));
  else console.log(green("✓") + ` #${todo.id} ${todo.pinned ? "pinned" : "unpinned"}`);
}

// --- Duplicate command -----------------------------------------------------
function cmdDuplicate(todos, { positional }) {
  const todo = duplicateTodo(todos, positional[0]);
  if (!todo) console.log(red(`✗ No todo with id ${positional[0]}`));
  else console.log(green("✓ Duplicated:") + ` #${todo.id} ${todo.title}`);
}

// --- Timer commands --------------------------------------------------------
function cmdTimer(todos, { positional }) {
  const subcmd = positional[0];
  const id = positional[1];
  if (!subcmd) {
    const active = getActiveTimer(todos);
    if (active) {
      const elapsed = Math.round((Date.now() - new Date(active.timerStartedAt).getTime()) / 60000);
      console.log(green("⏱ Active timer:") + ` #${active.id} ${active.title} — ${formatTime(elapsed)} elapsed`);
    } else {
      console.log(dim("  No active timer"));
    }
    return;
  }
  switch (subcmd) {
    case "start": {
      const todo = startTimer(todos, id);
      if (!todo) console.log(red(`✗ No todo with id ${id}`));
      else console.log(green("⏱ Timer started:") + ` #${todo.id} ${todo.title}`);
      break;
    }
    case "stop": {
      const todo = stopTimer(todos, id);
      if (!todo) console.log(red(`✗ No todo with id ${id}`));
      else console.log(green("⏱ Timer stopped:") + ` #${todo.id} — ${formatTime(todo.timeLogged)} total`);
      break;
    }
    case "log": {
      const minutes = positional[2];
      const todo = logTime(todos, id, minutes);
      if (!todo) console.log(red(`✗ No todo with id ${id}`));
      else console.log(green("✓ Time logged:") + ` #${todo.id} — ${formatTime(todo.timeLogged)} total`);
      break;
    }
    default:
      console.log(red(`✗ Usage: todo timer [start|stop|log] <id> [minutes]`));
  }
}

// --- Dependency commands ---------------------------------------------------
function cmdDep(todos, { positional }) {
  const subcmd = positional[0];
  const id = positional[1];
  if (!subcmd || !id) {
    console.log(red("✗ Usage: todo dep <add|remove> <todoId> <depId>"));
    return;
  }
  switch (subcmd) {
    case "add": {
      const todo = addDependency(todos, id, positional[2]);
      if (!todo) console.log(red(`✗ No todo with id ${id}`));
      else console.log(green("✓ Dependency added:") + ` #${todo.id} depends on #${positional[2]}`);
      break;
    }
    case "remove":
    case "rm": {
      const todo = removeDependency(todos, id, positional[2]);
      if (!todo) console.log(red(`✗ No todo with id ${id}`));
      else console.log(green("✓ Dependency removed:") + ` #${todo.id} no longer depends on #${positional[2]}`);
      break;
    }
    default:
      console.log(red(`✗ Unknown dep command: ${subcmd}`));
  }
}

// --- Recurring commands ----------------------------------------------------
function cmdRecur(todos, { positional }) {
  const id = positional[0];
  const freq = positional[1];
  if (!id) {
    console.log(red("✗ Usage: todo recur <id> <daily|weekly|monthly|yearly> [interval]  (or: todo recur <id> off)"));
    return;
  }
  if (!freq || freq === "off" || freq === "none") {
    const todo = removeRecurring(todos, id);
    if (!todo) console.log(red(`✗ No todo with id ${id}`));
    else console.log(green("✓ Recurrence removed:") + ` #${todo.id}`);
    return;
  }
  const interval = positional[2] ? Number(positional[2]) : 1;
  const todo = setRecurring(todos, id, freq, interval);
  if (!todo) console.log(red(`✗ No todo with id ${id}`));
  else console.log(green("✓ Recurrence set:") + ` #${todo.id} every ${todo.recurring.interval} ${todo.recurring.freq}(s)`);
}

// --- Bulk commands ---------------------------------------------------------
function cmdBulk(todos, { positional, flags }) {
  const subcmd = positional[0];
  const ids = positional.slice(1).map(Number).filter((n) => !isNaN(n));
  if (!subcmd || ids.length === 0) {
    console.log(red("✗ Usage: todo bulk <done|delete|priority|category|assign> <ids...> [value]"));
    return;
  }
  switch (subcmd) {
    case "done":
    case "complete": {
      const n = bulkComplete(todos, ids);
      console.log(green(`✓ Completed ${n} item(s)`));
      break;
    }
    case "delete":
    case "rm": {
      const n = bulkDelete(todos, ids);
      console.log(green(`✓ Deleted ${n} item(s)`));
      break;
    }
    case "priority":
    case "p": {
      const p = flags.priority || ids.shift();
      const n = bulkPriority(todos, ids, p);
      console.log(green(`✓ Updated priority on ${n} item(s)`));
      break;
    }
    case "category":
    case "c": {
      const c = flags.category;
      const n = bulkCategory(todos, ids, c);
      console.log(green(`✓ Updated category on ${n} item(s)`));
      break;
    }
    case "assign": {
      const a = flags.assignee;
      const n = bulkAssign(todos, ids, a);
      console.log(green(`✓ Updated assignee on ${n} item(s)`));
      break;
    }
    default:
      console.log(red(`✗ Unknown bulk command: ${subcmd}`));
  }
}

// --- Web server command ----------------------------------------------------
function cmdWeb(todos, { positional }) {
  const port = Number(positional[0]) || 3456;
  import("./server.js").then(({ startServer }) => startServer(port));
}

function cmdUpdate(_todos, { flags }) {
  const tmpDir = path.join(os.tmpdir(), "todo-cli-update");
  const run = (cmd) => execSync(cmd, { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();

  console.log(bold("todo update") + dim(" — checking for updates...\n"));

  let latestVersion;
  try {
    if (fs.existsSync(path.join(tmpDir, ".git"))) {
      console.log(dim("  pulling latest..."));
      run(`git -C ${tmpDir} pull origin main`);
    } else {
      console.log(dim("  cloning repository..."));
      fs.rmSync(tmpDir, { recursive: true, force: true });
      run(`git clone --depth 1 ${REPO_URL} ${tmpDir}`);
    }

    latestVersion = JSON.parse(
      fs.readFileSync(path.join(tmpDir, "package.json"), "utf8")
    ).version;
  } catch (err) {
    console.error(red(`✗ Failed to fetch latest version: ${err.message}`));
    process.exitCode = 1;
    return;
  }

  console.log(`  ${dim("current:")} v${VERSION}`);
  console.log(`  ${dim("latest:")}  v${latestVersion}`);

  if (latestVersion === VERSION) {
    console.log(green("\n✓ Already up to date."));
    return;
  }

  if (flags.check) {
    console.log(yellow(`\n⬆ Update available: v${VERSION} → v${latestVersion}`));
    console.log(dim("  Run `todo update` to install."));
    return;
  }

  try {
    console.log(dim("\n  installing..."));
    execSync(`npm install -g ${tmpDir}`, { stdio: "inherit" });
    console.log(green(`\n✓ Updated: v${VERSION} → v${latestVersion}`));
  } catch (err) {
    console.error(red(`\n✗ Install failed: ${err.message}`));
    process.exitCode = 1;
  }
}

function cmdHelp() {
  console.log(
    bold("todo") +
      dim(" — a CLI todo list manager\n") +
      `
${bold("USAGE")}
  todo <command> [args] [flags]

${bold("COMMANDS")}
  add <title>            Add a new todo
  list                   List todos (default command)
  show <id>              Show detailed info for a todo
  done <id>              Mark a todo as done  (done --all = complete every pending)
  toggle <id>            Toggle done / pending
  edit <id>              Edit a todo (use --title, -p, -t, -c, -d, --note)
  delete <id>            Delete a todo (alias: rm, remove)
  duplicate <id>         Clone a todo (alias: dup)
  pin <id>               Toggle pin status
  search <keyword>       Search title / tags / category / note / assignee
  stats                  Show summary statistics
  export [json|csv|md]   Export all todos to stdout
  archive                Remove all completed todos
  clear                  Remove ALL todos
  web [port]             Start the web UI server (default port: 3456)
  update                 Update todo-cli to the latest version (--check = check only)
  help                   Show this help

${bold("SUBTASKS")}
  subtask add <id> <title>        Add a subtask
  subtask done <id> <sid>         Toggle subtask done
  subtask edit <id> <sid> <title> Edit subtask title
  subtask delete <id> <sid>       Delete a subtask

${bold("RECURRING")}
  recur <id> <daily|weekly|monthly|yearly> [interval]  Set recurrence
  recur <id> off                                      Remove recurrence

${bold("DEPENDENCIES")}
  dep add <id> <depId>     Add dependency (blocks completion)
  dep remove <id> <depId>  Remove dependency

${bold("TIME TRACKING")}
  timer start <id>         Start timer (stops any other running timer)
  timer stop <id>          Stop timer and log elapsed time
  timer log <id> <minutes> Manually log time
  timer                    Show active timer

${bold("BULK OPERATIONS")}
  bulk done <ids...>              Complete multiple todos
  bulk delete <ids...>            Delete multiple todos
  bulk priority <ids...> -p <val> Set priority on multiple todos
  bulk category <ids...> -c <val> Set category on multiple todos
  bulk assign <ids...> --assignee <val>  Set assignee on multiple todos

${bold("GLOBAL FLAGS")}
  -h, --help             Show help (works with any command)
  -v, --version          Show version

${bold("FLAGS")}
  -p, --priority <v>     Priority: high | medium | low  (default: medium)
  -t, --tags <v>         Comma-separated tags
  -c, --category <v>     Category name
  -d, --due <v>          Due date YYYY-MM-DD
  --note <v>             Longer note / description
  --title <v>            New title (for edit)
  --status <v>           Filter: all | done | pending   (list only)
  --tag <v>              Filter by tag                  (list only)
  --sort <v>             Sort: priority | created | due | id | title | updated | order
  --assignee <v>         Filter/set assignee
  --estimate <v>         Estimated minutes to complete
  --color <v>            Custom color label
  --all                  Complete all pending           (done only)
  --pinned               Pin the new todo               (add only)
  --overdue              Show only overdue items         (list only)
  --due-today            Show only items due today       (list only)
  --check                Check for update without installing (update only)
  --format <v>           Export format json|csv|md      (export only)

${bold("EXAMPLES")}
  todo add "Buy groceries" -p high -t shopping,weekend -c personal -d 2025-01-15
  todo add "Write report" --note "Focus on Q4 metrics" -p high -c work --estimate 120
  todo add "Team meeting" --assignee alice -p medium
  todo list --status pending --sort due
  todo list --overdue
  todo show 3
  todo search report
  todo done 3
  todo done --all
  todo edit 2 --title "Updated" -p low -d 2025-02-01
  todo pin 1
  todo duplicate 3
  todo subtask add 1 "Research options"
  todo subtask done 1 1
  todo recur 1 weekly 2
  todo dep add 3 1
  todo timer start 2
  todo timer stop 2
  todo timer log 2 30
  todo bulk done 1 2 3
  todo bulk priority 1 2 3 -p high
  todo stats
  todo export csv
  todo archive
  todo web 8080
  todo update --check
`
  );
}

// --- dispatch --------------------------------------------------------------
const COMMANDS = {
  add: cmdAdd,
  list: cmdList,
  ls: cmdList,
  show: cmdShow,
  done: cmdDone,
  toggle: cmdToggle,
  edit: cmdEdit,
  delete: cmdDelete,
  rm: cmdDelete,
  remove: cmdDelete,
  duplicate: cmdDuplicate,
  dup: cmdDuplicate,
  pin: cmdPin,
  search: cmdSearch,
  stats: cmdStats,
  export: cmdExport,
  archive: cmdArchive,
  update: cmdUpdate,
  clear: cmdClear,
  subtask: cmdSubtask,
  recur: cmdRecur,
  dep: cmdDep,
  timer: cmdTimer,
  bulk: cmdBulk,
  web: cmdWeb,
  help: cmdHelp,
};

/** Main entry point. argv should be process.argv.slice(2). */
export function run(argv) {
  if (argv.some((a) => a === "-h" || a === "--help")) {
    cmdHelp();
    return;
  }

  if (argv.some((a) => a === "-v" || a === "--version")) {
    console.log(`todo v${VERSION}`);
    return;
  }

  const store = createStore();
  const todos = store.load();
  migrateAll(todos);

  const command = argv[0] && !argv[0].startsWith("-") ? argv[0] : "list";
  const rest = command === "list" && argv[0] && !argv[0].startsWith("-")
    ? argv.slice(1)
    : (command === "list" ? argv : argv.slice(1));

  const handler = COMMANDS[command] ?? cmdHelp;
  const parsed = parseArgs(rest);

  if (command === "help") {
    cmdHelp();
    return;
  }

  try {
    handler(todos, parsed);
    // Don't save if the server is running (it manages its own saves).
    if (command !== "web" && command !== "update") {
      store.save(todos);
    }
  } catch (err) {
    console.error(red(`✗ ${err.message}`));
    process.exitCode = 1;
  }
}