<script lang="ts">
  import { onMount, untrack } from 'svelte'
  import {
    fetchAllEntities,
    fetchOntology,
    fetchGraph,
    type Entity,
    type EntityType,
    type Ontology
  } from '$lib/api'
  import { liveEvents, subscribeEvents } from '$lib/stores/events'
  import { isHealthEvent, applyHealthEvent } from '$lib/health'
  import EntityTable from '$lib/components/EntityTable.svelte'
  import FleetMap from '$lib/components/FleetMap.svelte'
  import { openEntityWindow, wmState } from '$lib/stores/windows'
  import { Button } from '$lib/components/ui/button'
  import { Input } from '$lib/components/ui/input'
  import { Switch } from '$lib/components/ui/switch'
  import { Label } from '$lib/components/ui/label'
  import NetworkIcon from '@lucide/svelte/icons/share-2'
  import TableIcon from '@lucide/svelte/icons/table-2'

  type View = 'graph' | 'table'

  function loadView(): View {
    if (typeof localStorage === 'undefined') return 'graph'
    return localStorage.getItem('oikos-kb-view') === 'table' ? 'table' : 'graph'
  }

  let view = $state<View>(loadView())
  // Shared between table (filters rows) and graph (highlights/searches
  // nodes) — one search box instead of two differently-labeled ones, since
  // both views are asking the same underlying question ("show me X").
  let search = $state('')
  // Tracks only the most recently opened entity, for row/node highlight —
  // actual detail viewing now happens in floating windows (WindowLayer, mounted inside Desktop.svelte),
  // which can have several entities open at once.
  let lastOpened = $state<string | null>(null)

  function setView(v: View) {
    view = v
    if (typeof localStorage !== 'undefined') localStorage.setItem('oikos-kb-view', v)
  }

  function select(slug: string | null) {
    lastOpened = slug
    openEntityWindow(slug)
  }

  // Drop the highlight once its window is closed (from WindowLayer, not
  // necessarily from here) rather than leaving a row/node marked "open" when
  // it isn't anymore.
  $effect(() => {
    if (lastOpened && !$wmState.windows[lastOpened]) lastOpened = null
  })

  // ─── entities: fetched here (not inside EntityTable) so the search toolbar
  // lives in the shared page toolbar instead of the resizable browse pane,
  // where its width is at the mercy of the divider and it would truncate.
  // Both views show the same set: the *fleet* — the exact scope of the fleet
  // map (see isFleetType), not a generic browse-everything entity list.
  let allEntities = $state<Entity[]>([])
  let entitiesLoading = $state(true)
  let showInactive = $state(false)
  // child entity slug -> parent entity slug, derived from the ontology graph
  // (see loadGrouping). Feeds EntityTable's treegrid grouping, nesting the
  // fleet exactly as the fleet map's lanes do: host -> lxc/vm -> service.
  let childToParent = $state<Map<string, string> | null>(null)

  let ontologyPromise: Promise<Ontology> | null = null
  function getOntology(): Promise<Ontology> {
    ontologyPromise ??= fetchOntology()
    return ontologyPromise
  }

  // The fleet = the same three kinds the fleet map shows: machines, VMs and
  // containers (everything under the abstract `compute-entity` root) plus
  // `service`. Derived from the ontology's own parent chain, not a hardcoded
  // type list, so any new compute/container subtype is included for free —
  // and generic software-domain types (config-repo, deploy-pipeline,
  // cluster, ...) that aren't fleet topology stay out.
  function isFleetType(byName: Map<string, EntityType>, typeName: string): boolean {
    if (typeName === 'service') return true
    let t = byName.get(typeName)
    for (let i = 0; t && i < 10; i++) {
      if (t.name === 'compute-entity') return true
      t = t.parent_type ? byName.get(t.parent_type) : undefined
    }
    return false
  }

  // Distance from the ontology's abstract root ("entity") down to typeName —
  // 0 for entity itself, 1 for its direct subtypes, etc. Used as a
  // specificity score: a relationship whose parent-side type is a generic
  // ancestor (e.g. `configured-by`'s source is literally `entity` — almost
  // everything is configured-by a repo) is a weaker structural signal than
  // one whose parent-side type is narrow and concrete (e.g. `hosts`' source
  // is `machine`, `member-of`'s is `proxmox-host`).
  function typeDepth(byName: Map<string, EntityType>, typeName: string): number {
    let depth = 0
    let t = byName.get(typeName)
    for (; t?.parent_type && depth < 10; depth++) t = byName.get(t.parent_type)
    return depth
  }

  // Nesting is derived entirely from the ontology's own relationship
  // definitions — no relationship or entity type names hardcoded here. Every
  // relationship whose cardinality isn't many-to-many has exactly one "one"
  // side, which is the parent: one-to-many/one-to-one -> source is parent
  // (e.g. machine hosts many compute-entities); many-to-one -> target is
  // parent (e.g. many hosts are located-at one site). many-to-many
  // relationships (mounts, stores-on, backs-up-to, ...) have no single
  // parent, so they're excluded from tree nesting. A candidate parent that
  // isn't actually part of the set being browsed (e.g. `cluster` or `site`,
  // outside the fleet scope) is dropped rather than kept as a dangling
  // pointer — that's
  // also what lets `located-at` surface as a host's parent instead of
  // `member-of` without any special-cased priority: with cluster absent,
  // member-of simply has nothing valid to point at. An entity can still be
  // the child end of several different *remaining* relationship types at
  // once (e.g. a service is `provides`-d by its lxc AND `configured-by` a
  // repo) — only one can win as its tree parent, so ties go to the more
  // specific relationship (see typeDepth) rather than whichever was fetched
  // last.
  async function loadGrouping(entities: Entity[]): Promise<Map<string, string>> {
    const { entityTypes, relationshipTypes } = await getOntology()
    const byName = new Map(entityTypes.map((t) => [t.name, t]))
    const hierRels = relationshipTypes.filter((rt) => rt.cardinality !== 'many-to-many')
    const relTypeNames = hierRels.map((rt) => rt.name)
    const cardinalityByType = new Map(hierRels.map((rt) => [rt.name, rt.cardinality]))
    const specificityByType = new Map(
      hierRels.map((rt) => [rt.name, typeDepth(byName, rt.source_type)])
    )
    const slugs = new Set(entities.map((e) => e.slug))

    // One whole-graph fetch instead of one rooted fetch per candidate parent
    // — now that grouping runs over the entire entity set rather than a
    // ~50-entity category, firing a request per entity blew past the
    // browser's concurrent-connection limit (ERR_INSUFFICIENT_RESOURCES).
    const g = await fetchGraph({ relType: relTypeNames })
    const pairs = (g?.edges ?? [])
      .filter(
        (edge) =>
          cardinalityByType.has(edge.type) && slugs.has(edge.source) && slugs.has(edge.target)
      )
      .map((edge) => {
        const [child, parent] =
          cardinalityByType.get(edge.type) === 'many-to-one'
            ? [edge.source, edge.target] // source is the child; target is the "one" (parent)
            : [edge.target, edge.source] // source is the "one" (parent); target is the child
        return { child, parent, weight: specificityByType.get(edge.type) ?? 0 }
      })
    const best = new Map<string, { parent: string; weight: number }>()
    for (const { child, parent, weight } of pairs) {
      const current = best.get(child)
      if (!current || weight > current.weight) best.set(child, { parent, weight })
    }
    return new Map([...best].map(([child, { parent }]) => [child, parent]))
  }

  async function loadEntities() {
    entitiesLoading = true
    const { entityTypes } = await getOntology()
    const byName = new Map(entityTypes.map((t) => [t.name, t]))
    // Scope to the fleet up front. This also drops non-fleet parents (site,
    // cluster) from the grouping input, so a host's `located-at`/`member-of`
    // edge has no valid parent to point at and the host stays top-level —
    // which is exactly the fleet map's arrangement (hosts anchor the tree).
    const fetched = (await fetchAllEntities()).filter((e) => isFleetType(byName, e.type))
    childToParent = await loadGrouping(fetched)
    allEntities = fetched
    entitiesLoading = false
  }

  onMount(() => {
    loadEntities()
    const unsubscribe = subscribeEvents()
    return unsubscribe
  })

  // Health arrives on its own events, not entity.*, so the table's Health
  // column used to sit at whatever it was when the page mounted while the
  // graph view beside it updated live.
  //
  // Patched in place rather than refetched: the event already carries the new
  // health, so a fleet-wide reload (plus its parent-grouping pass) would be a
  // round trip to learn something we were just told — and would churn the
  // whole table on every transition. Only entity.* changes the SET of
  // entities, so only that needs a fetch.
  $effect(() => {
    const ev = $liveEvents[0]
    if (!ev) return
    if (ev.type.startsWith('entity.')) {
      loadEntities()
      return
    }
    // untrack: reads and writes allEntities. applyHealthEvent returns the
    // same reference when nothing changed so it would settle, but relying on
    // that to break a feedback loop is too subtle to leave implicit.
    if (isHealthEvent(ev)) untrack(() => (allEntities = applyHealthEvent(allEntities, ev)))
  })

  const filteredEntities = $derived.by(() => {
    const q = search.trim().toLowerCase()
    return allEntities.filter((e) => {
      if (q && !e.slug.toLowerCase().includes(q) && !e.name.toLowerCase().includes(q)) return false
      // entities with no tracked lifecycle state (state is null) aren't
      // "destroyed or inactive" — only hide ones whose tracked state has
      // moved off `active`.
      if (!showInactive && e.state && e.state !== 'active') return false
      return true
    })
  })
