<script lang="ts">
  // Left pane of the Knowledge wiki: a tree over every live note, with a
  // grouping switch so the same 102 notes are reachable four different
  // ways — which one helps depends on what the operator already remembers
  // about the thing they're looking for (its topic, its type, a tag, or the
  // machine it concerns).
  import type { KnowledgeListItem } from '$lib/api'
  import { groupNotes, type GroupBy } from './wikiText'
  import { Input } from '$lib/components/ui/input'
  import * as Select from '$lib/components/ui/select'
  import * as Collapsible from '$lib/components/ui/collapsible'
  import { Button } from '$lib/components/ui/button'
  import { kindMeta, isAgentAuthored } from './kinds'
  import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
  import SearchIcon from '@lucide/svelte/icons/search'
  import PlusIcon from '@lucide/svelte/icons/plus'
  import BotIcon from '@lucide/svelte/icons/bot'
  import FolderTreeIcon from '@lucide/svelte/icons/folder-tree'

  let {
    items,
    selectedSlug,
    onSelect,
    onNew
  }: {
    items: KnowledgeListItem[]
    selectedSlug: string | null
    onSelect: (slug: string) => void
    onNew: () => void
  } = $props()

  const GROUP_LABELS: Record<GroupBy, string> = {
    folder: 'Folder',
    kind: 'Type',
    tag: 'Tag',
    entity: 'Entity'
  }

  function loadGroupBy(): GroupBy {
    if (typeof localStorage === 'undefined') return 'folder'
    const v = localStorage.getItem('oikos-wiki-groupby')
    return v === 'kind' || v === 'tag' || v === 'entity' ? v : 'folder'
  }

  let groupBy = $state<GroupBy>(loadGroupBy())
  let filter = $state('')

  function setGroupBy(v: string): void {
    if (v !== 'folder' && v !== 'kind' && v !== 'tag' && v !== 'entity') return
    groupBy = v
    if (typeof localStorage !== 'undefined') localStorage.setItem('oikos-wiki-groupby', v)
  }

  const filtered = $derived.by(() => {
    const q = filter.trim().toLowerCase()
    if (!q) return items
    return items.filter(
      (it) =>
        it.title.toLowerCase().includes(q) ||
        it.slug.toLowerCase().includes(q) ||
        it.tags.some((t) => t.toLowerCase().includes(q))
    )
  })

  const groups = $derived(groupNotes(filtered, groupBy))

  // Every group starts open when the filter is active (so a match is never
  // hidden inside a collapsed group) and only the group containing the
  // current selection starts open otherwise — with 102 notes across ~15
  // folders, all-open-by-default would just be a long undifferentiated
  // scroll.
  let openGroups = $state<Set<string>>(new Set())
  $effect(() => {
    if (filter.trim()) {
      openGroups = new Set(groups.map((g) => g.key))
      return
    }
    const owning = groups.find((g) => g.items.some((it) => it.slug === selectedSlug))
    openGroups = new Set(owning ? [owning.key] : groups[0] ? [groups[0].key] : [])
  })

  function toggleGroup(key: string): void {
    const next = new Set(openGroups)
    if (next.has(key)) next.delete(key)
    else next.add(key)
    openGroups = next
  }
</script>

