import { readFile } from "node:fs/promises";

import type { TaskStatus, TaskTreeCommand } from "../../internal/tasktree/contracts.ts";
import { createTaskTreeDispatcher } from "../../internal/tasktree/dispatcher.ts";
import { loadMdtodoImportPayload } from "../../internal/tasktree/mdtodo-import.ts";
import { taskTreeRuntime } from "../../internal/tasktree/runtime.ts";

export async function runTaskTreeCli(argv: string[], env: Record<string, string | undefined> = process.env, stdin?: string) {
  const parsed = parse(argv);
  if (parsed.help || parsed.positionals.length === 0) return help();
  validateArguments(parsed);
  const command = await commandFrom(parsed, stdin);
  if (parsed.overapi) return overApi(command, env, parsed);
  const runtime = taskTreeRuntime(env);
  try {
    return await createTaskTreeDispatcher(runtime)(command);
  } finally {
    await runtime.store.close();
  }
}

function help() {
  return {
    ok: true,
    operation: "help",
    mode: "local-dispatcher-default",
    usage: [
      "hwlab-cli tasktree health [--overapi]",
      "hwlab-cli tasktree group list|overview",
      "hwlab-cli tasktree group get|delete|stats --group ID",
      "hwlab-cli tasktree group create --name TEXT [--description TEXT]",
      "hwlab-cli tasktree group import-markdown --file PATH [--name TEXT] [--dry-run]",
      "hwlab-cli tasktree task list --group ID",
      "hwlab-cli tasktree task get --task ID",
      "hwlab-cli tasktree task create --group ID [--parent ID] (--title TEXT|--stdin) [--description TEXT] [--start ISO] [--due ISO]",
      "hwlab-cli tasktree task create-batch --group ID [--parent ID] [--description TEXT] [--start ISO] [--due ISO] TITLE...",
      "hwlab-cli tasktree task update --task ID [--title TEXT|--stdin] [--description TEXT] [--status STATUS] [--start ISO|null] [--due ISO|null]",
      "hwlab-cli tasktree task start|complete|done|delete|remove --task ID",
      "hwlab-cli tasktree milestone create --group ID [--task ID] --title TEXT --at ISO",
      "hwlab-cli tasktree report list|get --task ID|--report ID",
      "hwlab-cli tasktree report write --task ID --title TEXT (--body TEXT|--body-file PATH|--stdin) [--status STATUS]",
      "hwlab-cli tasktree timeline --group ID [--overapi]",
      "hwlab-cli tasktree backup create [--dry-run] [--reason TEXT]",
      "hwlab-cli tasktree backup status|verify",
      "hwlab-cli tasktree backup restore [--confirm --expected-sha SHA]",
      "hwlab-cli tasktree workflow start --task ID"
    ],
    transportContract: "--overapi only changes transport; commands and options are identical",
    transportOptions: "--overapi [--api-url URL]",
    hierarchy: "taskgroup -> task -> subtask -> subsubtask",
    lifecycleContract: "start pending work before execution; write an execution report and complete accepted work before handoff",
    completionContract: "a task requires at least one execution report before completion"
  };
}

