// CSVViewer.tsx — Inline preview for CSV / TSV artifacts. Renders a compact
// table with sticky header, truncated cells, right-aligned numeric columns,
// and a show-all toggle. In-house parser, no deps.
"use client";

import { useEffect, useMemo, useState } from "react";

export interface CSVViewerProps {
  url: string;
  /** Used to pick the delimiter when the URL is a data: URL (extensionless). */
  name?: string;
  bytes?: number;
}

interface ParsedCsv {
  header: string[];
  rows: string[][];
}

/** Split text into logical lines, respecting double-quoted fields that may
 *  contain embedded newlines. Only breaks on \n (or \r\n) when outside quotes. */
function splitLines(text: string): string[] {
  const lines: string[] = [];
  let current = "";
  let inQuotes = false;
  for (let i = 0; i < text.length; i++) {
    const ch = text[i];
    if (ch === '"') {
      inQuotes = !inQuotes;
      current += ch;
    } else if (ch === "\r" && text[i + 1] === "\n" && !inQuotes) {
      lines.push(current);
      current = "";
      i++; // skip the \n
    } else if (ch === "\n" && !inQuotes) {
      lines.push(current);
      current = "";
    } else {
      current += ch;
    }
  }
  if (current.length > 0) lines.push(current);
  return lines;
}

function parseCsv(input: string, delim: string): ParsedCsv {
  let text = input;
  if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);

  const lines = splitLines(text);
  const parsed: string[][] = [];
  for (const line of lines) {
    if (line.length === 0) continue;
    parsed.push(parseLine(line, delim));
  }

  while (parsed.length > 0) {
    const last = parsed[parsed.length - 1];
    if (last.every((c) => c === "")) parsed.pop();
    else break;
  }

  if (parsed.length === 0) return { header: [], rows: [] };
  const [header, ...rows] = parsed;
  return { header, rows };
}

function parseLine(line: string, delim: string): string[] {
  const out: string[] = [];
  let field = "";
  let inQuotes = false;
  for (let i = 0; i < line.length; i++) {
    const ch = line[i];
    if (inQuotes) {
      if (ch === '"') {
        if (line[i + 1] === '"') {
          field += '"';
          i++;
        } else {
          inQuotes = false;
        }
      } else {
        field += ch;
      }
    } else {
      if (ch === '"' && field === "") {
        inQuotes = true;
      } else if (ch === delim) {
        out.push(field);
        field = "";
      } else {
        field += ch;
      }
    }
  }
  out.push(field);
  return out;
}

function isNumericValue(v: string): boolean {
  if (v === "" || v == null) return false;
  const trimmed = v.trim();
  if (trimmed === "") return false;
  if (!/^-?\d*\.?\d+(?:[eE][-+]?\d+)?$/.test(trimmed)) return false;
  return Number.isFinite(Number(trimmed));
}

function detectNumericColumns(header: string[], rows: string[][]): boolean[] {
  return header.map((_, col) => {
    let total = 0;
    let numeric = 0;
    for (const row of rows) {
      const v = row[col];
      if (v === undefined || v === "") continue;
      total++;
      if (isNumericValue(v)) numeric++;
    }
    if (total === 0) return false;
    return numeric / total >= 0.8;
  });
}

function formatBytes(n: number | undefined): string {
  if (!n && n !== 0) return "";
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
  return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}

const DEFAULT_ROWS_SHOWN = 25;

