import type { AppConfig, HandoffRecord } from "@openhandoff/shared";
import { spawnSync } from "node:child_process";
import { createBackendClientFromConfig, filterHandoffs, formatRelativeAge, groupHandoffStatus } from "@openhandoff/client";
import { CLI_BUILD_ID } from "./build-id.js";
import { resolveTuiTheme, type TuiTheme } from "./theme.js";

interface KeyEventLike {
  name?: string;
  ctrl?: boolean;
  meta?: boolean;
}

const HELP_LINES = [
  "Shortcuts",
  "Ctrl-H           toggle cheatsheet",
  "Enter            switch to branch",
  "Ctrl-A           attach to session",
  "Ctrl-O           open PR in browser",
  "Ctrl-X           archive branch / close PR",
  "Ctrl-Y           merge highlighted PR",
  "Ctrl-S           sync handoff with remote",
  "Ctrl-N / Down    next row",
  "Ctrl-P / Up      previous row",
  "Backspace        delete filter",
  "Type             filter by branch/PR/author",
  "Esc / Ctrl-C     cancel",
  "",
  "Legend",
  "Agent: \u{1F916} running  \u{1F4AC} idle  \u25CC queued",
];

const COLUMN_WIDTHS = {
  diff: 10,
  agent: 5,
  pr: 6,
  author: 10,
  ci: 7,
  review: 8,
  age: 5,
} as const;

interface DisplayRow {
  name: string;
  diff: string;
  agent: string;
  pr: string;
  author: string;
  ci: string;
  review: string;
  age: string;
}

interface RenderOptions {
  width?: number;
  height?: number;
}

function pad(input: string, width: number): string {
  if (width <= 0) {
    return "";
  }
  const chars = Array.from(input);
  const text = chars.length > width ? `${chars.slice(0, Math.max(1, width - 1)).join("")}…` : input;
  return text.padEnd(width, " ");
}

function truncateToLen(input: string, maxLen: number): string {
  if (maxLen <= 0) {
    return "";
  }
  return Array.from(input).slice(0, maxLen).join("");
}

function fitLine(input: string, width: number): string {
  if (width <= 0) {
    return "";
  }
  const clipped = truncateToLen(input, width);
  const len = Array.from(clipped).length;
  if (len >= width) {
    return clipped;
  }
  return `${clipped}${" ".repeat(width - len)}`;
}

function overlayLine(base: string, overlay: string, startCol: number, width: number): string {
  const out = Array.from(fitLine(base, width));
  const src = Array.from(truncateToLen(overlay, Math.max(0, width - startCol)));
  for (let i = 0; i < src.length; i += 1) {
    const col = startCol + i;
    if (col >= 0 && col < out.length) {
      out[col] = src[i] ?? " ";
    }
  }
  return out.join("");
}

function buildFooterLine(width: number, segments: string[], right: string): string {
  if (width <= 0) {
    return "";
  }

  const rightLen = Array.from(right).length;
  if (width <= rightLen + 1) {
    return truncateToLen(right, width);
  }

  const leftMax = width - rightLen - 1;
  let used = 0;
  let left = "";
  let first = true;

  for (const segment of segments) {
    const chunk = first ? segment : ` | ${segment}`;
    const clipped = truncateToLen(chunk, leftMax - used);
    if (!clipped) {
      break;
    }
    left += clipped;
    used += Array.from(clipped).length;
    first = false;
    if (used >= leftMax) {
      break;
    }
  }

  const padding = " ".repeat(Math.max(0, leftMax - used) + 1);
  return `${left}${padding}${right}`;
}

function agentSymbol(status: HandoffRecord["status"]): string {
  const group = groupHandoffStatus(status);
  if (group === "running") return "🤖";
  if (group === "idle") return "💬";
  if (group === "error") return "⚠";
  if (group === "queued") return "◌";
  return "-";
}

function toDisplayRow(row: HandoffRecord): DisplayRow {
  const conflictPrefix = row.conflictsWithMain === "true" ? "\u26A0 " : "";

  const prLabel = row.prUrl ? `#${row.prUrl.match(/\/pull\/(\d+)/)?.[1] ?? "?"}` : row.prSubmitted ? "sub" : "-";

  const ciLabel = row.ciStatus ?? "-";
  const reviewLabel = row.reviewStatus
    ? row.reviewStatus === "approved"
      ? "ok"
      : row.reviewStatus === "changes_requested"
        ? "chg"
        : row.reviewStatus === "pending"
          ? "..."
          : row.reviewStatus
    : "-";

  return {
    name: `${conflictPrefix}${row.title || row.branchName}`,
    diff: row.diffStat ?? "-",
    agent: agentSymbol(row.status),
    pr: prLabel,
    author: row.prAuthor ?? "-",
    ci: ciLabel,
    review: reviewLabel,
    age: formatRelativeAge(row.updatedAt),
  };
}

