<script lang="ts">
  import type { Entity, EntityHealth } from '$lib/api'
  import { relativeTime } from '$lib/utils'
  import * as Table from '$lib/components/ui/table'
  import { Badge } from '$lib/components/ui/badge'
  import { Skeleton } from '$lib/components/ui/skeleton'
  import ArrowUpIcon from '@lucide/svelte/icons/arrow-up'
  import ArrowDownIcon from '@lucide/svelte/icons/arrow-down'
  import ArrowUpDownIcon from '@lucide/svelte/icons/arrow-up-down'
  import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
  import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'

  let {
    entities,
    loading,
    selectedSlug = null,
    onSelect,
    childToParent = null
  }: {
    entities: Entity[]
    loading: boolean
    selectedSlug?: string | null
    onSelect: (slug: string) => void
    // child entity slug -> parent entity slug, derived from the ontology
    // graph (arbitrary relationship types, not a fixed list — see
    // KnowledgeBase.svelte). When set, rows nest under their parent —
    // possibly several levels deep (host -> lxc -> service) — instead of
    // rendering flat. Since the parent for a given child can come from
    // whichever relationship happened to be processed last, a cycle across
    // relationship types isn't structurally impossible; `row` tracks the
    // ancestor chain and drops a child that would re-enter it, rather than
    // recursing forever.
    childToParent?: Map<string, string> | null
  } = $props()

  type SortKey = 'slug' | 'type' | 'name' | 'state' | 'health'
  let sortKey = $state<SortKey>('slug')
  let sortDir = $state<'asc' | 'desc'>('asc')
  let collapsedNodes = $state<Set<string>>(new Set())

  function toggleNode(slug: string, e: Event) {
    e.stopPropagation()
    const next = new Set(collapsedNodes)
    if (next.has(slug)) next.delete(slug)
    else next.add(slug)
    collapsedNodes = next
  }

  function sortBy(key: SortKey) {
    if (sortKey === key) {
      sortDir = sortDir === 'asc' ? 'desc' : 'asc'
    } else {
      sortKey = key
      sortDir = 'asc'
    }
  }

  const healthRank: Record<EntityHealth, number> = { down: 0, degraded: 1, stale: 2, unknown: 3, healthy: 4 }

  function sortValue(entity: Entity, key: SortKey): string | number {
    if (key === 'health') return entity.health ? healthRank[entity.health] : -1
    return (entity[key] ?? '').toString().toLowerCase()
  }

  const sortedEntities = $derived.by(() => {
    const sorted = [...entities].sort((a, b) => {
      const av = sortValue(a, sortKey)
      const bv = sortValue(b, sortKey)
      if (av < bv) return -1
      if (av > bv) return 1
      return 0
    })
    if (sortDir === 'desc') sorted.reverse()
    return sorted
  })

  // ─── treegrid grouping: nest entities under their parent (per
  // childToParent — host->lxc via `hosts`, lxc/vm/host->service via
  // `provides`, chained to whatever depth the relationships form). An entity
  // whose parent got filtered out of `entities` (e.g. by the type dropdown)
  // has no parent row to nest under, so it falls back to rendering top-level
  // rather than disappearing.
  const childrenByParent = $derived.by(() => {
    const map = new Map<string, Entity[]>()
    if (!childToParent) return map
    const visibleSlugs = new Set(entities.map((e) => e.slug))
    for (const e of sortedEntities) {
      const parentSlug = childToParent.get(e.slug)
      if (parentSlug && visibleSlugs.has(parentSlug)) {
        if (!map.has(parentSlug)) map.set(parentSlug, [])
        map.get(parentSlug)!.push(e)
      }
    }
    return map
  })

  const nestedSlugs = $derived.by(() => {
    const set = new Set<string>()
    for (const children of childrenByParent.values()) for (const c of children) set.add(c.slug)
    return set
  })

  const topLevelEntities = $derived.by(() =>
    childToParent ? sortedEntities.filter((e) => !nestedSlugs.has(e.slug)) : sortedEntities
  )

  function stateVariant(state?: string | null): 'default' | 'secondary' | 'outline' {
    if (!state) return 'outline'
    if (state === 'active' || state === 'healthy') return 'default'
    return 'secondary'
  }

  const healthDot: Record<EntityHealth, string> = {
    healthy: 'bg-success',
    degraded: 'bg-warning',
    down: 'bg-destructive',
    stale: 'bg-warning/50',
    unknown: 'bg-muted-foreground/40'
  }

  function healthTitle(entity: Entity): string {
    if (!entity.health) return 'not monitored'
    if (entity.health === 'stale') return `stale — last checked ${relativeTime(entity.last_check_at)}`
    return `${entity.health} — checked ${relativeTime(entity.last_check_at)}`
  }

  // Widths vary per row so the skeleton reads as text, not a stack of identical bars.
  const skeletonSlugWidths = ['w-24', 'w-20', 'w-28', 'w-16', 'w-24', 'w-20', 'w-28', 'w-16']
  const skeletonNameWidths = ['w-32', 'w-40', 'w-24', 'w-36', 'w-28', 'w-40', 'w-24', 'w-32']
</script>

