#!/usr/bin/env node

"use strict";

/**
 * manage-tasks.js — Local task state manager
 * Manages .claude/tasks.json per-project task tracking.
 *
 * Subcommands:
 *   init [--tracker github|jira|linear|none] [--url <url>] [--project-key <key>]
 *   create <title> [--type t] [--priority p] [--epic e] [--ac ids] [--deps ids] [--est S|M|L|XL] [--wave n] [--feature slug] [--description d]
 *   read <task-id>
 *   update <task-id> [--title t] [--priority p] [--labels l1,l2] [--description d]
 *   transition <task-id> <status>
 *   close <task-id>
 *   list [--status s] [--epic id] [--feature slug] [--wave n]
 *   link <task-id> <remote-id> [<remote-url>]
 *   unsynced
 *   export-tasks-md <feature-slug> [--output path]
 *   epic-create <title> [--feature slug]
 *   epic-list [--status s]
 */

const fs = require("fs");
const path = require("path");

// ─── Constants ───────────────────────────────────────────────────────────────

const SCHEMA_VERSION = 1;

const VALID_STATUSES = ["open", "in-progress", "done", "closed"];
const VALID_PRIORITIES = ["low", "medium", "high", "urgent"];
const VALID_ESTIMATES = ["S", "M", "L", "XL"];
const VALID_TRACKER_TYPES = ["github", "jira", "linear", "none"];

const ERROR_CODES = {
  NOT_INITIALIZED: "E001",
  TASK_NOT_FOUND: "E002",
  EPIC_NOT_FOUND: "E003",
  INVALID_STATUS: "E004",
  LOCK_TIMEOUT: "E005",
  SCHEMA_ERROR: "E006",
  DUPLICATE_ID: "E007",
  INVALID_INPUT: "E008",
};

// ─── File Locking ─────────────────────────────────────────────────────────────

const LOCK_RETRY_MS = 50;
const LOCK_DEADLINE_MS = 5000;
const LOCK_STALE_MS = 10000;

function resolveLockPath() {
  return resolveTasksPath() + ".lock";
}

/**
 * Acquire an exclusive file lock using O_CREAT|O_EXCL (wx).
 * Spin-retries with jitter up to a 5s deadline. Detects stale locks via mtime.
 * Writes PID to lock file for diagnostics.
 * Returns a release function.
 */
function acquireLock() {
  const lockPath = resolveLockPath();
  const dir = path.dirname(lockPath);
  fs.mkdirSync(dir, { recursive: true });

  const start = Date.now();
  while (true) {
    try {
      const fd = fs.openSync(lockPath, "wx");
      fs.writeSync(fd, String(process.pid));
      fs.closeSync(fd);
      return function release() {
        try {
          fs.unlinkSync(lockPath);
        } catch {
          /* ignore — already released */
        }
      };
    } catch (err) {
      if (err.code !== "EEXIST") throw err;

      // Check for stale lock
      try {
        const stat = fs.statSync(lockPath);
        if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
          // Stale lock — break it
          try {
            fs.unlinkSync(lockPath);
          } catch {
            /* race — another process may have already broken it */
          }
          continue;
        }
      } catch {
        // Lock disappeared between open and stat — retry
        continue;
      }

      if (Date.now() - start >= LOCK_DEADLINE_MS) {
        console.error(
          `[${ERROR_CODES.LOCK_TIMEOUT}] LOCK_TIMEOUT: Could not acquire lock within ${LOCK_DEADLINE_MS}ms: ${lockPath}`,
        );
        process.exit(1);
      }

      // Spin-wait with jitter — intentional busy wait since Node.js has no
      // synchronous sleep. Same pattern in manage-features.js, update-lifecycle.js,
      // and sync-tracker.js. (#623)
      const jitter = Math.random() * LOCK_RETRY_MS;
      const waitUntil = Date.now() + LOCK_RETRY_MS + jitter;
      while (Date.now() < waitUntil) {
        /* busy wait */
      }
    }
  }
}

/**
 * Execute fn() while holding the tasks lock.
 * Lock is released in the finally block AND via a process exit handler
 * (process.exit() bypasses finally blocks in Node.js).
 */
function withTasksLock(fn) {
  const release = acquireLock();
  const onExit = () => release();
  process.on("exit", onExit);
  try {
    return fn();
  } finally {
    process.removeListener("exit", onExit);
    release();
  }
}

