<script lang="ts">
  import { untrack } from 'svelte'
  import { slide } from 'svelte/transition'
  import type { ActivityEntry } from '$lib/stores/activity'
  import type { PlanStep } from '$lib/api'
  import Spinner from './Spinner.svelte'
  import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
  import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
  import CheckIcon from '@lucide/svelte/icons/check'
  import XIcon from '@lucide/svelte/icons/x'
  import PauseIcon from '@lucide/svelte/icons/pause'
  import SlashIcon from '@lucide/svelte/icons/slash'
  import WrenchIcon from '@lucide/svelte/icons/wrench'
  import MilestoneIcon from '@lucide/svelte/icons/milestone'
  import SparklesIcon from '@lucide/svelte/icons/sparkles'
  import HelpCircleIcon from '@lucide/svelte/icons/help-circle'
  import FlagIcon from '@lucide/svelte/icons/flag'

  // Merged plan + activity timeline, designed for the narrow rail:
  // - ordered newest-first: what the agent is doing right now is at the top,
  //   history flows downward, and the goal sits at the bottom where the task
  //   began (see the sort in `items`)
  // - one continuous vertical "backbone"; every item owns a segment of it,
  //   colored by state (done = filled primary, running = faint primary,
  //   pending/future = muted) so the line visibly fills in as work completes
  // - plan steps are filled status nodes ON the backbone; their tool calls
  //   branch off with horizontal stubs
  // - flat entries (goal/knowledge/complete/orphan tools) are milestone
  //   markers on the same backbone
  // - the running step auto-expands and the view auto-scrolls to keep the
  //   current step visible while the agent works (follow mode disengages if
  //   the operator scrolls down into history, re-engages when streaming
  //   starts again)
  let {
    entries,
    planSteps: steps,
    streaming = false
  }: {
    entries: ActivityEntry[]
    planSteps: PlanStep[]
    streaming?: boolean
  } = $props()

  // Explicit user toggles only — default open state derives from step status
  // (running = expanded, everything else = collapsed) so a step collapses
  // itself the moment it finishes unless the operator pinned it open.
  let stepToggles = $state(new Map<string, boolean>())
  let expandedTools = $state(new Set<string>())

  function stepOpen(step: PlanStep): boolean {
    return stepToggles.get(step.id) ?? step.status === 'running'
  }
  function toggleStep(step: PlanStep) {
    stepToggles.set(step.id, !stepOpen(step))
    stepToggles = new Map(stepToggles)
  }
  // Pin each streaming output pane to its tail as chunks arrive. Keyed by
  // tool id because several run entries can be on screen, though only the
  // newest one is ever actually streaming.
  let liveOutputEls = $state<Record<string, HTMLPreElement | null>>({})
  $effect(() => {
    // Depend on entries only. liveOutputEls is written by bind:this, so
    // tracking it here would let a re-render re-trigger this effect.
    const current = entries
    untrack(() => {
      for (const e of current) {
        if (!e.liveOutput) continue
        const el = liveOutputEls[e.id]
        if (el) el.scrollTop = el.scrollHeight
      }
    })
  })

  function toggleTool(id: string) {
    if (expandedTools.has(id)) expandedTools.delete(id)
    else expandedTools.add(id)
    expandedTools = new Set(expandedTools)
  }

  // ── Timeline model ────────────────────────────────────────────────────────
  type TLItem =
    | { kind: 'step'; step: PlanStep; tools: ActivityEntry[]; ts: number }
    | { kind: 'entry'; entry: ActivityEntry; ts: number }

  const items = $derived.by<TLItem[]>(() => {
    const stepIds = new Set(steps.map((s) => s.id))
    const out: TLItem[] = []

    for (const s of steps) {
      if (s.status === 'pending' && !entries.some((e) => e.stepSeq === s.seq)) {
        // Pending steps with no activity yet still show on the timeline so
        // the operator sees what's coming — but only if a plan exists. ts 0
        // parks them at the tail of the newest-first sort below (see there).
        if (steps.length > 0) {
          out.push({ kind: 'step', step: s, tools: [], ts: 0 })
        }
        continue
      }
      const tools = entries.filter(
        (e) =>
          e.stepSeq === s.seq &&
          (e.type === 'tool_running' || e.type === 'tool_done' || e.type === 'tool_error')
      )
      const stepEntry = entries.find((e) => e.id === s.id)
      // Timed from the step's own entry, else its earliest tool — so a step
      // is placed by when it started, not by its latest activity.
      const ts = stepEntry?.timestamp ?? tools[0]?.timestamp ?? Date.now()
      // Tools inside a step run newest-first too, matching the outer order.
      out.push({ kind: 'step', step: s, tools: [...tools].reverse(), ts })
    }

    for (const e of entries) {
      const isTool = e.type === 'tool_running' || e.type === 'tool_done' || e.type === 'tool_error'
      if (isTool && e.stepSeq != null) continue // nested under its step
      if (!isTool && stepIds.has(e.id)) continue // rendered as step node
      out.push({ kind: 'entry', entry: e, ts: e.timestamp })
    }

    // Newest first: whatever the agent is doing right now sits at the top of
    // the rail, with history flowing downward. The two ts-0 groups fall to
    // the bottom for free, which is where both belong in this order: the goal
    // (timestamp 0 — where the task started) and not-yet-run plan steps.
    // Sorting the latter by their future position would put them *above* the
    // running step and push it off the top, which is exactly what this
    // ordering exists to prevent. Array.sort is stable, so each group keeps
    // its insertion order (plan steps in seq order).
    out.sort((a, b) => b.ts - a.ts)
    return out
  })

  // ── Current activity + auto-scroll ────────────────────────────────────────
  const currentId = $derived.by<string | null>(() => {
    const runningTool = entries.find((e) => e.type === 'tool_running' && e.status === 'running')
    if (runningTool) return runningTool.id
    const runningStep = steps.find((s) => s.status === 'running')
    if (runningStep) return runningStep.id
    return null
  })

  let container = $state<HTMLDivElement | null>(null)
  let follow = $state(true)

  // Newest-first, so "following the agent" means being parked at the top —
  // the mirror of the bottom-anchored follow this had when it ran oldest-first.
  function onScroll() {
    if (!container) return
    follow = container.scrollTop < 80
  }

  // A new turn re-engages follow mode even if the operator had scrolled up.
  let wasStreaming = $state(false)
  $effect(() => {
    if (streaming && !wasStreaming) follow = true
    wasStreaming = streaming
  })

  // Scroll to the current step/tool whenever it changes (smooth) or when new
  // entries land while following (instant, to avoid scroll-queue jank).
  $effect(() => {
    if (!currentId || !follow || !container) return
    container
      .querySelector(`[data-tl-id="${CSS.escape(currentId)}"]`)
      ?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
  })
  let lastEntryCount = 0
  $effect(() => {
    const n = entries.length
    if (n === lastEntryCount) return
    lastEntryCount = n
    if (!follow || !container) return
    const target = currentId
      ? container.querySelector(`[data-tl-id="${CSS.escape(currentId)}"]`)
      : null
    if (target) target.scrollIntoView({ behavior: 'auto', block: 'nearest' })
    else container.scrollTop = 0
  })

  // ── Presentation helpers ──────────────────────────────────────────────────
  // Segment geometry: the backbone's center runs at x=17.5px (node center:
  // px-3 = 11.25px at the app's 15px root font-size + half of the 13px node),
  // so the 1px line sits at left-17px. Each item's segment spans its full
  // height so tools inside an expanded step stay on the line; first/last
  // items clip theirs to their node/tool centers so the line never dangles
  // past the timeline's ends.
  function segClass(
    status: string,
    isFirst: boolean,
    isLast: boolean,
    expandedWithTools: boolean
  ): string {
    let color = 'bg-border'
    if (status === 'done') color = 'bg-primary/60'
    else if (status === 'running') color = 'bg-primary/40'
    else if (status === 'failed') color = 'bg-destructive/40'

    if (isFirst && isLast) return `${color} top-[13px] h-0`
    if (isFirst) return `${color} top-[13px] bottom-0`
    if (isLast && expandedWithTools) return `${color} top-0 bottom-[11px]`
    if (isLast) return `${color} top-0 bottom-[calc(100%-13px)]`
    return `${color} top-0 bottom-0`
  }

  function entryIcon(entry: ActivityEntry) {
    switch (entry.type) {
      case 'goal':
        return MilestoneIcon
      case 'knowledge':
        return SparklesIcon
      case 'complete':
        return FlagIcon
      case 'question':
        return HelpCircleIcon
      default:
        return WrenchIcon
    }
  }
  function hhmm(ts: number): string {
    if (!ts || ts > Number.MAX_SAFE_INTEGER - 1000) return ''
    return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
  }
  function hhmmss(ts: number): string {
    if (!ts) return ''
    return new Date(ts).toLocaleTimeString([], {
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit'
    })
  }
  function prettyPrint(raw: string): string {
    try {
      return JSON.stringify(JSON.parse(raw), null, 2)
    } catch {
      return raw
    }
  }