function helpLines(width: number): string[] {
  const popupWidth = Math.max(40, Math.min(width - 2, 100));
  const innerWidth = Math.max(2, popupWidth - 2);
  const borderTop = `┌${"─".repeat(innerWidth)}┐`;
  const borderBottom = `└${"─".repeat(innerWidth)}┘`;

  const lines = [borderTop];
  for (const line of HELP_LINES) {
    lines.push(`│${pad(line, innerWidth)}│`);
  }
  lines.push(borderBottom);
  return lines;
}

export function formatRows(
  rows: HandoffRecord[],
  selected: number,
  workspaceId: string,
  status: string,
  searchQuery = "",
  showHelp = false,
  options: RenderOptions = {},
): string {
  const totalWidth = options.width ?? process.stdout.columns ?? 120;
  const totalHeight = Math.max(6, options.height ?? process.stdout.rows ?? 24);
  const fixedWidth =
    COLUMN_WIDTHS.diff + COLUMN_WIDTHS.agent + COLUMN_WIDTHS.pr + COLUMN_WIDTHS.author + COLUMN_WIDTHS.ci + COLUMN_WIDTHS.review + COLUMN_WIDTHS.age;
  const separators = 7;
  const prefixWidth = 2;
  const branchWidth = Math.max(20, totalWidth - (fixedWidth + separators + prefixWidth));

  const branchHeader = searchQuery ? `Branch/PR: ${searchQuery}_` : "Branch/PR (type to filter)";
  const header = [
    `  ${pad(branchHeader, branchWidth)} ${pad("Diff", COLUMN_WIDTHS.diff)} ${pad("Agent", COLUMN_WIDTHS.agent)} ${pad("PR", COLUMN_WIDTHS.pr)} ${pad("Author", COLUMN_WIDTHS.author)} ${pad("CI", COLUMN_WIDTHS.ci)} ${pad("Review", COLUMN_WIDTHS.review)} ${pad("Age", COLUMN_WIDTHS.age)}`,
    "-".repeat(Math.max(24, Math.min(totalWidth, 180))),
  ];

  const body =
    rows.length === 0
      ? ["No branches found."]
      : rows.map((row, index) => {
          const marker = index === selected ? "┃ " : "  ";
          const display = toDisplayRow(row);
          return `${marker}${pad(display.name, branchWidth)} ${pad(display.diff, COLUMN_WIDTHS.diff)} ${pad(display.agent, COLUMN_WIDTHS.agent)} ${pad(display.pr, COLUMN_WIDTHS.pr)} ${pad(display.author, COLUMN_WIDTHS.author)} ${pad(display.ci, COLUMN_WIDTHS.ci)} ${pad(display.review, COLUMN_WIDTHS.review)} ${pad(display.age, COLUMN_WIDTHS.age)}`;
        });

  const footer = fitLine(buildFooterLine(totalWidth, ["Ctrl-H:cheatsheet", `workspace:${workspaceId}`, status], `v${CLI_BUILD_ID}`), totalWidth);

  const contentHeight = totalHeight - 1;
  const lines = [...header, ...body].map((line) => fitLine(line, totalWidth));
  const page = lines.slice(0, contentHeight);
  while (page.length < contentHeight) {
    page.push(" ".repeat(totalWidth));
  }

  if (showHelp) {
    const popup = helpLines(totalWidth);
    const startRow = Math.max(0, Math.floor((contentHeight - popup.length) / 2));
    for (let i = 0; i < popup.length; i += 1) {
      const target = startRow + i;
      if (target >= page.length) {
        break;
      }
      const popupLine = popup[i] ?? "";
      const popupLen = Array.from(popupLine).length;
      const startCol = Math.max(0, Math.floor((totalWidth - popupLen) / 2));
      page[target] = overlayLine(page[target] ?? "", popupLine, startCol, totalWidth);
    }
  }

  return [...page, footer].join("\n");
}

interface OpenTuiLike {
  createCliRenderer?: (options?: Record<string, unknown>) => Promise<any>;
  TextRenderable?: new (
    ctx: any,
    options: { id: string; content: string },
  ) => {
    content: unknown;
    fg?: string;
    bg?: string;
  };
  fg?: (color: string) => (input: unknown) => unknown;
  bg?: (color: string) => (input: unknown) => unknown;
  StyledText?: new (chunks: unknown[]) => unknown;
}

interface StyledTextApi {
  fg: (color: string) => (input: unknown) => unknown;
  bg: (color: string) => (input: unknown) => unknown;
  StyledText: new (chunks: unknown[]) => unknown;
}

