import { useEffect, useRef, useState } from "react";
import type { ReactNode } from "react";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";

const copyIcon = (
  <svg
    aria-hidden="true"
    viewBox="0 0 24 24"
    width="14"
    height="14"
    fill="none"
    stroke="currentColor"
    strokeWidth="2"
    strokeLinecap="round"
    strokeLinejoin="round"
  >
    <rect x="9" y="9" width="11" height="11" rx="2" />
    <path d="M5 15V5a2 2 0 0 1 2-2h10" />
  </svg>
);

const downloadIcon = (
  <svg
    aria-hidden="true"
    viewBox="0 0 24 24"
    width="14"
    height="14"
    fill="none"
    stroke="currentColor"
    strokeWidth="2"
    strokeLinecap="round"
    strokeLinejoin="round"
  >
    <path d="M12 4v12" />
    <path d="M6 12l6 6 6-6" />
    <path d="M4 20h16" />
  </svg>
);

const checkIcon = (
  <svg
    aria-hidden="true"
    viewBox="0 0 24 24"
    width="14"
    height="14"
    fill="none"
    stroke="currentColor"
    strokeWidth="2.4"
    strokeLinecap="round"
    strokeLinejoin="round"
  >
    <path d="M5 12.5l4.5 4.5L19 7" />
  </svg>
);

const writeToClipboard = async (text: string): Promise<boolean> => {
  if (!text) return false;
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch {
    // Fallback for non-secure contexts.
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.style.position = "fixed";
    textarea.style.opacity = "0";
    document.body.appendChild(textarea);
    textarea.select();
    let ok = false;
    try {
      ok = document.execCommand("copy");
    } catch {
      ok = false;
    } finally {
      document.body.removeChild(textarea);
    }
    return ok;
  }
};

const CodeBlock = ({ children }: { children?: ReactNode }) => {
  const [copied, setCopied] = useState(false);
  const codeRef = useRef<HTMLElement | null>(null);
  const timerRef = useRef<number | null>(null);

  useEffect(() => {
    return () => {
      if (timerRef.current !== null) {
        window.clearTimeout(timerRef.current);
      }
    };
  }, []);

  const handleCopy = async () => {
    const text = codeRef.current?.innerText ?? "";
    await writeToClipboard(text);
    setCopied(true);
    if (timerRef.current !== null) {
      window.clearTimeout(timerRef.current);
    }
    timerRef.current = window.setTimeout(() => setCopied(false), 1500);
  };

  const captureCodeRef = (element: HTMLElement | null) => {
    codeRef.current = element?.querySelector("code") ?? null;
  };

  return (
    <div className="codeBlock">
      <button
        aria-label={copied ? "Copied" : "Copy code"}
        className={`codeCopy${copied ? " copied" : ""}`}
        onClick={handleCopy}
        title={copied ? "Copied" : "Copy code"}
        type="button"
      >
        {copied ? checkIcon : copyIcon}
      </button>
      <pre ref={captureCodeRef}>{children}</pre>
    </div>
  );
};

type TableCell = { text: string; isHeader: boolean };

const readTable = (table: HTMLTableElement): { headers: string[]; rows: string[][] } => {
  const rows = Array.from(table.querySelectorAll("tr"));
  const matrix: TableCell[][] = rows.map((row) =>
    Array.from(row.querySelectorAll("th,td")).map((cell) => ({
      text: (cell.textContent ?? "").replace(/\s+/g, " ").trim(),
      isHeader: cell.tagName.toLowerCase() === "th",
    })),
  );

  if (matrix.length === 0) return { headers: [], rows: [] };

  // If the first row is a header row, split it; otherwise synthesize generic headers.
  const firstRow = matrix[0] ?? [];
  const hasHeader = firstRow.length > 0 && firstRow.every((cell) => cell.isHeader);
  const headers = hasHeader
    ? firstRow.map((cell) => cell.text)
    : (matrix[0] ?? []).map((_, index) => `Column ${index + 1}`);
  const dataRows = hasHeader ? matrix.slice(1) : matrix;

  return {
    headers,
    rows: dataRows.map((row) => row.map((cell) => cell.text)),
  };
};