</script>

<div class="flex h-full flex-col gap-3 p-2">
  <!-- single toolbar row: the search box is shared by both views (graph
       highlights nodes, table filters rows); the Inactive toggle + count are
       table-only; the graph/table switch sits inline on the right. -->
  <div class="flex flex-wrap items-center gap-2">
    <Input
      placeholder="Filter / highlight by slug or name…"
      bind:value={search}
      class="h-8 max-w-xs text-xs"
    />

    {#if view === 'table'}
      <div class="flex items-center gap-1.5">
        <Switch id="show-inactive" bind:checked={showInactive} />
        <Label for="show-inactive" class="text-xs font-normal text-muted-foreground">Inactive</Label
        >
      </div>
      <span class="text-xs text-muted-foreground"
        >{filteredEntities.length} of {allEntities.length}</span
      >
    {/if}

    <div class="ml-auto inline-flex overflow-hidden rounded-md border">
      <Button
        variant={view === 'graph' ? 'secondary' : 'ghost'}
        size="sm"
        class="h-8 rounded-none border-0 px-2"
        onclick={() => setView('graph')}
        title="Graph view"
        aria-label="Graph view"
      >
        <NetworkIcon class="size-3.5" />
      </Button>
      <Button
        variant={view === 'table' ? 'secondary' : 'ghost'}
        size="sm"
        class="h-8 rounded-none border-0 px-2"
        onclick={() => setView('table')}
        title="Table view"
        aria-label="Table view"
      >
        <TableIcon class="size-3.5" />
      </Button>
    </div>
  </div>

  <!-- browse pane — selecting an entity opens it in a floating window
       (WindowLayer, mounted globally inside Desktop.svelte) instead of a sidebar. -->
  <div class="flex min-h-0 min-w-0 flex-1 flex-col">
    {#if view === 'graph'}
      <FleetMap selectedSlug={lastOpened} onSelect={select} {search} />
    {:else}
      <EntityTable
        entities={filteredEntities}
        loading={entitiesLoading}
        selectedSlug={lastOpened}
        onSelect={select}
        {childToParent}
      />
    {/if}
  </div>
</div>