// ─── Tasks File ───────────────────────────────────────────────────────────────

/**
 * Resolve the path to tasks.json by walking up from cwd looking for .claude/
 */
function resolveTasksPath() {
  if (process.env.CLAUDE_PROJECT_DIR) {
    return path.join(process.env.CLAUDE_PROJECT_DIR, ".claude", "tasks.json");
  }

  // Walk up from cwd to find .claude/ directory
  let dir = process.cwd();
  while (true) {
    const claudeDir = path.join(dir, ".claude");
    if (fs.existsSync(claudeDir) && fs.statSync(claudeDir).isDirectory()) {
      return path.join(claudeDir, "tasks.json");
    }
    const parent = path.dirname(dir);
    if (parent === dir) {
      // Reached filesystem root — fall back to cwd
      return path.join(process.cwd(), ".claude", "tasks.json");
    }
    dir = parent;
  }
}

// ─── Schema Migrations ────────────────────────────────────────────────────────

function migrateToV1(data) {
  if (data.schema_version >= 1) return data;
  // Future: add migration from v0 to v1 if needed
  data.schema_version = 1;
  return data;
}

function emptyStore(projectKey) {
  return {
    schema_version: SCHEMA_VERSION,
    project_key: projectKey || "PROJECT",
    sequence: 0,
    sync: {
      tracker_type: "none",
      tracker_url: null,
      enabled: false,
      last_sync_at: null,
      last_sync_seq: 0,
      preflight_passed_at: null,
    },
    tasks: {},
    epics: {},
    changelog: [],
  };
}

function readTasks() {
  const filePath = resolveTasksPath();
  if (!fs.existsSync(filePath)) {
    return null;
  }
  try {
    const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
    return migrateToV1(data);
  } catch (err) {
    console.error(
      `[${ERROR_CODES.SCHEMA_ERROR}] SCHEMA_ERROR: Failed to parse tasks.json: ${err.message}`,
    );
    process.exit(1);
  }
}

function writeTasks(data) {
  const filePath = resolveTasksPath();
  const dir = path.dirname(filePath);
  fs.mkdirSync(dir, { recursive: true });
  const tmpPath = filePath + ".tmp";
  fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
  fs.renameSync(tmpPath, filePath);
}

/**
 * Read tasks.json and exit with E001 if it doesn't exist.
 */
function requireTasks() {
  const data = readTasks();
  if (!data) {
    console.error(
      `[${ERROR_CODES.NOT_INITIALIZED}] NOT_INITIALIZED: tasks.json does not exist. Run 'manage-tasks.js init' first.`,
    );
    process.exit(1);
  }
  return data;
}

// ─── ID Generation ────────────────────────────────────────────────────────────

/**
 * Generate next monotonic task ID: T-001, T-002, etc.
 */
function nextTaskId(data) {
  data.sequence = (data.sequence || 0) + 1;
  const id = `T-${String(data.sequence).padStart(3, "0")}`;
  if (data.tasks[id]) {
    console.error(
      `[${ERROR_CODES.DUPLICATE_ID}] DUPLICATE_ID: Generated ID already exists: ${id}`,
    );
    process.exit(1);
  }
  return id;
}

/**
 * Generate next monotonic epic ID: EPIC-001, EPIC-002, etc.
 */
function nextEpicId(data) {
  // Count existing epics to derive next number
  const existingNums = Object.keys(data.epics || {})
    .map((k) => {
      const m = k.match(/^EPIC-(\d+)$/);
      return m ? parseInt(m[1], 10) : 0;
    })
    .filter((n) => n > 0);
  const next = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1;
  const id = `EPIC-${String(next).padStart(3, "0")}`;
  if ((data.epics || {})[id]) {
    console.error(
      `[${ERROR_CODES.DUPLICATE_ID}] DUPLICATE_ID: Generated epic ID already exists: ${id}`,
    );
    process.exit(1);
  }
  return id;
}

// ─── Changelog ────────────────────────────────────────────────────────────────

function appendChangelog(data, op, taskId) {
  data.sequence = (data.sequence || 0) + 1;
  if (!data.changelog) data.changelog = [];
  data.changelog.push({
    seq: data.sequence,
    op,
    task_id: taskId,
    ts: new Date().toISOString(),
  });
  return data.sequence;
}

// ─── Argument Parsing ─────────────────────────────────────────────────────────

