<script>
  import { onMount } from "svelte";
  import { api } from "../lib/api.js";
  import Icon from "../lib/Icon.svelte";

  const MAX_LINES = 2000;
  const LEVELS = ["全部", "INF", "WRN", "ERR"];

  let lines = $state([]);
  let level = $state("全部");
  let search = $state("");
  let paused = $state(false);
  let connected = $state(false);
  let rate = $state(0);
  let logView;
  let nextId = 0;

  let ws = null;
  let reconnectTimer = null;
  let rateTimer = null;
  let timestamps = [];
  let destroyed = false;

  const filtered = $derived.by(() => {
    const needle = search.trim().toLowerCase();
    return lines.filter((line) => {
      if (level !== "全部" && line.level !== level) return false;
      if (needle && !line.message.toLowerCase().includes(needle) && !line.target.toLowerCase().includes(needle)) return false;
      return true;
    });
  });

  function normalizeLevel(raw) {
    const upper = (raw || "").toUpperCase();
    if (upper === "INFO") return "INF";
    if (upper === "WARN") return "WRN";
    if (upper === "ERROR") return "ERR";
    return "DBG";
  }

  function parseHistoryLine(raw) {
    const match = raw.match(/^(\S+)\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+(?:ThreadId\(\d+\)\s+)?([^:]+):\s*(.*)$/);
    if (match) {
      return { id: nextId++, ts: match[1], level: normalizeLevel(match[2]), target: match[3].trim(), message: match[4] };
    }
    return { id: nextId++, ts: "", level: "DBG", target: "", message: raw };
  }

  function appendLine(entry) {
    entry.id = nextId++;
    lines = [...lines, entry];
    if (lines.length > MAX_LINES) {
      lines = lines.slice(lines.length - MAX_LINES);
    }
    timestamps.push(Date.now());
    if (!paused) {
      requestAnimationFrame(() => {
        if (logView) logView.scrollTop = logView.scrollHeight;
      });
    }
  }

  function connect() {
    if (destroyed) return;
    const proto = location.protocol === "https:" ? "wss:" : "ws:";
    ws = new WebSocket(`${proto}//${location.host}/ws/logs`);

    ws.onopen = () => {
      connected = true;
    };

    ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        appendLine({
          ts: data.ts || "",
          level: normalizeLevel(data.level),
          target: data.target || "",
          message: data.message || ""
        });
      } catch {}
    };

    ws.onclose = () => {
      connected = false;
      ws = null;
      if (!destroyed) {
        reconnectTimer = setTimeout(async () => {
          await loadHistory();
          connect();
        }, 3000);
      }
    };

    ws.onerror = () => {
      ws?.close();
    };
  }

  async function loadHistory() {
    try {
      const result = await api("/api/logs?lines=200");
      lines = (result.lines || []).map(parseHistoryLine);
      requestAnimationFrame(() => {
        if (logView) logView.scrollTop = logView.scrollHeight;
      });
    } catch {}
  }

  function download() {
    const text = filtered.map((l) => `${l.ts} ${l.level} ${l.target}: ${l.message}`).join("\n");
    const blob = new Blob([text], { type: "text/plain" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `picobot-${new Date().toISOString().slice(0, 19).replaceAll(":", "")}.log`;
    a.click();
    URL.revokeObjectURL(url);
  }

  onMount(() => {
    loadHistory().then(() => connect());

    rateTimer = setInterval(() => {
      const cutoff = Date.now() - 60000;
      timestamps = timestamps.filter((t) => t > cutoff);
      rate = timestamps.length;
    }, 5000);

    return () => {
      destroyed = true;
      clearTimeout(reconnectTimer);
      clearInterval(rateTimer);
      ws?.close();
    };
  });
</script>

<section class="page active content-page logs-page">
  <div class="toolbar filters">
    <div class="chips">
      {#each LEVELS as lv}
        <button class="chip" class:active={level === lv} onclick={() => (level = lv)}>{lv}</button>
      {/each}
    </div>
    <label class="search grow"><Icon name="search" size={16} /><input bind:value={search} placeholder="搜索日志" /></label>
    <button class="secondary" class:active={paused} onclick={() => (paused = !paused)}>
      {paused ? "▶ 继续" : "⏸ 暂停"}
    </button>
    <button class="secondary" onclick={download}><Icon name="download" size={16} />下载</button>
  </div>

  <div class="status-bar" class:ok={connected} class:fail={!connected}>
    {#if connected}
      <span class="dot"></span> 实时推送中 · {rate} 行/分
    {:else}
      <span class="dot"></span> 已断开 — 重连中
    {/if}
  </div>

  <div bind:this={logView} class="log-view">
    {#each filtered as line (line.id)}
      <div class="log-line">
        <span class="ts">{line.ts}</span>
        <span class="lvl lvl-{line.level}">{line.level}</span>
        <span class="target">{line.target}</span>
        <span class="msg">{line.message}</span>
      </div>
    {/each}
    {#if filtered.length === 0}
      <div class="empty">没有匹配的日志</div>
    {/if}
  </div>
</section>

<style>
  .status-bar {
    display: flex;
    align-items: center;
    gap: 7px;
    font-size: 12px;
    color: var(--muted);
    margin-bottom: 8px;
  }

  .status-bar.ok {
    color: var(--signal);
  }

  .status-bar.fail {
    color: var(--danger);
  }

  .dot {
    animation: dot-pulse 2s infinite;
  }

  @keyframes dot-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.4; }
  }

  .logs-page {
    display: flex;
    flex-direction: column;
  }

  .log-view {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
  }

  .log-line {
    display: flex;
    gap: 8px;
    padding: 1px 0;
    line-height: 1.65;
  }

  .ts {
    color: var(--faint);
    flex-shrink: 0;
  }

  .lvl {
    flex-shrink: 0;
    width: 3ch;
    font-weight: 700;
  }

  .lvl-INF { color: var(--signal); }
  .lvl-WRN { color: var(--accent); }
  .lvl-ERR { color: var(--danger); }
  .lvl-DBG { color: var(--muted); }

  .target {
    color: var(--text-soft);
    flex-shrink: 0;
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .msg {
    color: var(--text);
    word-break: break-all;
  }

  .empty {
    color: var(--muted);
    padding: 24px 0;
    text-align: center;
  }

  .secondary.active {
    background: var(--accent-soft);
    border-color: var(--accent-border);
    color: var(--accent);
  }
</style>