function buildStyledContent(content: string, theme: TuiTheme, api: StyledTextApi): unknown {
  const lines = content.split("\n");
  const chunks: unknown[] = [];
  const footerIndex = Math.max(0, lines.length - 1);

  for (let i = 0; i < lines.length; i += 1) {
    const line = lines[i] ?? "";

    let fgColor = theme.text;
    let bgColor: string | undefined;

    if (line.startsWith("┃ ")) {
      const marker = "┃ ";
      const rest = line.slice(marker.length);
      bgColor = theme.highlightBg;
      const markerChunk = api.bg(bgColor)(api.fg(theme.selectionBorder)(marker));
      const restChunk = api.bg(bgColor)(api.fg(theme.highlightFg)(rest));
      chunks.push(markerChunk);
      chunks.push(restChunk);
      if (i < lines.length - 1) {
        chunks.push(api.fg(theme.text)("\n"));
      }
      continue;
    }

    if (i === 0) {
      fgColor = theme.header;
    } else if (i === 1) {
      fgColor = theme.muted;
    } else if (i === footerIndex) {
      fgColor = theme.status;
    } else if (line.startsWith("┌") || line.startsWith("│") || line.startsWith("└")) {
      fgColor = theme.info;
    }

    let chunk: unknown = api.fg(fgColor)(line);
    if (bgColor) {
      chunk = api.bg(bgColor)(chunk);
    }
    chunks.push(chunk);

    if (i < lines.length - 1) {
      chunks.push(api.fg(theme.text)("\n"));
    }
  }

  return new api.StyledText(chunks);
}