function parseFlag(args, flag) {
  const idx = args.indexOf(flag);
  return idx >= 0 && args[idx + 1] ? args[idx + 1] : null;
}

// ─── project-constants.md Parsing ────────────────────────────────────────────

/**
 * Read TRACKER_TYPE from project-constants.md if it exists.
 * Returns null if not found or file doesn't exist.
 */
function readTrackerTypeFromConstants() {
  const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
  const constantsPath = path.join(cwd, ".claude", "project-constants.md");
  if (!fs.existsSync(constantsPath)) return null;
  try {
    const content = fs.readFileSync(constantsPath, "utf-8");
    const m = content.match(/TRACKER_TYPE[:\s]+([a-z]+)/i);
    return m ? m[1].toLowerCase() : null;
  } catch {
    return null;
  }
}

// ─── Subcommands ─────────────────────────────────────────────────────────────

function cmdInit(args) {
  let trackerType = parseFlag(args, "--tracker");
  const trackerUrl = parseFlag(args, "--url");
  const projectKey = parseFlag(args, "--project-key");

  // Fall back to project-constants.md
  if (!trackerType) {
    trackerType = readTrackerTypeFromConstants();
  }
  if (!trackerType) {
    trackerType = "none";
  }

  if (!VALID_TRACKER_TYPES.includes(trackerType)) {
    console.error(
      `Invalid tracker type: ${trackerType}. Valid: ${VALID_TRACKER_TYPES.join(", ")}`,
    );
    process.exit(1);
  }

  withTasksLock(() => {
    const filePath = resolveTasksPath();
    const dir = path.dirname(filePath);
    fs.mkdirSync(dir, { recursive: true });

    let data;
    if (fs.existsSync(filePath)) {
      // Re-init: update sync config but preserve existing tasks
      data = readTasks();
      data.sync.tracker_type = trackerType;
      if (trackerUrl) data.sync.tracker_url = trackerUrl;
      if (projectKey) data.project_key = projectKey;
      if (trackerType !== "none" && trackerUrl) {
        data.sync.enabled = true;
      }
    } else {
      data = emptyStore(projectKey || "PROJECT");
      data.sync.tracker_type = trackerType;
      if (trackerUrl) data.sync.tracker_url = trackerUrl;
      if (trackerType !== "none" && trackerUrl) {
        data.sync.enabled = true;
      }
    }

    writeTasks(data);

    // Output relative path for readability
    const relPath = path.relative(process.cwd(), filePath);
    console.log(
      JSON.stringify({ created: true, path: relPath || ".claude/tasks.json" }),
    );
  });
}

function cmdCreate(args) {
  const title = args[0];
  if (!title) {
    console.error(
      "Usage: create <title> [--type t] [--priority p] [--epic e] [--ac ids] [--deps ids] [--est S|M|L|XL] [--wave n] [--feature slug] [--description d]",
    );
    process.exit(1);
  }

  const type = parseFlag(args, "--type");
  const priorityRaw = parseFlag(args, "--priority") || "medium";
  const epicId = parseFlag(args, "--epic");
  const acRaw = parseFlag(args, "--ac");
  const depsRaw = parseFlag(args, "--deps");
  const estimate = parseFlag(args, "--est");
  const waveRaw = parseFlag(args, "--wave");
  const feature = parseFlag(args, "--feature");
  const description = parseFlag(args, "--description");

  if (!VALID_PRIORITIES.includes(priorityRaw)) {
    console.error(
      `Invalid priority: ${priorityRaw}. Valid: ${VALID_PRIORITIES.join(", ")}`,
    );
    process.exit(1);
  }

  if (estimate && !VALID_ESTIMATES.includes(estimate)) {
    console.error(
      `Invalid estimate: ${estimate}. Valid: ${VALID_ESTIMATES.join(", ")}`,
    );
    process.exit(1);
  }

  const acIds = acRaw
    ? acRaw
        .split(",")
        .map((s) => s.trim())
        .filter(Boolean)
    : [];
  const deps = depsRaw
    ? depsRaw
        .split(",")
        .map((s) => s.trim())
        .filter(Boolean)
    : [];
  const wave = waveRaw ? parseInt(waveRaw, 10) : null;

  withTasksLock(() => {
    const data = requireTasks();

    if (epicId && !data.epics[epicId]) {
      console.error(
        `[${ERROR_CODES.EPIC_NOT_FOUND}] EPIC_NOT_FOUND: Epic not found: ${epicId}`,
      );
      process.exit(1);
    }

    const id = nextTaskId(data);
    const now = new Date().toISOString();

    data.tasks[id] = {
      title,
      description: description || null,
      status: "open",
      priority: priorityRaw,
      type: type || null,
      labels: [],
      epic: epicId || null,
      feature: feature || null,
      dependencies: deps,
      estimate: estimate || null,
      ac_ids: acIds,
      wave,
      created_at: now,
      updated_at: now,
      completed_at: null,
      remote: {
        id: null,
        url: null,
        synced_at: null,
        synced_seq: 0,
        status: "pending",
      },
    };

    const seq = appendChangelog(data, "task-create", id);

    writeTasks(data);

    console.log(JSON.stringify({ id, title, seq }));
  });
}