const toTsv = (headers: string[], rows: string[][]): string => {
  const escape = (value: string) => value.replace(/\t/g, " ").replace(/\n/g, " ");
  return [headers, ...rows].map((row) => row.map(escape).join("\t")).join("\n");
};

const toCsv = (headers: string[], rows: string[][]): string => {
  // RFC 4180: wrap fields containing comma, quote, or newline in double quotes;
  // escape internal double quotes by doubling them.
  const escape = (value: string) => {
    if (/[",\n\r]/.test(value)) {
      return `"${value.replace(/"/g, '""')}"`;
    }
    return value;
  };
  return [headers, ...rows].map((row) => row.map(escape).join(",")).join("\r\n");
};

const downloadFile = (filename: string, content: string, mime: string) => {
  const blob = new Blob([content], { type: mime });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  link.rel = "noreferrer";
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  // Defer revoke so the download starts cleanly.
  window.setTimeout(() => URL.revokeObjectURL(url), 0);
};

const TableBlock = ({ children }: { children?: ReactNode }) => {
  const [copied, setCopied] = useState(false);
  const tableRef = useRef<HTMLTableElement | null>(null);
  const timerRef = useRef<number | null>(null);

  useEffect(() => {
    return () => {
      if (timerRef.current !== null) {
        window.clearTimeout(timerRef.current);
      }
    };
  }, []);

  const flashCopied = () => {
    setCopied(true);
    if (timerRef.current !== null) {
      window.clearTimeout(timerRef.current);
    }
    timerRef.current = window.setTimeout(() => setCopied(false), 1500);
  };

  const handleCopy = async () => {
    if (!tableRef.current) return;
    const { headers, rows } = readTable(tableRef.current);
    await writeToClipboard(toTsv(headers, rows));
    flashCopied();
  };

  const handleDownloadCsv = () => {
    if (!tableRef.current) return;
    const { headers, rows } = readTable(tableRef.current);
    const filename = `table-${new Date().toISOString().replace(/[:.]/g, "-")}.csv`;
    // Prefix with UTF-8 BOM so Excel opens it correctly.
    downloadFile(filename, `\uFEFF${toCsv(headers, rows)}`, "text/csv;charset=utf-8");
  };

  return (
    <div className="tableBlock">
      <div className="tableBlockActions">
        <button
          aria-label="Download table as CSV"
          className="tableDownload"
          onClick={handleDownloadCsv}
          title="Download as CSV"
          type="button"
        >
          {downloadIcon}
        </button>
        <button
          aria-label={copied ? "Copied" : "Copy table as TSV"}
          className={`tableCopy${copied ? " copied" : ""}`}
          onClick={handleCopy}
          title={copied ? "Copied" : "Copy as TSV"}
          type="button"
        >
          {copied ? checkIcon : copyIcon}
        </button>
      </div>
      <div className="tableScroll">
        <table ref={tableRef}>{children}</table>
      </div>
    </div>
  );
};

export const normalizeMarkdown = (value: string) =>
  value.replace(/\\n/g, "\n").replace(/\\t/g, "\t");

export const markdownComponents = {
  a: ({ href, children }: { href?: string; children?: ReactNode }) => (
    <a href={href} rel="noreferrer" target="_blank">
      {children}
    </a>
  ),
  pre: ({ children }: { children?: ReactNode }) => <CodeBlock>{children}</CodeBlock>,
  table: ({ children }: { children?: ReactNode }) => <TableBlock>{children}</TableBlock>,
};

export const MarkdownView = ({ source }: { source: string }) => (
  <Markdown components={markdownComponents} remarkPlugins={[remarkGfm]} skipHtml>
    {normalizeMarkdown(source)}
  </Markdown>
);

export { CodeBlock };