export async function runTui(config: AppConfig, workspaceId: string): Promise<void> {
  const core = (await import("@opentui/core")) as OpenTuiLike;
  const createCliRenderer = core.createCliRenderer;
  const TextRenderable = core.TextRenderable;
  const styleApi = core.fg && core.bg && core.StyledText ? { fg: core.fg, bg: core.bg, StyledText: core.StyledText } : null;

  if (!createCliRenderer || !TextRenderable) {
    throw new Error("OpenTUI runtime missing createCliRenderer/TextRenderable exports");
  }

  const themeResolution = resolveTuiTheme(config);
  const client = createBackendClientFromConfig(config);
  const renderer = await createCliRenderer({ exitOnCtrlC: false });
  const text = new TextRenderable(renderer, {
    id: "openhandoff-switch",
    content: "Loading...",
  });
  text.fg = themeResolution.theme.text;
  text.bg = themeResolution.theme.background;
  renderer.root.add(text);
  renderer.start();

  let allRows: HandoffRecord[] = [];
  let filteredRows: HandoffRecord[] = [];
  let selected = 0;
  let searchQuery = "";
  let showHelp = false;
  let status = "loading...";
  let busy = false;
  let closed = false;
  let timer: ReturnType<typeof setInterval> | null = null;

  const clampSelected = (): void => {
    if (filteredRows.length === 0) {
      selected = 0;
      return;
    }
    if (selected < 0) {
      selected = 0;
      return;
    }
    if (selected >= filteredRows.length) {
      selected = filteredRows.length - 1;
    }
  };

  const render = (): void => {
    if (closed) {
      return;
    }
    const output = formatRows(filteredRows, selected, workspaceId, status, searchQuery, showHelp, {
      width: renderer.width ?? process.stdout.columns,
      height: renderer.height ?? process.stdout.rows,
    });
    text.content = styleApi ? buildStyledContent(output, themeResolution.theme, styleApi) : output;
    renderer.requestRender();
  };

  const refresh = async (): Promise<void> => {
    if (closed) {
      return;
    }
    try {
      allRows = await client.listHandoffs(workspaceId);
      if (closed) {
        return;
      }
      filteredRows = filterHandoffs(allRows, searchQuery);
      clampSelected();
      status = `handoffs=${allRows.length} filtered=${filteredRows.length}`;
    } catch (err) {
      if (closed) {
        return;
      }
      status = err instanceof Error ? err.message : String(err);
    }
    render();
  };

  const selectedRow = (): HandoffRecord | null => {
    if (filteredRows.length === 0) {
      return null;
    }
    return filteredRows[selected] ?? null;
  };

  let resolveDone: () => void = () => {};
  const done = new Promise<void>((resolve) => {
    resolveDone = () => resolve();
  });

  const close = (output?: string): void => {
    if (closed) {
      return;
    }
    closed = true;
    if (timer) {
      clearInterval(timer);
      timer = null;
    }
    process.off("SIGINT", handleSignal);
    process.off("SIGTERM", handleSignal);
    renderer.destroy();
    if (output) {
      console.log(output);
    }
    resolveDone();
  };

  const handleSignal = (): void => {
    close();
  };

  const runActionWithRefresh = async (label: string, fn: () => Promise<void>, success: string): Promise<void> => {
    if (busy) {
      return;
    }
    busy = true;
    status = `${label}...`;
    render();
    try {
      await fn();
      status = success;
      await refresh();
    } catch (err) {
      status = err instanceof Error ? err.message : String(err);
      render();
    } finally {
      busy = false;
    }
  };

  await refresh();
  timer = setInterval(() => {
    void refresh();
  }, 10_000);
  process.once("SIGINT", handleSignal);
  process.once("SIGTERM", handleSignal);

  const keyInput = (renderer.keyInput ?? renderer.keyHandler) as { on: (name: string, cb: (event: KeyEventLike) => void) => void } | undefined;

  if (!keyInput) {
    clearInterval(timer);
    renderer.destroy();
    throw new Error("OpenTUI key input handler is unavailable");
  }

  keyInput.on("keypress", (event: KeyEventLike) => {
    if (closed) {
      return;
    }

    const name = event.name ?? "";
    const ctrl = Boolean(event.ctrl);

    if (ctrl && name === "h") {
      showHelp = !showHelp;
      render();
      return;
    }

    if (showHelp) {
      if (name === "escape") {
        showHelp = false;
        render();
      }
      return;
    }

    if (name === "q" || name === "escape" || (ctrl && name === "c")) {
      close();
      return;
    }

    if ((ctrl && name === "n") || name === "down") {
      if (filteredRows.length > 0) {
        selected = selected >= filteredRows.length - 1 ? 0 : selected + 1;
        render();
      }
      return;
    }

    if ((ctrl && name === "p") || name === "up") {
      if (filteredRows.length > 0) {
        selected = selected <= 0 ? filteredRows.length - 1 : selected - 1;
        render();
      }
      return;
    }

    if (name === "backspace") {
      searchQuery = searchQuery.slice(0, -1);
      filteredRows = filterHandoffs(allRows, searchQuery);
      selected = 0;
      render();
      return;
    }

    if (name === "return" || name === "enter") {
      const row = selectedRow();
      if (!row || busy) {
        return;
      }
      busy = true;
      status = `switching ${row.handoffId}...`;
      render();
      void (async () => {
        try {
          const result = await client.switchHandoff(workspaceId, row.handoffId);
          close(`cd ${result.switchTarget}`);
        } catch (err) {
          busy = false;
          status = err instanceof Error ? err.message : String(err);
          render();
        }
      })();
      return;
    }

    if (ctrl && name === "a") {
      const row = selectedRow();
      if (!row || busy) {
        return;
      }
      busy = true;
      status = `attaching ${row.handoffId}...`;
      render();
      void (async () => {
        try {
          const result = await client.attachHandoff(workspaceId, row.handoffId);
          close(`target=${result.target} session=${result.sessionId ?? "none"}`);
        } catch (err) {
          busy = false;
          status = err instanceof Error ? err.message : String(err);
          render();
        }
      })();
      return;
    }

    if (ctrl && name === "x") {
      const row = selectedRow();
      if (!row) {
        return;
      }
      void runActionWithRefresh(`archiving ${row.handoffId}`, async () => client.runAction(workspaceId, row.handoffId, "archive"), `archived ${row.handoffId}`);
      return;
    }

    if (ctrl && name === "s") {
      const row = selectedRow();
      if (!row) {
        return;
      }
      void runActionWithRefresh(`syncing ${row.handoffId}`, async () => client.runAction(workspaceId, row.handoffId, "sync"), `synced ${row.handoffId}`);
      return;
    }

    if (ctrl && name === "y") {
      const row = selectedRow();
      if (!row) {
        return;
      }
      void runActionWithRefresh(
        `merging ${row.handoffId}`,
        async () => {
          await client.runAction(workspaceId, row.handoffId, "merge");
          await client.runAction(workspaceId, row.handoffId, "archive");
        },
        `merged+archived ${row.handoffId}`,
      );
      return;
    }

    if (ctrl && name === "o") {
      const row = selectedRow();
      if (!row?.prUrl) {
        status = "no PR URL available for this handoff";
        render();
        return;
      }
      const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
      spawnSync(openCmd, [row.prUrl], { stdio: "ignore" });
      status = `opened ${row.prUrl}`;
      render();
      return;
    }

    if (!ctrl && !event.meta && name.length === 1) {
      searchQuery += name;
      filteredRows = filterHandoffs(allRows, searchQuery);
      selected = 0;
      render();
    }
  });

  await done;
}