function cmdRead(args) {
  const taskId = args[0];
  if (!taskId) {
    console.error("Usage: read <task-id>");
    process.exit(1);
  }

  const data = requireTasks();
  const task = data.tasks[taskId];
  if (!task) {
    console.error(
      `[${ERROR_CODES.TASK_NOT_FOUND}] TASK_NOT_FOUND: Task not found: ${taskId}`,
    );
    process.exit(1);
  }

  console.log(JSON.stringify({ id: taskId, ...task }, null, 2));
}

function cmdUpdate(args) {
  const taskId = args[0];
  if (!taskId) {
    console.error(
      "Usage: update <task-id> [--title t] [--priority p] [--labels l1,l2] [--description d]",
    );
    process.exit(1);
  }

  const newTitle = parseFlag(args, "--title");
  const newPriority = parseFlag(args, "--priority");
  const newLabelsRaw = parseFlag(args, "--labels");
  const newDescription = parseFlag(args, "--description");

  if (newPriority && !VALID_PRIORITIES.includes(newPriority)) {
    console.error(
      `Invalid priority: ${newPriority}. Valid: ${VALID_PRIORITIES.join(", ")}`,
    );
    process.exit(1);
  }

  withTasksLock(() => {
    const data = requireTasks();
    const task = data.tasks[taskId];
    if (!task) {
      console.error(
        `[${ERROR_CODES.TASK_NOT_FOUND}] TASK_NOT_FOUND: Task not found: ${taskId}`,
      );
      process.exit(1);
    }

    const updatedFields = [];

    if (newTitle) {
      task.title = newTitle;
      updatedFields.push("title");
    }
    if (newPriority) {
      task.priority = newPriority;
      updatedFields.push("priority");
    }
    if (newLabelsRaw !== null) {
      task.labels = newLabelsRaw
        ? newLabelsRaw
            .split(",")
            .map((s) => s.trim())
            .filter(Boolean)
        : [];
      updatedFields.push("labels");
    }
    if (newDescription !== null && newDescription !== undefined) {
      task.description = newDescription || null;
      updatedFields.push("description");
    }

    if (updatedFields.length === 0) {
      console.error(
        "No fields to update. Provide at least one of: --title, --priority, --labels, --description",
      );
      process.exit(1);
    }

    task.updated_at = new Date().toISOString();

    const seq = appendChangelog(data, "task-update", taskId);

    writeTasks(data);

    console.log(
      JSON.stringify({ id: taskId, updated_fields: updatedFields, seq }),
    );
  });
}

function cmdTransition(args) {
  const taskId = args[0];
  const newStatus = args[1];
  if (!taskId || !newStatus) {
    console.error("Usage: transition <task-id> <status>");
    process.exit(1);
  }

  if (!VALID_STATUSES.includes(newStatus)) {
    console.error(
      `[${ERROR_CODES.INVALID_STATUS}] INVALID_STATUS: Invalid status: ${newStatus}. Valid: ${VALID_STATUSES.join(", ")}`,
    );
    process.exit(1);
  }

  withTasksLock(() => {
    const data = requireTasks();
    const task = data.tasks[taskId];
    if (!task) {
      console.error(
        `[${ERROR_CODES.TASK_NOT_FOUND}] TASK_NOT_FOUND: Task not found: ${taskId}`,
      );
      process.exit(1);
    }

    const previousStatus = task.status;
    const now = new Date().toISOString();

    task.status = newStatus;
    task.updated_at = now;
    task.remote.status = "pending";

    if (newStatus === "done" || newStatus === "closed") {
      task.completed_at = now;
    } else {
      task.completed_at = null;
    }

    const seq = appendChangelog(data, "task-transition", taskId);

    writeTasks(data);

    console.log(
      JSON.stringify({
        id: taskId,
        previous_status: previousStatus,
        status: newStatus,
        seq,
      }),
    );
  });
}

