<script lang="ts">
  // Center pane: read a note, edit it in place, or browse its history.
  //
  // `item` carries the list-derived metadata (kind, tags, about, edited_by —
  // everything WikiTree already has); the full body is fetched here lazily
  // per selection, same split as the API (serveKnowledgeList never returns
  // content — see knowledge_write.go — so the tree stays cheap and only the
  // note actually being read pays for its body).
  import {
    fetchKnowledgeContent,
    fetchKnowledgeRevisions,
    updateKnowledge,
    deleteKnowledge,
    KnowledgeApiError,
    type KnowledgeListItem,
    type KnowledgeContent,
    type KnowledgeRevision
  } from '$lib/api'
  import { renderWikiMarkdown, slugFromKbHref, diffLines } from './wikiText'
  import { kindMeta, isAgentAuthored } from './kinds'
  import WikiOverview from './WikiOverview.svelte'
  import { openEntityWindow } from '$lib/stores/windows'
  import { relativeTime } from '$lib/utils'
  import { toast } from 'svelte-sonner'
  import * as Tabs from '$lib/components/ui/tabs'
  import * as Dialog from '$lib/components/ui/dialog'
  import { Button } from '$lib/components/ui/button'
  import { Input } from '$lib/components/ui/input'
  import { Textarea } from '$lib/components/ui/textarea'
  import { Skeleton } from '$lib/components/ui/skeleton'
  import PencilIcon from '@lucide/svelte/icons/pencil'
  import TrashIcon from '@lucide/svelte/icons/trash-2'
  import HistoryIcon from '@lucide/svelte/icons/history'
  import BotIcon from '@lucide/svelte/icons/bot'
  import XIcon from '@lucide/svelte/icons/x'
  import SaveIcon from '@lucide/svelte/icons/save'

  let {
    item,
    allItems,
    knownSlugs,
    onNavigate,
    onNew,
    onChanged,
    dirty = $bindable(false)
  }: {
    item: KnowledgeListItem | null
    // The whole collection — only used for the resting-state overview shown
    // when nothing is selected (WikiOverview derives its figures from it).
    allItems: KnowledgeListItem[]
    knownSlugs: Set<string>
    onNavigate: (slug: string) => void
    onNew: () => void
    // Fired after a save or delete that the parent's cached list needs to
    // reflect (title/tags changed, or the note is gone). Parent decides
    // whether to refetch the whole list or patch locally.
    onChanged: () => void
    // True while there's an in-progress edit that would be silently
    // discarded if `item` changed out from under this component. Knowledge.svelte
    // reads this before switching the selection (tree click, quick-open,
    // etc.) so it can confirm with the operator first — see its
    // requestSelect. Deliberately "in edit mode" rather than a real dirty
    // diff against the loaded content: simpler, and erring toward "ask
    // even if nothing actually changed" is the safe direction for a
    // destructive-by-default operation.
    dirty?: boolean
  } = $props()

  let content = $state<KnowledgeContent | null>(null)
  let loading = $state(false)
  let mode = $state<'read' | 'edit'>('read')
  let tab = $state<'note' | 'history'>('note')
  let saveError = $state('')
  let saving = $state(false)

  let draftTitle = $state('')
  let draftContent = $state('')
  let draftTags = $state('')
  let draftAbout = $state('')

  let revisions = $state<KnowledgeRevision[] | null>(null)
  let revisionsLoading = $state(false)
  let selectedRevisionId = $state<number | null>(null)

  async function load(slug: string): Promise<void> {
    loading = true
    mode = 'read'
    dirty = false
    tab = 'note'
    revisions = null
    selectedRevisionId = null
    saveError = ''
    content = await fetchKnowledgeContent(slug)
    loading = false
  }

  $effect(() => {
    if (item) load(item.slug)
    else content = null
  })

  function startEdit(): void {
    if (!content || !item) return
    draftTitle = content.title
    draftContent = content.content
    draftTags = content.tags.join(', ')
    draftAbout = item.about.join(', ')
    saveError = ''
    mode = 'edit'
    dirty = true
  }

  function cancelEdit(): void {
    mode = 'read'
    dirty = false
    saveError = ''
  }

  async function save(): Promise<void> {
    if (!item) return
    const title = draftTitle.trim()
    const body = draftContent.trim()
    if (!title || !body) {
      saveError = 'Title and content cannot be empty.'
      return
    }
    saving = true
    saveError = ''
    const about = draftAbout
      .split(',')
      .map((s) => s.trim())
      .filter(Boolean)
    try {
      const result = await updateKnowledge(item.slug, {
        title,
        content: body,
        tags: draftTags
          .split(',')
          .map((t) => t.trim())
          .filter(Boolean),
        about
      })
      // A typo'd entity slug in "About" fails to link server-side with only
      // a log line (see linkKnowledgeAbout) — diff what came back against
      // what was submitted so that doesn't happen silently.
      const unresolved = about.filter((s) => !result.linked?.includes(s))
      if (unresolved.length > 0) {
        toast.error(`Couldn't link to: ${unresolved.join(', ')} — check the slug is correct.`)
      }
      mode = 'read'
      dirty = false
      await load(item.slug)
      onChanged()
    } catch (e) {
      saveError =
        e instanceof KnowledgeApiError
          ? `${e.message}${e.detail ? ` — ${e.detail}` : ''}`
          : 'Save failed.'
    } finally {
      saving = false
    }
  }

  let confirmDeleteOpen = $state(false)
  let deleting = $state(false)

  // Soft delete (see migrations/022_knowledge_revisions.up.sql) — the note
  // goes to trash and can be brought back, so this is a lightweight confirm
  // rather than anything heavier. It's an in-app Dialog rather than the
  // browser's native confirm(): this app runs inside a custom floating
  // window (its own desktop-shell chrome), and a native confirm() blocks
  // the entire page's JS event loop until dismissed — in testing that froze
  // the tab hard enough that automated clicks stopped registering
  // entirely. A real dialog stays inside Svelte's event handling and can't
  // wedge the app that way.
  async function confirmDelete(): Promise<void> {
    if (!item) return
    deleting = true
    try {
      await deleteKnowledge(item.slug)
      confirmDeleteOpen = false
      onChanged()
    } catch (e) {
      saveError = e instanceof KnowledgeApiError ? e.message : 'Delete failed.'
    } finally {
      deleting = false
    }
  }

  async function openHistory(): Promise<void> {
    tab = 'history'
    if (revisions !== null || !item) return
    revisionsLoading = true
    revisions = await fetchKnowledgeRevisions(item.slug)
    selectedRevisionId = revisions[0]?.id ?? null
    revisionsLoading = false
  }

  // Bare slugs are auto-linked (see wikiText.ts) as `#kb:<slug>` anchors.
  // Intercepted here via event delegation on the rendered container — the
  // markdown body is injected with {@html}, so component-level click
  // bindings can't attach to individual links, but a plain bubbling
  // listener on the wrapper works the same as it would for real DOM.
  function handleContentClick(e: MouseEvent): void {
    const anchor = (e.target as HTMLElement).closest('a')
    if (!anchor) return
    const slug = slugFromKbHref(anchor.getAttribute('href'))
    if (!slug) return
    e.preventDefault()
    if (knownSlugs.has(slug)) onNavigate(slug)
    else openEntityWindow(slug)
  }

  const selectedRevision = $derived(revisions?.find((r) => r.id === selectedRevisionId) ?? null)
  // Diff against the CURRENT live body, not the next revision — the History
  // tab answers "what did this look like before it became what it is now,"
  // not "what changed between two arbitrary edits."
  const diff = $derived(
    selectedRevision && content ? diffLines(selectedRevision.content, content.content) : null
  )
