<script lang="ts">
  import { onMount } from "svelte";
  import {
    listDistros,
    saveWorkspace,
    loadWorkspace,
    writeToPane,
    killPane,
  } from "./ipc";
  import Pane from "./lib/layout/Pane.svelte";
  import Notifications from "./components/Notifications.svelte";
  import Palette from "./components/Palette.svelte";
  import {
    provideOrchestration,
    type TreeOps,
  } from "./lib/layout/orchestration.svelte";
  import {
    type TreeNode,
    type NodeId,
    type Orientation,
    type LeafNode,
    newLeaf,
    splitLeaf,
    closeLeaf,
    findLeaf,
    leafCount,
    walkLeaves,
    changeDistro,
    changeLabel,
    toggleBroadcast as toggleBroadcastInTree,
    serialize,
    deserialize,
    presetSingle,
    presetTwoColumns,
    presetThreeColumns,
    presetTwoRows,
    presetTwoByTwo,
  } from "./lib/layout/tree";

  const LEGACY_STORAGE_KEY = "tiletopia.tree.v1";

  let defaultDistro = $state<string | undefined>(undefined);
  let ready = $state(false);
  let tree = $state<TreeNode>(newLeaf());
  let paletteOpen = $state(false);

  // activeLeafId lives here (not on the orch class) because Svelte 5 didn't
  // reliably track class-field $state reads from child components that
  // obtained the orch instance via getContext. Local $state drilled via
  // prop works.
  let activeLeafId = $state<NodeId | null>(null);
  function setActive(id: NodeId) {
    activeLeafId = id;
  }
  function clearActiveIf(id: NodeId) {
    if (activeLeafId === id) activeLeafId = null;
  }

  // ---- tree mutation handlers (closures over tree $state) -----------------
  function handleSplit(leafId: NodeId, orientation: Orientation) {
    const parent = findLeaf(tree, leafId);
    const inherit = parent
      ? { distro: parent.distro ?? defaultDistro, cwd: parent.cwd }
      : { distro: defaultDistro };
    tree = splitLeaf(tree, leafId, orientation, inherit);
  }

  function handleClose(leafId: NodeId) {
    // Kill the PTY explicitly — Svelte's onDestroy on LeafPane won't fire
    // because Svelte isn't unmounting the component (same reactivity gap as
    // the active/broadcast bugs).
    const paneId = orch.paneIdByLeaf.get(leafId);
    if (paneId != null) {
      void killPane(paneId).catch((e) => console.warn("killPane failed:", e));
      orch.paneIdByLeaf.delete(leafId);
    }

    // Brute-force hide the closed pane's flex side + the adjacent gutter so
    // the sibling pane visually fills the freed space.
    const leafEl = document.querySelector(`[data-leaf-id="${leafId}"]`);
    const sideEl = leafEl?.closest(".side") as HTMLElement | null;
    const splitEl = sideEl?.parentElement;
    if (sideEl && splitEl) {
      sideEl.style.display = "none";
      Array.from(splitEl.children).forEach((c) => {
        const child = c as HTMLElement;
        if (child.classList.contains("gutter")) child.style.display = "none";
      });
    }

    // Update tree state (used by broadcast routing, palette, persistence).
    const next = closeLeaf(tree, leafId);
    tree = next ?? newLeaf({ distro: defaultDistro });
    clearActiveIf(leafId);
  }

  function handleSetDistro(leafId: NodeId, distro: string) {
    tree = changeDistro(tree, leafId, distro);
  }

  function handleSetLabel(leafId: NodeId, label: string | undefined) {
    tree = changeLabel(tree, leafId, label);
  }

  function handleToggleBroadcast(leafId: NodeId) {
    tree = toggleBroadcastInTree(tree, leafId);
  }

  function handleBroadcastFrom(originLeafId: NodeId, dataB64: string) {
    let peers = 0;
    for (const leaf of walkLeaves(tree)) {
      if (leaf.id === originLeafId) continue;
      if (!leaf.broadcast) continue;
      const paneId = orch.paneIdByLeaf.get(leaf.id);
      if (paneId == null) {
        console.warn("[tiletopia] broadcast peer has no paneId yet:", leaf.id);
        continue;
      }
      peers++;
      writeToPane(paneId, dataB64).catch((e) =>
        console.warn("[tiletopia] broadcast write failed:", e),
      );
    }
    console.log("[tiletopia] broadcastFrom", originLeafId, "→", peers, "peer(s)");
  }

  const treeOps: TreeOps = {
    split: handleSplit,
    close: handleClose,
    setDistro: handleSetDistro,
    setLabel: handleSetLabel,
    toggleBroadcast: handleToggleBroadcast,
    broadcastFrom: handleBroadcastFrom,
  };

  // Provide the orchestration store. All Pane / SplitNode / LeafPane
  // descendants consume it via `useOrchestration()` — no prop drilling.
  const orch = provideOrchestration(treeOps);
  orch.configureActiveHandlers(setActive, clearActiveIf);

  function isInteractiveDistro(name: string): boolean {
    return !name.toLowerCase().startsWith("docker-desktop");
  }

  onMount(async () => {
    // 1. Try APPDATA persistence.
    let loaded: TreeNode | null = null;
    try {
      const json = await loadWorkspace();
      if (json) loaded = deserialize(json);
    } catch (e) {
      console.warn("loadWorkspace failed:", e);
    }

    // 2. Migrate from M2 localStorage if APPDATA is empty.
    if (!loaded) {
      try {
        const legacy = localStorage.getItem(LEGACY_STORAGE_KEY);
        if (legacy) {
          loaded = deserialize(legacy);
          if (loaded) void saveWorkspace(legacy);
          localStorage.removeItem(LEGACY_STORAGE_KEY);
        }
      } catch (e) {
        console.warn("legacy localStorage migration failed:", e);
      }
    }

    if (loaded) tree = loaded;

    // 3. Resolve default distro.
    try {
      const ds = await listDistros();
      orch.distros = ds;
      defaultDistro = ds.find(isInteractiveDistro) ?? ds[0];
    } catch (e) {
      console.warn("list_distros failed:", e);
    }

    if (defaultDistro) backfillDistro(tree, defaultDistro);
    ready = true;
  });

  function backfillDistro(node: TreeNode, fallback: string) {
    if (node.kind === "leaf") {
      if (!node.distro) node.distro = fallback;
    } else {
      backfillDistro(node.a, fallback);
      backfillDistro(node.b, fallback);
    }
  }

  // ---- debounced auto-save -------------------------------------------------
  let saveTimer: number | null = null;
  const SAVE_DEBOUNCE_MS = 500;

  $effect(() => {
    if (!ready) return;
    const json = serialize(tree);
    if (saveTimer != null) clearTimeout(saveTimer);
    saveTimer = window.setTimeout(() => {
      saveTimer = null;
      saveWorkspace(json).catch((e) =>
        console.warn("saveWorkspace failed:", e),
      );
    }, SAVE_DEBOUNCE_MS);
  });

  // ---- Ctrl+K palette toggle ----------------------------------------------
  // Capture phase so we win over xterm.js's keystroke capture inside terminals.
  $effect(() => {
    function onKey(e: KeyboardEvent) {
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
        console.log("[tiletopia] Ctrl+K caught, paletteOpen ->", !paletteOpen);
        e.preventDefault();
        e.stopPropagation();
        paletteOpen = !paletteOpen;
      }
    }
    window.addEventListener("keydown", onKey, true);
    return () => window.removeEventListener("keydown", onKey, true);
  });

  // ---- Active-pane detector via active-element polling --------------------
  // We tried letting Svelte handle class:active reactively in LeafPane, but
  // through this app's component chain the prop changes don't trigger a
  // template re-evaluation reliably (root cause unclear — likely a Svelte 5
  // interaction with our recursive Pane / setInterval pattern). So we ALSO
  // manipulate `.leaf.active` directly via DOM as a backstop.
  $effect(() => {
    let lastLeafId: string | null = null;
    const interval = window.setInterval(() => {
      const el = document.activeElement;
      const leafEl = el?.closest("[data-leaf-id]");
      const id = leafEl?.getAttribute("data-leaf-id") ?? null;
      if (id && id !== lastLeafId) {
        lastLeafId = id;
        setActive(id);
      }
      // Active-border DOM sync (same workaround as below).
      if (id) {
        document.querySelectorAll("[data-leaf-id].leaf").forEach((el) => {
          if (el.getAttribute("data-leaf-id") === id) el.classList.add("active");
          else el.classList.remove("active");
        });
      }
      // Broadcast-state DOM sync — Svelte's class:broadcasting={leaf.broadcast}
      // and class:on={leaf.broadcast} bindings in LeafPane don't propagate
      // either. Walk the tree and force the classes to match leaf.broadcast.
      for (const leaf of walkLeaves(tree)) {
        const leafEl = document.querySelector(`[data-leaf-id="${leaf.id}"]`);
        if (!leafEl) continue;
        leafEl.classList.toggle("broadcasting", !!leaf.broadcast);
        const chip = leafEl.querySelector(".bcast-chip");
        if (chip) chip.classList.toggle("on", !!leaf.broadcast);
      }
    }, 250);
    return () => clearInterval(interval);
  });

  // ---- preset layouts ------------------------------------------------------
  function applyPreset(make: (d: { distro?: string }) => TreeNode) {
    const count = leafCount(tree);
    if (count > 1 && !confirm(`Replace current layout (${count} panes)? This kills all open shells.`)) {
      return;
    }
    tree = make({ distro: defaultDistro });
  }

  // ---- palette feed --------------------------------------------------------
  const paletteLeaves = $derived.by<LeafNode[]>(() => {
    if (!paletteOpen) return [];
    return Array.from(walkLeaves(tree));
  });

  function onPalettePick(leafId: string) {
    setActive(leafId);
    paletteOpen = false;
  }
