// Jobs + Kanban board — wired to HERMES (jobs/cron + claude-tasks). Jobs keep a
// standalone design preview; Tasks fail visibly instead of substituting sample
// cards that could be mistaken for imported GitHub issues.
const { useState, useEffect, useCallback, useRef } = React;

// ---- Jobs ---------------------------------------------------------------

// Normalize an upstream job (gateway/dashboard /api/cron/jobs shape is loose) to
// the row shape the design renders: {id, name, status, started, progress, agent,
// logs, enabled, raw}. Guard every field — never crash on a missing one.
function normalizeJob(j) {
  const id = j.id != null ? String(j.id) : (j.jobId != null ? String(j.jobId) : String(j.name || Math.random().toString(36).slice(2)));
  const name = j.name || j.title || j.description || j.command || id;
  const enabled = j.enabled != null ? !!j.enabled : (j.disabled != null ? !j.disabled : true);
  // derive a status from whatever the backend gives us
  let status = (j.status || j.state || j.lastStatus || j.last_status || "").toString().toLowerCase();
  if (status === "success" || status === "succeeded" || status === "completed" || status === "ok") status = "done";
  if (status === "error" || status === "failure") status = "failed";
  if (status === "active" || status === "in_progress") status = "running";
  if (!["running", "queued", "done", "failed"].includes(status)) status = "queued";
  const progress = typeof j.progress === "number" ? Math.max(0, Math.min(1, j.progress)) : (status === "done" ? 1 : status === "running" ? 0.5 : 0);
  const started = j.started || j.lastRun || j.last_run || j.lastRunAt || j.schedule || j.cron || "—";
  const logs = j.logs != null ? j.logs : (j.logLines != null ? j.logLines : (j.runs != null ? j.runs : 0));
  const agent = j.agent || j.assignee || j.owner || "nova";
  return { id, name, status, started: String(started), progress, agent, logs, enabled, raw: j };
}

