import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  api, assetFileURL, connectWS, deleteAsset, listAssets, listJobs,
  listOperators, runJob, uploadFile,
} from "./api";
import Viewer from "./Viewer";

const MODEL_EXTS = ["glb", "gltf", "obj", "fbx", "ply", "stl"];
const fmtSize = (n) =>
  n > 1e9 ? (n / 1e9).toFixed(1) + " GB" : n > 1e6 ? (n / 1e6).toFixed(1) + " MB"
  : n > 1e3 ? (n / 1e3).toFixed(0) + " KB" : n + " B";
const KIND_ICON = { video: "🎬", image: "🖼", model: "🧊", frames: "🎞", other: "📄" };

function ParamForm({ schema, values, onChange }) {
  const props = schema?.properties || {};
  if (!Object.keys(props).length) return <p className="dim">No parameters.</p>;
  return (
    <div className="params">
      {Object.entries(props).map(([key, def]) => {
        const val = values[key] ?? def.default ?? "";
        const set = (v) => onChange({ ...values, [key]: v });
        return (
          <label key={key} title={def.description || ""}>
            <span>{key}</span>
            {def.enum ? (
              <select value={val} onChange={(e) => set(e.target.value)}>
                {def.enum.map((o) => <option key={o}>{o}</option>)}
              </select>
            ) : def.type === "boolean" ? (
              <input type="checkbox" checked={!!val} onChange={(e) => set(e.target.checked)} />
            ) : def.type === "number" || def.type === "integer" ? (
              <input type="number" value={val} step={def.type === "integer" ? 1 : "any"}
                min={def.minimum} max={def.maximum}
                onChange={(e) => set(def.type === "integer" ? parseInt(e.target.value || 0) : parseFloat(e.target.value || 0))} />
            ) : (
              <input type="text" value={val} onChange={(e) => set(e.target.value)} />
            )}
          </label>
        );
      })}
    </div>
  );
}

function AssetPreview({ asset }) {
  const [folder, setFolder] = useState(null);
  const ext = asset ? asset.name.split(".").pop().toLowerCase() : "";
  useEffect(() => {
    setFolder(null);
    if (asset && (asset.kind === "frames" || asset.kind === "folder")) {
      api(assetFileURL(asset.id)).then(setFolder).catch(() => {});
    }
  }, [asset?.id]);

  if (!asset) return <div className="preview empty">Select an asset — or drop / paste files anywhere</div>;
  if (MODEL_EXTS.includes(ext))
    return <Viewer url={assetFileURL(asset.id)} ext={ext} />;
  if (asset.kind === "image")
    return <div className="preview"><img src={assetFileURL(asset.id)} alt={asset.name} /></div>;
  if (asset.kind === "video")
    return <div className="preview"><video src={assetFileURL(asset.id)} controls /></div>;
  if (folder?.files)
    return (
      <div className="preview grid">
        {folder.files.slice(0, 60).map((f) => (
          <img key={f} src={assetFileURL(asset.id, f)} loading="lazy" alt={f} />
        ))}
        {folder.files.length > 60 && <span className="dim">+{folder.files.length - 60} more</span>}
      </div>
    );
  if (ext === "json")
    return <JsonPreview url={assetFileURL(asset.id)} />;
  return <div className="preview empty">No preview for .{ext}</div>;
}

function JsonPreview({ url }) {
  const [text, setText] = useState("…");
  useEffect(() => {
    fetch(url).then((r) => r.text()).then((t) => setText(t.slice(0, 20000)));
  }, [url]);
  return <pre className="preview json">{text}</pre>;
}

