// AI Assistant chat page — multiple conversations with separate context,
// persisted per user in localStorage. Answers come from the real ai-provider
// (Gemini/Claude) via window.API.aiChat over the поручения register.

const AI_SUGGESTIONS = [
  "Сколько всего поручений за 2025 год?",
  "Сколько поручений просрочено?",
  "Сводка по отделам за 2025",
  "Какие поручения у Фроловой В.Ю.?",
];

function aiInitials(name) {
  const parts = String(name || "").trim().split(/\s+/).filter(Boolean);
  if (!parts.length) return "Я";
  if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
  return (parts[0][0] + parts[1][0]).toUpperCase();
}

function nowTime() {
  const d = new Date();
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
}

// Per-user storage key so different accounts on one machine keep separate chats.
function chatsKey() {
  return `crm_ai_chats_v1_${(window.ME && window.ME.login) || "anon"}`;
}

function greetingMsg() {
  return {
    id: 1,
    from: "ai",
    time: nowTime(),
    text: "Здравствуйте! Я отвечаю по реестру поручений: сколько всего, что просрочено, разбивка по отделам и сотрудникам. Спросите — отвечу по реальным данным.",
    suggestions: AI_SUGGESTIONS,
  };
}

function makeChat() {
  return {
    id: `c${Date.now()}_${Math.floor(Math.random() * 100000)}`,
    title: "Новый чат",
    messages: [greetingMsg()],
    updated: nowTime(),
  };
}

function loadChats() {
  try {
    const data = JSON.parse(localStorage.getItem(chatsKey()));
    if (data && Array.isArray(data.chats) && data.chats.length) {
      const activeId = data.chats.some(c => c.id === data.activeId) ? data.activeId : data.chats[0].id;
      return { chats: data.chats, activeId };
    }
  } catch (e) { /* ignore corrupt storage */ }
  const c = makeChat();
  return { chats: [c], activeId: c.id };
}

function saveChats(chats, activeId) {
  try { localStorage.setItem(chatsKey(), JSON.stringify({ chats, activeId })); } catch (e) { /* quota / private mode */ }
}

// Approximate size of a conversation (sum of message text lengths) — used to warn
// the user when the dialog gets large enough to slow answers.
function chatSize(messages) {
  return (messages || []).reduce((n, m) => n + ((m.text || "").length), 0);
}
const CTX_WARN_CHARS = 16000; // показать подсказку «начните новый чат» выше этого размера
const CTX_BUDGET_CHARS = 6000; // бюджет контекста, отправляемого модели (история)

// trimHistory builds the model context newest-first up to a character budget, then
// restores chronological order. Long answers (big tables) stop bloating the context
// → fewer timeouts and lower cost. The latest turn is always kept.
function trimHistory(messages, budget) {
  const turns = (messages || []).filter(m => (m.from === "user" || m.from === "ai") && (m.text || "").trim());
  const out = [];
  let used = 0;
  for (let i = turns.length - 1; i >= 0; i--) {
    const text = turns[i].text || "";
    if (out.length && used + text.length > budget) break; // keep at least the latest turn
    used += text.length;
    out.push({ role: turns[i].from === "ai" ? "model" : "user", text });
  }
  return out.reverse();
}

