import type { UniDeskConfig } from "./config";
import { coreInternalFetch } from "./microservices";
import type { RenderedCliResult } from "./output";

interface TodoInstanceSummary {
  id: string;
  name: string;
  todoCount: number;
  completedCount: number;
  updatedAt: string;
}

interface TodoItem {
  id: string;
  title: string;
  completed: boolean;
  reminderAt: string | null;
  updatedAt: string;
  children: TodoItem[];
}

interface TodoInstance {
  id: string;
  name: string;
  historyPointer: number;
  updatedAt: string;
  todos: TodoItem[];
}

interface FlatTodo {
  item: TodoItem;
  depth: number;
  path: string;
}

type TodoNoteFetcher = (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>;

class TodoNoteRequestError extends Error {
  constructor(message: string, readonly status: number | null) {
    super(message);
    this.name = "TodoNoteRequestError";
  }
}

function asRecord(value: unknown): Record<string, unknown> | null {
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}

function stringField(value: unknown, label: string): string {
  if (typeof value !== "string" || value.length === 0) throw new Error(`${label} must be a non-empty string`);
  return value;
}

function option(args: string[], name: string): string | null {
  const index = args.indexOf(name);
  if (index === -1) return null;
  const value = args[index + 1];
  if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
  return value;
}

function hasFlag(args: string[], name: string): boolean {
  return args.includes(name);
}

function positiveIntegerOption(args: string[], name: string, fallback: number): number {
  const raw = option(args, name);
  if (raw === null) return fallback;
  const value = Number(raw);
  if (!Number.isInteger(value) || value < 1) throw new Error(`${name} must be a positive integer`);
  return value;
}

function errorMessage(body: unknown): string {
  const record = asRecord(body);
  return typeof record?.error === "string" ? record.error : typeof record?.message === "string" ? record.message : "request failed";
}

function responseStatus(value: unknown): number | null {
  const status = typeof value === "number" ? value : Number(value);
  return Number.isInteger(status) && status > 0 ? status : null;
}

function isRetryableProxyError(error: unknown): boolean {
  return error instanceof TodoNoteRequestError && error.status !== null && [502, 503, 504].includes(error.status);
}

async function request(fetcher: TodoNoteFetcher, path: string, init?: { method?: string; body?: unknown }): Promise<unknown> {
  const response = asRecord(await fetcher(`/api/microservices/todo-note/proxy${path}`, init));
  if (response === null) throw new Error(`todo-note ${path} returned an invalid response`);
  const body = response.body;
  if (response.ok !== true || asRecord(body)?.ok === false) {
    const status = responseStatus(response.status);
    throw new TodoNoteRequestError(`todo-note ${init?.method ?? "GET"} ${path} failed (${String(status ?? "unknown")}): ${errorMessage(body)}`, status);
  }
  return body;
}

async function listInstances(fetcher: TodoNoteFetcher): Promise<TodoInstanceSummary[]> {
  const body = asRecord(await request(fetcher, "/api/instances"));
  const rows = Array.isArray(body?.instances) ? body.instances : [];
  return rows.map((value, index) => {
    const row = asRecord(value);
    if (row === null) throw new Error(`todo-note instances[${index}] must be an object`);
    return {
      id: stringField(row.id, `instances[${index}].id`),
      name: stringField(row.name, `instances[${index}].name`),
      todoCount: Number(row.todoCount ?? 0),
      completedCount: Number(row.completedCount ?? 0),
      updatedAt: typeof row.updatedAt === "string" ? row.updatedAt : "-",
    };
  });
}

async function resolveInstance(fetcher: TodoNoteFetcher, token: string | undefined): Promise<TodoInstanceSummary> {
  if (!token) throw new Error("todo-note command requires a list name or instance id");
  const rows = await listInstances(fetcher);
  const exact = rows.filter((row) => row.id === token || row.name === token);
  if (exact.length === 1) return exact[0];
  const folded = rows.filter((row) => row.name.toLocaleLowerCase() === token.toLocaleLowerCase());
  if (folded.length === 1) return folded[0];
  if (exact.length > 1 || folded.length > 1) throw new Error(`todo-note list is ambiguous: ${token}`);
  throw new Error(`todo-note list not found: ${token}; available: ${rows.map((row) => row.name).join(", ")}`);
}

function parseTodo(value: unknown, label: string): TodoItem {
  const row = asRecord(value);
  if (row === null) throw new Error(`${label} must be an object`);
  return {
    id: stringField(row.id, `${label}.id`),
    title: stringField(row.title, `${label}.title`),
    completed: row.completed === true,
    reminderAt: typeof row.reminderAt === "string" ? row.reminderAt : null,
    updatedAt: typeof row.updatedAt === "string" ? row.updatedAt : "-",
    children: Array.isArray(row.children) ? row.children.map((child, index) => parseTodo(child, `${label}.children[${index}]`)) : [],
  };
}

async function getInstance(fetcher: TodoNoteFetcher, summary: TodoInstanceSummary): Promise<TodoInstance> {
  const body = asRecord(await request(fetcher, `/api/instances/${encodeURIComponent(summary.id)}`));
  if (body === null) throw new Error(`todo-note list ${summary.name} returned an invalid body`);
  return {
    id: stringField(body.id, "instance.id"),
    name: stringField(body.name, "instance.name"),
    historyPointer: Number(body.historyPointer ?? 0),
    updatedAt: typeof body.updatedAt === "string" ? body.updatedAt : "-",
    todos: Array.isArray(body.todos) ? body.todos.map((todo, index) => parseTodo(todo, `todos[${index}]`)) : [],
  };
}

function flattenTodos(items: TodoItem[], depth = 0, parents: string[] = []): FlatTodo[] {
  const rows: FlatTodo[] = [];
  for (const item of items) {
    const path = [...parents, item.title];
    rows.push({ item, depth, path: path.join(" > ") });
    rows.push(...flattenTodos(item.children, depth + 1, path));
  }
  return rows;
}

function resolveTodo(instance: TodoInstance, token: string | undefined): FlatTodo {
  if (!token) throw new Error("todo-note command requires a todo id or exact title");
  const rows = flattenTodos(instance.todos);
  const exact = rows.filter((row) => row.item.id === token || row.item.title === token);
  if (exact.length === 1) return exact[0];
  const folded = rows.filter((row) => row.item.title.toLocaleLowerCase() === token.toLocaleLowerCase());
  if (folded.length === 1) return folded[0];
  if (exact.length > 1 || folded.length > 1) throw new Error(`todo-note title is ambiguous; use todo id: ${token}`);
  throw new Error(`todo-note item not found in ${instance.name}: ${token}`);
}

async function applyAction(fetcher: TodoNoteFetcher, instanceId: string, action: Record<string, unknown>): Promise<TodoInstance> {
  const body = await request(fetcher, `/api/instances/${encodeURIComponent(instanceId)}/actions`, { method: "POST", body: { action } });
  const record = asRecord(body);
  if (record === null) throw new Error("todo-note action returned an invalid instance");
  const summary: TodoInstanceSummary = {
    id: stringField(record.id, "instance.id"),
    name: stringField(record.name, "instance.name"),
    todoCount: 0,
    completedCount: 0,
    updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : "-",
  };
  return {
    ...summary,
    historyPointer: Number(record.historyPointer ?? 0),
    todos: Array.isArray(record.todos) ? record.todos.map((todo, index) => parseTodo(todo, `todos[${index}]`)) : [],
  };
}

async function applyCompletion(
  fetcher: TodoNoteFetcher,
  summary: TodoInstanceSummary,
  todoId: string,
  desiredCompleted: boolean,
): Promise<{ instance: TodoInstance; changed: boolean }> {
  const apply = () => applyAction(fetcher, summary.id, { type: "toggleTodoCompleted", todoId });
  try {
    return { instance: await apply(), changed: true };
  } catch (firstError) {
    if (!isRetryableProxyError(firstError)) throw firstError;
    const observed = await getInstance(fetcher, summary);
    if (resolveTodo(observed, todoId).item.completed === desiredCompleted) return { instance: observed, changed: true };
    try {
      return { instance: await apply(), changed: true };
    } catch (secondError) {
      if (!isRetryableProxyError(secondError)) throw secondError;
      const final = await getInstance(fetcher, summary);
      if (resolveTodo(final, todoId).item.completed === desiredCompleted) return { instance: final, changed: true };
      throw secondError;
    }
  }
}

function short(value: string, max = 64): string {
  const compact = value.replace(/\s+/gu, " ").trim();
  return compact.length <= max ? compact : `${compact.slice(0, Math.max(1, max - 3))}...`;
}

function timeText(value: string | null): string {
  if (value === null) return "-";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return value;
  return new Intl.DateTimeFormat("zh-CN", {
    timeZone: "Asia/Shanghai",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    hour12: false,
  }).format(date).replaceAll("/", "-");
}

function displayWidth(value: string): number {
  return Array.from(value).reduce((width, character) => width + ((character.codePointAt(0) ?? 0) > 0xff ? 2 : 1), 0);
}

function table(headers: string[], rows: string[][]): string[] {
  const widths = headers.map((header, index) => Math.max(displayWidth(header), ...rows.map((row) => displayWidth(row[index] ?? ""))));
  const line = (row: string[]) => row.map((cell, index) => `${cell}${" ".repeat(Math.max(0, (widths[index] ?? 0) - displayWidth(cell)))}`).join("  ").trimEnd();
  return [line(headers), widths.map((width) => "-".repeat(width)).join("  "), ...rows.map(line)];
}

function rendered(command: string, lines: string[], ok = true): RenderedCliResult {
  return { ok, command, renderedText: `${lines.join("\n")}\n`, contentType: "text/plain" };
}

function mutationResult(command: string, action: string, instance: TodoInstance, todo: FlatTodo | null, changed = true): RenderedCliResult {
  return rendered(command, [
    `TODO NOTE ${action.toUpperCase()}`,
    ...table(["LIST", "TODO", "STATE", "CHANGED", "TITLE"], [[
      instance.name,
      todo?.item.id ?? "-",
      todo === null ? "-" : todo.item.completed ? "completed" : "open",
      String(changed),
      short(todo?.item.title ?? instance.name, 72),
    ]]),
  ]);
}

function help(): RenderedCliResult {
  return rendered("todo-note help", [
    "TODO NOTE",
    "  unidesk todo-note list [--json]",
    "  unidesk todo-note show <list|instance-id> [--all] [--limit N] [--json]",
    "  unidesk todo-note add <list> --title <text> [--parent <todo-id>]",
    "  unidesk todo-note update <list> <todo-id|exact-title> --title <text>",
    "  unidesk todo-note complete|reopen <list> <todo-id|exact-title>",
    "  unidesk todo-note remind <list> <todo-id|exact-title> (--at <ISO>|--clear)",
    "  unidesk todo-note delete <list> <todo-id|exact-title> --confirm",
    "  unidesk todo-note undo|redo <list>",
    "  unidesk todo-note create <name>",
    "  unidesk todo-note rename <list> --name <name>",
    "  unidesk todo-note delete-list <list> --confirm",
    "  unidesk todo-note health [--json]",
  ]);
}

export async function runTodoNoteCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
  const fetcher: TodoNoteFetcher = async (path, init) => coreInternalFetch(path, {
    method: init?.method,
    body: init?.body,
    maxResponseBytes: 5_000_000,
    timeoutMs: 30_000,
  });
  return runTodoNoteCommandAsync(_config, args, fetcher);
}

