/* Knowledge — biblioteca para subir libros / artículos / apuntes que el coach
 * usa como referencia al generar planes.
 *
 * Backend: /knowledge/documents (POST upload multipart, GET list, DELETE).
 * El parser extrae texto (PDF / Markdown / .txt / EPUB), lo parte en chunks y
 * los embebe contra el LLM activo. Cuando el coach genera un plan, el módulo
 * `plan_evidence.build_references_block` consulta el vector store y le pasa
 * los hits relevantes al LLM con instrucción de citar por título.
 */

const SUPPORTED_EXT = [".pdf", ".md", ".markdown", ".txt", ".epub"];
const KIND_OPTIONS = [
  { value: "book", label: "Libro" },
  { value: "article", label: "Artículo / paper" },
  { value: "notes", label: "Apuntes" },
];

/* ── Global upload queue singleton ──
 *
 * Vive fuera de React así sobrevive a navegaciones. Cuando el user sale de
 * Biblioteca durante un upload, el drain loop sigue corriendo (la promise
 * del fetch no depende del componente). Cuando vuelve, se re-suscribe y ve
 * el estado actual.
 *
 * API:
 *   - getUploadQueue()                  → singleton.
 *   - queue.subscribe(listener)         → re-render trigger; devuelve unsub.
 *   - queue.snapshot()                  → array inmutable de items para render.
 *   - queue.enqueue(files, kind)        → encolar varios + arrancar drain.
 *   - queue.removeItem(id)              → remueve done/error/pending.
 *   - queue.clear()                     → limpia los terminales (no in-flight).
 *   - queue.setOnDocComplete(fn)        → fn() corre tras cada upload exitoso.
 *
 * Item shape: {id, file, title, kind, status, error?, startedAt?, durationMs?}.
 */
function getUploadQueue() {
  if (typeof window === "undefined") return null;
  if (window.__af_uploadQueue__) return window.__af_uploadQueue__;

  const state = {
    items: [],
    listeners: new Set(),
    draining: false,
    onDocComplete: null,
  };

  const notify = () => {
    for (const fn of state.listeners) {
      try { fn(); } catch (_) { /* listener crash no debe matar el queue */ }
    }
  };

  const updateItem = (id, patch) => {
    state.items = state.items.map((x) => (x.id === id ? { ...x, ...patch } : x));
    notify();
  };

  const drainNext = async () => {
    if (state.draining) return;
    const next = state.items.find((q) => q.status === "pending");
    if (!next) return;
    state.draining = true;
    notify();
    const startedAt = Date.now();
    updateItem(next.id, { status: "uploading", startedAt });
    try {
      if (window.API && window.API.demoMode && window.API.demoMode()) {
        await new Promise((r) => setTimeout(r, 1200));
      } else {
        await window.API.uploadDocument({ file: next.file, title: next.title.trim() || next.file.name, kind: next.kind });
      }
      updateItem(next.id, { status: "done", durationMs: Date.now() - startedAt });
      if (typeof state.onDocComplete === "function") {
        try { await state.onDocComplete(); } catch (_) {}
      }
    } catch (e) {
      let msg;
      if (e.status === 412) msg = "Falta configurar un LLM en Configuración → LLM.";
      else if (e.status === 413) msg = "Archivo demasiado grande (máx 50 MB).";
      else if (e.status === 415) msg = "Formato no soportado.";
      else msg = e.detail?.detail || e.message || "No se pudo subir.";
      updateItem(next.id, { status: "error", error: msg, durationMs: Date.now() - startedAt });
    } finally {
      state.draining = false;
      notify();
      // Cola en serie: si quedan pendientes, encadenamos en el próximo tick.
      if (state.items.some((q) => q.status === "pending")) {
        setTimeout(drainNext, 50);
      }
    }
  };

  const queue = {
    subscribe(fn) {
      state.listeners.add(fn);
      return () => state.listeners.delete(fn);
    },
    snapshot() {
      return state.items.slice();
    },
    enqueue(files, kind) {
      if (!files || files.length === 0) return;
      const additions = [];
      for (const f of files) {
        const lc = f.name.toLowerCase();
        if (!SUPPORTED_EXT.some((ext) => lc.endsWith(ext))) {
          additions.push({
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            file: f, title: f.name.replace(/\.[^.]+$/, ""), kind, status: "error",
            error: `Formato no soportado: ${f.name}`,
          });
          continue;
        }
        additions.push({
          id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
          file: f, title: f.name.replace(/\.[^.]+$/, ""), kind, status: "pending",
        });
      }
      state.items = [...state.items, ...additions];
      notify();
      drainNext();
    },
    removeItem(id) {
      // Nunca remover un upload en curso — esperá a que termine.
      state.items = state.items.filter((x) => !(x.id === id && x.status !== "uploading"));
      notify();
    },
    clear() {
      state.items = state.items.filter((x) => x.status === "uploading" || x.status === "pending");
      notify();
    },
    setOnDocComplete(fn) {
      state.onDocComplete = fn;
    },
    isBusy() {
      return state.items.some((x) => x.status === "uploading" || x.status === "pending");
    },
    activeCount() {
      return state.items.filter((x) => x.status === "uploading" || x.status === "pending").length;
    },
  };

  window.__af_uploadQueue__ = queue;
  return queue;
}