</script>

<div class="flex h-full flex-col">
  <div class="flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
    {#if items.length === 0}
      <div class="flex flex-col items-center gap-2 px-3 py-6 text-center">
        <svg viewBox="0 0 64 110" class="h-14 w-auto text-muted-foreground/40" fill="none">
          <line
            x1="32"
            y1="8"
            x2="32"
            y2="102"
            stroke="currentColor"
            stroke-width="1"
            stroke-dasharray="2.5 4"
            opacity="0.35"
          />
          <circle cx="32" cy="22" r="4" fill="currentColor">
            <animate
              attributeName="opacity"
              values="0.25;0.9;0.25"
              dur="2.4s"
              repeatCount="indefinite"
            />
          </circle>
          <circle cx="32" cy="55" r="4" fill="currentColor">
            <animate
              attributeName="opacity"
              values="0.25;0.9;0.25"
              dur="2.4s"
              begin="0.6s"
              repeatCount="indefinite"
            />
          </circle>
          <circle cx="32" cy="55" r="4" fill="none" stroke="currentColor" stroke-width="1.5">
            <animate
              attributeName="r"
              values="4;11;4"
              dur="2.4s"
              begin="0.6s"
              repeatCount="indefinite"
            />
            <animate
              attributeName="opacity"
              values="0.6;0;0.6"
              dur="2.4s"
              begin="0.6s"
              repeatCount="indefinite"
            />
          </circle>
          <circle cx="32" cy="88" r="4" fill="currentColor">
            <animate
              attributeName="opacity"
              values="0.25;0.9;0.25"
              dur="2.4s"
              begin="1.2s"
              repeatCount="indefinite"
            />
          </circle>
        </svg>
        <p class="text-[11px] leading-relaxed text-muted-foreground">Waiting for activity…</p>
      </div>
    {:else}
      <div class="flex flex-col py-1">
        {#each items as item, i (item.kind === 'step' ? item.step.id : item.entry.id)}
          {@const isFirst = i === 0}
          {@const isLast = i === items.length - 1}
          {#if item.kind === 'step'}
            {@const st = item.step.status}
            {@const open = stepOpen(item.step)}
            {@const hasDetail = !!item.step.detail?.trim()}
            {@const expandable = item.tools.length > 0 || hasDetail}
            {@const expandedWithTools = open && item.tools.length > 0}
            <!-- Step node on the backbone -->
            <div class="relative" data-tl-id={item.step.id}>
              <span
                class="pointer-events-none absolute left-[17px] w-px {segClass(
                  st,
                  isFirst,
                  isLast,
                  expandedWithTools
                )}"
                aria-hidden="true"
              ></span>
              <button
                type="button"
                class="relative flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-xs {expandable
                  ? 'cursor-pointer hover:bg-muted/30'
                  : 'cursor-default'} {st === 'running' ? 'bg-primary/5' : ''}"
                onclick={() => expandable && toggleStep(item.step)}
                aria-expanded={open}
                disabled={!expandable}
              >
                <!-- Filled status node -->
                <span
                  class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full
                  {st === 'done'
                    ? 'bg-primary'
                    : st === 'running'
                      ? 'bg-background'
                      : st === 'failed'
                        ? 'bg-destructive'
                        : st === 'blocked'
                          ? 'bg-warning/25 border border-warning'
                          : st === 'skipped' || st === 'replaced'
                            ? 'bg-muted'
                            : 'bg-background border border-muted-foreground/40'}"
                >
                  {#if st === 'running'}
                    <span class="absolute -inset-0.5 animate-ping rounded-full bg-primary/30"
                    ></span>
                    <Spinner class="relative size-3.5 text-primary" />
                  {:else if st === 'done'}
                    <CheckIcon class="size-2.5 text-primary-foreground" strokeWidth={3.5} />
                  {:else if st === 'failed'}
                    <XIcon class="size-2.5 text-destructive-foreground" strokeWidth={3.5} />
                  {:else if st === 'blocked'}
                    <PauseIcon class="size-2 text-warning" strokeWidth={3} />
                  {:else if st === 'skipped' || st === 'replaced'}
                    <SlashIcon class="size-2 text-muted-foreground" strokeWidth={3} />
                  {/if}
                </span>
                <span
                  title={item.step.title}
                  class="min-w-0 flex-1 leading-snug {open
                    ? 'whitespace-normal'
                    : 'truncate'} {st === 'done'
                    ? 'text-muted-foreground'
                    : st === 'running'
                      ? 'font-medium text-foreground'
                      : 'text-muted-foreground'}"
                >
                  {item.step.title}
                </span>
                {#if hhmm(item.ts)}
                  <span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/60"
                    >{hhmm(item.ts)}</span
                  >
                {/if}
                {#if item.tools.length > 0}
                  <span class="shrink-0 text-muted-foreground/60">
                    {#if open}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon
                        class="size-3"
                      />{/if}
                  </span>
                {/if}
              </button>

              {#if expandedWithTools}
                <div transition:slide={{ duration: 150 }} class="flex flex-col">
                  {#each item.tools as tool (tool.id)}
                    {@const tOpen = expandedTools.has(tool.id) || !!tool.liveOutput}
                    <div class="relative" data-tl-id={tool.id}>
                      <!-- Branch stub: backbone → tool -->
                      <span
                        class="pointer-events-none absolute left-[17px] top-[9.5px] h-px w-[17px] {tool.status ===
                        'failed'
                          ? 'bg-destructive/40'
                          : 'bg-border'}"
                        aria-hidden="true"
                      ></span>
                      <button
                        type="button"
                        class="flex w-full items-center gap-1.5 py-1 pl-9 pr-3 text-left text-[11px] {tool.args ||
                        tool.detail ||
                        tool.liveOutput
                          ? 'cursor-pointer hover:bg-muted/20'
                          : 'cursor-default'}"
                        onclick={() =>
                          (tool.args || tool.detail || tool.liveOutput) && toggleTool(tool.id)}
                      >
                        <span class="flex size-3 shrink-0 items-center justify-center">
                          {#if tool.status === 'running'}
                            <Spinner class="size-2.5 text-primary" />
                          {:else if tool.status === 'failed'}
                            <XIcon class="size-2.5 text-destructive" strokeWidth={3.5} />
                          {:else}
                            <CheckIcon class="size-2.5 text-primary/70" strokeWidth={3.5} />
                          {/if}
                        </span>
                        <span
                          title={tool.description}
                          class="min-w-0 flex-1 truncate leading-snug {tool.status === 'done'
                            ? 'text-muted-foreground'
                            : tool.status === 'failed'
                              ? 'text-destructive'
                              : 'text-foreground/80'}"
                        >
                          {tool.description}
                        </span>
                        <span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/50"
                          >{hhmm(tool.timestamp)}</span
                        >
                      </button>
                      {#if tOpen}
                        <div
                          transition:slide={{ duration: 120 }}
                          class="flex flex-col gap-1 pb-1.5 pl-[52px] pr-3"
                        >
                          <div
                            class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70"
                          >
                            <span class="capitalize">{tool.status}</span>
                            <span aria-hidden="true">·</span>
                            <span>{hhmmss(tool.timestamp)}</span>
                            {#if tool.toolName}<span aria-hidden="true">·</span><code
                                class="font-mono">{tool.toolName}</code
                              >{/if}
                          </div>
                          {#if tool.args}
                            <pre
                              class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{prettyPrint(
                                tool.args
                              )}</pre>
                          {/if}
                          {#if tool.liveOutput}
                            <!-- Streaming while the command runs. Bound so it
                                 can be pinned to the tail as chunks arrive. -->
                            <pre
                              bind:this={liveOutputEls[tool.id]}
                              class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{tool.liveOutput}</pre>
                          {/if}
                          {#if tool.detail}
                            <pre
                              class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed {tool.status ===
                              'failed'
                                ? 'text-destructive'
                                : 'text-muted-foreground'}">{prettyPrint(tool.detail)}</pre>
                          {/if}
                        </div>
                      {/if}
                    </div>
                  {/each}
                </div>
              {/if}
            </div>
          {:else}
            <!-- Flat entry: milestone marker on the backbone -->
            {@const e = item.entry}
            {@const Icon = entryIcon(e)}
            {@const eOpen = expandedTools.has(e.id)}
            <div class="relative" data-tl-id={e.id}>
              <span
                class="pointer-events-none absolute left-[17px] w-px {segClass(
                  e.status,
                  isFirst,
                  isLast,
                  false
                )}"
                aria-hidden="true"
              ></span>
              <button
                type="button"
                class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] {e.args ||
                e.detail
                  ? 'cursor-pointer hover:bg-muted/30'
                  : 'cursor-default'}"
                onclick={() => (e.args || e.detail) && toggleTool(e.id)}
              >
                <span
                  class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full border bg-background
                  {e.status === 'failed'
                    ? 'border-destructive text-destructive'
                    : e.status === 'running'
                      ? 'border-primary text-primary'
                      : 'border-border text-primary'}"
                >
                  {#if e.status === 'running'}
                    <Spinner class="size-2.5" />
                  {:else if e.status === 'failed'}
                    <XIcon class="size-2" strokeWidth={3.5} />
                  {:else}
                    <Icon class="size-2" strokeWidth={2.5} />
                  {/if}
                </span>
                <span
                  title={e.description}
                  class="min-w-0 flex-1 truncate leading-snug {e.status === 'done'
                    ? 'text-muted-foreground'
                    : 'text-foreground/80'}"
                >
                  {e.description}
                </span>
                <span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/60"
                  >{hhmm(e.timestamp)}</span
                >
              </button>
              {#if eOpen}
                <div
                  transition:slide={{ duration: 120 }}
                  class="flex flex-col gap-1 pb-1.5 pl-9 pr-3"
                >
                  <div class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70">
                    <span class="capitalize">{e.status}</span>
                    <span aria-hidden="true">·</span>
                    <span>{hhmmss(e.timestamp)}</span>
                    {#if e.toolName}<span aria-hidden="true">·</span><code class="font-mono"
                        >{e.toolName}</code
                      >{/if}
                  </div>
                  {#if e.args}
                    <pre
                      class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{prettyPrint(
                        e.args
                      )}</pre>
                  {/if}
                  {#if e.detail}
                    <pre
                      class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed {e.status ===
                      'failed'
                        ? 'text-destructive'
                        : 'text-muted-foreground'}">{prettyPrint(e.detail)}</pre>
                  {/if}
                </div>
              {/if}
            </div>
          {/if}
        {/each}
      </div>
    {/if}
  </div>
</div>