export async function runTodoNoteCommandAsync(_config: UniDeskConfig, args: string[], fetcher: TodoNoteFetcher): Promise<unknown> {
  try {
    return await runTodoNoteCommandInner(args, fetcher);
  } catch (error) {
    if (hasFlag(args, "--json")) throw error;
    const message = error instanceof Error ? error.message : String(error);
    return rendered("todo-note error", ["TODO NOTE ERROR", `  ${message}`, "  help: unidesk todo-note --help"], false);
  }
}

async function runTodoNoteCommandInner(args: string[], fetcher: TodoNoteFetcher): Promise<unknown> {
  const [action = "list", listToken, todoToken] = args;
  const json = hasFlag(args, "--json");
  if (action === "help" || action === "--help" || action === "-h") return help();

  if (action === "list") {
    const rows = await listInstances(fetcher);
    if (json) return { ok: true, instances: rows };
    return rendered("todo-note list", [
      "TODO NOTE LISTS",
      ...table(["LIST", "OPEN", "DONE", "TOTAL", "ID"], rows.map((row) => [
        row.name,
        String(row.todoCount - row.completedCount),
        String(row.completedCount),
        String(row.todoCount),
        row.id,
      ])),
    ]);
  }

  if (action === "health") {
    const response = asRecord(await fetcher("/api/microservices/todo-note/health"));
    const body = asRecord(response?.body);
    if (response?.ok !== true || body?.ok !== true) throw new Error(`todo-note health failed: ${errorMessage(response?.body)}`);
    if (json) return { ok: true, health: body };
    const storage = asRecord(body.storage);
    const config = asRecord(storage?.config);
    const verification = asRecord(storage?.verification);
    return rendered("todo-note health", [
      "TODO NOTE HEALTH",
      ...table(["STATUS", "STORAGE", "INSTANCES", "DB/GIT", "HEAD"], [[
        String(body.status ?? "-"),
        String(config?.primary ?? "-"),
        String(verification?.instances ?? "-"),
        verification?.databaseHash === verification?.gitHash ? "aligned" : "mismatch",
        String(asRecord(config?.head)?.shortCommit ?? "-"),
      ]]),
    ]);
  }

  if (action === "create") {
    const name = option(args, "--name") ?? listToken;
    if (!name) throw new Error("todo-note create requires a name or --name <text>");
    const body = asRecord(await request(fetcher, "/api/instances", { method: "POST", body: { name } }));
    if (body === null) throw new Error("todo-note create returned an invalid instance");
    const created = await getInstance(fetcher, { id: stringField(body.id, "instance.id"), name: stringField(body.name, "instance.name"), todoCount: 0, completedCount: 0, updatedAt: String(body.updatedAt ?? "-") });
    return json ? { ok: true, instance: created } : mutationResult("todo-note create", "create", created, null);
  }

  const summary = await resolveInstance(fetcher, listToken);
  const instance = await getInstance(fetcher, summary);

  if (action === "show") {
    const all = hasFlag(args, "--all");
    const limit = positiveIntegerOption(args, "--limit", 100);
    const rows = flattenTodos(instance.todos).filter((row) => all || !row.item.completed).slice(0, limit);
    if (json) return { ok: true, instance, items: rows.map((row) => ({ ...row.item, depth: row.depth, path: row.path })) };
    return rendered("todo-note show", [
      `TODO NOTE ${instance.name}`,
      `OPEN ${flattenTodos(instance.todos).filter((row) => !row.item.completed).length}  SHOWN ${rows.length}  HISTORY ${instance.historyPointer}`,
      ...table(["STATE", "ID", "REMINDER", "TITLE"], rows.map((row) => [
        row.item.completed ? "done" : "open",
        row.item.id,
        timeText(row.item.reminderAt),
        `${"  ".repeat(row.depth)}${short(row.item.title, 76)}`,
      ])),
    ]);
  }

  if (action === "undo" || action === "redo") {
    const body = asRecord(await request(fetcher, `/api/instances/${encodeURIComponent(instance.id)}/${action}`, { method: "POST", body: {} }));
    if (body === null) throw new Error(`todo-note ${action} returned an invalid instance`);
    const updated = await getInstance(fetcher, { ...summary, updatedAt: String(body.updatedAt ?? summary.updatedAt) });
    return json ? { ok: true, instance: updated } : mutationResult(`todo-note ${action}`, action, updated, null);
  }

  if (action === "rename") {
    const name = option(args, "--name");
    if (!name) throw new Error("todo-note rename requires --name <text>");
    const updated = await applyAction(fetcher, instance.id, { type: "renameInstance", name });
    return json ? { ok: true, instance: updated } : mutationResult("todo-note rename", "rename", updated, null);
  }

  if (action === "delete-list") {
    if (!hasFlag(args, "--confirm")) throw new Error("todo-note delete-list requires --confirm");
    await request(fetcher, `/api/instances/${encodeURIComponent(instance.id)}`, { method: "DELETE" });
    return json ? { ok: true, deleted: true, instance: summary } : rendered("todo-note delete-list", [
      "TODO NOTE DELETE LIST",
      ...table(["LIST", "ID", "DELETED"], [[summary.name, summary.id, "true"]]),
    ]);
  }

  if (action === "add") {
    const title = option(args, "--title");
    if (!title) throw new Error("todo-note add requires --title <text>");
    const beforeIds = new Set(flattenTodos(instance.todos).map((row) => row.item.id));
    const parentToken = option(args, "--parent");
    const parentId = parentToken === null ? undefined : resolveTodo(instance, parentToken).item.id;
    const updated = await applyAction(fetcher, instance.id, { type: "addTodo", title, ...(parentId ? { parentId } : {}) });
    const added = flattenTodos(updated.todos).find((row) => !beforeIds.has(row.item.id)) ?? null;
    if (added === null) throw new Error("todo-note add succeeded but the new todo was not found");
    return json ? { ok: true, instance: updated, todo: added.item } : mutationResult("todo-note add", "add", updated, added);
  }

  const selected = resolveTodo(instance, todoToken);

  if (action === "update") {
    const title = option(args, "--title");
    if (!title) throw new Error("todo-note update requires --title <text>");
    const updated = await applyAction(fetcher, instance.id, { type: "updateTodoTitle", todoId: selected.item.id, title });
    const changed = resolveTodo(updated, selected.item.id);
    return json ? { ok: true, instance: updated, todo: changed.item } : mutationResult("todo-note update", "update", updated, changed);
  }

  if (action === "complete" || action === "done" || action === "reopen") {
    const desiredCompleted = action !== "reopen";
    if (selected.item.completed === desiredCompleted) {
      return json
        ? { ok: true, changed: false, instance, todo: selected.item }
        : mutationResult(`todo-note ${action}`, action, instance, selected, false);
    }
    const result = await applyCompletion(fetcher, summary, selected.item.id, desiredCompleted);
    const changed = resolveTodo(result.instance, selected.item.id);
    return json ? { ok: true, changed: result.changed, instance: result.instance, todo: changed.item } : mutationResult(`todo-note ${action}`, action, result.instance, changed, result.changed);
  }

  if (action === "remind") {
    const clear = hasFlag(args, "--clear");
    const reminderAt = clear ? null : option(args, "--at");
    if (!clear && reminderAt === null) throw new Error("todo-note remind requires --at <ISO> or --clear");
    if (reminderAt !== null && Number.isNaN(new Date(reminderAt).getTime())) throw new Error("--at must be a valid ISO date/time");
    const updated = await applyAction(fetcher, instance.id, { type: "setTodoReminder", todoId: selected.item.id, reminderAt });
    const changed = resolveTodo(updated, selected.item.id);
    return json ? { ok: true, instance: updated, todo: changed.item } : mutationResult("todo-note remind", "remind", updated, changed);
  }

  if (action === "delete") {
    if (!hasFlag(args, "--confirm")) throw new Error("todo-note delete requires --confirm");
    const updated = await applyAction(fetcher, instance.id, { type: "deleteTodo", todoId: selected.item.id });
    return json ? { ok: true, deleted: true, instance: updated, todo: selected.item } : rendered("todo-note delete", [
      "TODO NOTE DELETE",
      ...table(["LIST", "TODO", "DELETED", "TITLE"], [[instance.name, selected.item.id, "true", short(selected.item.title, 72)]]),
    ]);
  }

  throw new Error("todo-note command must be one of: list, show, add, update, complete, reopen, remind, delete, undo, redo, create, rename, delete-list, health, help");
}