/* React hook que sigue al singleton: re-renderiza cuando cambia el estado. */
function useUploadQueue() {
  const queue = React.useMemo(() => getUploadQueue(), []);
  const [, setTick] = React.useState(0);
  React.useEffect(() => {
    if (!queue) return undefined;
    return queue.subscribe(() => setTick((t) => t + 1));
  }, [queue]);
  return queue;
}

const Knowledge = ({ inDemo }) => {
  const [docs, setDocs] = React.useState(null);   // null=loading, []=empty
  const [err, setErr] = React.useState(null);

  const refresh = React.useCallback(async () => {
    if (inDemo) {
      // Demo no toca el backend — mostramos un par de ejemplos visualmente.
      setDocs([
        { id: "book:demo_daniels", title: "Daniels Running Formula (demo)", kind: "book", chunks: 142, bytes_size: 4200000, uploaded_at: new Date().toISOString() },
      ]);
      return;
    }
    setErr(null);
    try {
      const res = await API.listDocuments();
      const list = Array.isArray(res?.documents) ? res.documents : [];
      setDocs(list);
    } catch (e) {
      if (e.status === 401 || e.status === 403) return;
      setErr(e.message || "No se pudo cargar la biblioteca.");
      setDocs([]);
    }
  }, [inDemo]);

  React.useEffect(() => { refresh(); }, [refresh]);

  // Cuando un upload termina, el singleton dispara `onDocComplete` para que
  // refresquemos la lista. Re-registramos el callback en cada mount porque
  // la closure captura el `refresh` del render actual.
  React.useEffect(() => {
    const queue = getUploadQueue();
    if (!queue) return undefined;
    queue.setOnDocComplete(refresh);
    return () => { queue.setOnDocComplete(null); };
  }, [refresh]);

  return (
    <div className="af-padded" style={{ padding: "var(--sp-6) var(--sp-8) 80px", display: "flex", flexDirection: "column", gap: 18 }}>
      <div>
        <div className="eyebrow" style={{ marginBottom: 4 }}>BIBLIOTECA</div>
        <h2 className="display" style={{ margin: 0, fontSize: 28, fontWeight: 400, letterSpacing: "-0.02em" }}>
          Conocimiento que alimenta tu coach.
        </h2>
        <div style={{ marginTop: 6, fontSize: 13, color: "var(--text-2)", maxWidth: 720, lineHeight: 1.55 }}>
          Subí libros (Daniels, Pfitzinger, Friel…), artículos o tus propios apuntes en PDF, Markdown, .txt o EPUB.
          Cuando el coach genere un plan, va a buscar fragmentos relevantes y los va a citar por título en la justificación de cada sesión.
        </div>
      </div>

      {err && (
        <div style={{
          padding: "10px 12px",
          background: "var(--bad-soft)",
          border: "1px solid color-mix(in srgb, var(--bad) 30%, transparent)",
          borderRadius: "var(--r-2)", fontSize: 12, color: "var(--text-2)",
        }}>
          <strong style={{ color: "var(--bad)" }}>Error:</strong> {err}
        </div>
      )}

      <UploadCard inDemo={inDemo} />

      <DocumentsList docs={docs} inDemo={inDemo} onChanged={refresh} />
    </div>
  );
};