</script>

<div class="app">
  <header class="titlebar">
    <span class="label">tiletopia</span>

    <span class="distros">
      {#if orch.distros.length === 0}
        <span class="muted">no distros enumerated</span>
      {:else}
        <span class="muted">default:</span>
        {#each orch.distros as d}
          <button
            class="distro-btn"
            class:active={d === defaultDistro}
            onclick={() => (defaultDistro = d)}
            title="Set default distro for new panes"
          >{d}</button>
        {/each}
      {/if}
    </span>

    <span class="presets">
      <span class="muted">layout:</span>
      <button class="preset-btn" title="Single pane" onclick={() => applyPreset(presetSingle)}>1</button>
      <button class="preset-btn" title="Two columns" onclick={() => applyPreset(presetTwoColumns)}>2H</button>
      <button class="preset-btn" title="Three columns" onclick={() => applyPreset(presetThreeColumns)}>3H</button>
      <button class="preset-btn" title="Two rows" onclick={() => applyPreset(presetTwoRows)}>2V</button>
      <button class="preset-btn" title="2 × 2 grid" onclick={() => applyPreset(presetTwoByTwo)}>2×2</button>
    </span>

    <button class="palette-btn" onclick={() => (paletteOpen = true)} title="Jump to pane (Ctrl+K)">
      ⌘K
    </button>
    <button class="palette-btn" onclick={() => orch.notify("test toast at " + new Date().toLocaleTimeString())} title="Fire a test toast">
      🔔
    </button>

    <span class="layout-info">
      {leafCount(tree)} pane{leafCount(tree) === 1 ? "" : "s"}
    </span>
  </header>

  <div class="pane-wrap">
    {#if ready}
      <Pane node={tree} {activeLeafId} />
    {/if}
  </div>

  <Notifications
    notifications={orch.notifications}
    onDismiss={(id) => orch.dismiss(id)}
  />

  {#if paletteOpen}
    <Palette
      leaves={paletteLeaves}
      onPick={onPalettePick}
      onClose={() => (paletteOpen = false)}
    />
  {/if}
</div>

<style>
  .distros, .presets {
    display: flex;
    gap: 4px;
    align-items: center;
  }
  .distro-btn, .preset-btn, .palette-btn {
    font: inherit;
    font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace;
    font-size: 11px;
    background: #222;
    color: #aaa;
    border: 1px solid #333;
    border-radius: 3px;
    padding: 2px 8px;
    cursor: pointer;
  }
  .distro-btn:hover, .preset-btn:hover, .palette-btn:hover {
    background: #2a2a2a;
    color: #ddd;
  }
  .distro-btn.active {
    background: #1a3a5c;
    color: #cce6ff;
    border-color: #2a5a8c;
  }
  .preset-btn {
    min-width: 28px;
    text-align: center;
  }
  .muted {
    color: #666;
    font-style: italic;
  }
  .layout-info {
    margin-left: auto;
    font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace;
    color: #777;
    font-size: 11px;
  }
</style>