async function commandFrom(parsed: Parsed, stdin?: string): Promise<TaskTreeCommand> {
  const [group, action = ""] = parsed.positionals;
  if (group === "health") return { operation: "health" };
  if (group === "timeline") return { operation: "timeline.get", groupId: required(parsed, "group") };
  if (group === "group" && action === "list") return { operation: "group.list" };
  if (group === "group" && action === "overview") return { operation: "group.overview" };
  if (group === "group" && action === "get") return { operation: "group.get", groupId: required(parsed, "group") };
  if (group === "group" && action === "create") return { operation: "group.create", name: required(parsed, "name"), description: parsed.values.description };
  if (group === "group" && action === "delete") return { operation: "group.delete", groupId: required(parsed, "group") };
  if (group === "group" && action === "stats") return { operation: "group.stats", groupId: required(parsed, "group") };
  if (group === "group" && action === "import-markdown") return {
    operation: "group.import-markdown",
    ...await loadMdtodoImportPayload(required(parsed, "file"), { groupName: parsed.values.name, dryRun: parsed.flags.has("dry-run") })
  };
  if (group === "task" && action === "list") return { operation: "task.list", groupId: required(parsed, "group") };
  if (group === "task" && action === "get") return { operation: "task.get", taskId: required(parsed, "task") };
  if (group === "task" && action === "create") return {
    operation: "task.create",
    groupId: required(parsed, "group"),
    parentId: parsed.values.parent,
    title: titleInput(parsed, stdin),
    description: parsed.values.description,
    startAt: parsed.values.start,
    dueAt: parsed.values.due
  };
  if (group === "task" && action === "create-batch") return {
    operation: "task.create-batch",
    groupId: required(parsed, "group"),
    parentId: parsed.values.parent,
    titles: parsed.positionals.slice(2),
    description: parsed.values.description,
    startAt: parsed.values.start,
    dueAt: parsed.values.due
  };
  if (group === "task" && action === "update") {
    const command: TaskTreeCommand = {
      operation: "task.update",
      taskId: required(parsed, "task"),
      title: parsed.flags.has("stdin") ? titleInput(parsed, stdin) : parsed.values.title,
      description: parsed.values.description,
      status: parsed.values.status as TaskStatus | undefined,
      startAt: nullableDateOption(parsed, "start"),
      dueAt: nullableDateOption(parsed, "due")
    };
    if (command.title === undefined && command.description === undefined && command.status === undefined && command.startAt === undefined && command.dueAt === undefined) {
      throw codedError("update_patch_required", "task update requires at least one field");
    }
    return command;
  }
  if (group === "task" && (action === "delete" || action === "remove")) return { operation: "task.delete", taskId: required(parsed, "task") };
  if (group === "task" && action === "start") return { operation: "task.start", taskId: required(parsed, "task") };
  if (group === "task" && (action === "complete" || action === "done")) return { operation: "task.complete", taskId: required(parsed, "task") };
  if (group === "milestone" && action === "create") return { operation: "milestone.create", groupId: required(parsed, "group"), taskId: parsed.values.task, title: required(parsed, "title"), occursAt: required(parsed, "at") };
  if (group === "report" && action === "list") return { operation: "report.list", taskId: required(parsed, "task") };
  if (group === "report" && action === "get") return { operation: "report.get", reportId: required(parsed, "report") };
  if (group === "report" && action === "write") return { operation: "report.write", taskId: required(parsed, "task"), title: required(parsed, "title"), body: await bodyInput(parsed, stdin), status: parsed.values.status };
  if (group === "backup" && action === "create") return {
    operation: "backup.create",
    dryRun: parsed.flags.has("dry-run"),
    reason: parsed.values.reason
  };
  if (group === "backup" && action === "status") return { operation: "backup.status" };
  if (group === "backup" && action === "verify") return { operation: "backup.verify" };
  if (group === "backup" && action === "restore") return {
    operation: "backup.restore",
    confirm: parsed.flags.has("confirm"),
    expectedSha256: parsed.values["expected-sha"]
  };
  if (group === "workflow" && action === "start") return { operation: "workflow.start", taskId: required(parsed, "task") };
  throw codedError("unsupported_command", `unsupported TaskTree command: ${group} ${action}`.trim());
}

async function overApi(command: TaskTreeCommand, env: Record<string, string | undefined>, parsed: Parsed) {
  const baseUrl = parsed.values["api-url"] || env.HWLAB_TASKTREE_API_URL;
  if (!baseUrl) throw codedError("missing_api_url", "HWLAB_TASKTREE_API_URL or --api-url is required with --overapi");
  let response: Response;
  try {
    response = await fetch(`${baseUrl.replace(/\/$/u, "")}/v1/tasktree/commands`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(command)
    });
  } catch {
    throw codedError("api_unreachable", "TaskTree API request failed");
  }
  const body = await response.json().catch(() => null);
  if (
    !body
    || typeof body !== "object"
    || typeof (body as any).ok !== "boolean"
    || (body as any).operation !== command.operation
    || response.ok !== (body as any).ok
  ) {
    throw codedError("invalid_api_response", `TaskTree API returned an invalid response (${response.status})`);
  }
  return { ...(body as object), transport: "api", httpStatus: response.status };
}

type Parsed = { positionals: string[]; values: Record<string, string>; flags: Set<string>; overapi: boolean; help: boolean };
function parse(argv: string[]): Parsed {
  const parsed: Parsed = { positionals: [], values: {}, flags: new Set(), overapi: false, help: false };
  for (let index = 0; index < argv.length; index += 1) {
    const token = argv[index];
    if (token === "--overapi") {
      if (parsed.overapi) throw codedError("duplicate_option", "--overapi may only be provided once");
      parsed.overapi = true;
    }
    else if (token === "--dry-run" || token === "--stdin" || token === "--confirm") {
      const flag = token.slice(2);
      if (parsed.flags.has(flag)) throw codedError("duplicate_option", `${token} may only be provided once`);
      parsed.flags.add(flag);
    }
    else if (token === "--help" || token === "-h") parsed.help = true;
    else if (token.startsWith("--")) {
      const key = token.slice(2);
      const value = argv[index + 1];
      if (parsed.values[key] !== undefined) throw codedError("duplicate_option", `${token} may only be provided once`);
      if (value === undefined || value.startsWith("--")) throw codedError("missing_option_value", `${token} requires a value`);
      parsed.values[key] = value;
      index += 1;
    } else parsed.positionals.push(token);
  }
  return parsed;
}
function required(parsed: Parsed, key: string) { const value = parsed.values[key]; if (!value) throw codedError("missing_option", `--${key} is required`); return value; }
function titleInput(parsed: Parsed, stdin?: string) {
  if (parsed.flags.has("stdin") && parsed.values.title !== undefined) throw codedError("title_source_conflict", "--title and --stdin are mutually exclusive");
  const value = parsed.flags.has("stdin") ? stripOneLineEnding(stdin ?? "") : parsed.values.title;
  if (value === undefined || value.length === 0) throw codedError("title_required", "--title or --stdin is required");
  return value;
}
async function bodyInput(parsed: Parsed, stdin?: string) {
  const sources = Number(parsed.values.body !== undefined) + Number(parsed.values["body-file"] !== undefined) + Number(parsed.flags.has("stdin"));
  if (sources !== 1) throw codedError("body_source_invalid", "exactly one of --body, --body-file, or --stdin is required");
  if (parsed.flags.has("stdin")) return stdin ?? "";
  return parsed.values.body ?? await readFile(required(parsed, "body-file"), "utf8");
}
function nullableDateOption(parsed: Parsed, key: string): string | null | undefined {
  const value = parsed.values[key];
  return value === "null" ? null : value;
}
function stripOneLineEnding(value: string) { return value.endsWith("\r\n") ? value.slice(0, -2) : value.endsWith("\n") ? value.slice(0, -1) : value; }
function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }

type CommandSpec = {
  positionals: number | { min: number; max?: number };
  options?: string[];
  flags?: string[];
};

const COMMAND_SPECS: Record<string, CommandSpec> = {
  health: { positionals: 1 },
  timeline: { positionals: 1, options: ["group"] },
  "group list": { positionals: 2 },
  "group overview": { positionals: 2 },
  "group get": { positionals: 2, options: ["group"] },
  "group create": { positionals: 2, options: ["name", "description"] },
  "group delete": { positionals: 2, options: ["group"] },
  "group stats": { positionals: 2, options: ["group"] },
  "group import-markdown": { positionals: 2, options: ["file", "name"], flags: ["dry-run"] },
  "task list": { positionals: 2, options: ["group"] },
  "task get": { positionals: 2, options: ["task"] },
  "task create": { positionals: 2, options: ["group", "parent", "title", "description", "start", "due"], flags: ["stdin"] },
  "task create-batch": { positionals: { min: 3 }, options: ["group", "parent", "description", "start", "due"] },
  "task update": { positionals: 2, options: ["task", "title", "description", "status", "start", "due"], flags: ["stdin"] },
  "task delete": { positionals: 2, options: ["task"] },
  "task remove": { positionals: 2, options: ["task"] },
  "task start": { positionals: 2, options: ["task"] },
  "task complete": { positionals: 2, options: ["task"] },
  "task done": { positionals: 2, options: ["task"] },
  "milestone create": { positionals: 2, options: ["group", "task", "title", "at"] },
  "report list": { positionals: 2, options: ["task"] },
  "report get": { positionals: 2, options: ["report"] },
  "report write": { positionals: 2, options: ["task", "title", "body", "body-file", "status"], flags: ["stdin"] },
  "backup create": { positionals: 2, options: ["reason"], flags: ["dry-run"] },
  "backup status": { positionals: 2 },
  "backup verify": { positionals: 2 },
  "backup restore": { positionals: 2, options: ["expected-sha"], flags: ["confirm"] },
  "workflow start": { positionals: 2, options: ["task"] }
};

function validateArguments(parsed: Parsed) {
  const key = parsed.positionals.slice(0, 2).join(" ");
  const spec = COMMAND_SPECS[key] ?? COMMAND_SPECS[parsed.positionals[0]];
  if (!spec) throw codedError("unsupported_command", `unsupported TaskTree command: ${key}`);
  const positionals = typeof spec.positionals === "number"
    ? { min: spec.positionals, max: spec.positionals }
    : spec.positionals;
  if (parsed.positionals.length < positionals.min || (positionals.max !== undefined && parsed.positionals.length > positionals.max)) {
    throw codedError("invalid_arguments", `unexpected positional arguments for ${key}`);
  }
  const allowedOptions = new Set(spec.options ?? []);
  if (parsed.overapi) allowedOptions.add("api-url");
  for (const option of Object.keys(parsed.values)) {
    if (!allowedOptions.has(option)) throw codedError("invalid_option", `--${option} is not valid for ${key}`);
  }
  if (parsed.values["api-url"] !== undefined && !parsed.overapi) {
    throw codedError("invalid_option", "--api-url requires --overapi");
  }
  const allowedFlags = new Set(spec.flags ?? []);
  for (const flag of parsed.flags) {
    if (!allowedFlags.has(flag)) throw codedError("invalid_option", `--${flag} is not valid for ${key}`);
  }
  if (key === "backup restore") {
    const confirm = parsed.flags.has("confirm");
    const expectedSha = parsed.values["expected-sha"];
    if (confirm !== (expectedSha !== undefined)) {
      throw codedError("restore_confirmation_invalid", "--confirm and --expected-sha must be provided together");
    }
  }
}
