<script lang="ts">
  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:
  // - 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 up, 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)
  }
  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.
        if (steps.length > 0) {
          out.push({ kind: 'step', step: s, tools: [], ts: Number.MAX_SAFE_INTEGER - s.seq })
        }
        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)
      const ts = stepEntry?.timestamp ?? tools[0]?.timestamp ?? Date.now()
      out.push({ kind: 'step', step: s, tools, 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 })
    }

    out.sort((a, b) => a.ts - b.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)

  function onScroll() {
    if (!container) return
    follow = container.scrollHeight - container.scrollTop - container.clientHeight < 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 = container.scrollHeight
  })

  // ── 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)}
                    <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) ? 'cursor-pointer hover:bg-muted/20' : 'cursor-default'}"
                        onclick={() => (tool.args || tool.detail) && 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.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>
