<script lang="ts">
  // Maintenance view for the knowledge base's own drift — duplicates, tag
  // casing splits, orphaned notes, and the trash. Surfaced as its own mode
  // rather than folded into the main three-pane view because none of this
  // is "browse a note," it's "audit the collection," and mixing the two
  // would clutter the read/edit flow with tools most visits don't need.
  //
  // Every action here (merge, rename, restore) is deliberately one click
  // away from a review step, never automatic — see fetchKnowledgeDuplicates'
  // own doc comment on why title-similarity clustering can't be trusted as
  // a verdict (the five "Lifecycle: <verb> a node" runbooks cluster despite
  // being genuinely distinct documents).
  import {
    fetchKnowledgeDuplicates,
    fetchKnowledgeTags,
    fetchKnowledgeOrphans,
    fetchKnowledgeTrash,
    renameKnowledgeTag,
    mergeKnowledge,
    restoreKnowledge,
    KnowledgeApiError,
    type KnowledgeDuplicateCluster,
    type KnowledgeTag,
    type KnowledgeOrphan,
    type KnowledgeTrashItem
  } from '$lib/api'
  import * as Tabs from '$lib/components/ui/tabs'
  import { Button } from '$lib/components/ui/button'
  import { Input } from '$lib/components/ui/input'
  import { Badge } from '$lib/components/ui/badge'
  import { Skeleton } from '$lib/components/ui/skeleton'
  import { toast } from 'svelte-sonner'
  import { kindMeta } from './kinds'
  import { relativeTime } from '$lib/utils'
  import CopyIcon from '@lucide/svelte/icons/copy'
  import TagIcon from '@lucide/svelte/icons/tag'
  import GhostIcon from '@lucide/svelte/icons/ghost'
  import Trash2Icon from '@lucide/svelte/icons/trash-2'
  import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw'
  import CheckIcon from '@lucide/svelte/icons/check'

  let {
    onSelect,
    onChanged
  }: {
    onSelect: (slug: string) => void
    // Fired after any mutation (merge, tag rename, restore) so the parent's
    // note list — which this view reads a filtered copy of, indirectly —
    // stays in sync.
    onChanged: () => void
  } = $props()

  let tab = $state<'duplicates' | 'tags' | 'orphans' | 'trash'>('duplicates')

  // Shared across every loader/action below: the read helpers in api.ts now
  // throw KnowledgeApiError on a failed request instead of quietly returning
  // an empty list, so a real outage can't be mistaken for "nothing to
  // clean up" — see api.ts's comment on listKnowledge for the same fix
  // applied to the main note list.
  function errMsg(e: unknown): string {
    return e instanceof KnowledgeApiError ? e.message : 'Request failed.'
  }

  // ─── Duplicates ───────────────────────────────────────────────────────
  let clusters = $state<KnowledgeDuplicateCluster[] | null>(null)
  let duplicatesError = $state('')
  // Per cluster (indexed by the cluster's first member slug — stable across
  // a reload since clusters are keyed by content, not array position):
  // which slug is the merge target and which sources are checked.
  let mergeTarget = $state<Record<string, string>>({})
  let mergeSources = $state<Record<string, Set<string>>>({})
  let merging = $state<string | null>(null)
  let mergeError = $state('')

  async function loadDuplicates(): Promise<void> {
    clusters = null
    duplicatesError = ''
    try {
      const result = await fetchKnowledgeDuplicates()
      clusters = result
      const targets: Record<string, string> = {}
      const sources: Record<string, Set<string>> = {}
      for (const c of result) {
        const key = c.members[0].slug
        targets[key] = c.members[0].slug // newest first — see the Go handler's sort
        sources[key] = new Set(c.members.slice(1).map((m) => m.slug))
      }
      mergeTarget = targets
      mergeSources = sources
    } catch (e) {
      duplicatesError = errMsg(e)
      clusters = [] // clear the loading skeleton — the error message above explains the empty state
    }
  }

  // A merge target switch leaves the PREVIOUS target unchecked (it's not in
  // `sources` since it used to be excluded as "the target"), so recompute
  // the whole source set relative to the new target rather than leaving it
  // stale — otherwise the old target silently drops out of the merge
  // instead of folding in like every other member.
  function setMergeTarget(clusterKey: string, newTarget: string, allSlugs: string[]): void {
    mergeTarget = { ...mergeTarget, [clusterKey]: newTarget }
    mergeSources = {
      ...mergeSources,
      [clusterKey]: new Set(allSlugs.filter((s) => s !== newTarget))
    }
  }

  function toggleSource(clusterKey: string, slug: string): void {
    const set = new Set(mergeSources[clusterKey])
    if (set.has(slug)) set.delete(slug)
    else set.add(slug)
    mergeSources = { ...mergeSources, [clusterKey]: set }
  }

  async function doMerge(clusterKey: string): Promise<void> {
    const target = mergeTarget[clusterKey]
    const sources = [...(mergeSources[clusterKey] ?? [])]
    if (!target || sources.length === 0) return
    merging = clusterKey
    mergeError = ''
    try {
      const result = await mergeKnowledge(target, sources)
      toast.success(`Merged ${result.merged.length} note${result.merged.length === 1 ? '' : 's'}`)
      onChanged()
      await loadDuplicates()
    } catch (e) {
      mergeError = errMsg(e)
    } finally {
      merging = null
    }
  }

  // ─── Tags ─────────────────────────────────────────────────────────────
  let tags = $state<KnowledgeTag[] | null>(null)
  let tagsError = $state('')
  let renaming = $state<string | null>(null)
  let renameDraft = $state('')
  let renameBusy = $state(false)

  async function loadTags(): Promise<void> {
    tags = null
    tagsError = ''
    try {
      tags = await fetchKnowledgeTags()
    } catch (e) {
      tagsError = errMsg(e)
      tags = []
    }
  }

  async function normalize(t: KnowledgeTag): Promise<void> {
    renameBusy = true
    try {
      const n = await renameKnowledgeTag(t.variants, t.tag)
      toast.success(`Normalized "${t.tag}" across ${n} note${n === 1 ? '' : 's'}`)
      onChanged()
      await loadTags()
    } catch (e) {
      toast.error(errMsg(e))
    } finally {
      renameBusy = false
    }
  }

  function startRename(t: KnowledgeTag): void {
    renaming = t.tag
    renameDraft = t.tag
  }

  async function confirmRename(t: KnowledgeTag): Promise<void> {
    const to = renameDraft.trim().toLowerCase()
    if (!to || to === t.tag) {
      renaming = null
      return
    }
    renameBusy = true
    try {
      const n = await renameKnowledgeTag(t.variants, to)
      toast.success(`Renamed "${t.tag}" to "${to}" across ${n} note${n === 1 ? '' : 's'}`)
      onChanged()
      await loadTags()
      renaming = null
    } catch (e) {
      // Leave the rename input open on failure — the operator's typed value
      // (and their reason for changing it) shouldn't vanish along with the
      // error, forcing them to retype it to try again.
      toast.error(errMsg(e))
    } finally {
      renameBusy = false
    }
  }

  // ─── Orphans ──────────────────────────────────────────────────────────
  let orphans = $state<KnowledgeOrphan[] | null>(null)
  let orphanCounts = $state<Record<string, number>>({})
  let orphansError = $state('')

  async function loadOrphans(): Promise<void> {
    orphans = null
    orphansError = ''
    try {
      const result = await fetchKnowledgeOrphans()
      orphans = result.items
      orphanCounts = result.counts
    } catch (e) {
      orphansError = errMsg(e)
      orphans = []
    }
  }

  // ─── Trash ────────────────────────────────────────────────────────────
  let trash = $state<KnowledgeTrashItem[] | null>(null)
  let trashError = $state('')
  let restoring = $state<string | null>(null)

  async function loadTrash(): Promise<void> {
    trash = null
    trashError = ''
    try {
      trash = await fetchKnowledgeTrash()
    } catch (e) {
      trashError = errMsg(e)
      trash = []
    }
  }

  async function doRestore(slug: string): Promise<void> {
    restoring = slug
    try {
      await restoreKnowledge(slug)
      toast.success('Note restored')
      onChanged()
      await loadTrash()
    } catch (e) {
      toast.error(errMsg(e))
    } finally {
      restoring = null
    }
  }

  function activate(t: typeof tab): void {
    tab = t
    if (t === 'duplicates' && clusters === null) loadDuplicates()
    else if (t === 'tags' && tags === null) loadTags()
    else if (t === 'orphans' && orphans === null) loadOrphans()
    else if (t === 'trash' && trash === null) loadTrash()
  }

  // Initial tab's data.
  loadDuplicates()