function PageJobs({ toast }) {
  const [jobs, setJobs] = useState(null); // null = loading
  const [live, setLive] = useState(null); // null=loading, true=backend, false=offline
  const [busy, setBusy] = useState(null); // id of a job mid-action

  const load = useCallback(async () => {
    try {
      const res = await HERMES.listJobs();
      const list = Array.isArray(res) ? res : (res?.jobs || []);
      setJobs(list.map(normalizeJob));
      setLive(true);
    } catch {
      setJobs((window.DATA.jobs || []).map((j) => ({ ...j, enabled: j.status !== "failed" })));
      setLive(false);
    }
  }, []);
  useEffect(() => { load(); }, [load]);

  const rows = jobs || [];
  const tone = { running: "warn", queued: "", done: "ok", failed: "bad" };
  const counts = {
    running: rows.filter((j) => j.status === "running").length,
    done: rows.filter((j) => j.status === "done").length,
  };

  const run = async (j) => {
    if (!live) { toast("Connect Hermes to run jobs"); return; }
    setBusy(j.id);
    try {
      await HERMES.runJob(j.id);
      toast("Started " + j.name);
      await load();
    } catch (e) { toast(e.message); }
    setBusy(null);
  };

  const toggle = async (j) => {
    const next = !j.enabled;
    // optimistic
    setJobs((js) => (js || []).map((x) => x.id === j.id ? { ...x, enabled: next } : x));
    if (!live) { toast(next ? "Enabled (preview)" : "Disabled (preview)"); return; }
    try {
      await HERMES.toggleJob(j.id, next);
      toast(next ? "Enabled " + j.name : "Disabled " + j.name);
    } catch (e) {
      setJobs((js) => (js || []).map((x) => x.id === j.id ? { ...x, enabled: !next } : x)); // revert
      toast(e.message);
    }
  };

  return (
    <div className="page">
      <PageHead eyebrow="Runtime" title="Jobs" sub="Background runs spawned by agents — tests, indexing, deploys, scrapes.">
        {live === true && <span className="badge ok"><span className="d" />live</span>}
        {live === false && <span className="badge warn"><span className="d" />preview data</span>}
        {live === null && <span className="badge"><span className="d" />loading…</span>}
        <span className="badge warn"><span className="d" />{counts.running} running</span>
        <button className="btn btn-ghost" onClick={() => { load(); toast("Queue refreshed"); }}><Icon name="refresh" size={16} />Refresh</button>
      </PageHead>
      <div className="card">
        <div className="card-head"><div><h2>Run queue</h2><p>{rows.length} jobs · {counts.done} completed today</p></div></div>
        <div className="rows">
          {jobs === null && <div className="empty">Loading jobs…</div>}
          {jobs !== null && !rows.length && <div className="empty">No jobs yet.</div>}
          {rows.map((j) => {
            const a = agentById(j.agent);
            return (
              <div className="job-row" key={j.id} style={{ opacity: j.enabled === false ? 0.55 : 1 }}>
                <div className="flex" style={{ gap: 11, minWidth: 0 }}>
                  <AgentBot agent={a} size={30} />
                  <div style={{ minWidth: 0 }}>
                    <div style={{ fontSize: 13.5, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{j.name}</div>
                    <div className="row-sub mono"><span>{a.name}</span><span>·</span><span>{j.started}</span><span>·</span><span>{j.logs} log lines</span></div>
                  </div>
                </div>
                <div>{j.status === "running" ? <Meter value={j.progress} tone="warn" /> : j.status === "failed" ? <Meter value={j.progress} tone="warn" /> : <Meter value={j.progress} tone="ok" />}</div>
                <span className={cls("badge", tone[j.status])}><StatusDot status={j.status === "running" ? "provisioning" : j.status === "failed" ? "attention" : j.status === "done" ? "running" : "idle"} live={j.status === "running"} />{j.status}</span>
                <div className="flex" style={{ gap: 8, justifyContent: "flex-end", alignItems: "center" }}>
                  <Toggle on={j.enabled !== false} onChange={() => toggle(j)} />
                  <button className="btn btn-subtle btn-sm" disabled={busy === j.id} onClick={() => run(j)} title="Run now"><Icon name={busy === j.id ? "refresh" : "play"} size={13} /></button>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}
window.PageJobs = PageJobs;

// ---- Kanban -------------------------------------------------------------

// Spec columns (router:58711) — order matters for the board left→right.
const KANBAN_COLS = [
  { id: "triage", name: "Triage", dot: "var(--faint)" },
  { id: "todo", name: "To do", dot: "#4d8bf5" },
  { id: "scheduled", name: "Scheduled", dot: "#7d8590" },
  { id: "ready", name: "Ready", dot: "#36a3ff" },
  { id: "running", name: "Running", dot: "#f0883e" },
  { id: "review", name: "Review", dot: "#a06cf0" },
  { id: "blocked", name: "Blocked", dot: "#f0617b" },
  { id: "done", name: "Done", dot: "var(--success)" },
];

// Hermes needs granular states for scheduling and dispatch. The board groups
// those states into five human-scale lanes so the workflow fits in one view.
const KANBAN_LANES = [
  {
    id: "queue",
    name: "Queue",
    note: "Triage and to do",
    dot: "#4d8bf5",
    statuses: ["triage", "todo"],
    target: "todo",
    createIn: "triage",
  },
  {
    id: "planned",
    name: "Planned",
    note: "Scheduled or ready",
    dot: "#36a3ff",
    statuses: ["scheduled", "ready"],
    target: "ready",
    createIn: "ready",
  },
  {
    id: "active",
    name: "Active",
    note: "Dispatcher controlled",
    dot: "#f0883e",
    statuses: ["running"],
    target: null,
    createIn: null,
  },
  {
    id: "review",
    name: "Review",
    note: "Needs verification",
    dot: "#a06cf0",
    statuses: ["review"],
    target: "review",
    createIn: "review",
  },
  {
    id: "blocked",
    name: "Blocked",
    note: "Needs intervention",
    dot: "#f0617b",
    statuses: ["blocked"],
    target: "blocked",
    createIn: "blocked",
  },
];

// Normalize an upstream task to the card shape the board renders.
function normalizeTask(t) {
  const id = t.id != null ? String(t.id) : String(t.taskId || Math.random().toString(36).slice(2));
  const col = (t.status || t.column || t.col || "triage").toString();
  const rawPriority = Number(t.priority);
  const priority = Number.isFinite(rawPriority)
    ? (rawPriority >= 10 ? "high" : rawPriority <= -10 ? "low" : "med")
    : (t.priority || "med").toString();
  return {
    id,
    title: t.title || t.name || "(untitled)",
    body: t.description || t.body || "",
    col: KANBAN_COLS.some((c) => c.id === col) ? col : "triage",
    assignee: t.assignee || null,
    agent: t.agent || t.assignee_agent || "nova",
    priority: priority === "medium" ? "med" : priority,
    repo: t.repo || (Array.isArray(t.tags) ? t.tags[0] : t.tags) || "",
    number: t.number != null ? t.number : (t.issueNumber != null ? t.issueNumber : null),
    url: t.url || t.html_url || t.issue_url || t.issueUrl || "",
    createdAt: t.created_at || t.createdAt || "",
    updatedAt: t.updated_at || t.updatedAt || "",
  };
}

// ISO/epoch/anything Date can parse -> a short readable string. Returns "" if
// the value is empty or unparseable so callers can decide whether to render it.
function fmtWhen(v) {
  if (!v) return "";
  const d = new Date(v);
  if (isNaN(d.getTime())) return String(v);
  return d.toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
}

// Render task body text with line breaks preserved and bare URLs turned into
// links that open in a new tab. Kept intentionally simple (no markdown).
function TaskBodyText({ text }) {
  if (!text) return <span className="faint">No description.</span>;
  const urlRe = /(https?:\/\/[^\s<>()]+)/g;
  // split() with a capturing group interleaves text and URL parts — odd indexes
  // are the captured URLs. (Never re-.test() a /g regex per part: lastIndex
  // carries over between calls and misclassifies alternating parts.)
  const parts = text.split(urlRe);
  return (
    <div style={{ whiteSpace: "pre-wrap", wordBreak: "break-word", lineHeight: 1.55, fontSize: 13.5 }}>
      {parts.map((part, i) => (
        i % 2 === 1
          ? <a key={i} href={part} target="_blank" rel="noopener noreferrer">{part}</a>
          : <React.Fragment key={i}>{part}</React.Fragment>
      ))}
    </div>
  );
}

// Detail modal for a task card — full title/body, status, assignee, source
// link, and timestamps if the backend provided them.
function TaskDetailModal({ task, onClose }) {
  const col = KANBAN_COLS.find((c) => c.id === task.col);
  return (
    <div className="modal-scrim" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal" role="dialog" aria-modal="true">
        <div className="modal-head">
          <div>
            <h2>{task.title}</h2>
            <p className="flex" style={{ gap: 8, flexWrap: "wrap", marginTop: 6 }}>
              <span className="badge"><span style={{ width: 7, height: 7, borderRadius: "50%", background: col ? col.dot : "var(--faint)", display: "inline-block", marginRight: 6 }} />{col ? col.name : task.col}</span>
              <span className="mono faint">{task.assignee || "Unassigned"}</span>
              {task.repo && <span className="tag">{task.repo}{task.number != null ? ` #${task.number}` : ""}</span>}
            </p>
          </div>
          <button className="btn btn-ghost btn-sm x" onClick={onClose}><Icon name="x" size={15} /></button>
        </div>
        <div className="modal-body">
          <div className="field">
            <label>Description</label>
            <TaskBodyText text={task.body} />
          </div>
          {task.url && (
            <div className="field">
              <label>Source</label>
              <a href={task.url} target="_blank" rel="noopener noreferrer" className="flex" style={{ gap: 6, alignItems: "center", fontSize: 13.5 }}>
                <Icon name="external" size={14} />{task.url}
              </a>
            </div>
          )}
          {(task.createdAt || task.updatedAt) && (
            <div className="flex" style={{ gap: 18, flexWrap: "wrap" }}>
              {task.createdAt && <div className="faint mono" style={{ fontSize: 12 }}>Created {fmtWhen(task.createdAt)}</div>}
              {task.updatedAt && <div className="faint mono" style={{ fontSize: 12 }}>Updated {fmtWhen(task.updatedAt)}</div>}
            </div>
          )}
        </div>
        <div className="modal-foot">
          <button className="btn btn-ghost" onClick={onClose}>Close</button>
        </div>
      </div>
    </div>
  );
}

function PageKanban({ toast }) {
  const [tasks, setTasks] = useState(null); // null = loading
  const [live, setLive] = useState(null);
  const [loadError, setLoadError] = useState("");
  const [drag, setDrag] = useState(null);
  const [over, setOver] = useState(null);
  const [showDone, setShowDone] = useState(false);
  const [doneQuery, setDoneQuery] = useState("");
  const [adding, setAdding] = useState(null); // column id we're adding to
  const [newTitle, setNewTitle] = useState("");
  const [detail, setDetail] = useState(null); // task open in the detail modal
  const draggedRef = useRef(false); // suppress the click-to-open right after a drag

  const load = useCallback(async () => {
    setLoadError("");
    try {
      const res = await HERMES.listTasks({ include_done: true });
      const list = Array.isArray(res) ? res : (res?.tasks || []);
      setTasks(list.map(normalizeTask));
      setLive(true);
    } catch (e) {
      setTasks([]);
      setLive(false);
      setLoadError(e?.message || "The live Hermes task API is unavailable.");
    }
  }, []);
  useEffect(() => { load(); }, [load]);
  useEffect(() => {
    if (!showDone) return undefined;
    const closeOnEscape = (event) => {
      if (event.key === "Escape") setShowDone(false);
    };
    window.addEventListener("keydown", closeOnEscape);
    return () => window.removeEventListener("keydown", closeOnEscape);
  }, [showDone]);

  const rows = tasks || [];
  const total = rows.filter((t) => t.col !== "done").length;
  const running = rows.filter((t) => t.col === "running").length;
  const blocked = rows.filter((t) => t.col === "blocked").length;
  const doneRows = rows
    .filter((t) => t.col === "done")
    .sort((a, b) => new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0));
  const normalizedDoneQuery = doneQuery.trim().toLowerCase();
  const visibleDoneRows = normalizedDoneQuery
    ? doneRows.filter((t) => [t.title, t.body, t.assignee, t.repo].some((value) => String(value || "").toLowerCase().includes(normalizedDoneQuery)))
    : doneRows;
  const donePct = rows.length ? Math.round((rows.filter((t) => t.col === "done").length / rows.length) * 100) : 0;
  const prio = { high: "var(--danger)", med: "var(--warn)", low: "var(--faint)" };

  const drop = async (lane) => {
    const id = drag;
    setDrag(null); setOver(null);
    if (!id) return;
    if (!lane.target) { toast("Active work is controlled by the Hermes dispatcher"); return; }
    const task = rows.find((t) => t.id === id);
    const colId = lane.target;
    if (!task || task.col === colId) return;
    const prev = task.col;
    setTasks((ts) => (ts || []).map((t) => t.id === id ? { ...t, col: colId } : t)); // optimistic
    if (!live) { toast("Task API unavailable; nothing was changed"); return; }
    try {
      await HERMES.updateTask(id, { column: colId });
      toast("Moved to " + colId);
    } catch (e) {
      setTasks((ts) => (ts || []).map((t) => t.id === id ? { ...t, col: prev } : t)); // revert
      toast(e.message);
    }
  };

  const startAdd = (colId) => {
    if (!live) { toast("Task API unavailable; new tasks are disabled"); return; }
    if (colId === "running") { toast("Create in Triage, To do, or Ready; the dispatcher controls Running"); return; }
    setAdding(colId); setNewTitle("");
  };
  const submitAdd = async () => {
    const title = newTitle.trim();
    const colId = adding;
    if (!title) { setAdding(null); return; }
    setAdding(null); setNewTitle("");
    if (!live) {
      toast("Task API unavailable; nothing was created");
      return;
    }
    try {
      const res = await HERMES.createTask({ title, column: colId });
      const created = res?.task ? normalizeTask(res.task) : normalizeTask({ id: "tmp-" + Date.now(), title, column: colId });
      setTasks((ts) => [...(ts || []), created]);
      toast("Created task");
    } catch (e) { toast(e.message); }
  };

  return (
    <div className="page">
      <div className="card" style={{ marginBottom: "var(--gap)" }}>
        <div className="card-head" style={{ marginBottom: 0 }}>
          <div>
            <h2 style={{ fontSize: 20 }}>Tasks</h2>
            <p>Five focused lanes summarize the detailed Hermes workflow.</p>
          </div>
          <div className="kanban-actions">
            {live === true && <span className="badge ok"><span className="d" />live</span>}
            {live === false && <span className="badge bad"><span className="d" />task API offline</span>}
            {live === null && <span className="badge"><span className="d" />loading…</span>}
            <button
              className="btn btn-ghost btn-sm"
              aria-expanded={showDone}
              aria-controls="completed-task-archive"
              onClick={() => { setDoneQuery(""); setShowDone(true); }}>
              Completed <span className="btn-count">{doneRows.length}</span>
            </button>
            <button className="icon-btn" style={{ width: 34, height: 34 }} aria-label="Refresh tasks" title="Refresh tasks" onClick={() => { load(); toast("Synced"); }}><Icon name="refresh" size={15} /></button>
            <button className="btn btn-primary btn-sm" disabled={live !== true} onClick={() => startAdd("triage")}><Icon name="plus" size={14} />New task</button>
          </div>
        </div>
        <div className="kanban-overview" aria-label="Task summary">
          <div className="kanban-stat"><strong>{total}</strong><span>Open</span></div>
          <div className="kanban-stat"><strong>{running}</strong><span>Active</span></div>
          <div className={cls("kanban-stat", blocked > 0 && "attention")}><strong>{blocked}</strong><span>Blocked</span></div>
          <div className="kanban-progress">
            <div><span>Overall completion</span><strong>{donePct}%</strong></div>
            <div className="kanban-progress-track" aria-hidden="true"><span style={{ width: `${donePct}%` }} /></div>
          </div>
        </div>
      </div>

      {live === false && (
        <div className="card" style={{ marginBottom: "var(--gap)", borderColor: "var(--danger)" }} role="alert">
          <div className="flex" style={{ alignItems: "flex-start", gap: 10 }}>
            <Icon name="activity" size={17} />
            <div><b>Live tasks could not be loaded.</b><p style={{ marginTop: 4 }}>No preview cards are being shown. Check the Hermes workspace BFF and <span className="mono">/api/claude-tasks</span>, then refresh.</p><small className="faint mono">{loadError}</small></div>
          </div>
        </div>
      )}

      <div className="board">
        {KANBAN_LANES.map((lane) => {
          const items = rows.filter((t) => lane.statuses.includes(t.col));
          return (
            <div key={lane.id} className={cls("kcol", over === lane.id && "over", !lane.target && "locked")}
              onDragOver={(e) => { e.preventDefault(); setOver(lane.id); }}
              onDragLeave={() => setOver((o) => o === lane.id ? null : o)}
              onDrop={() => drop(lane)}>
              <div className="kcol-head">
                <span className="lane-dot" style={{ background: lane.dot }} />
                <span className="lane-title"><b>{lane.name}</b><small>{lane.note}</small></span>
                <span className="cnt">{items.length}</span>
                {lane.createIn && (
                  <button className="add" aria-label={`Add task to ${lane.name}`} title={`Add task to ${lane.name}`} onClick={() => startAdd(lane.createIn)}>
                    <Icon name="plus" size={13} />
                  </button>
                )}
              </div>
              <div className="kcol-body">
                {lane.createIn && adding === lane.createIn && (
                  <div className="kcard">
                    <input className="input" autoFocus value={newTitle} placeholder="Task title…"
                      onChange={(e) => setNewTitle(e.target.value)}
                      onKeyDown={(e) => { if (e.key === "Enter") submitAdd(); if (e.key === "Escape") setAdding(null); }}
                      onBlur={submitAdd} style={{ height: 34, fontSize: 13 }} />
                  </div>
                )}
                {tasks === null && <div className="kempty">Loading…</div>}
                {items.map((t) => {
                  const a = agentById(t.agent);
                  const status = KANBAN_COLS.find((c) => c.id === t.col);
                  return (
                    <div key={t.id} className={cls("kcard", drag === t.id && "dragging")} draggable
                      style={{ "--agent-color": a.color }}
                      onDragStart={() => { draggedRef.current = false; setDrag(t.id); }}
                      onDrag={() => { draggedRef.current = true; }}
                      onDragEnd={() => { setDrag(null); setOver(null); }}
                      onClick={() => { if (draggedRef.current) { draggedRef.current = false; return; } setDetail(t); }}>
                      <div className="kt kt-clamp">{t.title}</div>
                      <div className="kf">
                        <span style={{ width: 7, height: 7, borderRadius: "50%", background: prio[t.priority] || "var(--faint)" }} />
                        <span className="mono">{t.assignee || "Unassigned"}</span>
                        {lane.statuses.length > 1 && <span className="status-chip">{status ? status.name : t.col}</span>}
                        {t.repo && <span style={{ marginLeft: "auto" }} className="tag">{t.repo}{t.number != null ? ` #${t.number}` : ""}</span>}
                      </div>
                    </div>
                  );
                })}
                {tasks !== null && !items.length && (!lane.createIn || adding !== lane.createIn) && (
                  <div className="kempty">{lane.target ? "Drop a task here" : "Waiting for the dispatcher"}</div>
                )}
              </div>
            </div>
          );
        })}
      </div>
      {showDone && (
        <div className="archive-scrim" onMouseDown={(e) => { if (e.target === e.currentTarget) setShowDone(false); }}>
          <section id="completed-task-archive" className="archive-panel" role="dialog" aria-modal="true" aria-labelledby="completed-task-title">
            <div className="archive-head">
              <div>
                <span className="archive-kicker">Task history</span>
                <h2 id="completed-task-title">Completed <span>{doneRows.length}</span></h2>
                <p>Finished work stays searchable without crowding the active board.</p>
              </div>
              <button className="icon-btn" aria-label="Close completed tasks" title="Close" onClick={() => setShowDone(false)}><Icon name="x" size={16} /></button>
            </div>
            <div className="archive-search">
              <Icon name="search" size={15} />
              <input
                autoFocus
                value={doneQuery}
                placeholder="Search completed tasks…"
                aria-label="Search completed tasks"
                onChange={(e) => setDoneQuery(e.target.value)}
              />
              {doneQuery && <button aria-label="Clear search" onClick={() => setDoneQuery("")}><Icon name="x" size={13} /></button>}
            </div>
            <div className="archive-result-count">{visibleDoneRows.length} {visibleDoneRows.length === 1 ? "result" : "results"}</div>
            <div className="archive-list">
              {visibleDoneRows.map((t) => (
                <button key={t.id} className="archive-row" onClick={() => { setShowDone(false); setDetail(t); }}>
                  <span className="archive-row-main">
                    <strong>{t.title}</strong>
                    <small>{t.repo ? `${t.repo}${t.number != null ? ` #${t.number}` : ""}` : t.assignee || "Unassigned"}</small>
                  </span>
                  <span className="archive-row-meta">{fmtWhen(t.updatedAt || t.createdAt) || "Completed"}</span>
                  <Icon name="chevron" size={15} />
                </button>
              ))}
              {!visibleDoneRows.length && <div className="archive-empty">{doneRows.length ? "No completed tasks match that search." : "No completed tasks yet."}</div>}
            </div>
          </section>
        </div>
      )}
      {detail && <TaskDetailModal task={detail} onClose={() => setDetail(null)} />}
    </div>
  );
}
window.PageKanban = PageKanban;