<div class="flex h-full flex-col gap-1.5">
  <!-- Search and "new note" share a row: both act on the list as a whole,
       and pairing them lets the field take the remaining width instead of
       being squeezed by a fixed-width control beside it. -->
  <div class="flex items-center gap-1.5">
    <div class="relative flex-1">
      <SearchIcon class="absolute top-1/2 left-2 size-3.5 -translate-y-1/2 text-muted-foreground" />
      <Input placeholder="Filter notes…" bind:value={filter} class="h-7 pl-7 text-xs" />
    </div>
    <Button
      size="sm"
      variant="outline"
      class="size-7 shrink-0 p-0"
      onclick={onNew}
      title="New note"
      aria-label="New note"
    >
      <PlusIcon class="size-3.5" />
    </Button>
  </div>

  <!-- Group-by reads as a caption for the tree rather than a third boxed
       input: it labels how the list below is arranged, so it's styled like
       the group headers it governs (same muted 11px) and only reveals itself
       as a control on hover. Its own row of chrome was competing with the
       search field for attention while doing far less work. -->
  <Select.Root type="single" value={groupBy} onValueChange={setGroupBy}>
    <Select.Trigger
      size="sm"
      class="h-auto w-fit gap-1 rounded border-0 bg-transparent px-1 py-0.5 text-[11px] font-normal tracking-wide text-muted-foreground uppercase shadow-none hover:bg-muted/40 hover:text-foreground focus-visible:ring-0 data-[size=sm]:h-auto dark:bg-transparent dark:hover:bg-muted/40"
      title="Change how notes are grouped"
    >
      <FolderTreeIcon class="size-3 opacity-70" />
      by {GROUP_LABELS[groupBy]}
    </Select.Trigger>
    <Select.Content>
      {#each Object.entries(GROUP_LABELS) as [key, label] (key)}
        <Select.Item value={key} {label}>{label}</Select.Item>
      {/each}
    </Select.Content>
  </Select.Root>

  <div class="-mx-1 min-h-0 flex-1 overflow-y-auto px-1">
    {#each groups as group (group.key)}
      {@const isOpen = openGroups.has(group.key)}
      <Collapsible.Root open={isOpen} onOpenChange={() => toggleGroup(group.key)}>
        <Collapsible.Trigger
          class="group/grp flex w-full cursor-pointer items-center gap-1.5 rounded-md px-1.5 py-1.5 text-left select-none hover:bg-muted/40"
        >
          <ChevronRightIcon
            class="size-3 shrink-0 text-muted-foreground transition-transform duration-150 {isOpen
              ? 'rotate-90'
              : ''}"
          />
          <span
            class="min-w-0 flex-1 truncate text-[11px] font-medium tracking-wide text-muted-foreground uppercase group-hover/grp:text-foreground"
            >{group.label}</span
          >
          <span class="shrink-0 text-[10px] tabular-nums text-muted-foreground/70"
            >{group.items.length}</span
          >
        </Collapsible.Trigger>
        <Collapsible.Content>
          <!-- The guide rule sits inside the indent rather than on each row so
               it reads as one continuous line down the group. -->
          <div class="mb-1 ml-[13px] flex flex-col border-l border-border/60 pl-1.5">
            {#each group.items as it (it.slug + group.key)}
              {@const selected = selectedSlug === it.slug}
              {@const Icon = kindMeta(it.kind).icon}
              <button
                type="button"
                title={it.title}
                class="relative flex items-center gap-2 rounded-md py-1.5 pr-1.5 pl-2 text-left text-xs transition-colors {selected
                  ? 'bg-primary/10 font-medium text-primary'
                  : 'hover:bg-muted/50'}"
                onclick={() => onSelect(it.slug)}
              >
                <!-- Selection also gets an accent bar on the guide rule: the
                     background tint alone is easy to lose against the window's
                     own surface at this size. -->
                {#if selected}
                  <span
                    class="absolute top-1 bottom-1 -left-[7px] w-[2px] rounded-full bg-primary"
                    aria-hidden="true"
                  ></span>
                {/if}
                <Icon
                  class="size-3.5 shrink-0 {selected ? 'text-primary' : 'text-muted-foreground/70'}"
                />
                <span class="min-w-0 flex-1 truncate">{it.title}</span>
                {#if isAgentAuthored(it.edited_by)}
                  <BotIcon
                    class="size-3 shrink-0 {selected
                      ? 'text-primary/70'
                      : 'text-muted-foreground/50'}"
                  />
                {/if}
              </button>
            {/each}
          </div>
        </Collapsible.Content>
      </Collapsible.Root>
    {:else}
      <p class="px-2 py-8 text-center text-xs text-muted-foreground">
        No notes match &ldquo;{filter}&rdquo;.
      </p>
    {/each}
  </div>
</div>