function cmdClose(args) {
  const taskId = args[0];
  if (!taskId) {
    console.error("Usage: close <task-id>");
    process.exit(1);
  }
  cmdTransition([taskId, "closed"]);
}

function cmdList(args) {
  const filterStatus = parseFlag(args, "--status");
  const filterEpic = parseFlag(args, "--epic");
  const filterFeature = parseFlag(args, "--feature");
  const filterWaveRaw = parseFlag(args, "--wave");
  const filterWave =
    filterWaveRaw !== null ? parseInt(filterWaveRaw, 10) : null;

  if (filterStatus && !VALID_STATUSES.includes(filterStatus)) {
    console.error(
      `[${ERROR_CODES.INVALID_STATUS}] INVALID_STATUS: Invalid status: ${filterStatus}. Valid: ${VALID_STATUSES.join(", ")}`,
    );
    process.exit(1);
  }

  const data = requireTasks();
  const tasks = [];

  for (const [id, task] of Object.entries(data.tasks)) {
    if (filterStatus && task.status !== filterStatus) continue;
    if (filterEpic && task.epic !== filterEpic) continue;
    if (filterFeature && task.feature !== filterFeature) continue;
    if (filterWave !== null && task.wave !== filterWave) continue;
    tasks.push({ id, ...task });
  }

  // Sort by created_at ascending
  tasks.sort((a, b) => (a.created_at || "").localeCompare(b.created_at || ""));

  console.log(JSON.stringify({ tasks, count: tasks.length }, null, 2));
}

function cmdLink(args) {
  const taskId = args[0];
  const remoteId = args[1];
  const remoteUrl = args[2] || null;

  if (!taskId || !remoteId) {
    console.error("Usage: link <task-id> <remote-id> [<remote-url>]");
    process.exit(1);
  }

  withTasksLock(() => {
    const data = requireTasks();
    const task = data.tasks[taskId];
    if (!task) {
      console.error(
        `[${ERROR_CODES.TASK_NOT_FOUND}] TASK_NOT_FOUND: Task not found: ${taskId}`,
      );
      process.exit(1);
    }

    task.remote.id = remoteId;
    if (remoteUrl) task.remote.url = remoteUrl;
    task.updated_at = new Date().toISOString();

    writeTasks(data);

    console.log(
      JSON.stringify({
        id: taskId,
        remote_id: remoteId,
        remote_url: task.remote.url,
      }),
    );
  });
}

function cmdUnsynced() {
  const data = requireTasks();
  const tasks = [];

  for (const [id, task] of Object.entries(data.tasks)) {
    if (!task.remote || task.remote.status !== "synced") {
      tasks.push({ id, ...task });
    }
  }

  tasks.sort((a, b) => (a.created_at || "").localeCompare(b.created_at || ""));

  console.log(JSON.stringify({ tasks, count: tasks.length }, null, 2));
}