function ChatPage() {
  const [state, setState] = React.useState(loadChats);
  const { chats, activeId } = state;
  const active = chats.find(c => c.id === activeId) || chats[0];
  const [draft, setDraft] = React.useState("");
  const [thinking, setThinking] = React.useState(false);
  const [aiInfo, setAiInfo] = React.useState(null);
  const [ctxWarnOff, setCtxWarnOff] = React.useState(false); // user dismissed the "big dialog" hint
  const streamRef = React.useRef(null);

  // Persist on every change.
  React.useEffect(() => { saveChats(chats, activeId); }, [chats, activeId]);

  // Show which provider/model is answering.
  React.useEffect(() => {
    if (window.API && window.API.aiStatus) {
      window.API.aiStatus().then(setAiInfo).catch(() => {});
    }
  }, []);

  // Keep the stream scrolled to the latest message.
  React.useEffect(() => {
    if (streamRef.current) streamRef.current.scrollTop = streamRef.current.scrollHeight;
  }, [active && active.messages, thinking, activeId]);

  // Apply an update to the currently active chat only (separate context per chat).
  const patchActive = (updater) => {
    setState(s => ({ ...s, chats: s.chats.map(c => (c.id === s.activeId ? updater(c) : c)) }));
  };

  // Patch a specific chat by id — used during streaming so deltas land in the
  // right conversation even if the user switches chats mid-answer.
  const patchChat = (id, updater) => {
    setState(s => ({ ...s, chats: s.chats.map(c => (c.id === id ? updater(c) : c)) }));
  };

  // Persist an action's resolved state onto the message (so a confirmed/cancelled
  // ActionCard doesn't reappear as a fresh form after a page refresh).
  const resolveAction = (msgId, idx, patch) => patchChat(activeId, c => ({
    ...c,
    messages: c.messages.map(m => (m.id === msgId && Array.isArray(m.actions)
      ? { ...m, actions: m.actions.map((a, j) => (j === idx ? { ...a, ...patch } : a)) }
      : m)),
  }));

  // Resume / clean up interrupted answers (e.g. after a page refresh). A message
  // left streaming with a jobId is re-attached to its server-side generation;
  // one without a jobId can't be recovered, so it's finalized as interrupted.
  const resumedRef = React.useRef(new Set());
  React.useEffect(() => {
    const chat = chats.find(c => c.id === activeId);
    if (!chat) return;
    const setAi = (msgId, fn) => patchChat(activeId, c => ({ ...c, messages: c.messages.map(m => (m.id === msgId ? fn(m) : m)) }));
    chat.messages.forEach(m => {
      if (m.from !== "ai" || !m.streaming) return;
      if (m.jobId && window.API && window.API.aiResumeStream) {
        if (resumedRef.current.has(m.jobId)) return;
        resumedRef.current.add(m.jobId);
        const msgId = m.id;
        setAi(msgId, x => ({ ...x, text: "" })); // job replays all deltas from the start
        setThinking(false);
        window.API.aiResumeStream(m.jobId, {
          onDelta: (d) => setAi(msgId, x => ({ ...x, text: x.text + d })),
          onChart: (chart) => setAi(msgId, x => ({ ...x, card: chart })),
          onActions: (actions) => setAi(msgId, x => ({ ...x, actions })),
          onDone: (meta) => setAi(msgId, x => ({ ...x, text: meta && meta.text ? meta.text : x.text, streaming: false })),
          onError: (msg, evt) => setAi(msgId, x => ({ ...x, streaming: false, text: (evt && evt.expired) ? "⚠️ Ответ устарел — задайте вопрос ещё раз." : (x.text || ("⚠️ " + msg)) })),
        });
      } else if (!m.jobId) {
        setAi(m.id, x => ({ ...x, streaming: false, text: x.text || "⚠️ Ответ был прерван — задайте вопрос ещё раз." }));
      }
    });
  }, [activeId]); // eslint-disable-line

  const selectChat = (id) => { setThinking(false); setCtxWarnOff(false); setState(s => ({ ...s, activeId: id })); };

  const createChat = () => {
    const c = makeChat();
    setDraft("");
    setThinking(false);
    setCtxWarnOff(false);
    setState(s => ({ chats: [c, ...s.chats], activeId: c.id }));
  };

  const deleteChat = (id, e) => {
    e.stopPropagation();
    setState(s => {
      let chats = s.chats.filter(c => c.id !== id);
      if (!chats.length) chats = [makeChat()];
      const activeId = s.activeId === id ? chats[0].id : s.activeId;
      return { chats, activeId };
    });
  };

  const send = async (text) => {
    const v = (text ?? draft).trim();
    if (!v || thinking) return;
    const time = nowTime();
    const chatId = state.activeId;

    // Build history from THIS chat's messages (its own context), trimmed to a
    // character budget so long answers (big tables) don't bloat the context.
    const cur = (state.chats.find(c => c.id === chatId) || {}).messages || [];
    const history = trimHistory(cur, CTX_BUDGET_CHARS);

    // Add the user message + an empty AI placeholder we fill as deltas arrive.
    // Derive both ids from one timestamp so they can never collide (a second
    // Date.now() call could tick into the next ms and equal aiId).
    const baseId = Date.now();
    const userId = baseId;
    const aiId = baseId + 1;
    patchChat(chatId, c => ({
      ...c,
      title: c.title === "Новый чат" ? (v.length > 42 ? v.slice(0, 42) + "…" : v) : c.title,
      messages: [
        ...c.messages,
        { id: userId, from: "user", text: v, time },
        { id: aiId, from: "ai", text: "", time: nowTime(), streaming: true },
      ],
      updated: time,
    }));
    setDraft("");
    setThinking(true);

    const setAi = (fn) => patchChat(chatId, c => ({ ...c, messages: c.messages.map(m => (m.id === aiId ? fn(m) : m)), updated: nowTime() }));

    if (!(window.API && window.API.aiChatStream)) {
      setAi(m => ({ ...m, text: "⚠️ ИИ-сервис недоступен.", streaming: false }));
      setThinking(false);
      return;
    }

    let got = false;
    const finish = (full, chart, actions) => {
      setAi(m => ({ ...m, text: full != null ? full : m.text, card: chart || m.card, actions: actions || m.actions, streaming: false }));
      setThinking(false);
    };
    const fail = async (msg) => {
      if (!got && window.API.aiChat) {
        // Streaming failed before any text — try the plain (non-stream) endpoint.
        try { const res = await window.API.aiChat(v, history); finish(res.text || "(пустой ответ)", res.chart, res.actions); return; }
        catch (e) { msg = String((e && e.message) || e); }
      }
      setAi(m => ({ ...m, text: (m.text ? m.text + "\n\n" : "") + "⚠️ " + msg, streaming: false }));
      setThinking(false);
    };

    try {
      await window.API.aiChatStream(v, history, {
        onJob: (id) => setAi(m => ({ ...m, jobId: id })),
        onDelta: (d) => { got = true; setThinking(false); setAi(m => ({ ...m, text: m.text + d })); },
        onChart: (chart) => setAi(m => ({ ...m, card: chart })),
        onActions: (actions) => setAi(m => ({ ...m, actions })),
        onDone: (meta) => finish(meta && meta.text ? meta.text : null),
        onError: (m) => fail(m),
      });
      // Stream ended without an explicit done/error (e.g. dropped connection).
      setThinking(false);
      setAi(m => (m.streaming ? { ...m, streaming: false } : m));
    } catch (e) {
      fail(String((e && e.message) || e));
    }
  };

  const onKey = (e) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      send();
    }
  };

  const subtitle = "Ответы по реальному реестру поручений";

  return (
    <div className="page chat-page">
      <div className="page-head">
        <div>
          <h1 className="page-title">ИИ-ассистент</h1>
          <p className="page-subtitle">{subtitle}</p>
        </div>
        <div style={{ display: "flex", gap: 8 }}>
          <button className="btn btn-outline" onClick={createChat}><I.Plus size={13} /> Новый чат</button>
        </div>
      </div>

      <div className="chat-layout">
        <div className="chat-sidebar">
          <div style={{ padding: "12px 14px", borderBottom: "1px solid var(--ink-100)", fontSize: 12, color: "var(--ink-400)", textTransform: "uppercase", letterSpacing: ".06em", fontWeight: 500 }}>
            История · {chats.length}
          </div>
          <div style={{ flex: 1, overflowY: "auto" }}>
            {chats.map((c) => {
              const last = c.messages[c.messages.length - 1];
              const preview = last ? last.text : "";
              return (
                <div
                  key={c.id}
                  className={`chat-history-item ${c.id === activeId ? "active" : ""}`}
                  onClick={() => selectChat(c.id)}
                  style={{ position: "relative", cursor: "pointer" }}
                >
                  <h4 style={{ paddingRight: 22, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{c.title}</h4>
                  <p style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{preview}</p>
                  <div style={{ fontSize: 11, color: "var(--ink-300)", marginTop: 4 }}>{c.updated}</div>
                  <button
                    className="icon-btn"
                    title="Удалить чат"
                    onClick={(e) => deleteChat(c.id, e)}
                    style={{ position: "absolute", top: 8, right: 6, color: "var(--ink-300)", padding: 2 }}
                  >
                    <I.Trash size={13} />
                  </button>
                </div>
              );
            })}
          </div>
        </div>

        <div className="chat-main">
          <div className="chat-stream" ref={streamRef}>
            {active.messages.map(m => <Message key={m.id} msg={m} onSuggest={send} onResolveAction={resolveAction} />)}
          </div>

          {chatSize(active.messages) > CTX_WARN_CHARS && !ctxWarnOff && (
            <div className="ctx-warn">
              <I.Alert size={14} />
              <span style={{ flex: 1 }}>Диалог стал большим — ответы могут замедлиться. Для точных и быстрых ответов начните новый чат.</span>
              <button className="btn btn-outline" style={{ padding: "4px 10px", fontSize: 12 }} onClick={createChat}><I.Plus size={12} /> Новый чат</button>
              <button className="icon-btn" title="Скрыть" onClick={() => setCtxWarnOff(true)} style={{ color: "var(--ink-400)" }}><I.X size={14} /></button>
            </div>
          )}

          <div className="composer">
            <div className="composer-inner">
              <textarea
                placeholder="Спросите про поручения: количество, просрочки, по отделам или сотруднику…"
                value={draft}
                onChange={e => setDraft(e.target.value)}
                onKeyDown={onKey}
                rows={1}
              />
              <div className="composer-tools">
                <button className="icon-btn" style={{ color: "var(--ink-400)" }} title="Прикрепить"><I.Paperclip size={15} /></button>
                <MicButton size={15} title="Голосовой ввод вопроса" onText={(t) => setDraft(d => (d ? d.trimEnd() + " " : "") + t)} />
              </div>
              <button className="send" onClick={() => send()} disabled={!draft.trim() || thinking}>
                <I.Send size={14} />
              </button>
            </div>
            <div className="composer-hint">
              <I.Sparkle size={11} /> Ответы основаны на данных реестра. Проверяйте важные цифры.
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function Message({ msg, onSuggest, onResolveAction }) {
  const isUser = msg.from === "user";
  const me = window.ME || {};
  const pending = !isUser && msg.streaming && !msg.text; // generating, no text yet → "думает"
  return (
    <div className={`msg ${isUser ? "user" : "ai"}`}>
      <div className="av">
        {isUser ? aiInitials(me.name || me.login) : <I.Sparkle size={14} />}
      </div>
      <div className="body">
        <div className="author">
          {isUser ? (me.name || me.login || "Вы") : "Ассистент"}
          <span className="time">{pending ? "думает…" : msg.time}</span>
        </div>
        {pending ? (
          <div className="bubble"><div className="typing"><span></span><span></span><span></span></div></div>
        ) : (msg.text || !msg.streaming) ? (
          <div className="bubble">
            {isUser ? msg.text : <Markdown text={msg.text} />}
            {msg.streaming && <span className="stream-caret">▍</span>}
          </div>
        ) : null}
        {msg.card && <InlineCard card={msg.card} />}
        {msg.actions && msg.actions.map((a, i) => <ActionCard key={i} action={a} onResolved={(patch) => onResolveAction && onResolveAction(msg.id, i, patch)} />)}
        {msg.suggestions && (
          <div className="suggestions">
            {msg.suggestions.map((s, i) => (
              <button key={i} className="suggestion" onClick={() => onSuggest(s)}>{s}</button>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// InlineCard renders a chart the model asked for. The model only chooses the axes
// (chart/chartBy/chartType); the numbers come from Go's real aggregates over the
// register. Specs: single-series → {kind:"bars"|"line", data:[{name,value}]};
// multi-series → {kind:"stacked"|"grouped"|"mline", data:[{name,<series>:n,…}], series:[…]}.
const CHART_COLORS = ["var(--accent)", "var(--ok)", "var(--warn)", "var(--err)", "var(--accent-strong)", "var(--ink-300)", "#7e6bc4", "#cf7bb3"];

function InlineCard({ card }) {
  const { LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, CartesianGrid } = Recharts;
  const data = Array.isArray(card.data) ? card.data : [];
  const series = Array.isArray(card.series) ? card.series : null;
  const tipStyle = { background: "var(--ink-800)", border: "none", borderRadius: 6, color: "#fff", fontSize: 11 };
  const tipLabel = { color: "rgba(255,255,255,.55)" };
  const tick = { fill: "var(--ink-400)", fontSize: 10 };

  // Categorical horizontal bars (по отделам / по статусам / по сотрудникам).
  if (card.kind === "bars") {
    return (
      <div className="ai-card" style={{ maxWidth: 520 }}>
        <div className="ai-card-title">{card.title || "Диаграмма"}</div>
        <div style={{ height: Math.max(120, data.length * 30 + 24) }}>
          <ResponsiveContainer width="100%" height="100%">
            <BarChart data={data} layout="vertical" margin={{ top: 4, right: 16, left: 6, bottom: 0 }} barCategoryGap={8}>
              <CartesianGrid stroke="var(--ink-100)" horizontal={false} />
              <XAxis type="number" tick={tick} axisLine={false} tickLine={false} allowDecimals={false} />
              <YAxis type="category" dataKey="name" width={120} tick={{ fill: "var(--ink-500)", fontSize: 11 }} axisLine={false} tickLine={false} />
              <Tooltip cursor={{ fill: "var(--ink-50)" }} contentStyle={tipStyle} labelStyle={tipLabel} />
              <Bar dataKey="value" fill="var(--accent)" radius={[0, 3, 3, 0]} />
            </BarChart>
          </ResponsiveContainer>
        </div>
      </div>
    );
  }

  // Single-series line over months (динамика).
  if (card.kind === "line") {
    return (
      <div className="ai-card" style={{ maxWidth: 560 }}>
        <div className="ai-card-title">{card.title || "Динамика"}</div>
        <div style={{ height: 220 }}>
          <ResponsiveContainer width="100%" height="100%">
            <LineChart data={data} margin={{ top: 8, right: 16, left: -8, bottom: 0 }}>
              <CartesianGrid stroke="var(--ink-100)" vertical={false} />
              <XAxis dataKey="name" tick={tick} axisLine={false} tickLine={false} interval="preserveStartEnd" />
              <YAxis tick={tick} axisLine={false} tickLine={false} width={34} allowDecimals={false} />
              <Tooltip cursor={{ stroke: "var(--ink-300)", strokeDasharray: "3 3" }} contentStyle={tipStyle} labelStyle={tipLabel} />
              <Line type="monotone" dataKey="value" name="Поручений" stroke="var(--accent-strong)" strokeWidth={2} dot={{ r: 2.5, fill: "var(--accent-strong)" }} activeDot={{ r: 4 }} />
            </LineChart>
          </ResponsiveContainer>
        </div>
      </div>
    );
  }

  // Multi-series line (несколько серий по месяцам).
  if (card.kind === "mline" && series) {
    return (
      <div className="ai-card" style={{ maxWidth: 560 }}>
        <div className="ai-card-title">{card.title || "Динамика"}</div>
        <div style={{ height: 250 }}>
          <ResponsiveContainer width="100%" height="100%">
            <LineChart data={data} margin={{ top: 8, right: 16, left: -8, bottom: 0 }}>
              <CartesianGrid stroke="var(--ink-100)" vertical={false} />
              <XAxis dataKey="name" tick={tick} axisLine={false} tickLine={false} interval="preserveStartEnd" />
              <YAxis tick={tick} axisLine={false} tickLine={false} width={34} allowDecimals={false} />
              <Tooltip contentStyle={tipStyle} labelStyle={tipLabel} />
              <Legend wrapperStyle={{ fontSize: 11 }} />
              {series.map((s, i) => (
                <Line key={s} type="monotone" dataKey={s} stroke={CHART_COLORS[i % CHART_COLORS.length]} strokeWidth={2} dot={false} activeDot={{ r: 4 }} />
              ))}
            </LineChart>
          </ResponsiveContainer>
        </div>
      </div>
    );
  }

  // Vertical multi-series bars — stacked or grouped (напр. месяц × статус: «слева
  // серии, снизу ось X»).
  if ((card.kind === "stacked" || card.kind === "grouped") && series) {
    const stack = card.kind === "stacked";
    const dense = data.length > 7;
    return (
      <div className="ai-card" style={{ maxWidth: 560 }}>
        <div className="ai-card-title">{card.title || "Сравнение"}</div>
        <div style={{ height: 270 }}>
          <ResponsiveContainer width="100%" height="100%">
            <BarChart data={data} margin={{ top: 8, right: 16, left: -8, bottom: 0 }} barCategoryGap={stack ? 6 : 10}>
              <CartesianGrid stroke="var(--ink-100)" vertical={false} />
              <XAxis dataKey="name" tick={tick} axisLine={false} tickLine={false} interval={0} angle={dense ? -30 : 0} textAnchor={dense ? "end" : "middle"} height={dense ? 52 : 24} />
              <YAxis tick={tick} axisLine={false} tickLine={false} width={34} allowDecimals={false} />
              <Tooltip cursor={{ fill: "var(--ink-50)" }} contentStyle={tipStyle} labelStyle={tipLabel} />
              <Legend wrapperStyle={{ fontSize: 11 }} />
              {series.map((s, i) => (
                <Bar key={s} dataKey={s} stackId={stack ? "a" : undefined} fill={CHART_COLORS[i % CHART_COLORS.length]} radius={stack ? 0 : [3, 3, 0, 0]} />
              ))}
            </BarChart>
          </ResponsiveContainer>
        </div>
      </div>
    );
  }

  return null;
}

// --- Markdown rendering for AI answers (safe: builds React nodes, no innerHTML) ---

// mdInline renders inline markup: **bold**, *italic*/_italic_, `code`, [text](url).
function mdInline(s, kp) {
  const out = [];
  const re = /\*\*([^*]+)\*\*|__([^_]+)__|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)|\*([^*\n]+)\*|_([^_\n]+)_/g;
  let last = 0, m, k = 0;
  while ((m = re.exec(s))) {
    if (m.index > last) out.push(<React.Fragment key={kp + k++}>{s.slice(last, m.index)}</React.Fragment>);
    if (m[1] != null) out.push(<strong key={kp + k++}>{m[1]}</strong>);
    else if (m[2] != null) out.push(<strong key={kp + k++}>{m[2]}</strong>);
    else if (m[3] != null) out.push(<code key={kp + k++} style={{ background: "var(--ink-50)", padding: "0 4px", borderRadius: 4, fontSize: ".92em" }}>{m[3]}</code>);
    else if (m[4] != null) out.push(<a key={kp + k++} href={m[5]} target="_blank" rel="noreferrer" style={{ color: "var(--accent-strong)" }}>{m[4]}</a>);
    else if (m[6] != null) out.push(<em key={kp + k++}>{m[6]}</em>);
    else if (m[7] != null) out.push(<em key={kp + k++}>{m[7]}</em>);
    last = re.lastIndex;
  }
  if (last < s.length) out.push(<React.Fragment key={kp + k++}>{s.slice(last)}</React.Fragment>);
  return out;
}

// --- table helpers (GFM): split a row into cells, read column alignment ---
// Strips one optional leading/trailing pipe, then splits on pipes.
function mdTableCells(line) {
  let s = line.trim();
  if (s.startsWith("|")) s = s.slice(1);
  if (s.endsWith("|")) s = s.slice(0, -1);
  return s.split("|").map(c => c.trim());
}
// A separator row like |---|:--:|---| (≥2 columns) marks the header underline.
const MD_TABLE_SEP = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)+\|?\s*$/;
function mdAligns(sepLine) {
  return mdTableCells(sepLine).map(c => {
    const l = c.startsWith(":"), r = c.endsWith(":");
    return l && r ? "center" : r ? "right" : l ? "left" : "";
  });
}
const MD_HR = /^([-*_])\1{2,}$/; // --- *** ___ (spaces stripped)

// Markdown renders a practical GFM subset as safe React nodes (no innerHTML):
// headings, lists, tables, fenced code, blockquotes, rules, paragraphs.
function Markdown({ text }) {
  const lines = String(text || "").split(/\r?\n/);
  const blocks = [];
  let i = 0;
  const isSep = (l) => MD_TABLE_SEP.test(l);
  while (i < lines.length) {
    const t = lines[i].trim();
    if (t === "") { i++; continue; }

    // fenced code: ``` or ~~~ … (model output, incl. accidental mermaid) → code block
    const fence = t.match(/^(```|~~~)/);
    if (fence) {
      const marker = fence[1];
      i++;
      const code = [];
      while (i < lines.length && !lines[i].trim().startsWith(marker)) { code.push(lines[i]); i++; }
      if (i < lines.length) i++; // skip closing fence
      blocks.push({ type: "code", text: code.join("\n") });
      continue;
    }

    // table: a row with pipes immediately followed by a |---|---| separator
    if (t.includes("|") && i + 1 < lines.length && isSep(lines[i + 1])) {
      const head = mdTableCells(t);
      const align = mdAligns(lines[i + 1]);
      i += 2;
      const rows = [];
      while (i < lines.length && lines[i].trim() !== "" && lines[i].includes("|") && !isSep(lines[i])) {
        rows.push(mdTableCells(lines[i])); i++;
      }
      blocks.push({ type: "table", head, align, rows });
      continue;
    }

    const h = t.match(/^(#{1,6})\s+(.*)$/);
    if (h) { blocks.push({ type: "h", text: h[2] }); i++; continue; }

    if (MD_HR.test(t.replace(/\s+/g, ""))) { blocks.push({ type: "hr" }); i++; continue; }

    if (/^>\s?/.test(t)) {
      const q = [];
      while (i < lines.length && /^>\s?/.test(lines[i].trim())) { q.push(lines[i].trim().replace(/^>\s?/, "")); i++; }
      blocks.push({ type: "quote", lines: q }); continue;
    }

    if (/^[*\-•]\s+/.test(t)) {
      const items = [];
      while (i < lines.length && /^\s*[*\-•]\s+/.test(lines[i])) { items.push(lines[i].replace(/^\s*[*\-•]\s+/, "")); i++; }
      blocks.push({ type: "ul", items }); continue;
    }
    if (/^\d+\.\s+/.test(t)) {
      const items = [];
      while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) { items.push(lines[i].replace(/^\s*\d+\.\s+/, "")); i++; }
      blocks.push({ type: "ol", items }); continue;
    }

    const para = [t]; i++;
    while (i < lines.length && lines[i].trim() !== ""
      && !/^\s*(#{1,6}\s|[*\-•]\s|\d+\.\s|>|```|~~~)/.test(lines[i])
      && !MD_HR.test(lines[i].replace(/\s+/g, ""))
      && !(lines[i].includes("|") && i + 1 < lines.length && isSep(lines[i + 1]))) {
      para.push(lines[i].trim()); i++;
    }
    blocks.push({ type: "p", text: para.join(" ") });
  }
  return (
    <div className="md" style={{ display: "flex", flexDirection: "column", gap: 4 }}>
      {blocks.map((b, k) => {
        if (b.type === "h") return <div key={k} style={{ fontWeight: 600, margin: "2px 0 0" }}>{mdInline(b.text, "h" + k + "-")}</div>;
        if (b.type === "hr") return <hr key={k} className="md-hr" />;
        if (b.type === "code") return <pre key={k} className="md-pre"><code>{b.text}</code></pre>;
        if (b.type === "quote") return <blockquote key={k} className="md-quote">{b.lines.map((ln, j) => <div key={j}>{mdInline(ln, k + "q" + j + "-")}</div>)}</blockquote>;
        if (b.type === "table") return (
          <div key={k} className="md-tbl-wrap">
            <table className="md-tbl">
              <thead><tr>{b.head.map((c, j) => <th key={j} style={b.align[j] ? { textAlign: b.align[j] } : null}>{mdInline(c, k + "h" + j + "-")}</th>)}</tr></thead>
              <tbody>{b.rows.map((r, ri) => <tr key={ri}>{b.head.map((_, j) => <td key={j} style={b.align[j] ? { textAlign: b.align[j] } : null}>{mdInline(r[j] || "", k + "r" + ri + "c" + j + "-")}</td>)}</tr>)}</tbody>
            </table>
          </div>
        );
        if (b.type === "ul") return <ul key={k} style={{ margin: 0, paddingLeft: 18 }}>{b.items.map((it, j) => <li key={j} style={{ margin: "1px 0" }}>{mdInline(it, k + "-" + j + "-")}</li>)}</ul>;
        if (b.type === "ol") return <ol key={k} style={{ margin: 0, paddingLeft: 20 }}>{b.items.map((it, j) => <li key={j} style={{ margin: "1px 0" }}>{mdInline(it, k + "-" + j + "-")}</li>)}</ol>;
        return <div key={k}>{mdInline(b.text, k + "-")}</div>;
      })}
    </div>
  );
}

// --- AI write-actions (proposed by the model, confirmed here by the user) ---

const STATUS_RU = { draft: "Черновик", in_progress: "В работе", sent: "Отправлено", accepted: "Принято", done: "Выполнено", cancelled: "Отменено" };
const MONTHS_RU = ["январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"];

function ruToISO(s) {
  const m = String(s || "").match(/(\d{2})\.(\d{2})\.(\d{4})/);
  return m ? `${m[3]}-${m[2]}-${m[1]}` : "";
}

// ActionCard renders a write-action the AI proposed and lets the user confirm it.
// Nothing is executed until the user clicks — execution goes through the normal
// REST API (so RBAC still applies). Create is an editable form; status/send are
// a plain confirm.
function ActionCard({ action, onResolved }) {
  const me = window.ME || {};
  const canWrite = me.role === "admin" || me.role === "controller";
  const dir = useDirectory();
  const p = (action && action.params) || {};

  // Initial state comes from the persisted resolution so a confirmed/cancelled
  // card stays resolved after a page refresh (it won't reappear as a form).
  const [st, setSt] = React.useState(action._resolved || "pending"); // pending | busy | done | error | cancelled
  const [err, setErr] = React.useState("");
  const [okMsg, setOkMsg] = React.useState(action._result || "");

  // create_task editable fields (declared unconditionally — hooks rule)
  const [text, setText] = React.useState(p.text || "");
  const [assignee, setAssignee] = React.useState(p.assignee || "");
  const [dept, setDept] = React.useState(p.dept || "");
  const [month, setMonth] = React.useState(MONTHS_RU.includes(p.month) ? p.month : MONTHS_RU[new Date().getMonth()]);
  const [year, setYear] = React.useState(p.year || 2025);
  const [due, setDue] = React.useState(ruToISO(p.dateEnd));
  const [notify, setNotify] = React.useState(!!p.notify); // notify executor in Telegram after create

  const run = async (fn, ok) => {
    if (!window.API) return;
    setSt("busy"); setErr("");
    try {
      const r = await fn();
      const msg = typeof ok === "function" ? ok(r) : ok;
      setOkMsg(msg);
      setSt("done");
      if (onResolved) onResolved({ _resolved: "done", _result: msg });
    } catch (e) {
      setErr(String((e && e.message) || e));
      setSt("error");
    }
  };
  const cancel = () => { setSt("cancelled"); if (onResolved) onResolved({ _resolved: "cancelled" }); };

  const busy = st === "busy";
  const disabledStyle = (cond) => (cond ? { opacity: 0.4, pointerEvents: "none" } : null);

  const shell = (icon, title, body, confirmLabel, onConfirm, confirmDisabled) => (
    <div className="ai-card" style={{ maxWidth: 540, borderLeft: "3px solid var(--accent)" }}>
      <div className="ai-card-title" style={{ display: "flex", alignItems: "center", gap: 6 }}>{icon} {title}</div>
      {st === "done" ? (
        <div style={{ color: "var(--ok)", fontSize: 12.5, display: "flex", alignItems: "center", gap: 6 }}><I.CheckCircle size={14} /> {okMsg}</div>
      ) : st === "cancelled" ? (
        <div style={{ color: "var(--ink-400)", fontSize: 12.5 }}>Отменено</div>
      ) : (
        <>
          {body}
          {err && <div style={{ color: "var(--err)", fontSize: 12, marginTop: 8 }}>{err}</div>}
          {!canWrite && <div style={{ color: "var(--warn)", fontSize: 12, marginTop: 8 }}>Изменения доступны контролёру или администратору.</div>}
          <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 10 }}>
            <button className="btn btn-ghost" style={{ padding: "6px 12px", fontSize: 12 }} onClick={cancel}>Отмена</button>
            <button className="btn btn-accent" style={{ padding: "6px 12px", fontSize: 12, ...disabledStyle(!canWrite || busy || confirmDisabled) }} onClick={onConfirm}>
              {busy ? "…" : confirmLabel}
            </button>
          </div>
        </>
      )}
    </div>
  );

  if (action.type === "create_task") {
    const peopleOpts = Object.keys(dir.people || {}).map(n => ({ value: n, label: n, sub: (dir.people[n] || {}).dept }));
    const body = (
      <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
        <div>
          <label className="fld-label">Формулировка</label>
          <textarea className="fld" rows={2} style={{ width: "100%", resize: "vertical" }} value={text} onChange={e => setText(e.target.value)} />
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <div>
            <label className="fld-label">Ответственный</label>
            <Combobox options={peopleOpts} value={assignee} onSelect={(name) => { setAssignee(name); const pp = (dir.people || {})[name]; if (pp && pp.dept) setDept(pp.dept); }} placeholder="ФИО…" />
          </div>
          <div>
            <label className="fld-label">Отдел</label>
            <select className="fld" style={{ width: "100%" }} value={dept} onChange={e => setDept(e.target.value)}>
              <option value="">—</option>
              {(dir.departments || []).map(d => <option key={d} value={d}>{d}</option>)}
            </select>
          </div>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
          <div>
            <label className="fld-label">Месяц</label>
            <select className="fld" style={{ width: "100%" }} value={month} onChange={e => setMonth(e.target.value)}>
              {MONTHS_RU.map(m => <option key={m} value={m}>{m}</option>)}
            </select>
          </div>
          <div>
            <label className="fld-label">Год</label>
            <input className="fld" type="number" style={{ width: "100%" }} value={year} onChange={e => setYear(e.target.value)} />
          </div>
          <div>
            <label className="fld-label">Срок</label>
            <input className="fld" type="date" style={{ width: "100%" }} value={due} onChange={e => setDue(e.target.value)} />
          </div>
        </div>
        <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5, color: "var(--ink-600)", cursor: "pointer" }}>
          <input type="checkbox" checked={notify} onChange={e => setNotify(e.target.checked)} />
          Уведомить исполнителя в Telegram после создания
        </label>
      </div>
    );
    return shell(
      <I.Plus size={14} />, "Создать поручение", body, "Создать",
      () => run(async () => {
        const created = await window.API.createTask({ mon: month, year: Number(year) || undefined, text: text.trim(), assignee, dept, dateEnd: due });
        let suffix = "";
        if (notify && window.API.sendTelegram) {
          try { await window.API.sendTelegram(created.id); suffix = ", отправлено в Telegram"; }
          catch (e) { suffix = " (в Telegram не отправлено: " + String((e && e.message) || e) + ")"; }
        }
        return { num: created.num, suffix };
      }, r => `Создано поручение №${r.num}${r.suffix}`),
      !text.trim()
    );
  }

  if (action.type === "set_task_status") {
    const body = <div style={{ fontSize: 13, color: "var(--ink-600)" }}>Поручение #{p.id}: сменить статус на «<b>{STATUS_RU[p.status] || p.status}</b>».</div>;
    return shell(
      <I.Edit size={14} />, "Сменить статус", body, "Подтвердить",
      () => run(() => window.API.updateTask(p.id, { status: p.status }), "Статус обновлён"),
      !p.id || !p.status
    );
  }

  if (action.type === "send_task_telegram") {
    const body = <div style={{ fontSize: 13, color: "var(--ink-600)" }}>Отправить поручение #{p.id} исполнителю в Telegram.</div>;
    return shell(
      <I.Plane size={14} />, "Отправить в Telegram", body, "Отправить",
      () => run(() => window.API.sendTelegram(p.id), "Отправлено в Telegram"),
      !p.id
    );
  }

  return null;
}

window.ChatPage = ChatPage;