</script>

<div class="flex h-full min-w-0 flex-col gap-2">
  {#if !item}
    <WikiOverview items={allItems} onSelect={onNavigate} {onNew} />
  {:else if loading}
    <!-- Shaped like the loaded header/tags/body below rather than a
         centered spinner, so the switch from "loading" to "loaded" is a
         content swap, not a layout jump — the title, meta line, tag row,
         and first few lines of body all keep their real position. -->
    <div class="flex items-start justify-between gap-2 border-b pb-2.5">
      <div class="min-w-0 flex-1">
        <div class="flex items-center gap-2">
          <Skeleton class="size-4 shrink-0 rounded" />
          <Skeleton class="h-5 w-56" />
        </div>
        <div class="mt-2 flex items-center gap-2">
          <Skeleton class="h-3 w-16" />
          <Skeleton class="h-3 w-20" />
          <Skeleton class="h-3 w-24" />
        </div>
      </div>
      <Skeleton class="h-7 w-16 shrink-0" />
    </div>
    <div class="flex flex-wrap gap-1.5 pt-3">
      <Skeleton class="h-5 w-14 rounded-full" />
      <Skeleton class="h-5 w-16 rounded-full" />
    </div>
    <div class="flex flex-col gap-2.5 pt-2">
      {#each Array(6) as _, i (i)}
        <Skeleton class="h-4" style="width: {i === 5 ? 45 : 96 - i * 4}%" />
      {/each}
    </div>
  {:else if !content}
    <div class="flex h-full items-center justify-center text-sm text-muted-foreground">
      Couldn't load this note.
    </div>
  {:else}
    <!-- Header: kind + title, then a single provenance line. Previously these
         were one wrapping row of badges and text fragments; splitting
         "what this is" from "where it came from" stops the title competing
         with its own metadata. -->
    <div class="flex items-start justify-between gap-2 border-b pb-2.5">
      <div class="min-w-0 flex-1">
        {#if mode === 'edit'}
          <Input bind:value={draftTitle} class="mb-1 h-8 font-medium" placeholder="Title" />
        {:else}
          {@const KindIcon = kindMeta(item.kind).icon}
          <div class="flex min-w-0 items-center gap-2">
            <KindIcon class="size-4 shrink-0 text-muted-foreground" />
            <h2 class="truncate text-base font-semibold">{content.title}</h2>
          </div>
        {/if}
        <div
          class="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground"
        >
          <span class="capitalize">{kindMeta(item.kind).label}</span>
          <span aria-hidden="true">·</span>
          {#if isAgentAuthored(content.edited_by)}
            <span class="flex items-center gap-1 text-primary">
              <BotIcon class="size-3" /> Nomos
            </span>
          {:else if content.edited_by}
            <span>{content.edited_by}</span>
          {:else}
            <span>unknown author</span>
          {/if}
          <span aria-hidden="true">·</span>
          <span>updated {relativeTime(content.updated_at)}</span>
          {#if content.revisions > 0}
            <span aria-hidden="true">·</span>
            <button
              type="button"
              class="underline decoration-dotted underline-offset-2 hover:text-foreground"
              onclick={openHistory}
            >
              {content.revisions} revision{content.revisions === 1 ? '' : 's'}
            </button>
          {/if}
        </div>
      </div>
      <div class="flex shrink-0 gap-1">
        {#if mode === 'read'}
          <Button size="sm" variant="outline" class="h-7 gap-1 text-xs" onclick={startEdit}>
            <PencilIcon class="size-3.5" /> Edit
          </Button>
          <Button
            size="sm"
            variant="ghost"
            class="h-7 gap-1 text-xs text-destructive"
            onclick={() => (confirmDeleteOpen = true)}
          >
            <TrashIcon class="size-3.5" />
          </Button>
        {:else}
          <Button
            size="sm"
            variant="ghost"
            class="h-7 gap-1 text-xs"
            onclick={cancelEdit}
            disabled={saving}
          >
            <XIcon class="size-3.5" /> Cancel
          </Button>
          <Button size="sm" class="h-7 gap-1 text-xs" onclick={save} disabled={saving}>
            <SaveIcon class="size-3.5" />
            {saving ? 'Saving…' : 'Save'}
          </Button>
        {/if}
      </div>
    </div>

    {#if saveError}
      <p
        class="rounded border border-destructive/30 bg-destructive/5 px-2 py-1 text-xs text-destructive"
      >
        {saveError}
      </p>
    {/if}

    {#if mode === 'edit'}
      <div class="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto">
        <Textarea
          bind:value={draftContent}
          class="min-h-[240px] flex-1 resize-none font-mono text-xs"
          placeholder="Markdown content…"
        />
        <label class="text-xs text-muted-foreground" for="wiki-tags">
          Tags (comma-separated)
          <Input
            id="wiki-tags"
            bind:value={draftTags}
            class="mt-1 h-7 text-xs"
            placeholder="oom, rclone, gotcha"
          />
        </label>
        <label class="text-xs text-muted-foreground" for="wiki-about">
          About (entity slugs, comma-separated)
          <Input
            id="wiki-about"
            bind:value={draftAbout}
            class="mt-1 h-7 text-xs"
            placeholder="host:strong, lxc:gitea"
          />
        </label>
      </div>
    {:else}
      <Tabs.Root bind:value={tab} class="flex min-h-0 flex-1 flex-col">
        <Tabs.List class="h-7 w-fit">
          <Tabs.Trigger value="note" class="text-xs">Note</Tabs.Trigger>
          <Tabs.Trigger value="history" class="gap-1 text-xs" onclick={openHistory}>
            <HistoryIcon class="size-3" /> History
          </Tabs.Trigger>
        </Tabs.List>

        <Tabs.Content value="note" class="min-h-0 flex-1 overflow-y-auto pt-3">
          {#if item.tags.length}
            <div class="mb-3 flex max-w-[68ch] flex-wrap gap-1.5">
              {#each item.tags as t (t)}<span
                  class="border px-2 py-0.5 text-[11px] text-muted-foreground"
                  >{t}</span
                >{/each}
            </div>
          {/if}
          <!-- event delegation over rendered markdown: the interactive elements are the <a>
               tags inside, already keyboard-operable on their own. -->
          <!-- svelte-ignore a11y_click_events_have_key_events -->
          <!-- svelte-ignore a11y_no_static_element_interactions -->
          <!-- max-w-[68ch]: without a measure the body ran the full width of a
               resizable pane, which at a wide split is well past the ~75ch
               where prose stops being comfortable to read. -->
          <div
            class="markdown-body max-w-[68ch] text-sm leading-relaxed"
            onclick={handleContentClick}
          >
            <!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify in renderWikiMarkdown -->
            {@html renderWikiMarkdown(content.content)}
          </div>
        </Tabs.Content>

        <Tabs.Content value="history" class="min-h-0 flex-1 overflow-y-auto pt-2">
          {#if revisionsLoading}
            <div class="flex gap-3">
              <div class="flex w-40 shrink-0 flex-col gap-2 px-2 py-1">
                {#each Array(4) as _, i (i)}
                  <div class="flex flex-col gap-1">
                    <Skeleton class="h-3 w-16" />
                    <Skeleton class="h-2.5 w-20" />
                  </div>
                {/each}
              </div>
              <div class="flex min-w-0 flex-1 flex-col gap-1.5 rounded border p-2">
                {#each Array(8) as _, i (i)}
                  <Skeleton class="h-3" style="width: {90 - (i % 4) * 15}%" />
                {/each}
              </div>
            </div>
          {:else if !revisions || revisions.length === 0}
            <p class="py-8 text-center text-xs text-muted-foreground">
              No prior revisions — this is the first version.
            </p>
          {:else}
            <div class="flex gap-3">
              <div class="flex w-40 shrink-0 flex-col gap-0.5">
                {#each revisions as rev (rev.id)}
                  <button
                    type="button"
                    class="rounded px-2 py-1 text-left text-[11px] hover:bg-muted/50 {selectedRevisionId ===
                    rev.id
                      ? 'bg-primary/10 text-primary'
                      : ''}"
                    onclick={() => (selectedRevisionId = rev.id)}
                  >
                    <div class="font-medium">{relativeTime(rev.version_at)}</div>
                    <div class="text-muted-foreground">{rev.edited_by || 'unknown'}</div>
                  </button>
                {/each}
              </div>
              <div class="min-w-0 flex-1 overflow-x-auto rounded border">
                {#if diff}
                  <pre class="p-2 text-[11px] leading-relaxed">{#each diff as op, i (i)}<div
                        class={op.type === 'add'
                          ? 'bg-success/10 text-success'
                          : op.type === 'remove'
                            ? 'bg-destructive/10 text-destructive line-through'
                            : ''}>{op.type === 'add'
                          ? '+ '
                          : op.type === 'remove'
                            ? '- '
                            : '  '}{op.line}</div>{/each}</pre>
                {/if}
              </div>
            </div>
          {/if}
        </Tabs.Content>
      </Tabs.Root>
    {/if}
  {/if}
</div>

<Dialog.Root bind:open={confirmDeleteOpen}>
  <Dialog.Content class="sm:max-w-sm">
    <Dialog.Header>
      <Dialog.Title>Delete note?</Dialog.Title>
      <Dialog.Description>
        {#if item}"{item.title}" will move to Trash and can be restored from there.{/if}
      </Dialog.Description>
    </Dialog.Header>
    {#if saveError}
      <p
        class="rounded border border-destructive/30 bg-destructive/5 px-2 py-1 text-xs text-destructive"
      >
        {saveError}
      </p>
    {/if}
    <Dialog.Footer>
      <Button variant="ghost" onclick={() => (confirmDeleteOpen = false)} disabled={deleting}
        >Cancel</Button
      >
      <Button variant="destructive" onclick={confirmDelete} disabled={deleting}>
        {deleting ? 'Deleting…' : 'Delete'}
      </Button>
    </Dialog.Footer>
  </Dialog.Content>
</Dialog.Root>