export default function CSVViewer({ url, name, bytes }: CSVViewerProps) {
  const [raw, setRaw] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [showAll, setShowAll] = useState(false);

  const delim = useMemo(() => {
    // Prefer the explicit filename; extensionless artifact URLs (/api/artifacts/<id>)
    // fall through to content-sniffing below.
    const n = (name || "").toLowerCase();
    if (n.endsWith(".tsv")) return "\t";
    return ","; // Content-sniffing happens after fetch if needed
  }, [name]);

  const MAX_PREVIEW_BYTES = 2 * 1024 * 1024; // 2 MB preview cap

  useEffect(() => {
    let cancelled = false;
    setRaw(null);
    setError(null);
    setShowAll(false);
    fetch(url)
      .then((r) => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.text();
      })
      .then((t) => {
        if (!cancelled) {
          // Cap preview at 2 MB to avoid freezing the chat tab on large CSVs
          if (t.length > MAX_PREVIEW_BYTES) {
            setRaw(t.slice(0, MAX_PREVIEW_BYTES));
          } else {
            setRaw(t);
          }
        }
      })
      .catch((e) => {
        if (!cancelled) setError(e.message || String(e));
      });
    return () => {
      cancelled = true;
    };
  }, [url]);

  const parsed = useMemo<ParsedCsv | null>(() => {
    if (raw == null) return null;
    try {
      const p = parseCsv(raw, delim);
      if (p.header.length === 0) return null;
      return p;
    } catch {
      return null;
    }
  }, [raw, delim]);

  const numericCols = useMemo(
    () => (parsed ? detectNumericColumns(parsed.header, parsed.rows) : []),
    [parsed],
  );

  const totalRows = parsed?.rows.length ?? 0;
  const visibleRows = showAll ? totalRows : Math.min(DEFAULT_ROWS_SHOWN, totalRows);
  const shownRows = parsed ? parsed.rows.slice(0, visibleRows) : [];
  const hasToggle = totalRows > DEFAULT_ROWS_SHOWN;

  return (
    <div
      data-testid="csv-viewer"
      className="relative w-full overflow-hidden rounded-lg border border-border-subtle bg-white"
    >
      <div className="flex items-center justify-between px-3 py-2 border-b border-border-subtle">
        <span className="text-[10px] font-bold uppercase tracking-widest text-muted-fg">
          {name ? `CSV · ${name}` : "CSV"}
        </span>
        <a
          href={url}
          download={name}
          className="text-[10px] text-muted-fg hover:text-accent underline underline-offset-2"
        >
          Download
        </a>
      </div>

      {error ? (
        <div className="p-4 text-xs text-red-600">
          Failed to load CSV: {error}
        </div>
      ) : raw == null ? (
        <div className="p-4 text-xs text-muted-fg">Loading CSV…</div>
      ) : parsed == null ? (
        <div className="p-4 text-xs text-red-600">Could not parse as CSV.</div>
      ) : (
        <>
          <div className="max-h-96 overflow-auto">
            <table className="w-full border-collapse font-mono text-[11px] leading-tight">
              <thead className="sticky top-0 z-10 bg-muted/80 backdrop-blur">
                <tr>
                  {parsed.header.map((h, idx) => (
                    <th
                      key={idx}
                      title={h}
                      className={`px-2 py-1.5 text-[10px] font-semibold uppercase tracking-wide text-muted-fg border-b border-border-subtle whitespace-nowrap ${
                        numericCols[idx] ? "text-right" : "text-left"
                      }`}
                      style={{ maxWidth: 140 }}
                    >
                      <span className="block overflow-hidden text-ellipsis whitespace-nowrap">
                        {h}
                      </span>
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {shownRows.map((row, rIdx) => (
                  <tr
                    key={rIdx}
                    className={rIdx % 2 === 1 ? "bg-muted/30" : undefined}
                  >
                    {parsed.header.map((_, cIdx) => {
                      const val = row[cIdx] ?? "";
                      return (
                        <td
                          key={cIdx}
                          title={val}
                          className={`px-2 py-1 border-b border-border-subtle/60 ${
                            numericCols[cIdx] ? "text-right tabular-nums" : "text-left"
                          }`}
                          style={{ maxWidth: 140 }}
                        >
                          <span className="block overflow-hidden text-ellipsis whitespace-nowrap">
                            {val}
                          </span>
                        </td>
                      );
                    })}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>

          <div className="flex items-center justify-between px-3 py-1.5 border-t border-border-subtle text-[10px] text-muted-fg">
            <span>
              {totalRows} row{totalRows === 1 ? "" : "s"} × {parsed.header.length} column
              {parsed.header.length === 1 ? "" : "s"}
              {bytes != null ? ` · ${formatBytes(bytes)}` : ""}
              {!showAll && hasToggle ? ` · showing ${visibleRows}` : ""}
            </span>
            {hasToggle ? (
              <button
                type="button"
                onClick={() => setShowAll((s) => !s)}
                className="text-[10px] text-muted-fg hover:text-accent underline underline-offset-2"
              >
                {showAll ? "Show fewer" : "Show all"}
              </button>
            ) : null}
          </div>
        </>
      )}
    </div>
  );
}

export { CSVViewer };
