/* ATLAS — right-side AI agent.
   Tools: set value(s) (auto-applied) + structural ops create/add/update/delete asset,
   input, output (staged in a review card before applying). Can ingest an uploaded .xlsx
   (parsed client-side with SheetJS) and, given a text description of the area, build a new
   asset by translating the spreadsheet formulas. All structural changes flow through
   ATLAS.ops.* so validation lives in one place. */
(function () {
  "use strict";
  const { useEffect, useMemo, useRef, useState } = React;
  const h = React.createElement;
  const MARKDOWN_OPTIONS = { gfm: true, breaks: true };
  const MAX_EXCEL_CELLS = 600;
  const STRUCTURAL = window.ATLAS_OPS ? window.ATLAS_OPS.OP_NAMES : [];
  const VALUE_TOOLS = ["set_calculator_value", "set_calculator_values"];

  const PLANT_PROMPTS = [
    "Analyse this plant's performance and suggest short-term decarbonisation levers",
    "Chart this plant's headline metrics against best-in-class benchmarks",
    "Generate a DOCX performance & decarbonisation report for this plant",
  ];

  // ---- streaming POST to /api/agent (SSE). Callbacks surface the live step trace and the
  //      answer streaming token-by-token; resolves with the canonical {text,toolUses}. ----
  async function streamAgent(payload, handlers) {
    const hx = handlers || {};
    const response = await fetch("/api/agent", {
      method: "POST",
      headers: { "Content-Type": "application/json", "Accept": "text/event-stream" },
      body: JSON.stringify(Object.assign({ stream: true }, payload)),
    });
    if (!response.ok || !response.body) {
      const j = await response.json().catch(() => ({}));
      throw new Error(j.error || "Agent request failed.");
    }
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "", result = null, errored = null;
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      let idx;
      while ((idx = buffer.indexOf("\n\n")) !== -1) {
        const chunk = buffer.slice(0, idx); buffer = buffer.slice(idx + 2);
        let event = "message", data = "";
        chunk.split("\n").forEach((line) => {
          if (line.startsWith("event:")) event = line.slice(6).trim();
          else if (line.startsWith("data:")) data += line.slice(5).trim();
        });
        if (!data) continue;
        let parsed; try { parsed = JSON.parse(data); } catch (e) { continue; }
        if (event === "status" && hx.onStatus) hx.onStatus(parsed);
        else if (event === "step" && hx.onStep) hx.onStep(parsed);
        else if (event === "delta" && hx.onDelta) hx.onDelta(parsed.text || "");
        else if (event === "result") result = parsed;
        else if (event === "error") errored = parsed.error || "Agent error.";
      }
    }
    if (errored) throw new Error(errored);
    return result || { text: "Done.", toolUses: [] };
  }
  const statusLabel = (s) => s && s.stage === "analysing"
    ? "Analysing data…" : s && s.stage === "thinking" ? "Thinking…" : "Working…";

  // Live step trace + streaming answer shown while the agent works.
  function StepTrace({ steps, streamText }) {
    if ((!steps || !steps.length) && !streamText) return null;
    const icon = (k) => k === "thinking" ? "✦" : k === "tool_result" ? "✓" : k === "round" ? "↻" : "→";
    return h("div", { className: "agent-msg assistant" },
      h("div", { className: "agent-role" }, "Agent"),
      h("div", { className: "agent-trace" },
        (steps || []).map((s, i) => h("div", { className: "trace-step " + s.kind, key: i },
          h("span", { className: "trace-ic" }, icon(s.kind)), h("span", null, s.label))),
        streamText ? h("div", { className: "agent-bubble markdown trace-answer", dangerouslySetInnerHTML: renderMarkdownToHtml(streamText) }) : null,
      ),
    );
  }

  function buildInputIndex(model) {
    const rows = {};
    (model.assets || []).forEach((asset) => {
      (asset.inputs || []).forEach((row) => {
        rows[row.id] = {
          id: row.id, label: row.label, unit: row.unit,
          group: row.group, assetId: asset.id, assetName: asset.name,
        };
      });
    });
    return rows;
  }

  // compact schema so the agent knows the namespace + avoids id collisions
  function buildModelSchema(model) {
    return {
      constants: Object.keys(model.constants || {}),
      finalMetrics: (model.finalMetrics || []).map((m) => m.id),
      assets: (model.assets || []).map((a) => ({
        id: a.id, name: a.name, section: a.section,
        inputIds: (a.inputs || []).map((i) => i.id),
        outputs: (a.outputs || []).map((o) => ({ id: o.id, expr: o.expr })),
      })),
    };
  }

  function buildAgentContext({ view, model, inputIndex, assetNodes, processNodes, inputs, derived, excel }) {
    const currentAsset = (view === "plant" || view === "site") ? null : (model.assets || []).find((a) => a.id === view) || null;
    const allNodes = [
      ...assetNodes.map((n) => Object.assign({ section: "plant_assets" }, n)),
      ...processNodes.map((n) => Object.assign({ section: "process" }, n)),
    ];
    const finalMetrics = ATLAS.finalMetricsWithFmt(model);

    return {
      plant: ATLAS.meta,
      screen: (view === "plant" || view === "site") ? "Plant Site" : currentAsset ? currentAsset.name : view,
      currentAsset: currentAsset ? {
        id: currentAsset.id, code: currentAsset.code, name: currentAsset.name,
        subtitle: currentAsset.subtitle, section: currentAsset.section, drives: currentAsset.drives,
      } : null,
      overviewAssets: allNodes.map((node) => ({
        id: node.id, name: node.name, section: node.section,
        active: !!node.active, modelled: !!(model.assets || []).find((a) => a.id === node.id),
        selected: view === node.id,
      })),
      finalMetrics: finalMetrics.map((metric) => ({
        id: metric.id, label: metric.label, unit: metric.unit,
        value: derived._final[metric.id], formatted: metric.fmt(derived._final[metric.id]),
        formula: metric.formula,
      })),
      modelSchema: buildModelSchema(model),
      editableInputs: Object.keys(inputIndex).map((id) => Object.assign({}, inputIndex[id], { value: inputs[id] })),
      calculatedOutputs: (model.assets || []).map((asset) => ({
        assetId: asset.id, assetName: asset.name,
        outputs: (asset.outputs || []).filter((o) => !o.hidden).map((row) => ({
          id: row.id, label: row.label, unit: row.unit,
          value: derived[row.id], formatted: ATLAS.fmtNum(derived[row.id]), formula: row.formulaText,
        })),
      })),
      excel: excel ? { fileName: excel.fileName, truncated: excel.truncated, sheets: excel.sheets } : undefined,
    };
  }

  // ---- value-tool handling (auto-applied) ----
  function valueActionsFromTool(tool, inputIndex) {
    if (!tool || !tool.input) return [];
    if (tool.name === "set_calculator_value") {
      return [{ id: tool.input.id, value: tool.input.value, reason: tool.input.reason || "" }];
    }
    if (tool.name === "set_calculator_values" && Array.isArray(tool.input.changes)) {
      return tool.input.changes.map((c) => ({ id: c.id, value: c.value, reason: c.reason || "" }));
    }
    return [];
  }
  function normalizeValueActions(toolUses, inputIndex) {
    const actions = [];
    (toolUses || []).forEach((tool) => {
      if (VALUE_TOOLS.indexOf(tool.name) === -1) return;
      valueActionsFromTool(tool, inputIndex).forEach((action) => {
        const value = Number(action.value);
        if (inputIndex[action.id] && Number.isFinite(value)) actions.push({ id: action.id, value, reason: action.reason });
      });
    });
    return actions;
  }
  function textFromValueActions(actions, inputIndex, inputs) {
    if (!actions.length) return "";
    return actions.map((action) => {
      const row = inputIndex[action.id];
      const unit = row && row.unit ? " " + row.unit : "";
      const label = row ? row.label : action.id;
      return label + ": " + ATLAS.fmtNum(inputs[action.id]) + " -> " + ATLAS.fmtNum(action.value) + unit;
    }).join("\n");
  }

  // ---- structural-op handling (staged for review, then applied sequentially) ----
  function summarizeOp(tool) {
    const i = tool.input || {};
    switch (tool.name) {
      case "create_asset": return "Create asset '" + i.id + "' (" + (i.name || "") + ") in " + i.section;
      case "delete_asset": return "Delete asset '" + i.id + "'";
      case "add_input": return "Add input '" + i.id + "' to " + i.assetId + (i.value != null ? " = " + i.value : "");
      case "update_input": return "Update input '" + i.id + "'";
      case "delete_input": return "Delete input '" + i.id + "'";
      case "add_output": return "Add output '" + i.id + "' to " + i.assetId;
      case "update_output": return "Update output '" + i.id + "'";
      case "delete_output": return "Delete output '" + i.id + "'";
      default: return tool.name;
    }
  }
  function applyStructuralOps(model, tools) {
    let m = model;
    const applied = [], errors = [];
    tools.forEach((tool) => {
      const r = ATLAS.ops.apply(m, tool.name, tool.input || {});
      if (r.ok) { m = r.model; applied.push(summarizeOp(tool)); }
      else errors.push(summarizeOp(tool) + " — " + r.error);
    });
    return { model: m, applied, errors };
  }

  // ---- Excel parsing (SheetJS) ----
  function parseWorkbook(arrayBuffer, fileName) {
    const wb = window.XLSX.read(arrayBuffer, { type: "array", cellFormula: true, cellNF: false });
    const sheets = [];
    let count = 0, truncated = false;
    for (const name of wb.SheetNames) {
      const ws = wb.Sheets[name];
      const ref = ws["!ref"];
      const cells = [];
      if (ref) {
        const range = window.XLSX.utils.decode_range(ref);
        for (let r = range.s.r; r <= range.e.r && !truncated; r++) {
          for (let c = range.s.c; c <= range.e.c; c++) {
            const addr = window.XLSX.utils.encode_cell({ r, c });
            const cell = ws[addr];
            if (!cell) continue;
            const value = cell.v !== undefined ? cell.v : null;
            const formula = cell.f ? "=" + cell.f : null;
            if (value === null && !formula) continue;
            cells.push([addr, value, formula]);
            if (++count >= MAX_EXCEL_CELLS) { truncated = true; break; }
          }
        }
      }
      sheets.push({ name, cells });
      if (truncated) break;
    }
    return { fileName, sheets, truncated, cellCount: count };
  }

  function escapeHtml(text) {
    return String(text || "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
  }
  function renderMarkdownToHtml(text) {
    const markdown = String(text || "");
    const markedParser = window.marked && window.marked.parse;
    if (!markedParser || !window.DOMPurify) return { __html: escapeHtml(markdown).replace(/\n/g, "<br>") };
    return { __html: window.DOMPurify.sanitize(markedParser(markdown, MARKDOWN_OPTIONS), { USE_PROFILES: { html: true } }) };
  }

  function AgentMessage({ message }) {
    // chart / report messages render their own widget (no bubble)
    if (message.role === "chart" && window.AtlasChartBlock) {
      return h("div", { className: "agent-msg assistant" },
        h("div", { className: "agent-role" }, "Chart"),
        h(window.AtlasChartBlock, { spec: message.spec }));
    }
    if (message.role === "report" && window.AtlasReportCard) {
      return h("div", { className: "agent-msg assistant" },
        h("div", { className: "agent-role" }, "Report"),
        h(window.AtlasReportCard, { spec: message.spec }));
    }
    const isTool = message.role === "tool";
    const bubbleClass = "agent-bubble" + (isTool ? " plain" : " markdown");
    return h("div", { className: "agent-msg " + message.role },
      h("div", { className: "agent-role" }, message.role === "user" ? "You" : message.role === "tool" ? "Tool" : "Agent"),
      isTool
        ? h("div", { className: bubbleClass }, message.text)
        : h("div", { className: bubbleClass, dangerouslySetInnerHTML: renderMarkdownToHtml(message.text) }),
    );
  }

  // ---- starter prompt chips (shown when the thread is fresh) ----
  function PromptChips({ prompts, onPick }) {
    return h("div", { className: "prompt-chips" },
      h("div", { className: "prompt-chips-label" }, "Try"),
      prompts.map((p, i) => h("button", { className: "prompt-chip", type: "button", key: i, onClick: () => onPick(p) }, p)),
    );
  }

  // review card for staged structural ops
  function ProposalCard({ tools, onApply, onDiscard }) {
    return h("div", { className: "proposal" },
      h("div", { className: "proposal-head" }, "Proposed changes (" + tools.length + ")"),
      h("div", { className: "proposal-list" },
        tools.map((t, i) => h("div", { className: "proposal-item", key: i },
          h("div", { className: "pi-summary" }, summarizeOp(t)),
          t.name === "add_output" || t.name === "update_output"
            ? h("div", { className: "pi-formula" },
                h("div", null, h("span", { className: "pi-k" }, "expr: "), t.input.expr),
                t.input.sourceFormula ? h("div", { className: "pi-src" },
                  h("span", { className: "pi-k" }, "excel: "), t.input.sourceFormula) : null,
              )
            : null,
        )),
      ),
      h("div", { className: "proposal-actions" },
        h("button", { className: "ef-ok", type: "button", onClick: onApply }, "Apply"),
        h("button", { className: "ef-cancel", type: "button", onClick: onDiscard }, "Discard"),
      ),
    );
  }

  function AgentPanel({ view, model, setModel, assetNodes, processNodes, inputs, derived, applyInputChanges, collapsed, onCollapsedChange, wide, onWideChange }) {
    const [messages, setMessages] = useState([{ role: "assistant", text: "Ready on Plant Site." }]);
    const [draft, setDraft] = useState("");
    const [busy, setBusy] = useState(false);
    const [status, setStatus] = useState(null);
    const [steps, setSteps] = useState([]);
    const [streamText, setStreamText] = useState("");
    const [excel, setExcel] = useState(null);
    const [pendingOps, setPendingOps] = useState(null);
    const scrollRef = useRef(null);
    const fileRef = useRef(null);
    const hasConversation = messages.some((m) => m.role === "user");

    const inputIndex = useMemo(() => buildInputIndex(model), [model]);
    const agentContext = useMemo(
      () => buildAgentContext({ view, model, inputIndex, assetNodes, processNodes, inputs, derived, excel }),
      [view, model, inputIndex, assetNodes, processNodes, inputs, derived, excel]);

    useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages, busy, pendingOps, steps, streamText]);

    useEffect(() => {
      setMessages((prev) => {
        const last = prev[prev.length - 1];
        const text = "Context: " + agentContext.screen + ".";
        if (last && last.role === "tool" && last.text === text) return prev;
        return prev.concat({ role: "tool", text });
      });
    }, [agentContext.screen]);

    function onFile(file) {
      const reader = new FileReader();
      reader.onload = () => {
        try {
          const parsed = parseWorkbook(reader.result, file.name);
          setExcel(parsed);
          setMessages((prev) => prev.concat({
            role: "tool",
            text: "Attached " + file.name + " — " + parsed.sheets.length + " sheet(s), " + parsed.cellCount + " cells" +
              (parsed.truncated ? " (truncated)" : "") + ". Describe the area to extract.",
          }));
        } catch (e) {
          setMessages((prev) => prev.concat({ role: "assistant", text: "Could not read spreadsheet: " + (e.message || e) }));
        }
      };
      reader.readAsArrayBuffer(file);
    }

    function applyProposal() {
      const result = applyStructuralOps(model, pendingOps);
      setModel(result.model);
      setPendingOps(null);
      const lines = [];
      if (result.applied.length) lines.push("Applied:\n" + result.applied.map((a) => "• " + a).join("\n"));
      if (result.errors.length) lines.push("Failed:\n" + result.errors.map((e) => "• " + e).join("\n"));
      setMessages((prev) => prev.concat({ role: "tool", text: lines.join("\n\n") || "No changes." }));
    }

    async function send(override) {
      const text = (typeof override === "string" ? override : draft).trim();
      if (!text || busy) return;
      const userMessage = { role: "user", text };
      const nextMessages = messages.concat(userMessage);
      setMessages(nextMessages);
      setDraft("");
      setBusy(true);
      setStatus({ stage: "thinking" });
      setSteps([]);
      setStreamText("");

      try {
        const payload = await streamAgent(
          {
            messages: nextMessages.filter((m) => m.role === "user" || m.role === "assistant"),
            context: agentContext,
          },
          {
            onStatus: (s) => setStatus(s),
            onStep: (s) => setSteps((prev) => prev.concat(s)),
            onDelta: (t) => setStreamText((prev) => prev + t),
          },
        );

        const toolUses = payload.toolUses || [];
        const valueActions = normalizeValueActions(toolUses, inputIndex);
        const structuralTools = toolUses.filter((t) => STRUCTURAL.indexOf(t.name) !== -1);
        const chartTools = toolUses.filter((t) => t.name === "render_chart");
        const reportTools = toolUses.filter((t) => t.name === "generate_document");

        const valueText = textFromValueActions(valueActions, inputIndex, inputs);
        if (valueActions.length) applyInputChanges(valueActions);
        if (structuralTools.length) setPendingOps(structuralTools);

        setMessages((prev) => {
          const additions = [];
          if (payload.text) additions.push({ role: "assistant", text: payload.text });
          if (valueText) additions.push({ role: "tool", text: valueText });
          chartTools.forEach((t) => additions.push({ role: "chart", spec: t.input }));
          reportTools.forEach((t) => additions.push({ role: "report", spec: t.input }));
          if (!additions.length && !structuralTools.length) additions.push({ role: "assistant", text: "Done." });
          return prev.concat(additions);
        });
      } catch (error) {
        setMessages((prev) => prev.concat({ role: "assistant", text: error.message || "Agent request failed." }));
      } finally {
        setBusy(false);
        setStatus(null);
        setSteps([]);
        setStreamText("");
      }
    }

    if (collapsed) {
      return h("aside", { className: "agent-panel collapsed", "aria-label": "AI Assistant" },
        h("div", { className: "agent-rail" },
          h("button", {
            className: "agent-toggle expand", type: "button",
            title: "Expand AI agent sidebar", "aria-label": "Expand AI agent sidebar",
            "aria-expanded": false, onClick: () => onCollapsedChange(false),
          }, h("span", { className: "agent-chevron", "aria-hidden": true })),
          h("div", { className: "agent-rail-title", "aria-hidden": true }, "AI"),
          h("div", { className: "agent-rail-status" + (busy ? " busy" : ""), title: busy ? "Thinking" : "Ready", "aria-hidden": true }),
        ),
      );
    }

    return h("aside", { className: "agent-panel", "aria-label": "AI Assistant" },
      h("div", { className: "agent-head" },
        h("div", { className: "agent-title-row" },
          h("div", { className: "agent-title" }, "AI Assistant"),
          h("div", { className: "agent-toggle-group" },
            onWideChange ? h("button", {
              className: "agent-toggle resize" + (wide ? " is-wide" : ""), type: "button",
              title: wide ? "Shrink chat pane" : "Widen chat pane to half screen",
              "aria-label": wide ? "Shrink chat pane" : "Widen chat pane",
              "aria-pressed": !!wide, onClick: () => onWideChange(!wide),
            }, h("span", { className: "agent-resize-ic", "aria-hidden": true })) : null,
            h("button", {
              className: "agent-toggle collapse", type: "button",
              title: "Collapse AI agent sidebar", "aria-label": "Collapse AI agent sidebar",
              "aria-expanded": true, onClick: () => onCollapsedChange(true),
            }, h("span", { className: "agent-chevron", "aria-hidden": true })),
          ),
        ),
        h("div", { className: "agent-context" },
          h("span", { className: "agent-chip" }, agentContext.screen),
          h("span", { className: "agent-chip" }, Object.keys(inputIndex).length + " inputs"),
          h("span", { className: "agent-chip" }, agentContext.overviewAssets.length + " assets"),
          excel ? h("span", { className: "agent-chip excel-chip", title: "Click to clear", onClick: () => setExcel(null) },
            "📎 " + excel.fileName) : null,
        ),
      ),
      h("div", { className: "agent-messages", ref: scrollRef },
        messages.map((message, idx) => h(AgentMessage, { key: idx, message })),
        pendingOps ? h(ProposalCard, {
          tools: pendingOps, onApply: applyProposal, onDiscard: () => {
            setPendingOps(null);
            setMessages((prev) => prev.concat({ role: "tool", text: "Discarded proposed changes." }));
          },
        }) : null,
        busy
          ? (steps.length || streamText
              ? h(StepTrace, { steps, streamText })
              : h("div", { className: "agent-msg assistant" },
                  h("div", { className: "agent-role" }, "Agent"),
                  h("div", { className: "agent-bubble" }, statusLabel(status))))
          : null,
      ),
      h("form", { className: "agent-form", onSubmit: (e) => { e.preventDefault(); send(); } },
        !hasConversation && !busy ? h(PromptChips, { prompts: PLANT_PROMPTS, onPick: (p) => send(p) }) : null,
        h("div", { className: "agent-form-row" },
          h("button", {
            className: "agent-attach", type: "button", title: "Attach .xlsx",
            onClick: () => fileRef.current && fileRef.current.click(),
          }, "📎"),
          h("input", {
            ref: fileRef, type: "file", accept: ".xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            style: { display: "none" },
            onChange: (e) => { const f = e.target.files && e.target.files[0]; if (f) onFile(f); e.target.value = ""; },
          }),
          h("textarea", {
            className: "agent-input", value: draft, placeholder: "Message the agent", rows: 2,
            onChange: (e) => setDraft(e.target.value),
            onKeyDown: (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } },
          }),
        ),
        h("button", { className: "agent-send", type: "submit", disabled: busy || !draft.trim() }, "SEND"),
      ),
    );
  }

  window.AgentPanel = AgentPanel;
})();