/* ───────── UploadCard ─────────
 *
 * Lee de `getUploadQueue()` (singleton fuera de React). Si el usuario navega
 * a Hoy/Métricas/etc. mientras un libro se procesa, vuelve a Biblioteca y la
 * fila sigue ahí con su estado actual (`uploading · 47s`). El tick de un
 * intervalo refresca el contador mientras haya un archivo en proceso.
 */
const UploadCard = ({ inDemo }) => {
  const queue = useUploadQueue();
  const items = queue ? queue.snapshot() : [];
  const [kind, setKind] = React.useState("book");
  const fileRef = React.useRef(null);

  // Tick para que el contador "procesando · Ns" avance sin esperar a un
  // cambio de estado del singleton. Lo paramos cuando no hay nada en curso.
  const [, setTick] = React.useState(0);
  const isBusy = queue ? queue.isBusy() : false;
  React.useEffect(() => {
    if (!isBusy) return undefined;
    const id = setInterval(() => setTick((t) => t + 1), 500);
    return () => clearInterval(id);
  }, [isBusy]);

  const enqueue = (filesList) => {
    if (!queue) return;
    queue.enqueue(Array.from(filesList || []), kind);
    if (fileRef.current) fileRef.current.value = "";
  };

  const inFlight = items.find((q) => q.status === "uploading");
  const pendingCount = items.filter((q) => q.status === "pending").length;
  const doneCount = items.filter((q) => q.status === "done").length;
  const errorCount = items.filter((q) => q.status === "error").length;

  return (
    <Card>
      <div style={{ display: "flex", alignItems: "center", marginBottom: 12 }}>
        <div className="eyebrow">SUBIR DOCUMENTOS</div>
        <div style={{ flex: 1 }} />
        {(doneCount > 0 || errorCount > 0) && !inFlight && pendingCount === 0 && queue && (
          <Btn size="sm" variant="ghost" onClick={() => queue.clear()}>
            Limpiar cola
          </Btn>
        )}
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1.5fr 1fr auto", gap: 12, alignItems: "end" }}>
        <div>
          <div className="eyebrow" style={{ fontSize: 9.5, marginBottom: 4 }}>ARCHIVOS (uno o varios)</div>
          <input
            ref={fileRef}
            type="file"
            accept={SUPPORTED_EXT.join(",")}
            multiple
            onChange={(e) => enqueue(e.target.files)}
            style={{
              width: "100%",
              padding: "8px 10px",
              background: "var(--bg-2)",
              border: "1px solid var(--line)",
              borderRadius: "var(--r-2)",
              fontSize: 12,
              color: "var(--text)",
            }}
          />
        </div>
        <div>
          <div className="eyebrow" style={{ fontSize: 9.5, marginBottom: 4 }}>TIPO</div>
          <select
            value={kind}
            onChange={(e) => setKind(e.target.value)}
            style={{
              width: "100%",
              padding: "8px 10px",
              background: "var(--bg-2)",
              border: "1px solid var(--line)",
              borderRadius: "var(--r-2)",
              fontSize: 13,
              color: "var(--text)",
            }}
          >
            {KIND_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
          </select>
        </div>
        <div style={{ fontSize: 11, color: "var(--text-3)", whiteSpace: "nowrap" }}>
          {items.length === 0 ? "elegí archivos" : `${doneCount}/${items.length} listos`}
        </div>
      </div>

      {items.length > 0 && (
        <div style={{ marginTop: 14, display: "flex", flexDirection: "column", gap: 6 }}>
          {items.map((it) => (
            <UploadQueueRow
              key={it.id}
              item={it}
              elapsedSec={it.id === inFlight?.id && it.startedAt ? Math.floor((Date.now() - it.startedAt) / 1000) : null}
              onRemove={() => queue && queue.removeItem(it.id)}
            />
          ))}
        </div>
      )}

      <div style={{ marginTop: 14, fontSize: 11, color: "var(--text-3)", lineHeight: 1.5 }}>
        Formatos soportados: <code>{SUPPORTED_EXT.join(", ")}</code>. Tope: 50 MB cada uno. Procesamos un archivo a la vez para no saturar el rate-limit del LLM.
        Un libro de ~250 chunks puede tardar 30–90 s mientras se generan los embeddings. Podés cambiar de pantalla durante el proceso — al volver, la cola sigue acá.
      </div>
    </Card>
  );
};

/* UploadQueueRow — fila por archivo: status + barra animada cuando upload */
const UploadQueueRow = ({ item, elapsedSec, onRemove }) => {
  const sizeMb = item.file ? (item.file.size / (1024 * 1024)).toFixed(1) : "?";
  const statusColor = {
    pending: "var(--text-3)",
    uploading: "var(--info)",
    done: "var(--good)",
    error: "var(--bad)",
  }[item.status] || "var(--text-3)";
  const statusLabel = {
    pending: "en cola",
    uploading: elapsedSec != null ? `procesando · ${elapsedSec}s` : "procesando",
    done: item.durationMs ? `listo · ${(item.durationMs / 1000).toFixed(1)}s` : "listo",
    error: "error",
  }[item.status] || item.status;

  return (
    <div style={{
      display: "grid", gridTemplateColumns: "1fr auto auto",
      gap: 12, alignItems: "center",
      padding: "8px 12px",
      background: "var(--bg-2)",
      border: "1px solid var(--line-soft)",
      borderRadius: "var(--r-2)",
    }}>
      <div style={{ minWidth: 0 }}>
        <div style={{ fontSize: 12.5, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
          {item.file?.name || item.title}
        </div>
        <div style={{ fontSize: 10.5, color: "var(--text-3)", fontFamily: "var(--font-mono)", marginTop: 2 }}>
          {sizeMb} MB · {item.kind}
          {item.error && <span style={{ color: "var(--bad)", marginLeft: 8 }}>· {item.error}</span>}
        </div>
        {item.status === "uploading" && (
          <div style={{ marginTop: 6, height: 3, background: "var(--line-soft)", borderRadius: 2, overflow: "hidden" }}>
            <div style={{
              width: "30%", height: "100%", background: "var(--info)",
              animation: "af-indeterminate 1.6s ease-in-out infinite",
            }} />
          </div>
        )}
      </div>
      <span style={{ fontSize: 10.5, color: statusColor, fontFamily: "var(--font-mono)", letterSpacing: 0.06, textTransform: "uppercase" }}>
        {statusLabel}
      </span>
      {item.status !== "uploading" && (
        <Btn size="sm" variant="ghost" onClick={onRemove}>
          {item.status === "pending" ? "Cancelar" : "✕"}
        </Btn>
      )}
    </div>
  );
};

/* ───────── DocumentsList ───────── */

const DocumentsList = ({ docs, inDemo, onChanged }) => {
  if (docs === null) {
    return (
      <Card>
        <div style={{ padding: "20px 0", textAlign: "center", color: "var(--text-3)", fontFamily: "var(--font-mono)", fontSize: 12 }}>
          cargando biblioteca…
        </div>
      </Card>
    );
  }
  if (docs.length === 0) {
    return (
      <Card>
        <div style={{ padding: "30px 10px", textAlign: "center", fontSize: 12.5, color: "var(--text-3)", fontFamily: "var(--font-mono)" }}>
          Sin documentos indexados todavía.<br />
          <span style={{ color: "var(--text-4)", fontSize: 10.5, marginTop: 6, display: "inline-block" }}>
            Subí tu primer libro arriba — el coach lo va a citar en el próximo plan.
          </span>
        </div>
      </Card>
    );
  }
  return (
    <Card padded={false}>
      <div style={{ padding: "12px 18px", borderBottom: "1px solid var(--line-soft)", display: "flex", alignItems: "center", gap: 10 }}>
        <div className="eyebrow">DOCUMENTOS INDEXADOS</div>
        <div style={{ flex: 1 }} />
        <span style={{ fontSize: 11, color: "var(--text-3)", fontFamily: "var(--font-mono)" }}>
          {docs.length} doc{docs.length === 1 ? "" : "s"} · {docs.reduce((s, d) => s + (d.chunks || 0), 0)} chunks totales
        </span>
      </div>
      <div>
        {docs.map((d, i) => (
          <DocumentRow
            key={d.id}
            doc={d}
            inDemo={inDemo}
            onChanged={onChanged}
            last={i === docs.length - 1}
          />
        ))}
      </div>
    </Card>
  );
};

const DocumentRow = ({ doc, inDemo, onChanged, last }) => {
  const [deleting, setDeleting] = React.useState(false);
  const [err, setErr] = React.useState(null);

  const sizeMb = doc.bytes_size != null ? (doc.bytes_size / (1024 * 1024)).toFixed(1) : null;
  const uploaded = doc.uploaded_at ? doc.uploaded_at.slice(0, 10) : "—";

  const remove = async () => {
    if (!confirm(`¿Eliminar "${doc.title}"? Esto borra el manifiesto y todos los chunks indexados.`)) return;
    setDeleting(true); setErr(null);
    try {
      if (inDemo) {
        await new Promise((r) => setTimeout(r, 400));
      } else {
        await API.deleteDocument(doc.id);
      }
      await onChanged();
    } catch (e) {
      setErr(e.message || "No se pudo eliminar.");
      setDeleting(false);
    }
  };

  return (
    <div style={{
      display: "grid",
      gridTemplateColumns: "auto 1fr auto auto auto",
      gap: 16,
      alignItems: "center",
      padding: "14px 18px",
      borderBottom: last ? "none" : "1px solid var(--line-soft)",
      fontSize: 12.5,
    }}>
      <div style={{
        width: 32, height: 32, borderRadius: "var(--r-2)",
        background: "var(--bg-2)", border: "1px solid var(--line-soft)",
        display: "grid", placeItems: "center",
      }}>
        <Icon name="book" size={14} color="var(--text-2)" />
      </div>
      <div>
        <div style={{ fontWeight: 500 }}>{doc.title}</div>
        <div style={{ fontSize: 10.5, color: "var(--text-3)", fontFamily: "var(--font-mono)", marginTop: 2 }}>
          {doc.id} · {doc.kind}
        </div>
        {err && <div style={{ marginTop: 4, fontSize: 11, color: "var(--bad)" }}>{err}</div>}
      </div>
      <Badge tone="default">{doc.chunks ?? 0} chunks</Badge>
      <span style={{ fontSize: 11, color: "var(--text-3)", fontFamily: "var(--font-mono)" }}>
        {uploaded}{sizeMb ? ` · ${sizeMb} MB` : ""}
      </span>
      <Btn size="sm" variant="ghost" onClick={remove} disabled={deleting}>
        {deleting ? "…" : "Eliminar"}
      </Btn>
    </div>
  );
};

window.Knowledge = Knowledge;
