// Live data loader. Replaces the static data.jsx + model-catalog.jsx.
// 1. Fetches /state for the initial snapshot.
// 2. Subscribes to /events SSE and merges diffs.
// 3. Mutates window.{AGENTS,COLUMNS,PROJECTS,ACTIVITY,MODEL_CATALOG,AGENT_INSTRUCTIONS}
//    in place and dispatches `cstl:update` so React re-renders.

window.AGENTS = window.AGENTS || {};
window.COLUMNS = [
  { id: "backlog", title: "Backlog" },
  { id: "queued", title: "Queued" },
  { id: "in_progress", title: "In Progress" },
  { id: "review", title: "Needs decision" },
  { id: "done", title: "Done" },
  { id: "failed", title: "Failed" },
];
window.PROJECTS = window.PROJECTS || [];
window.ACTIVITY = window.ACTIVITY || [];
window.MODEL_CATALOG = window.MODEL_CATALOG || [];
window.AGENT_INSTRUCTIONS = window.AGENT_INSTRUCTIONS || {};
window.PENDING_INPUT = window.PENDING_INPUT || [];
window.FINDINGS = window.FINDINGS || [];
window.CURRENT_PROJECT_ID = window.CURRENT_PROJECT_ID || null;

// Markdown helper (uses marked + DOMPurify if loaded; falls back to plain text)
window.renderMarkdown = function (text) {
  if (!text) return { __html: "" };
  if (window.marked && window.DOMPurify) {
    try {
      const html = window.marked.parse(String(text), { breaks: true, gfm: true });
      return { __html: window.DOMPurify.sanitize(html) };
    } catch {}
  }
  // very light fallback
  const escaped = String(text).replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" }[c]));
  return { __html: escaped.replace(/\n/g, "<br/>") };
};