export default function App() {
  const [assets, setAssets] = useState([]);
  const [operators, setOperators] = useState([]);
  const [jobs, setJobs] = useState([]);
  const [selected, setSelected] = useState(null);
  const [opId, setOpId] = useState(null);
  const [params, setParams] = useState({});
  const [openJob, setOpenJob] = useState(null);
  const fileInput = useRef(null);

  const refreshAssets = useCallback(() => listAssets().then(setAssets), []);
  const refreshJobs = useCallback(() => listJobs().then(setJobs), []);

  useEffect(() => {
    refreshAssets();
    refreshJobs();
    listOperators().then(setOperators);
    connectWS((msg) => {
      if (msg.type === "job") {
        setJobs((js) => {
          const i = js.findIndex((j) => j.id === msg.job.id);
          if (i === -1) return [msg.job, ...js];
          const copy = [...js]; copy[i] = msg.job; return copy;
        });
      }
      if (msg.type === "assets_changed") refreshAssets();
    });
  }, []);

  // drag-drop + paste ingest
  useEffect(() => {
    const drop = async (e) => {
      e.preventDefault();
      for (const f of e.dataTransfer?.files || []) await uploadFile(f);
      refreshAssets();
    };
    const paste = async (e) => {
      const items = [...(e.clipboardData?.items || [])];
      for (const it of items) {
        const f = it.getAsFile?.();
        if (f) { await uploadFile(f); }
      }
      refreshAssets();
    };
    const prevent = (e) => e.preventDefault();
    window.addEventListener("drop", drop);
    window.addEventListener("dragover", prevent);
    window.addEventListener("paste", paste);
    return () => {
      window.removeEventListener("drop", drop);
      window.removeEventListener("dragover", prevent);
      window.removeEventListener("paste", paste);
    };
  }, [refreshAssets]);

  const selectedAsset = assets.find((a) => a.id === selected) || null;
  const usableOps = useMemo(() =>
    operators.filter((op) => !selectedAsset || op.accepts.includes(selectedAsset.kind)),
    [operators, selectedAsset]);
  const activeOp = operators.find((o) => o.id === opId);

  useEffect(() => {
    if (activeOp) {
      const defaults = {};
      for (const [k, d] of Object.entries(activeOp.params_schema?.properties || {}))
        if (d.default !== undefined) defaults[k] = d.default;
      setParams(defaults);
    }
  }, [opId]);

  useEffect(() => {
    if (usableOps.length && !usableOps.find((o) => o.id === opId)) setOpId(usableOps[0].id);
  }, [usableOps]);

  const launch = async () => {
    if (!activeOp || !selectedAsset) return;
    await runJob(activeOp.id, selectedAsset.id, params);
    refreshJobs();
  };

  return (
    <div className="layout">
      <header>
        <h1>MODEL<span>BEAST</span></h1>
        <span className="dim">M3 Ultra · 256GB · local-first 3D factory</span>
        <button onClick={() => fileInput.current.click()}>+ Add files</button>
        <input ref={fileInput} type="file" multiple hidden
          onChange={async (e) => {
            for (const f of e.target.files) await uploadFile(f);
            e.target.value = ""; refreshAssets();
          }} />
      </header>

      <aside className="assets">
        <h2>Assets</h2>
        {assets.map((a) => (
          <div key={a.id}
            className={"asset" + (a.id === selected ? " sel" : "")}
            onClick={() => setSelected(a.id)}>
            <span className="icon">{KIND_ICON[a.kind] || "📄"}</span>
            <div className="meta">
              <div className="name">{a.name}</div>
              <div className="dim">{a.kind} · {fmtSize(a.size)}</div>
            </div>
            <button className="x" title="delete" onClick={(e) => {
              e.stopPropagation();
              deleteAsset(a.id).then(refreshAssets);
              if (selected === a.id) setSelected(null);
            }}>×</button>
          </div>
        ))}
        {!assets.length && <p className="dim">Drop videos, images, or 3D files anywhere.</p>}
      </aside>

      <main>
        <AssetPreview asset={selectedAsset} />
      </main>

      <aside className="workbench">
        <h2>Run</h2>
        <select value={opId || ""} onChange={(e) => setOpId(e.target.value)}>
          {usableOps.map((op) => <option key={op.id} value={op.id}>{op.name}</option>)}
        </select>
        {activeOp && <p className="dim">{activeOp.description}</p>}
        {activeOp && <ParamForm schema={activeOp.params_schema} values={params} onChange={setParams} />}
        <button className="go" disabled={!selectedAsset || !activeOp} onClick={launch}>
          ▶ Run {activeOp?.name || ""}
        </button>

        <h2>Jobs</h2>
        <div className="jobs">
          {jobs.map((j) => (
            <div key={j.id} className={"job " + j.status}
              onClick={() => setOpenJob(openJob === j.id ? null : j.id)}>
              <span className="status">{j.status === "running" ? "⏳" : j.status === "done" ? "✅" : j.status === "error" ? "❌" : "🕐"}</span>
              <span>{j.operator}</span>
              <span className="dim">{j.id}</span>
              {openJob === j.id && (
                <pre className="log">{j.error ? `ERROR: ${j.error}\n` : ""}{j.log || "(no output yet)"}</pre>
              )}
            </div>
          ))}
          {!jobs.length && <p className="dim">No jobs yet.</p>}
        </div>
      </aside>
    </div>
  );
}
