/* Shared pieces for the browser tools (Molecular and Population genetics).
   Files are read with the File API and never leave the browser. */

const TOOL_XLSX_SRC = "https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js";
const TOOL_XLSX_SRI = "sha384-EnyY0/GSHQGSxSgMwaIPzSESbqoOLSexfnSMN2AP+39Ckmn92stwABZynq1JyzdT";
let toolXlsxPromise = null;

// SheetJS is about 950 KB, so it loads only when someone opens a spreadsheet.
function toolLoadXlsx() {
  if (window.XLSX) return Promise.resolve(window.XLSX);
  if (!toolXlsxPromise) {
    toolXlsxPromise = new Promise((resolve, reject) => {
      const script = document.createElement("script");
      script.src = TOOL_XLSX_SRC;
      script.integrity = TOOL_XLSX_SRI;
      script.crossOrigin = "anonymous";
      script.onload = () => resolve(window.XLSX);
      script.onerror = () => {
        toolXlsxPromise = null;
        reject(new Error("The spreadsheet reader could not load. Save the sheet as CSV and try again."));
      };
      document.head.appendChild(script);
    });
  }
  return toolXlsxPromise;
}

function toolDecodeText(bytes) {
  const utf8 = new TextDecoder("utf-8").decode(bytes);
  if (!utf8.includes("�")) return utf8;
  try { return new TextDecoder("windows-1252").decode(bytes); } catch { return utf8; }
}

async function toolGunzip(bytes) {
  const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
  return new Uint8Array(await new Response(stream).arrayBuffer());
}

// Reads one File into { name, size, sha256, text, rows, kind }. `rows` is set for
// spreadsheets (first sheet that has data); `text` for everything else.
async function toolReadFile(file) {
  const buffer = await file.arrayBuffer();
  const bytes = new Uint8Array(buffer);
  const sha256 = await window.CBHash.sha256Hex(bytes);
  const lower = file.name.toLowerCase();
  const base = { name: file.name, size: file.size, sha256, lastModified: file.lastModified || null };
  if (/\.(xlsx|xlsm|xls)$/.test(lower)) {
    const XLSX = await toolLoadXlsx();
    const book = XLSX.read(bytes, { type: "array" });
    let rows = [];
    let sheetName = null;
    for (const name of book.SheetNames) {
      const sheetRows = XLSX.utils.sheet_to_json(book.Sheets[name], { header: 1, raw: false, defval: "" })
        .map((row) => row.map((cell) => String(cell ?? "").trim()));
      if (sheetRows.some((row) => row.some((cell) => cell !== ""))) { rows = sheetRows; sheetName = name; break; }
    }
    return { ...base, kind: "xlsx", rows, sheetName, text: null };
  }
  if (lower.endsWith(".gz")) {
    const inflated = await toolGunzip(bytes);
    return { ...base, kind: "gzip", text: toolDecodeText(inflated), rows: null };
  }
  return { ...base, kind: "text", text: toolDecodeText(bytes), rows: null };
}

// Builds a File-like object from a URL (used by the example data buttons).
async function toolFetchAsFile(url, name) {
  const response = await fetch(url, { cache: "no-cache" });
  if (!response.ok) throw new Error(`Could not load ${name} (${response.status}).`);
  const blob = await response.blob();
  return new File([blob], name, { type: blob.type || "text/plain" });
}

function toolDownload(name, content, type = "text/plain;charset=utf-8") {
  const blob = content instanceof Blob ? content : new Blob([content], { type });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = name;
  document.body.appendChild(link);
  link.click();
  link.remove();
  setTimeout(() => URL.revokeObjectURL(url), 2000);
}

