<script>
  import { onMount, tick } from "svelte";
  import { Tooltip } from "bits-ui";
  import { clientId, formatTime, randomId } from "../lib/api.js";
  import { chat } from "../lib/chat.svelte.js";
  import Markdown from "../lib/Markdown.svelte";
  import ToolCallCard from "../lib/ToolCallCard.svelte";
  import TurnView from "../lib/TurnView.svelte";
  import Icon from "../lib/Icon.svelte";

  let { notify } = $props();
  let sessions = $state([]);
  let currentId = $state(null);
  let messages = $state([]);
  let search = $state("");
  let draft = $state("");
  let commands = $state([]);
  let selectedCommand = $state(0);
  let commandMenuDismissed = $state(false);
  let thinking = $state(false);
  let activeTurn = $state(null);
  let historyRevision = $state(0);
  let pendingUploads = $state([]);
  let fileInput;
  let plansBySession = $state({});
  let unseenPlanSessions = $state({});
  let todoOpen = $state(false);
  let messageBox;
  let input;
  const currentSession = $derived(sessions.find((item) => item.session_id === currentId));
  const currentPlan = $derived(currentId ? plansBySession[currentId] || null : null);
  const completedItems = $derived(currentPlan?.items?.filter((item) => item.status === "completed").length || 0);
  const filteredSessions = $derived(sessions.filter((item) => item.title.toLowerCase().includes(search.toLowerCase())));
  const commandQuery = $derived(
    !commandMenuDismissed && draft.startsWith("/") && !/[\s]/.test(draft)
      ? draft.toLowerCase()
      : null
  );
  const commandSuggestions = $derived.by(() => {
    if (commandQuery === null) return [];
    const term = commandQuery.slice(1);
    return commands.flatMap((command) => {
      const aliases = command.aliases?.length ? command.aliases : [`/${command.name}`];
      return aliases
        .filter((alias) => alias.toLowerCase().startsWith(commandQuery)
          || command.name.toLowerCase().startsWith(term)
          || command.description.toLowerCase().includes(term))
        .map((alias) => ({ ...command, alias }));
    });
  });

  function send(frame) {
    if (!chat.send(frame)) notify("聊天连接尚未就绪", true);
  }

  function handleFrame(frame) {
    switch (frame.type) {
      case "session_established":
        currentId = frame.session_id;
        chat.currentSessionId = currentId;
        activeTurn = null;
        historyRevision = 0;
        send({ type: "get_session_stats", session_id: currentId });
        break;
      case "session_list":
        sessions = frame.sessions || [];
        if (frame.current_session_id) currentId = frame.current_session_id;
        chat.currentSessionId = currentId;
        if (currentId && messages.length === 0) loadSession(currentId);
        break;
      case "session_created":
        currentId = frame.session_id;
        chat.currentSessionId = currentId;
        messages = [];
        activeTurn = null;
        historyRevision = 0;
        send({ type: "list_sessions", include_archived: false });
        break;
      case "session_loaded":
        currentId = frame.session_id;
        chat.currentSessionId = currentId;
        activeTurn = null;
        historyRevision = 0;
        send({ type: "get_session_history", session_id: currentId, limit: 1000 });
        send({ type: "get_session_plan", session_id: currentId });
        send({ type: "get_session_stats", session_id: currentId });
        break;
      case "session_history":
        if (frame.session_id === currentId) {
          messages = frame.messages || [];
          historyRevision = Math.max(0, ...messages.map((message) => message.seq || 0));
          if (activeTurn?.status !== "running" && messages.some((message) => message.id === activeTurn?.message_id)) activeTurn = null;
          scrollToBottom();
        }
        break;
      case "session_plan":
        if (frame.plan) {
          const previous = plansBySession[frame.session_id];
          if (frame.plan.id !== previous?.id || frame.plan.version >= (previous?.version || 0)) {
            plansBySession[frame.session_id] = frame.plan;
          }
        } else if (plansBySession[frame.session_id]?.status !== "active") {
          plansBySession[frame.session_id] = null;
        }
        break;
      case "plan_updated": {
        const previousPlan = plansBySession[frame.session_id];
        const previousVersion = previousPlan?.version || 0;
        const nextVersion = frame.plan?.version || 0;
        if (frame.plan?.id === previousPlan?.id && nextVersion && nextVersion <= previousVersion) break;
        plansBySession[frame.session_id] = frame.plan;
        if (frame.session_id === currentId) {
          todoOpen = true;
          unseenPlanSessions[frame.session_id] = false;
        } else {
          unseenPlanSessions[frame.session_id] = true;
        }
        break;
      }
      case "slash_commands_list":
        commands = frame.commands || [];
        selectedCommand = 0;
        break;
      case "assistant_response":
        thinking = false;
        if (!frame.session_id || frame.session_id === currentId) {
          appendMessage(frame.role || "assistant", frame.content, frame.attachments || [], frame.id);
          if (currentId) send({ type: "get_session_history", session_id: currentId, limit: 1000 });
        }
        send({ type: "list_sessions", include_archived: false });
        if (currentId) send({ type: "get_session_stats", session_id: currentId });
        break;
      case "turn_updated": {
        const next = frame.snapshot;
        if (!next || next.session_id !== currentId) break;
        if (activeTurn?.id === next.id && activeTurn.revision >= next.revision) break;
        const firstSnapshotForTurn = activeTurn?.id !== next.id;
        activeTurn = next;
        thinking = next.status === "running";
        if (firstSnapshotForTurn) send({ type: "get_session_stats", session_id: currentId });
        if (next.status !== "running" && messages.some((message) => message.id === next.message_id)) {
          activeTurn = null;
        }
        scrollToBottom();
        if (next.status !== "running") {
          if (next.status !== "completed") {
            send({ type: "get_session_history", session_id: currentId, limit: 1000 });
          }
          send({ type: "list_sessions", include_archived: false });
        }
        break;
      }
      case "turn_committed": {
        if (frame.session_id !== currentId || frame.history_revision <= historyRevision) break;
        const committed = frame.messages || [];
        const committedIds = new Set(committed.map((message) => message.id));
        const firstOptimisticMatch = messages.findIndex((message) => committedIds.has(message.id));
        const retained = messages.filter((message) => !committedIds.has(message.id));
        const insertionIndex = firstOptimisticMatch < 0
          ? retained.length
          : messages.slice(0, firstOptimisticMatch)
            .filter((message) => !committedIds.has(message.id)).length;
        // Replace optimistic steering at its original position with the whole
        // durable batch.  A Map#set replacement would keep only the user's
        // old slot and incorrectly place earlier tool messages after it.
        messages = [
          ...retained.slice(0, insertionIndex),
          ...committed,
          ...retained.slice(insertionIndex)
        ];
        historyRevision = frame.history_revision;
        if (activeTurn?.status !== "running"
          && (frame.messages || []).some((message) => message.id === activeTurn?.message_id)) {
          activeTurn = null;
        }
        scrollToBottom();
        send({ type: "get_session_stats", session_id: currentId });
        break;
      }
      case "session_stats": break;
      case "system_notification":
        if (!frame.session_id || frame.session_id === currentId) appendMessage("assistant", frame.content);
        break;
      case "command_executed":
        thinking = false;
        appendMessage("assistant", frame.message);
        if (currentId) send({ type: "get_session_stats", session_id: currentId });
        break;
      case "error": thinking = false; notify(frame.message || frame.code, true); break;
    }
  }

  async function scrollToBottom() {
    await tick();
    if (messageBox) messageBox.scrollTop = messageBox.scrollHeight;
  }

  function appendMessage(role, content, attachments = [], id = randomId()) {
    messages = [...messages, { id, role, content, attachments }];
    scrollToBottom();
  }

  function loadSession(id) {
    if (!id) return;
    currentId = id;
    chat.currentSessionId = id;
    historyRevision = 0;
    clearPendingUploads();
    messages = [];
    activeTurn = null;
    thinking = false;
    todoOpen = Boolean(unseenPlanSessions[id]);
    unseenPlanSessions[id] = false;
    send({ type: "load_session", session_id: id });
  }

  function itemIcon(status) {
    if (status === "completed") return "✓";
    if (status === "in_progress") return "●";
    if (status === "blocked") return "!";
    return "○";
  }

  function toolResult(callId) {
    return messages.find((message) => message.role === "tool" && message.tool_call_id === callId) || null;
  }

  function submit() {
    const content = draft.trim();
    const ready = pendingUploads.filter((upload) => upload.status === "ready");
    if ((!content && !ready.length) || !chat.connected || pendingUploads.some((upload) => upload.status === "uploading")) return;
    const clientMessageId = randomId();
    appendMessage("user", content, ready.map((upload, index) => ({
      index,
      name: upload.name,
      media_type: upload.media_type,
      mime_type: upload.mime_type,
      local_url: upload.localUrl
    })), clientMessageId);
    thinking = true;
    send({
      type: "user_input",
      content,
      upload_ids: ready.map((upload) => upload.upload_id),
      client_message_id: clientMessageId
    });
    draft = "";
    pendingUploads = [];
    commandMenuDismissed = false;
    selectedCommand = 0;
    if (input) input.style.height = "auto";
  }

  function attachmentUrl(message, attachment, inline = false) {
    if (attachment.local_url) return attachment.local_url;
    if (!currentId || !message.id) return "";
    const base = `/api/chat/${encodeURIComponent(clientId())}/sessions/${encodeURIComponent(currentId)}/messages/${encodeURIComponent(message.id)}/attachments/${attachment.index}`;
    return inline ? `${base}?disposition=inline` : base;
  }

  function formatBytes(value) {
    if (!Number.isFinite(value)) return "";
    if (value < 1024) return `${value} B`;
    if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KiB`;
    return `${(value / 1024 / 1024).toFixed(1)} MiB`;
  }

  function canPreview(attachment) {
    return ["image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp", "image/avif", "image/x-icon"].includes(attachment.mime_type);
  }

  function addFiles(files) {
    for (const file of Array.from(files || [])) uploadFile(file);
  }

  function uploadFile(file) {
    if (!chat.connected) return notify("聊天连接尚未就绪", true);
    const localId = randomId();
    const localUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : null;
    pendingUploads = [...pendingUploads, {
      localId, name: file.name, size: file.size, progress: 0, status: "uploading", localUrl
    }];
    const body = new FormData();
    body.append("file", file, file.name);
    const request = new XMLHttpRequest();
    request.open("POST", `/api/chat/${encodeURIComponent(clientId())}/uploads`);
    request.upload.onprogress = (event) => {
      if (!event.lengthComputable) return;
      pendingUploads = pendingUploads.map((item) => item.localId === localId
        ? { ...item, progress: Math.round(event.loaded / event.total * 100) }
        : item);
    };
    request.onload = () => {
      let response = {};
      try { response = JSON.parse(request.responseText || "{}"); } catch {}
      if (request.status >= 200 && request.status < 300) {
        pendingUploads = pendingUploads.map((item) => item.localId === localId
          ? { ...item, ...response, status: "ready", progress: 100 }
          : item);
      } else {
        pendingUploads = pendingUploads.map((item) => item.localId === localId
          ? { ...item, status: "error", error: response.error || `上传失败 (${request.status})` }
          : item);
      }
    };
    request.onerror = () => {
      pendingUploads = pendingUploads.map((item) => item.localId === localId
        ? { ...item, status: "error", error: "网络错误" }
        : item);
    };
    request.send(body);
  }

  function removeUpload(localId) {
    const upload = pendingUploads.find((item) => item.localId === localId);
    if (upload?.localUrl) URL.revokeObjectURL(upload.localUrl);
    pendingUploads = pendingUploads.filter((item) => item.localId !== localId);
  }

  function clearPendingUploads() {
    for (const upload of pendingUploads) if (upload.localUrl) URL.revokeObjectURL(upload.localUrl);
    pendingUploads = [];
  }

  function dropFiles(event) {
    event.preventDefault();
    addFiles(event.dataTransfer?.files);
  }

  function pasteFiles(event) {
    const files = Array.from(event.clipboardData?.items || [])
      .filter((item) => item.kind === "file")
      .map((item) => item.getAsFile())
      .filter(Boolean);
    if (files.length) addFiles(files);
  }

  async function moveCommandSelection(offset) {
    const length = commandSuggestions.length;
    if (!length) return;
    selectedCommand = (selectedCommand + offset + length) % length;
    await tick();
    document.getElementById(`slash-command-${selectedCommand}`)?.scrollIntoView({ block: "nearest" });
  }

  async function completeCommand(index = selectedCommand) {
    const command = commandSuggestions[index];
    if (!command) return;
    draft = `${command.alias} `;
    selectedCommand = 0;
    commandMenuDismissed = true;
    await tick();
    input?.focus();
    if (input) input.style.height = "auto";
  }

  function keydown(event) {
    if (event.isComposing) return;
    if (commandSuggestions.length) {
      if (event.key === "ArrowDown") {
        event.preventDefault();
        moveCommandSelection(1);
        return;
      }
      if (event.key === "ArrowUp") {
        event.preventDefault();
        moveCommandSelection(-1);
        return;
      }
      if (event.key === "Tab" || (event.key === "Enter" && !event.shiftKey)) {
        event.preventDefault();
        completeCommand();
        return;
      }
      if (event.key === "Escape") {
        event.preventDefault();
        commandMenuDismissed = true;
        return;
      }
    }
    if (event.key === "Enter" && !event.shiftKey) {
      event.preventDefault();
      submit();
    }
  }

  function resize(event) {
    event.currentTarget.style.height = "auto";
    event.currentTarget.style.height = `${Math.min(event.currentTarget.scrollHeight, 180)}px`;
  }

  function inputChanged(event) {
    commandMenuDismissed = false;
    selectedCommand = 0;
    resize(event);
  }

  onMount(() => {
    const unsubscribe = chat.subscribe(handleFrame);
    const onOpen = (frame) => {
      if (frame.type !== "_open") return;
      plansBySession = {};
      unseenPlanSessions = {};
      todoOpen = false;
      send({ type: "list_sessions", include_archived: false });
      send({ type: "get_slash_commands" });
    };
    const unsubOpen = chat.subscribe(onOpen);
    if (chat.connected) onOpen({ type: "_open" });
    return () => {
      unsubscribe();
      unsubOpen();
      clearPendingUploads();
    };
  });
</script>

<section class:todo-open={todoOpen && currentPlan} class="page active chat-layout">
  <aside class="sessions-panel">
    <button class="primary full" onclick={() => send({ type: "create_session", title: null })}><Icon name="add" size={16} />新建对话</button>
    <label class="search"><Icon name="search" size={16} /><input bind:value={search} placeholder="搜索对话" /></label>
    <div class="session-list-head"><span class="label-caps">最近对话</span><span class="session-count">{filteredSessions.length}</span></div>
    <div class="session-list">
      {#each filteredSessions as session (session.session_id)}
        <button class:active={session.session_id === currentId} class="session-item" onclick={() => loadSession(session.session_id)}>
          <strong>{session.title}{#if unseenPlanSessions[session.session_id]}<i class="plan-unread" aria-label="任务计划有更新"></i>{/if}</strong><small><span>{session.message_count} 条消息</span><span>{formatTime(session.last_active_at).split(" ")[0]}</span></small>
        </button>
      {:else}<div class="empty-card compact">暂无对话</div>{/each}
    </div>
  </aside>
  <div class="chat-panel">
    <div class="chat-heading">
      <div><strong>{currentSession?.title || "新对话"}</strong><small>WebUI 会话</small></div>
      <div class="chat-heading-actions">
        {#if currentPlan}
          <button class:active={todoOpen} class="todo-toggle" onclick={() => todoOpen = !todoOpen} aria-expanded={todoOpen}>
            <Icon name="panel" size={14} />任务 {completedItems}/{currentPlan.items.length}
          </button>
        {/if}
        <Tooltip.Root>
          <Tooltip.Trigger class="icon-button" aria-label="刷新会话" onclick={() => { send({ type: "list_sessions", include_archived: false }); if (currentId) { send({ type: "get_session_plan", session_id: currentId }); send({ type: "get_session_stats", session_id: currentId }); } }}><Icon name="refresh" size={18} /></Tooltip.Trigger>
          <Tooltip.Portal><Tooltip.Content class="tooltip" sideOffset={7}>刷新会话<Tooltip.Arrow class="tooltip-arrow" /></Tooltip.Content></Tooltip.Portal>
        </Tooltip.Root>
      </div>
    </div>
    <div class="messages" bind:this={messageBox}>
      {#if messages.length === 0 && !activeTurn}
        <div class="empty"><div class="empty-logo"><Icon name="bot" size={26} /></div><h2>今天想做些什么？</h2><p>消息与 CLI 客户端使用同一套会话、记忆和工具能力。</p></div>
      {/if}
      {#each messages as message (message.id)}
        {#if message.role !== "tool" && (message.content || message.tool_calls?.length || message.attachments?.length)}
          <div class:user={message.role === "user"} class:assistant={message.role !== "user"} class:has-tools={message.tool_calls?.length} class="message">
            <div class="avatar">{#if message.role === "user"}你{:else}<Icon name="bot" size={15} />{/if}</div>
            <div class="message-content">
              {#if message.reasoning_content}
                <details class="reasoning-block historical">
                  <summary>思考过程</summary>
                  <div class="reasoning-content"><Markdown content={message.reasoning_content} /></div>
                </details>
              {/if}
              {#if message.content}<div class="bubble"><Markdown content={message.content} /></div>{/if}
              {#if message.completion_status && message.completion_status !== "completed"}
                <small class="completion-status">{message.completion_status === "cancelled" ? "已停止" : "回复中断"}</small>
              {/if}
              {#if message.attachments?.length}
                <div class="message-attachments">
                  {#each message.attachments as attachment (`${message.id}:${attachment.index}`)}
                    {#if canPreview(attachment)}
                      <article class="image-attachment">
                        <a class="image-preview-link" href={attachmentUrl(message, attachment)} target="_blank" rel="noreferrer" aria-label={`查看原图 ${attachment.name}`}>
                          <img class="image-preview" src={attachmentUrl(message, attachment, true)} alt={attachment.name} />
                        </a>
                        <div class="attachment-meta"><div><strong>{attachment.name}</strong><small>{attachment.mime_type || attachment.media_type}</small></div><a href={attachmentUrl(message, attachment)} download={attachment.name} aria-label={`下载 ${attachment.name}`}><Icon name="download" size={16} /></a></div>
                      </article>
                    {:else}
                      <article class="attachment-card">
                        <span class="attachment-icon"><Icon name="document" size={18} /></span>
                        <div><strong>{attachment.name}</strong><small>{attachment.mime_type || attachment.media_type}</small></div>
                        <a href={attachmentUrl(message, attachment)} download={attachment.name} aria-label={`下载 ${attachment.name}`}><Icon name="download" size={16} /></a>
                      </article>
                    {/if}
                  {/each}
                </div>
              {/if}
              {#if message.tool_calls?.length}
                <div class="tool-calls">
                  {#each message.tool_calls as call (call.id)}
                    <ToolCallCard {call} result={toolResult(call.id)} />
                  {/each}
                </div>
              {/if}
            </div>
          </div>
        {/if}
      {/each}
      {#if activeTurn}<TurnView turn={activeTurn} />
      {:else if thinking}<div class="message assistant typing"><div class="avatar"><Icon name="bot" size={15} /></div><div class="bubble"><span class="pulse"></span>正在思考…</div></div>{/if}
    </div>
    <form class="composer" onsubmit={(event) => { event.preventDefault(); submit(); }} ondragover={(event) => event.preventDefault()} ondrop={dropFiles}>
      {#if commandSuggestions.length}
        <div class="command-menu" id="slash-command-menu" role="listbox" aria-label="斜杠命令">
          <div class="command-menu-heading"><span>斜杠命令</span><kbd>↑↓ 选择 · Tab/Enter 补全 · Esc 关闭</kbd></div>
          {#each commandSuggestions as command, index (`${command.name}:${command.alias}`)}
            <button
              id={`slash-command-${index}`}
              type="button"
              role="option"
              aria-selected={index === selectedCommand}
              class:selected={index === selectedCommand}
              onmouseenter={() => selectedCommand = index}
              onclick={() => completeCommand(index)}
            >
              <code>{command.alias}</code><span>{command.description}</span>
            </button>
          {/each}
        </div>
      {/if}
      {#if pendingUploads.length}
        <div class="pending-uploads">
          {#each pendingUploads as upload (upload.localId)}
            <div class:error={upload.status === "error"} class="pending-upload">
              <Icon name="document" size={16} /><div><strong>{upload.name}</strong><small>{upload.status === "uploading" ? `上传中 ${upload.progress}%` : upload.status === "error" ? upload.error : formatBytes(upload.size)}</small></div>
              <button type="button" aria-label={`移除 ${upload.name}`} onclick={() => removeUpload(upload.localId)}><Icon name="dismiss" size={14} /></button>
            </div>
          {/each}
        </div>
      {/if}
      <input class="file-input" bind:this={fileInput} type="file" multiple onchange={(event) => { addFiles(event.currentTarget.files); event.currentTarget.value = ""; }} />
      <button class="attach" type="button" aria-label="添加附件" onclick={() => fileInput?.click()}><Icon name="attach" size={18} /></button>
      <textarea
        bind:this={input}
        bind:value={draft}
        onkeydown={keydown}
        oninput={inputChanged}
        onpaste={pasteFiles}
        rows="1"
        role="combobox"
        aria-autocomplete="list"
        aria-controls="slash-command-menu"
        aria-expanded={commandSuggestions.length > 0}
        aria-activedescendant={commandSuggestions.length ? `slash-command-${selectedCommand}` : undefined}
        placeholder="输入消息，输入 / 查看命令"
      ></textarea>
      <button class="send" type="submit" aria-label="发送" disabled={!chat.connected || (!draft.trim() && !pendingUploads.some((upload) => upload.status === "ready")) || pendingUploads.some((upload) => upload.status === "uploading")}><Icon name="send" size={18} /></button>
      <small><span class:online={chat.connected}>{chat.connected ? "已连接" : "已断开，正在重连"}</span><span>/ 打开命令 · Shift+Enter 换行</span></small>
    </form>
  </div>
  {#if currentPlan}
    <aside class="todo-panel" aria-label="当前任务计划">
      <div class="todo-heading">
        <div><small>当前计划</small><strong>{currentPlan.objective}</strong></div>
        <button class="icon-button" aria-label="关闭任务侧栏" onclick={() => todoOpen = false}><Icon name="dismiss" size={18} /></button>
      </div>
      <div class="todo-progress"><span style={`width:${currentPlan.items.length ? completedItems / currentPlan.items.length * 100 : 0}%`}></span></div>
      <div class="todo-summary"><span>{completedItems}/{currentPlan.items.length} 已完成</span><span>v{currentPlan.version}</span></div>
      <div class="todo-items">
        {#each currentPlan.items as item (item.id)}
          <article class:blocked={item.status === "blocked"} class:running={item.status === "in_progress"} class:done={item.status === "completed"} class="todo-item">
            <span class="todo-icon">{itemIcon(item.status)}</span>
            <div><strong>{item.id} · {item.title}</strong><small>{item.executor_kind === "sub_agent" ? "子 Agent" : item.status === "in_progress" ? "主 Agent" : item.status}</small>
              {#if item.result_summary}<p>{item.result_summary}</p>{/if}
              {#if item.error}<p class="error-text">{item.error}</p>{/if}
            </div>
          </article>
        {/each}
      </div>
    </aside>
  {/if}
</section>