</script>

<div class="flex h-full flex-col gap-2">
  <Tabs.Root bind:value={tab} class="flex min-h-0 flex-1 flex-col">
    <Tabs.List class="h-8 w-fit">
      <Tabs.Trigger value="duplicates" class="gap-1 text-xs" onclick={() => activate('duplicates')}>
        <CopyIcon class="size-3.5" /> Duplicates
      </Tabs.Trigger>
      <Tabs.Trigger value="tags" class="gap-1 text-xs" onclick={() => activate('tags')}>
        <TagIcon class="size-3.5" /> Tags
      </Tabs.Trigger>
      <Tabs.Trigger value="orphans" class="gap-1 text-xs" onclick={() => activate('orphans')}>
        <GhostIcon class="size-3.5" /> Orphans
      </Tabs.Trigger>
      <Tabs.Trigger value="trash" class="gap-1 text-xs" onclick={() => activate('trash')}>
        <Trash2Icon class="size-3.5" /> Trash
      </Tabs.Trigger>
    </Tabs.List>

    <Tabs.Content value="duplicates" class="min-h-0 flex-1 overflow-y-auto pt-2">
      <p class="mb-2 text-xs text-muted-foreground">
        Notes with near-identical titles, grouped for review — not a verdict. Pick a target and the
        sources to fold into it; sources are soft-deleted afterward and stay recoverable from Trash.
      </p>
      {#if mergeError}
        <p
          class="mb-2 rounded border border-destructive/30 bg-destructive/5 px-2 py-1 text-xs text-destructive"
        >
          {mergeError}
        </p>
      {/if}
      {#if duplicatesError}
        <p class="mb-2 flex items-center gap-2 text-xs text-destructive">
          {duplicatesError}
          <Button size="sm" variant="outline" class="h-6 text-xs" onclick={loadDuplicates}
            >Retry</Button
          >
        </p>
      {/if}
      {#if clusters === null}
        <div class="flex flex-col gap-3">
          {#each Array(2) as _, ci (ci)}
            <div class="overflow-hidden rounded-lg border">
              <div
                class="flex items-center justify-between gap-2 border-b bg-muted/30 px-2.5 py-1.5"
              >
                <Skeleton class="h-3 w-28" />
                <Skeleton class="h-6 w-28" />
              </div>
              <div class="flex flex-col gap-2 p-2.5">
                {#each Array(ci === 0 ? 3 : 2) as _, ri (ri)}
                  <Skeleton class="h-3.5" style="width: {70 - ri * 10}%" />
                {/each}
              </div>
            </div>
          {/each}
        </div>
      {:else if clusters.length === 0}
        <p class="py-8 text-center text-xs text-muted-foreground">No likely duplicates found.</p>
      {:else}
        <div class="flex flex-col gap-3">
          {#each clusters as c (c.members[0].slug)}
            {@const key = c.members[0].slug}
            {@const sourceCount = mergeSources[key]?.size ?? 0}
            <div class="overflow-hidden rounded-lg border">
              <div
                class="flex items-center justify-between gap-2 border-b bg-muted/30 px-2.5 py-1.5"
              >
                <div class="flex min-w-0 items-center gap-2 text-xs">
                  <span class="font-medium">{c.members.length} similar notes</span>
                  <!-- Similarity as a meter rather than only a number: it's a
                       ratio, and the bar makes a 93% pileup visibly different
                       from a borderline 61% at a glance down a long list. -->
                  <span
                    class="hidden h-1 w-12 shrink-0 overflow-hidden rounded-full bg-primary/15 sm:block"
                    title="{(c.top_similarity * 100).toFixed(0)}% title similarity"
                  >
                    <span
                      class="block h-full rounded-full bg-primary"
                      style="width: {c.top_similarity * 100}%"
                    ></span>
                  </span>
                  <span class="shrink-0 tabular-nums text-muted-foreground"
                    >{(c.top_similarity * 100).toFixed(0)}%</span
                  >
                </div>
                <Button
                  size="sm"
                  class="h-6 shrink-0 gap-1 text-xs"
                  disabled={merging === key || sourceCount === 0}
                  onclick={() => doMerge(key)}
                >
                  {merging === key ? 'Merging…' : `Merge ${sourceCount} into target`}
                </Button>
              </div>
              <!-- Two bare inputs per row read as "…what do these do?", so
                   name them once per cluster. An inline legend rather than
                   column headers: the controls are 14px wide and the words
                   are not, so headers sized to the columns just collide. -->
              <p class="flex items-center gap-3 px-2.5 pt-2 text-[10px] text-muted-foreground">
                <span class="flex items-center gap-1">
                  <span
                    class="inline-block size-2 rounded-full ring-1 ring-muted-foreground/60"
                    aria-hidden="true"
                  ></span> keep as target
                </span>
                <span class="flex items-center gap-1">
                  <span
                    class="inline-block size-2 rounded-[2px] ring-1 ring-muted-foreground/60"
                    aria-hidden="true"
                  ></span> fold into it
                </span>
              </p>
              <div class="flex flex-col p-1">
                {#each c.members as m (m.slug)}
                  {@const isTarget = mergeTarget[key] === m.slug}
                  {@const Icon = kindMeta(m.kind).icon}
                  <label
                    class="flex items-center gap-2 rounded px-1.5 py-1 text-xs {isTarget
                      ? 'bg-primary/5'
                      : 'hover:bg-muted/40'}"
                  >
                    <input
                      type="radio"
                      class="size-3.5 shrink-0 accent-[var(--primary)]"
                      name="target-{key}"
                      aria-label="Keep &quot;{m.title}&quot; as the merge target"
                      checked={isTarget}
                      onchange={() =>
                        setMergeTarget(
                          key,
                          m.slug,
                          c.members.map((mm) => mm.slug)
                        )}
                    />
                    <input
                      type="checkbox"
                      class="size-3.5 shrink-0 accent-[var(--primary)]"
                      aria-label="Fold &quot;{m.title}&quot; into the target"
                      disabled={isTarget}
                      checked={!isTarget && (mergeSources[key]?.has(m.slug) ?? false)}
                      onchange={() => toggleSource(key, m.slug)}
                    />
                    <Icon class="size-3 shrink-0 text-muted-foreground/70" />
                    <button
                      type="button"
                      class="min-w-0 flex-1 truncate text-left hover:underline {isTarget
                        ? 'font-medium text-primary'
                        : ''}"
                      onclick={() => onSelect(m.slug)}
                    >
                      {m.title}
                    </button>
                    <span class="shrink-0 text-[10px] text-muted-foreground"
                      >{relativeTime(m.updated_at)}</span
                    >
                    {#if isTarget}
                      <Badge
                        variant="outline"
                        class="shrink-0 border-primary/40 text-[9px] text-primary">target</Badge
                      >
                    {/if}
                  </label>
                {/each}
              </div>
            </div>
          {/each}
        </div>
      {/if}
    </Tabs.Content>

    <Tabs.Content value="tags" class="min-h-0 flex-1 overflow-y-auto pt-2">
      {#if tagsError}
        <p class="mb-2 flex items-center gap-2 text-xs text-destructive">
          {tagsError}
          <Button size="sm" variant="outline" class="h-6 text-xs" onclick={loadTags}>Retry</Button>
        </p>
      {/if}
      {#if tags === null}
        <table class="w-full text-xs">
          <thead>
            <tr class="border-b text-left text-muted-foreground">
              <th class="py-1 font-normal">Tag</th>
              <th class="py-1 font-normal" colspan="2">Uses</th>
              <th class="py-1 font-normal">Variants</th>
              <th class="py-1"></th>
            </tr>
          </thead>
          <tbody>
            {#each Array(10) as _, i (i)}
              <tr class="border-b border-border/50">
                <td class="w-32 py-1.5 pr-2"
                  ><Skeleton class="h-3" style="width: {60 - i * 3}%" /></td
                >
                <td class="w-8 py-1.5 pr-1"><Skeleton class="ml-auto h-3 w-4" /></td>
                <td class="w-24 py-1.5 pr-3">
                  <Skeleton class="h-1 rounded-full" style="width: {90 - i * 8}%" />
                </td>
                <td class="py-1.5 pr-2"><Skeleton class="h-3 w-6" /></td>
                <td class="py-1.5"></td>
              </tr>
            {/each}
          </tbody>
        </table>
      {:else}
        {@const maxUses = Math.max(1, ...tags.map((t) => t.uses))}
        <table class="w-full text-xs">
          <thead>
            <tr class="border-b text-left text-muted-foreground">
              <th class="py-1 font-normal">Tag</th>
              <th class="py-1 font-normal" colspan="2">Uses</th>
              <th class="py-1 font-normal">Variants</th>
              <th class="py-1"></th>
            </tr>
          </thead>
          <tbody>
            {#each tags as t (t.tag)}
              <tr class="border-b border-border/50">
                <td class="py-1 pr-2">
                  {#if renaming === t.tag}
                    <div class="flex items-center gap-1">
                      <Input bind:value={renameDraft} class="h-6 w-32 text-xs" />
                      <Button
                        size="sm"
                        class="h-6 px-1.5"
                        disabled={renameBusy}
                        onclick={() => confirmRename(t)}
                      >
                        <CheckIcon class="size-3" />
                      </Button>
                    </div>
                  {:else}
                    <button
                      type="button"
                      class="font-mono hover:underline"
                      onclick={() => startRename(t)}
                    >
                      {t.tag}
                    </button>
                  {/if}
                </td>
                <!-- tabular-nums here (unlike the overview's standalone
                     figures): these are a column that has to line up. -->
                <td class="w-8 py-1 pr-1 text-right tabular-nums">{t.uses}</td>
                <td class="w-24 py-1 pr-3">
                  <!-- Magnitude, so: one hue, length-encoded, scaled to the
                       most-used tag. Recessive by design — it's a reading aid
                       down the column, not the subject of the table. -->
                  <span class="block h-1 overflow-hidden rounded-full bg-primary/10">
                    <span
                      class="block h-full rounded-full bg-primary/60"
                      style="width: {(t.uses / maxUses) * 100}%"
                    ></span>
                  </span>
                </td>
                <td class="py-1 pr-2">
                  {#if t.split}
                    <span class="text-destructive">{t.variants.join(', ')}</span>
                  {:else}
                    <span class="text-muted-foreground">—</span>
                  {/if}
                </td>
                <td class="py-1 text-right">
                  {#if t.split}
                    <Button
                      size="sm"
                      variant="outline"
                      class="h-6 text-xs"
                      disabled={renameBusy}
                      onclick={() => normalize(t)}
                    >
                      Normalize
                    </Button>
                  {/if}
                </td>
              </tr>
            {/each}
          </tbody>
        </table>
      {/if}
    </Tabs.Content>

    <Tabs.Content value="orphans" class="min-h-0 flex-1 overflow-y-auto pt-2">
      <p class="mb-2 text-xs text-muted-foreground">
        Notes untagged, unlinked to any entity, or untouched for 90+ days — invisible to most
        navigation paths and easy to lose track of.
        {#if orphanCounts.untagged || orphanCounts.unlinked || orphanCounts.stale}
          ({orphanCounts.untagged ?? 0} untagged · {orphanCounts.unlinked ?? 0} unlinked · {orphanCounts.stale ??
            0} stale)
        {/if}
      </p>
      {#if orphansError}
        <p class="mb-2 flex items-center gap-2 text-xs text-destructive">
          {orphansError}
          <Button size="sm" variant="outline" class="h-6 text-xs" onclick={loadOrphans}
            >Retry</Button
          >
        </p>
      {/if}
      {#if orphans === null}
        <div class="flex flex-col gap-0.5">
          {#each Array(7) as _, i (i)}
            <div class="flex items-center gap-2 px-1.5 py-1.5">
              <Skeleton class="h-3.5 flex-1" style="max-width: {60 - (i % 4) * 8}%" />
              <Skeleton class="h-4 w-14 shrink-0 rounded-full" />
              <Skeleton class="h-3 w-10 shrink-0" />
            </div>
          {/each}
        </div>
      {:else if orphans.length === 0}
        <p class="py-8 text-center text-xs text-muted-foreground">Nothing orphaned.</p>
      {:else}
        <div class="flex flex-col gap-0.5">
          {#each orphans as o (o.slug)}
            <button
              type="button"
              class="flex items-center gap-2 rounded px-1.5 py-1 text-left text-xs hover:bg-muted/40"
              onclick={() => onSelect(o.slug)}
            >
              <span class="min-w-0 flex-1 truncate">{o.title}</span>
              {#each o.reasons as r (r)}<Badge variant="outline" class="shrink-0 text-[9px]"
                  >{r}</Badge
                >{/each}
              <span class="shrink-0 text-[10px] text-muted-foreground"
                >{relativeTime(o.updated_at)}</span
              >
            </button>
          {/each}
        </div>
      {/if}
    </Tabs.Content>

    <Tabs.Content value="trash" class="min-h-0 flex-1 overflow-y-auto pt-2">
      {#if trashError}
        <p class="mb-2 flex items-center gap-2 text-xs text-destructive">
          {trashError}
          <Button size="sm" variant="outline" class="h-6 text-xs" onclick={loadTrash}>Retry</Button>
        </p>
      {/if}
      {#if trash === null}
        <div class="flex flex-col gap-0.5">
          {#each Array(4) as _, i (i)}
            <div class="flex items-center gap-2 px-1.5 py-1.5">
              <Skeleton class="h-3.5 flex-1" style="max-width: {55 - i * 6}%" />
              <Skeleton class="h-3 w-32 shrink-0" />
              <Skeleton class="h-6 w-16 shrink-0" />
            </div>
          {/each}
        </div>
      {:else if trash.length === 0}
        <p class="py-8 text-center text-xs text-muted-foreground">Trash is empty.</p>
      {:else}
        <div class="flex flex-col gap-0.5">
          {#each trash as t (t.slug)}
            <div class="flex items-center gap-2 rounded px-1.5 py-1 text-xs hover:bg-muted/40">
              <span class="min-w-0 flex-1 truncate">{t.title}</span>
              <span class="shrink-0 text-[10px] text-muted-foreground">
                deleted {relativeTime(t.deleted_at)} by {t.deleted_by || 'unknown'}
              </span>
              <Button
                size="sm"
                variant="outline"
                class="h-6 shrink-0 gap-1 text-xs"
                disabled={restoring === t.slug}
                onclick={() => doRestore(t.slug)}
              >
                <RotateCcwIcon class="size-3" /> Restore
              </Button>
            </div>
          {/each}
        </div>
      {/if}
    </Tabs.Content>
  </Tabs.Root>
</div>