window.api = {
  async respondInput(request_id, { approved = true, value = "" } = {}) {
    return fetch("/input", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ request_id, approved, value }),
    }).then((r) => r.json());
  },
  async configureAgent(name, body) {
    return fetch(`/agents/${name}/configure`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    }).then((r) => r.json());
  },
  async commentTask(task_id, text) {
    return fetch(`/tasks/${task_id}/comment`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text }),
    }).then((r) => r.json());
  },
  async patchTask(task_id, body) {
    return fetch(`/tasks/${task_id}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    }).then((r) => r.json());
  },
  async deleteTask(task_id) {
    return fetch(`/tasks/${task_id}`, { method: "DELETE" }).then((r) => r.json());
  },
  async moveTask(task_id, column) {
    return fetch(`/tasks/${task_id}/move`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ column }),
    }).then((r) => r.json());
  },
  async forkTask(task_id) {
    return fetch(`/tasks/${task_id}/fork`, { method: "POST" }).then((r) => r.json());
  },
  async shareProject(project_id, public_) {
    return fetch(`/projects/${project_id}/share`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ public: public_ }),
    }).then((r) => r.json());
  },
  async createProject(name, kind) {
    return fetch("/projects", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name, kind: kind || "research" }),
    }).then((r) => r.json());
  },
  async deleteProject(project_id) {
    return fetch(`/projects/${project_id}`, { method: "DELETE" }).then((r) => r.json());
  },
  async createTask({ project_id, title, description, assignee, column }) {
    return fetch("/tasks", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ project_id, title, description, assignee, column: column || "backlog" }),
    }).then((r) => r.json());
  },
  async messages(task_id) {
    return fetch(`/tasks/${task_id}/messages`).then((r) => r.json());
  },
};

const dispatch = () => window.dispatchEvent(new CustomEvent("cstl:update"));

function applyState(s) {
  if (s.agents) window.AGENTS = { ...window.AGENTS, ...s.agents, you: window.AGENTS.you || { id: "you", name: "You", role: "", hue: 260, kind: "human" } };
  if (s.projects) window.PROJECTS = s.projects;
  if (s.activity) window.ACTIVITY = s.activity;
  if (s.model_catalog) window.MODEL_CATALOG = s.model_catalog;
  if (s.agent_instructions) window.AGENT_INSTRUCTIONS = s.agent_instructions;
  if (s.input_requests) window.PENDING_INPUT = s.input_requests;
  if (s.findings) window.FINDINGS = s.findings;
  if (s.current_project_id) window.CURRENT_PROJECT_ID = s.current_project_id;
  dispatch();
}

async function fetchState() {
  try {
    const r = await fetch("/state");
    if (r.ok) {
      const s = await r.json();
      applyState(s);
    }
  } catch (e) {
    console.warn("state fetch failed", e);
  }
}

function applyDiff(diff) {
  switch (diff.type) {
    case "input_request": {
      const r = diff.request;
      window.PENDING_INPUT = [...window.PENDING_INPUT.filter((p) => p.request_id !== r.request_id), r];
      // also flag matching task as needing a decision
      for (const p of window.PROJECTS) {
        for (const t of p.tasks) {
          if (t.column === "in_progress") {
            t.needs_decision = true;
            t.decision_prompt = r.prompt;
            t.decision_options = r.choices;
            t.decision_request_id = r.request_id;
            break;
          }
        }
      }
      break;
    }
    case "input_resolved": {
      window.PENDING_INPUT = window.PENDING_INPUT.filter((p) => p.request_id !== diff.request_id);
      for (const p of window.PROJECTS) {
        for (const t of p.tasks) {
          if (t.decision_request_id === diff.request_id) {
            t.needs_decision = false;
            t.decision_request_id = null;
          }
        }
      }
      break;
    }
    case "activity": {
      window.ACTIVITY = [diff.item, ...window.ACTIVITY].slice(0, 50);
      break;
    }
    case "task_cot_append_unbound": {
      // Best-effort: append to the most recently updated in-progress task assigned to that agent.
      const agent = diff.agent;
      let target = null;
      for (const p of window.PROJECTS) {
        for (const t of p.tasks) {
          if (t.assignee === agent && (t.column === "in_progress" || t.column === "review")) {
            if (!target || (t.updated_at || 0) > (target.updated_at || 0)) target = t;
          }
        }
      }
      if (target) {
        target.cot = [...(target.cot || []), diff.step];
        target.updated_at = Date.now() / 1000;
      }
      break;
    }
    case "db_task":
    case "db_entry":
    case "db_agent_stats":
      // Pull the full state on database notifications. Cheaper than fine-grained diffing
      // and keeps multiple browser tabs / workers consistent.
      fetchState();
      return;
    case "agent_upsert": {
      if (diff.agent && diff.agent.id) window.AGENTS[diff.agent.id] = { ...window.AGENTS[diff.agent.id], ...diff.agent };
      break;
    }
    case "project_upsert": {
      const p = window.PROJECTS.find((x) => x.id === diff.project_id);
      if (p && diff.visibility) p.visibility = diff.visibility;
      // also fetch full state so newly-created projects appear immediately
      fetchState();
      return;
    }
    case "project_delete": {
      window.PROJECTS = window.PROJECTS.filter((p) => p.id !== diff.project_id);
      window.FINDINGS = (window.FINDINGS || []).filter((f) => f.project_id !== diff.project_id);
      window.ACTIVITY = (window.ACTIVITY || []).filter((a) => a.project_id !== diff.project_id);
      break;
    }
    case "task_delete": {
      for (const p of window.PROJECTS) {
        p.tasks = (p.tasks || []).filter((t) => t.id !== diff.task_id);
      }
      break;
    }
    default:
      break;
  }
  dispatch();
}

function startSSE() {
  const es = new EventSource("/events");
  es.onmessage = (e) => {
    try {
      const diff = JSON.parse(e.data);
      applyDiff(diff);
    } catch {}
  };
  es.onerror = () => {
    // browser will retry on its own
  };
}

(async () => {
  await fetchState();
  startSSE();
  // periodic safety net
  setInterval(fetchState, 15000);
})();