function cmdExportTasksMd(args) {
  const featureSlug = args[0];
  if (!featureSlug) {
    console.error("Usage: export-tasks-md <feature-slug> [--output path]");
    process.exit(1);
  }

  const outputFlag = parseFlag(args, "--output");

  const data = requireTasks();
  const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();

  // Filter tasks by feature
  const featureTasks = Object.entries(data.tasks)
    .filter(([, task]) => task.feature === featureSlug)
    .map(([id, task]) => ({ id, ...task }))
    .sort((a, b) => (a.created_at || "").localeCompare(b.created_at || ""));

  // Build TASKS.md markdown
  const lines = [];
  lines.push(`# Tasks — ${featureSlug}`);
  lines.push("");
  lines.push(
    `| ID | Title | Status | Priority | Type | Wave | Estimate | AC IDs |`,
  );
  lines.push(`|---|---|---|---|---|---|---|---|`);

  for (const task of featureTasks) {
    const acIds = (task.ac_ids || []).join(", ") || "—";
    const wave = task.wave != null ? String(task.wave) : "—";
    const estimate = task.estimate || "—";
    const type = task.type || "—";
    lines.push(
      `| ${task.id} | ${task.title} | ${task.status} | ${task.priority} | ${type} | ${wave} | ${estimate} | ${acIds} |`,
    );
  }

  lines.push("");
  lines.push(`_Generated: ${new Date().toISOString()}_`);
  lines.push("");

  const content = lines.join("\n");

  // Resolve output path
  let outputPath;
  if (outputFlag) {
    outputPath = path.resolve(cwd, outputFlag);
    // Prevent path traversal outside the project directory
    if (!outputPath.startsWith(cwd + path.sep) && outputPath !== cwd) {
      console.error(
        `[${ERROR_CODES.INVALID_INPUT}] INVALID_INPUT: --output path must be within the project directory`,
      );
      process.exit(1);
    }
  } else {
    // Default: docs/FEAT-{slug}/TASKS.md
    // Look for a matching docs/FEAT-*-{slug}/ directory
    const docsDir = path.join(cwd, "docs");
    let featDir = null;
    if (fs.existsSync(docsDir)) {
      try {
        const entries = fs.readdirSync(docsDir);
        for (const entry of entries) {
          if (
            entry.startsWith("FEAT-") &&
            (entry.endsWith(`-${featureSlug}`) ||
              entry === `FEAT-${featureSlug}`)
          ) {
            featDir = path.join(docsDir, entry);
            break;
          }
        }
      } catch {
        /* ignore */
      }
    }
    if (!featDir) {
      featDir = path.join(docsDir, `FEAT-${featureSlug}`);
    }
    outputPath = path.join(featDir, "TASKS.md");
  }

  const outputDir = path.dirname(outputPath);
  fs.mkdirSync(outputDir, { recursive: true });

  const tmpPath = outputPath + ".tmp";
  fs.writeFileSync(tmpPath, content, "utf-8");
  fs.renameSync(tmpPath, outputPath);

  const relPath = path.relative(cwd, outputPath);
  console.log(
    JSON.stringify({
      path: relPath || outputPath,
      task_count: featureTasks.length,
    }),
  );
}

function cmdEpicCreate(args) {
  const title = args[0];
  if (!title) {
    console.error("Usage: epic-create <title> [--feature slug]");
    process.exit(1);
  }

  const feature = parseFlag(args, "--feature");

  withTasksLock(() => {
    const data = requireTasks();

    const id = nextEpicId(data);
    const now = new Date().toISOString();

    if (!data.epics) data.epics = {};

    data.epics[id] = {
      title,
      feature: feature || null,
      status: "open",
      created_at: now,
      updated_at: now,
    };

    data.sequence = (data.sequence || 0) + 1;
    const seq = data.sequence;

    if (!data.changelog) data.changelog = [];
    data.changelog.push({
      seq,
      op: "epic-create",
      task_id: id,
      ts: now,
    });

    writeTasks(data);

    console.log(JSON.stringify({ id, title, seq }));
  });
}

function cmdEpicList(args) {
  const filterStatus = parseFlag(args, "--status");

  const data = requireTasks();
  const epics = [];

  for (const [id, epic] of Object.entries(data.epics || {})) {
    if (filterStatus && epic.status !== filterStatus) continue;
    epics.push({ id, ...epic });
  }

  epics.sort((a, b) => (a.created_at || "").localeCompare(b.created_at || ""));

  console.log(JSON.stringify({ epics, count: epics.length }, null, 2));
}

// ─── Main ─────────────────────────────────────────────────────────────────────

function main() {
  const args = process.argv.slice(2);
  const command = args[0];
  const rest = args.slice(1);

  switch (command) {
    case "init":
      cmdInit(rest);
      break;
    case "create":
      cmdCreate(rest);
      break;
    case "read":
      cmdRead(rest);
      break;
    case "update":
      cmdUpdate(rest);
      break;
    case "transition":
      cmdTransition(rest);
      break;
    case "close":
      cmdClose(rest);
      break;
    case "list":
      cmdList(rest);
      break;
    case "link":
      cmdLink(rest);
      break;
    case "unsynced":
      cmdUnsynced();
      break;
    case "export-tasks-md":
      cmdExportTasksMd(rest);
      break;
    case "epic-create":
      cmdEpicCreate(rest);
      break;
    case "epic-list":
      cmdEpicList(rest);
      break;
    default:
      console.error(
        "Usage: manage-tasks.js <init|create|read|update|transition|close|list|link|unsynced|export-tasks-md|epic-create|epic-list> [args]",
      );
      process.exit(1);
  }
}

main();