function toolSlug(text, fallback = "study") {
  const slug = String(text || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
  return slug || fallback;
}

function toolFmtNum(value, digits = 1) {
  if (value === null || value === undefined || Number.isNaN(value)) return "n/a";
  return Number(value).toFixed(digits);
}

function toolFmtBytes(bytes) {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}

function toolFmtTime(iso) {
  if (!iso) return "";
  const date = new Date(iso);
  if (Number.isNaN(date.getTime())) return iso;
  return date.toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
}

// Waits for an engine that a <script type="module"> puts on window.
function useToolEngine(globalName) {
  const [engine, setEngine] = React.useState(() => window[globalName] || null);
  React.useEffect(() => {
    if (engine) return undefined;
    const timer = setInterval(() => {
      if (window[globalName]) { setEngine(window[globalName]); clearInterval(timer); }
    }, 50);
    return () => clearInterval(timer);
  }, [engine, globalName]);
  return engine;
}

function ToolSteps({ steps, current, onSelect }) {
  return (
    <nav className="tool-steps" aria-label="Steps">
      {steps.map((step, index) => (
        <button key={step.key} type="button" className={`tool-step${step.done ? " is-done" : ""}`}
                aria-current={current === step.key ? "step" : undefined}
                disabled={step.disabled} onClick={() => onSelect(step.key)}>
          <span className="tool-step-num">{step.done && current !== step.key ? "✓" : index + 1}</span>
          {step.label}
        </button>
      ))}
    </nav>
  );
}

function ToolDrop({ title, hint, accept, multiple, onFiles, disabled }) {
  const [over, setOver] = React.useState(false);
  const take = (list) => { const files = Array.from(list || []); if (files.length) onFiles(files); };
  return (
    <label className={`tool-drop${over ? " is-over" : ""}`} style={disabled ? { opacity: .55, pointerEvents: "none" } : undefined}
           onDragOver={(e) => { e.preventDefault(); setOver(true); }}
           onDragLeave={() => setOver(false)}
           onDrop={(e) => { e.preventDefault(); setOver(false); take(e.dataTransfer.files); }}>
      <input type="file" accept={accept} multiple={multiple} disabled={disabled}
             onChange={(e) => { take(e.target.files); e.target.value = ""; }} />
      <div className="tool-drop-title">{title}</div>
      {hint ? <div className="tool-drop-hint">{hint}</div> : null}
    </label>
  );
}

function ToolNotice({ tone, title, children, items }) {
  return (
    <div className={`tool-notice${tone ? ` tone-${tone}` : ""}`} role={tone === "block" ? "alert" : undefined}>
      <div style={{ minWidth: 0 }}>
        {title ? <strong style={{ display: "block" }}>{title}</strong> : null}
        {children}
        {items && items.length ? <ul>{items.map((item, i) => <li key={i}>{item}</li>)}</ul> : null}
      </div>
    </div>
  );
}

function ToolStat({ label, value, sub }) {
  return (
    <div className="tool-stat">
      <div className="tool-stat-label">{label}</div>
      <div className="tool-stat-value">{value}</div>
      {sub ? <div className="tool-stat-sub">{sub}</div> : null}
    </div>
  );
}

function ToolPanel({ title, actions, children, bodyStyle, id }) {
  return (
    <section className="panel" id={id}>
      {title ? (
        <div className="panel-head">
          <h3>{title}</h3>
          {actions ? <div style={{ display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>{actions}</div> : null}
        </div>
      ) : null}
      <div className="panel-body" style={bodyStyle}>{children}</div>
    </section>
  );
}

// Saved work (/api/saved). Every call is for the signed-in account; errors carry the
// server's plain message.
async function savedRequest(method, { id, body } = {}) {
  const response = await fetch(id ? `/api/saved?id=${encodeURIComponent(id)}` : "/api/saved", {
    method,
    credentials: "same-origin",
    cache: "no-store",
    headers: body ? { "Content-Type": "application/json" } : undefined,
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await response.json().catch(() => ({}));
  if (!response.ok) throw new Error(data.message || `Saved work is unavailable right now (${response.status}).`);
  return data;
}

const savedApi = {
  list: () => savedRequest("GET").then((data) => data.items),
  get: (id) => savedRequest("GET", { id }).then((data) => data.item),
  create: (item) => savedRequest("POST", { body: item }).then((data) => data.item),
  update: (id, patch) => savedRequest("PUT", { id, body: patch }).then((data) => data.item),
  remove: (id) => savedRequest("DELETE", { id }),
};

const SAVED_MODULES = {
  molecular: { label: "Molecular", noun: "decision pack", href: "/molecular/app" },
  population: { label: "Population genetics", noun: "result", href: "/population/app" },
  study: { label: "Study record", noun: "study record", href: "/study/app" },
};

// Signing in reloads the page, so "Sign in and save" carries the result through the
// sign-in round trip in this tab's sessionStorage; /saved finishes the save.
const PENDING_SAVE_KEY = "conductbiology_pending_save";

function toolStashPendingSave(item) {
  try {
    sessionStorage.setItem(PENDING_SAVE_KEY, JSON.stringify({ ...item, at: new Date().toISOString() }));
    return true;
  } catch (_error) {
    return false;
  }
}

function toolTakePendingSave() {
  try {
    const text = sessionStorage.getItem(PENDING_SAVE_KEY);
    sessionStorage.removeItem(PENDING_SAVE_KEY);
    return text ? JSON.parse(text) : null;
  } catch (_error) {
    return null;
  }
}

// The "Save to your account" panel for a finished result. getPayload builds the payload
// when the button is pressed; with savedId the panel updates that saved item instead.
function ToolSaveToAccount({ module, defaultTitle, summary, getPayload, savedId, onSaved }) {
  const account = useAccount();
  const [title, setTitle] = React.useState(defaultTitle || "");
  const [state, setState] = React.useState({ status: "idle", message: "" });
  React.useEffect(() => { setTitle((current) => current || defaultTitle || ""); }, [defaultTitle]);
  if (!account || !account.enabled || !account.saving) return null;
  const name = () => title.replace(/\s+/g, " ").trim() || defaultTitle || "Untitled";
  const noun = SAVED_MODULES[module].noun;

  async function save() {
    setState({ status: "saving", message: "" });
    try {
      const body = { title: name(), summary: summary || {}, payload: await getPayload() };
      const item = savedId ? await savedApi.update(savedId, body) : await savedApi.create({ module, ...body });
      setState({ status: "saved", message: "" });
      if (onSaved) onSaved(item);
    } catch (err) {
      setState({ status: "error", message: err.message || String(err) });
    }
  }

  async function signInAndSave() {
    setState({ status: "saving", message: "" });
    try {
      const stashed = toolStashPendingSave({ module, title: name(), summary: summary || {}, payload: await getPayload() });
      if (!stashed) throw new Error(`This ${noun} is too large to carry through sign-in. Download it, sign in, and run it again to save.`);
      window.location.assign(`/api/auth/conductscience/start?next=${encodeURIComponent("/saved?pending=1")}`);
    } catch (err) {
      setState({ status: "error", message: err.message || String(err) });
    }
  }

  return (
    <ToolPanel title="Save to your account">
      {account.user ? (
        <div style={{ display: "grid", gap: 10 }}>
          <label className="tool-field"><span>Name</span>
            <input className="tool-input" value={title} maxLength={200} onChange={(e) => setTitle(e.target.value)} />
          </label>
          <button type="button" className="btn btn-primary btn-tap" disabled={state.status === "saving"} onClick={save}>
            {state.status === "saving" ? "Saving…" : savedId ? "Save changes" : "Save to your account"}
          </button>
        </div>
      ) : (
        <div style={{ display: "grid", gap: 10 }}>
          <p style={{ margin: 0, fontSize: 13.5, lineHeight: 1.5, color: "var(--ink-700)" }}>
            Keep this {noun} in your ConductScience account and open it again later.
          </p>
          <button type="button" className="btn btn-primary btn-tap" disabled={state.status === "saving"} onClick={signInAndSave}>
            Sign in and save
          </button>
        </div>
      )}
      {state.status === "saved" ? (
        <div style={{ marginTop: 10 }}>
          <ToolNotice tone="ok" title="Saved">Open it any time from <a className="btn-link" href="/saved">Saved work</a>.</ToolNotice>
        </div>
      ) : null}
      {state.status === "error" ? <div style={{ marginTop: 10 }}><ToolNotice tone="block" title="Not saved">{state.message}</ToolNotice></div> : null}
      <p className="tool-muted" style={{ fontSize: 12, margin: "10px 0 0" }}>Saves the results and tables, not your files.</p>
    </ToolPanel>
  );
}

// Loads one saved item for a tool's ?saved= view.
function useSavedItem(id) {
  const [state, setState] = React.useState({ status: id ? "loading" : "none", item: null, message: "" });
  React.useEffect(() => {
    if (!id) return undefined;
    let live = true;
    savedApi.get(id)
      .then((item) => { if (live) setState({ status: "ready", item, message: "" }); })
      .catch((err) => { if (live) setState({ status: "error", item: null, message: err.message || String(err) }); });
    return () => { live = false; };
  }, [id]);
  return state;
}

function toolSavedParam() {
  return new URLSearchParams(window.location.search).get("saved");
}

// The header strip on a tool page that shows a saved item.
function ToolSavedBanner({ item, module }) {
  const info = SAVED_MODULES[module];
  return (
    <div className="tool-notice tone-info" style={{ marginBottom: 16 }}>
      <div style={{ minWidth: 0, display: "flex", gap: 12, flexWrap: "wrap", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
        <div style={{ minWidth: 0 }}>
          <strong style={{ display: "block" }}>Saved {info.noun}: {item.title}</strong>
          Saved {toolFmtTime(item.updatedAt)} · fingerprint <span className="tool-mono">{String(item.payloadSha256 || "").slice(0, 12)}…</span>
        </div>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <a className="btn btn-ghost btn-tap" href="/saved">Saved work</a>
          <a className="btn btn-ghost btn-tap" href={info.href}>New analysis</a>
        </div>
      </div>
    </div>
  );
}

function ToolPrivacyLine() {
  const account = useAccount();
  return (
    <>
      <p className="tool-muted" style={{ fontSize: 12.5, margin: "10px 0 0", display: "flex", gap: 6, alignItems: "center" }}>
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 018 0v4"/></svg>
        {account && account.user && account.saving
          ? "Files are read in your browser and never uploaded. Saving keeps results and tables in your account, not your files."
          : "Files are read in your browser and never uploaded."}
      </p>
      {account && !account.user ? (
        <p style={{ fontSize: 13, margin: "6px 0 0", color: "var(--ink-600)" }}>
          Free during the beta. <a href="/signup" className="btn-link">Join the beta</a> to get new features first.
        </p>
      ) : null}
    </>
  );
}

Object.assign(window, {
  toolLoadXlsx, toolReadFile, toolFetchAsFile, toolDownload, toolSlug, toolFmtNum, toolFmtBytes, toolFmtTime,
  useToolEngine, ToolSteps, ToolDrop, ToolNotice, ToolStat, ToolPanel, ToolPrivacyLine,
  savedApi, SAVED_MODULES, toolStashPendingSave, toolTakePendingSave, ToolSaveToAccount, useSavedItem, toolSavedParam, ToolSavedBanner,
});