{#if loading}
  <div class="h-full min-h-0 overflow-hidden rounded-md border">
    <Table.Root>
      <Table.Header>
        <Table.Row>
          <Table.Head>Slug</Table.Head>
          <Table.Head>Type</Table.Head>
          <Table.Head>Name</Table.Head>
          <Table.Head>State</Table.Head>
          <Table.Head>Health</Table.Head>
        </Table.Row>
      </Table.Header>
      <Table.Body>
        {#each skeletonSlugWidths as slugWidth, i}
          <Table.Row class="hover:bg-transparent">
            <Table.Cell><Skeleton class="h-4 {slugWidth}" /></Table.Cell>
            <Table.Cell><Skeleton class="h-5 w-16 rounded-full" /></Table.Cell>
            <Table.Cell><Skeleton class="h-4 {skeletonNameWidths[i]}" /></Table.Cell>
            <Table.Cell><Skeleton class="h-5 w-14 rounded-full" /></Table.Cell>
            <Table.Cell>
              <div class="flex items-center gap-1.5">
                <Skeleton class="size-2 shrink-0 rounded-full" />
                <Skeleton class="h-4 w-12" />
              </div>
            </Table.Cell>
          </Table.Row>
        {/each}
      </Table.Body>
    </Table.Root>
  </div>
{:else}
  {#snippet sortHead(key: SortKey, label: string)}
    <Table.Head>
      <button type="button" class="flex items-center gap-1 hover:text-foreground" onclick={() => sortBy(key)}>
        {label}
        {#if sortKey === key}
          {#if sortDir === 'asc'}
            <ArrowUpIcon class="size-3" />
          {:else}
            <ArrowDownIcon class="size-3" />
          {/if}
        {:else}
          <ArrowUpDownIcon class="size-3 text-muted-foreground/50" />
        {/if}
      </button>
    </Table.Head>
  {/snippet}
  {#snippet row(entity: Entity, level: number, ancestors: Set<string>)}
    {@const ancestorsWithSelf = new Set(ancestors).add(entity.slug)}
    {@const children = (childrenByParent.get(entity.slug) ?? []).filter((c) => !ancestorsWithSelf.has(c.slug))}
    <Table.Row
      class="cursor-pointer {entity.slug === selectedSlug ? 'bg-muted' : ''}"
      role="row"
      aria-level={level}
      aria-expanded={children.length > 0 ? !collapsedNodes.has(entity.slug) : undefined}
      tabindex={0}
      onclick={() => onSelect(entity.slug)}
      onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(entity.slug) } }}
    >
      <Table.Cell class="font-mono text-xs">
        <span class="flex items-center gap-1" style="padding-left: {(level - 1) * 1.25}rem">
          <span class="inline-flex size-3.5 shrink-0 items-center justify-center">
            {#if children.length > 0}
              <button
                type="button"
                class="rounded text-muted-foreground hover:text-foreground"
                onclick={(e) => toggleNode(entity.slug, e)}
                aria-label={collapsedNodes.has(entity.slug) ? `Expand ${entity.slug}` : `Collapse ${entity.slug}`}
              >
                {#if collapsedNodes.has(entity.slug)}
                  <ChevronRightIcon class="size-3.5" />
                {:else}
                  <ChevronDownIcon class="size-3.5" />
                {/if}
              </button>
            {/if}
          </span>
          {entity.slug}
          {#if children.length > 0}
            <span class="text-muted-foreground">({children.length})</span>
          {/if}
        </span>
      </Table.Cell>
      <Table.Cell><Badge variant="outline">{entity.type}</Badge></Table.Cell>
      <Table.Cell>{entity.name}</Table.Cell>
      <Table.Cell>
        {#if entity.state}
          <Badge variant={stateVariant(entity.state)}>{entity.state}</Badge>
        {:else}
          <span class="text-muted-foreground">—</span>
        {/if}
      </Table.Cell>
      <Table.Cell>
        {#if entity.health}
          <span class="flex items-center gap-1.5 text-xs" title={healthTitle(entity)}>
            <span class="size-2 shrink-0 rounded-full {healthDot[entity.health]}"></span>
            <span class="text-muted-foreground">{relativeTime(entity.last_check_at)}</span>
          </span>
        {:else}
          <span class="text-xs text-muted-foreground">—</span>
        {/if}
      </Table.Cell>
    </Table.Row>
    {#if children.length > 0 && !collapsedNodes.has(entity.slug)}
      {#each children as child (child.id)}
        {@render row(child, level + 1, ancestorsWithSelf)}
      {/each}
    {/if}
  {/snippet}
  <div class="h-full min-h-0 overflow-auto rounded-md border">
    <Table.Root role={childToParent ? 'treegrid' : undefined}>
      <Table.Header>
        <Table.Row>
          {@render sortHead('slug', 'Slug')}
          {@render sortHead('type', 'Type')}
          {@render sortHead('name', 'Name')}
          {@render sortHead('state', 'State')}
          {@render sortHead('health', 'Health')}
        </Table.Row>
      </Table.Header>
      <Table.Body>
        {#each topLevelEntities as entity (entity.id)}
          {@render row(entity, 1, new Set())}
        {:else}
          <Table.Row>
            <Table.Cell colspan={5} class="text-center text-muted-foreground"
              >No entities in this layer match the filter.</Table.Cell
            >
          </Table.Row>
        {/each}
      </Table.Body>
    </Table.Root>
  </div>
{/if}
