import {
  useState,
  useEffect,
  useRef,
  useCallback,
  type KeyboardEvent,
  type MouseEvent,
  type PointerEvent as ReactPointerEvent,
} from "react";
import { createPortal } from "react-dom";
import { type LeafNode, resolveFontSize, type LeafShellSpec } from "./tree";
import { useOrchestration } from "./orchestration";
import XtermPane from "../../components/XtermPane";
import type { SpawnSpec } from "../../ipc";
import "./LeafPane.css";

const IDLE_THRESHOLD_MS = 5000;

/** How far past a viewport edge the cursor must travel before a release is
 *  treated as "drag pane out of window" instead of "drop on empty space
 *  inside this window". Picked so an accidental release on the OS titlebar
 *  (~30px tall) stays inside the threshold. */
const PANE_DRAG_OUT_MARGIN = 60;

/** True when a point is past any viewport edge by PANE_DRAG_OUT_MARGIN. */
const isFarOutsideViewport = (x: number, y: number) =>
  x < -PANE_DRAG_OUT_MARGIN ||
  x > window.innerWidth + PANE_DRAG_OUT_MARGIN ||
  y < -PANE_DRAG_OUT_MARGIN ||
  y > window.innerHeight + PANE_DRAG_OUT_MARGIN;

export default function LeafPane({ leaf }: { leaf: LeafNode }) {
  const orch = useOrchestration();
  const isActive = orch.activeLeafId === leaf.id;
  const isBroadcasting = !!leaf.broadcast;

  // ---- status (from XtermPane) -------------------------------------------
  const [status, setStatus] = useState("starting…");
  const [statusOk, setStatusOk] = useState(true);

  // ---- label editing -----------------------------------------------------
  const [editingLabel, setEditingLabel] = useState(false);
  const [labelDraft, setLabelDraft] = useState("");
  const labelInputRef = useRef<HTMLInputElement | null>(null);

  const startEditLabel = useCallback(
    (e: MouseEvent) => {
      e.stopPropagation();
      setLabelDraft(leaf.label ?? "");
      setEditingLabel(true);
      // Focus on next tick so input is mounted
      queueMicrotask(() => labelInputRef.current?.select());
    },
    [leaf.label],
  );
  const commitLabel = useCallback(() => {
    if (!editingLabel) return;
    orch.setLabel(leaf.id, labelDraft);
    setEditingLabel(false);
  }, [editingLabel, orch.setLabel, leaf.id, labelDraft]);
  const cancelLabel = useCallback(() => setEditingLabel(false), []);
  const onLabelKey = useCallback(
    (e: KeyboardEvent<HTMLInputElement>) => {
      if (e.key === "Enter") {
        e.preventDefault();
        commitLabel();
      } else if (e.key === "Escape") {
        e.preventDefault();
        cancelLabel();
      }
    },
    [commitLabel, cancelLabel],
  );

  // ---- shell-picker popover ----------------------------------------------
  // Hierarchical menu: WSL distros, then Windows (PowerShell), then SSH
  // hosts + a "Manage hosts…" entry. Picking any item swaps the leaf id
  // (forces respawn).
  const [shellMenuOpen, setShellMenuOpen] = useState(false);
  const toggleShellMenu = useCallback((e: MouseEvent) => {
    e.stopPropagation();
    setShellMenuOpen((v) => !v);
  }, []);
  const pickShell = useCallback(
    (spec: LeafShellSpec) => {
      setShellMenuOpen(false);
      // Only respawn if the spec is actually different from what's running.
      if (spec.shellKind === "wsl" && leaf.shellKind === "wsl" && spec.distro === leaf.distro) {
        return;
      }
      if (spec.shellKind === "powershell" && leaf.shellKind === "powershell") {
        return;
      }
      if (
        spec.shellKind === "ssh" &&
        leaf.shellKind === "ssh" &&
        spec.sshHostId === leaf.sshHostId
      ) {
        return;
      }
      orch.setShell(leaf.id, spec);
    },
    [orch.setShell, leaf.id, leaf.shellKind, leaf.distro, leaf.sshHostId],
  );
  const onManageHosts = useCallback(
    (e: MouseEvent) => {
      e.stopPropagation();
      setShellMenuOpen(false);
      orch.openHostManager();
    },
    [orch.openHostManager],
  );
  // Dismiss popover on outside click
  useEffect(() => {
    if (!shellMenuOpen) return;
    const onDocClick = () => setShellMenuOpen(false);
    window.addEventListener("click", onDocClick);
    return () => window.removeEventListener("click", onDocClick);
  }, [shellMenuOpen]);

  // Label shown on the dropdown chip — tells the user what's currently
  // running without expanding the menu.
  const chipLabel =
    leaf.shellKind === "powershell"
      ? "PowerShell"
      : leaf.shellKind === "ssh"
        ? `ssh: ${orch.hosts.find((h) => h.id === leaf.sshHostId)?.label ?? "(missing host)"}`
        : (leaf.distro ?? "(default)");

  // ---- idle detection ----------------------------------------------------
  // Local boolean for the red border + status text on this pane; reported
  // up to App via orch.reportLeafIdle for the titlebar's "N idle" badge.
  const lastDataTimeRef = useRef(Date.now());
  const [isIdle, setIsIdle] = useState(false);
  const onDataReceived = useCallback(() => {
    lastDataTimeRef.current = Date.now();
    setIsIdle((cur) => {
      if (cur) orch.reportLeafIdle(leaf.id, false);
      return false;
    });
  }, [orch.reportLeafIdle, leaf.id]);
  useEffect(() => {
    const id = window.setInterval(() => {
      const dt = Date.now() - lastDataTimeRef.current;
      const nowIdle = dt >= IDLE_THRESHOLD_MS;
      setIsIdle((cur) => {
        if (cur === nowIdle) return cur;
        orch.reportLeafIdle(leaf.id, nowIdle);
        return nowIdle;
      });
    }, 1000);
    return () => clearInterval(id);
  }, [leaf.id, orch.reportLeafIdle]);
  // Clear from the app-level idle set when this pane unmounts.
  useEffect(() => {
    return () => orch.reportLeafIdle(leaf.id, false);
  }, [leaf.id, orch.reportLeafIdle]);

  // ---- broadcast ---------------------------------------------------------
  const onTerminalInput = useCallback(
    (b64: string) => {
      if (isBroadcasting) orch.broadcastFrom(leaf.id, b64);
    },
    [isBroadcasting, orch.broadcastFrom, leaf.id],
  );

  // ---- focus / active highlighting ---------------------------------------
  const [focusTrigger, setFocusTrigger] = useState(0);
  // When this leaf becomes active, bump focusTrigger so XtermPane refocuses.
  useEffect(() => {
    if (isActive) setFocusTrigger((n) => n + 1);
  }, [isActive]);

  const onPaneClick = useCallback(() => {
    orch.setActive(leaf.id);
  }, [orch.setActive, leaf.id]);

  const onPaneSpawned = useCallback(
    (paneId: number) => {
      orch.registerPaneId(leaf.id, paneId);
    },
    [orch.registerPaneId, leaf.id],
  );
  // Unregister on TRUE unmount only — depending on `orch` here would
  // delete the paneId from App's lookup on every activeLeafId change,
  // which broke broadcast routing (peers found, but their paneIds
  // had been silently removed from the map).
  useEffect(() => {
    return () => orch.registerPaneId(leaf.id, null);
  }, [orch.registerPaneId, leaf.id]);

  const onXtermFocus = useCallback(
    () => orch.setActive(leaf.id),
    [orch.setActive, leaf.id],
  );

  const onStatus = useCallback((msg: string, ok: boolean) => {
    setStatus(msg);
    setStatusOk(ok);
  }, []);

  // ---- right-click context menu ------------------------------------------
  // Single entry in v1: "Move to new window" (pops the pane out into a
  // fresh top-level tiletopia window without losing the PTY).
  const [menuPos, setMenuPos] = useState<{ x: number; y: number } | null>(null);
  const openContextMenu = useCallback(
    (e: MouseEvent<HTMLDivElement>) => {
      e.preventDefault();
      e.stopPropagation();
      setMenuPos({ x: e.clientX, y: e.clientY });
    },
    [],
  );
  const closeContextMenu = useCallback(() => setMenuPos(null), []);
  useEffect(() => {
    if (!menuPos) return;
    const onDocClick = () => setMenuPos(null);
    const onEsc = (e: globalThis.KeyboardEvent) => {
      if (e.key === "Escape") setMenuPos(null);
    };
    // Defer attaching the click listener so the click that opened the menu
    // doesn't immediately close it.
    const t = window.setTimeout(() => {
      window.addEventListener("click", onDocClick);
      window.addEventListener("keydown", onEsc, true);
    }, 0);
    return () => {
      clearTimeout(t);
      window.removeEventListener("click", onDocClick);
      window.removeEventListener("keydown", onEsc, true);
    };
  }, [menuPos]);

  // ---- header-drag swap ---------------------------------------------------
  // Drag the toolbar onto another pane's toolbar/body to swap their tree
  // positions. Uses a movement threshold so accidental tiny moves while
  // clicking a label etc don't initiate a drag.
  const DRAG_THRESHOLD_PX = 5;
  const dragStartRef = useRef<{ x: number; y: number; armed: boolean; dragging: boolean } | null>(
    null,
  );
  // Cursor-following ghost shown while dragging the toolbar. `detach` flips
  // true once the cursor is past the viewport edge by PANE_DRAG_OUT_MARGIN,
  // mirroring the release condition in onToolbarPointerUp so the ghost
  // previews what a release right now would do.
  const [dragGhost, setDragGhost] = useState<{
    x: number;
    y: number;
    detach: boolean;
    flipX: boolean;
    flipY: boolean;
  } | null>(null);
  const isDragSource = orch.dragSourceId === leaf.id;
  const isDragTarget =
    orch.dragOverId === leaf.id && orch.dragSourceId !== leaf.id;

  const onToolbarPointerDown = useCallback(
    (e: ReactPointerEvent<HTMLDivElement>) => {
      const target = e.target as HTMLElement;
      // Skip if the click landed on an interactive child.
      if (target.closest("button, input, .distro-menu")) return;
      (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
      dragStartRef.current = {
        x: e.clientX,
        y: e.clientY,
        armed: true,
        dragging: false,
      };
      // Make this pane active (since clicking the toolbar should focus it).
      orch.setActive(leaf.id);
    },
    [orch.setActive, leaf.id],
  );

  const onToolbarPointerMove = useCallback(
    (e: ReactPointerEvent<HTMLDivElement>) => {
      const st = dragStartRef.current;
      if (!st || !st.armed) return;
      const dx = e.clientX - st.x;
      const dy = e.clientY - st.y;
      if (!st.dragging) {
        if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return;
        st.dragging = true;
        orch.beginHeaderDrag(leaf.id);
        document.body.style.cursor = "grabbing";
      }
      // Find the leaf under the cursor.
      const el = document.elementFromPoint(e.clientX, e.clientY);
      const tEl = el?.closest("[data-leaf-id]");
      const targetId = tEl?.getAttribute("data-leaf-id") ?? null;
      orch.setHeaderDragOver(targetId);
      // Move the cursor-following ghost (B1). It has pointer-events:none so
      // it doesn't interfere with the elementFromPoint hit-test above.
      // A webview can't paint outside its own OS window, so once the cursor
      // crosses the edge we clamp the chip to the viewport (and flip it to
      // the cursor's inner side near right/bottom) so it stays visible and
      // its `detach` styling is what previews the release. `detach` itself
      // is computed from the RAW cursor position so the preview is accurate.
      const GHOST_PAD = 4;
      const FLIP_X_ZONE = 180; // ~max chip width
      const FLIP_Y_ZONE = 48;
      setDragGhost({
        x: Math.max(GHOST_PAD, Math.min(e.clientX, window.innerWidth - GHOST_PAD)),
        y: Math.max(GHOST_PAD, Math.min(e.clientY, window.innerHeight - GHOST_PAD)),
        detach: isFarOutsideViewport(e.clientX, e.clientY),
        flipX: e.clientX > window.innerWidth - FLIP_X_ZONE,
        flipY: e.clientY > window.innerHeight - FLIP_Y_ZONE,
      });
    },
    [orch.beginHeaderDrag, orch.setHeaderDragOver, leaf.id],
  );

  const onToolbarPointerUp = useCallback(
    (e: ReactPointerEvent<HTMLDivElement>) => {
      const st = dragStartRef.current;
      if (!st) return;
      (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
      const wasDragging = st.dragging;
      dragStartRef.current = null;
      setDragGhost(null);
      if (!wasDragging) return;
      document.body.style.cursor = "";

      const releasedFarOutside = isFarOutsideViewport(e.clientX, e.clientY);

      if (releasedFarOutside) {
        // Cancel any in-flight swap state without committing, then pop
        // this pane into a fresh window. moveToNewWindow handles the
        // PTY-handoff + closeLeaf in the source.
        orch.endHeaderDrag(false);
        orch.moveToNewWindow(leaf.id);
      } else {
        orch.endHeaderDrag(true);
      }
    },
    [orch.endHeaderDrag, orch.moveToNewWindow, leaf.id],
  );

  const onToolbarPointerCancel = useCallback(
    (e: ReactPointerEvent<HTMLDivElement>) => {
      const st = dragStartRef.current;
      if (!st) return;
      (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
      const wasDragging = st.dragging;
      dragStartRef.current = null;
      setDragGhost(null);
      if (wasDragging) {
        document.body.style.cursor = "";
        orch.endHeaderDrag(false);
      }
    },
    [orch.endHeaderDrag],
  );

  const labelText = leaf.label ?? "(unnamed)";

  // Resolve the SpawnSpec from the leaf + host table. If shellKind=ssh but
  // the referenced host was deleted, we surface an error in the toolbar
  // status instead of spawning an unrelated shell.
  const spec: SpawnSpec | null = (() => {
    if (leaf.shellKind === "wsl") {
      return { kind: "wsl", distro: leaf.distro, cwd: leaf.cwd };
    }
    if (leaf.shellKind === "powershell") {
      return { kind: "powershell" };
    }
    const host = orch.hosts.find((h) => h.id === leaf.sshHostId);
    if (!host) return null;
    return {
      kind: "ssh",
      host: host.hostname,
      user: host.user,
      port: host.port,
      identityFile: host.identityFile,
      jumpHost: host.jumpHost,
      extraArgs: host.extraArgs,
      hostId: host.id,
    };
  })();

  return (
    <div
      className={`leaf${isActive ? " active" : ""}${isBroadcasting ? " broadcasting" : ""}${isIdle ? " idle" : ""}${isDragSource ? " drag-source" : ""}${isDragTarget ? " drag-target" : ""}`}
      role="group"
      aria-label={`Terminal pane: ${leaf.label ?? leaf.distro ?? "unnamed"}`}
      data-leaf-id={leaf.id}
      onPointerDown={onPaneClick}
    >
      <div
        className="pane-toolbar"
        onPointerDown={onToolbarPointerDown}
        onPointerMove={onToolbarPointerMove}
        onPointerUp={onToolbarPointerUp}
        onPointerCancel={onToolbarPointerCancel}
        onContextMenu={openContextMenu}
      >
        {editingLabel ? (
          <input
            ref={labelInputRef}
            className="label-input"
            value={labelDraft}
            onChange={(e) => setLabelDraft(e.target.value)}
            onKeyDown={onLabelKey}
            onBlur={commitLabel}
            placeholder="(label)"
          />
        ) : (
          <button
            className="pane-label"
            onClick={startEditLabel}
            title="Click to rename pane"
          >
            {labelText}
          </button>
        )}

        <span className="distro-wrap">
          <button
            className="distro-chip"
            onClick={toggleShellMenu}
            title="Change shell (respawns the pane)"
          >
            {chipLabel} ▾
          </button>
          {shellMenuOpen && (
            <div
              className="distro-menu shell-menu"
              role="menu"
              onClick={(e) => e.stopPropagation()}
            >
              {orch.distros.length > 0 && (
                <>
                  <div className="shell-menu-header">WSL</div>
                  {orch.distros.map((d) => {
                    const active = leaf.shellKind === "wsl" && d === leaf.distro;
                    return (
                      <button
                        key={`wsl-${d}`}
                        className={`distro-menu-item${active ? " active" : ""}`}
                        onClick={() => pickShell({ shellKind: "wsl", distro: d })}
                      >
                        {d}
                      </button>
                    );
                  })}
                </>
              )}

              <div className="shell-menu-header">Windows</div>
              <button
                className={`distro-menu-item${leaf.shellKind === "powershell" ? " active" : ""}`}
                onClick={() => pickShell({ shellKind: "powershell" })}
              >
                PowerShell
              </button>

              <div className="shell-menu-header">SSH</div>
              {orch.hosts.length === 0 ? (
                <div className="shell-menu-empty">(no saved hosts)</div>
              ) : (
                orch.hosts.map((h) => {
                  const active =
                    leaf.shellKind === "ssh" && h.id === leaf.sshHostId;
                  return (
                    <button
                      key={`ssh-${h.id}`}
                      className={`distro-menu-item${active ? " active" : ""}`}
                      onClick={() =>
                        pickShell({ shellKind: "ssh", sshHostId: h.id })
                      }
                      title={
                        h.user
                          ? `${h.user}@${h.hostname}${h.port ? ":" + h.port : ""}`
                          : `${h.hostname}${h.port ? ":" + h.port : ""}`
                      }
                    >
                      {h.label || h.hostname}
                    </button>
                  );
                })
              )}
              <button
                className="distro-menu-item shell-menu-manage"
                onClick={onManageHosts}
              >
                Manage hosts…
              </button>
            </div>
          )}
        </span>

        <button
          className={`bcast-chip${isBroadcasting ? " on" : ""}`}
          onClick={(e) => {
            e.stopPropagation();
            orch.toggleBroadcast(leaf.id);
          }}
          title={
            isBroadcasting
              ? "Broadcasting (click or Ctrl+Shift+B to leave group)"
              : "Click or Ctrl+Shift+B to broadcast input to other broadcast panes"
          }
          aria-pressed={isBroadcasting ? "true" : "false"}
        >
          📡
        </button>

        <button
          className={`bcast-chip mcp-chip${leaf.mcpAllow ? " on" : ""}`}
          onClick={(e) => {
            e.stopPropagation();
            orch.toggleMcpAllow(leaf.id);
          }}
          title={
            leaf.mcpAllow
              ? "MCP can see this pane — click to revoke"
              : "MCP cannot see this pane — click to allow (only matters when the MCP server is on)"
          }
          aria-pressed={leaf.mcpAllow ? "true" : "false"}
        >
          🤖
        </button>

        {isIdle && statusOk ? (
          <span className="pane-status idle" title={`No output for ${IDLE_THRESHOLD_MS / 1000}s+`}>
            idle
          </span>
        ) : (
          <span className={`pane-status ${statusOk ? "ok" : "err"}`}>{status}</span>
        )}

        <span className="pane-actions">
          <button
            className="pane-btn"
            title="Split right (Ctrl+Shift+E)"
            onClick={(e) => {
              e.stopPropagation();
              orch.split(leaf.id, "h");
            }}
            aria-label="Split right"
          >
            ⇥
          </button>
          <button
            className="pane-btn"
            title="Split down (Ctrl+Shift+O)"
            onClick={(e) => {
              e.stopPropagation();
              orch.split(leaf.id, "v");
            }}
            aria-label="Split down"
          >
            ⇣
          </button>
          <button
            className="pane-btn close"
            title="Close pane (Ctrl+Shift+W)"
            onClick={(e) => {
              e.stopPropagation();
              orch.close(leaf.id);
            }}
            aria-label="Close pane"
          >
            ×
          </button>
        </span>
      </div>
      <div className="xterm-wrap">
        {spec ? (
          <XtermPane
            spec={spec}
            existingPaneId={orch.getInitialPaneIdFor(leaf.id)}
            onStatus={onStatus}
            onSpawn={onPaneSpawned}
            onInput={onTerminalInput}
            onDataReceived={onDataReceived}
            onFocus={onXtermFocus}
            focusTrigger={focusTrigger}
            fontSize={resolveFontSize(leaf.fontSizeOffset)}
          />
        ) : (
          <div className="leaf-missing-host">
            <p>SSH host not found</p>
            <p className="hint">
              Open the shell menu and pick another host, or add this host back
              via Manage hosts….
            </p>
          </div>
        )}
      </div>
      {menuPos && (
        <div
          className="pane-context-menu"
          style={{
            position: "fixed",
            top: menuPos.y,
            left: menuPos.x,
          }}
          role="menu"
          onClick={(e) => e.stopPropagation()}
          onContextMenu={(e) => e.preventDefault()}
        >
          <button
            type="button"
            className="pane-context-menu-item"
            role="menuitem"
            onClick={() => {
              closeContextMenu();
              orch.moveToNewWindow(leaf.id);
            }}
          >
            Move to new window
          </button>
        </div>
      )}
      {dragGhost &&
        createPortal(
          <div
            className={`pane-drag-ghost${dragGhost.detach ? " detach" : ""}`}
            style={{
              left: dragGhost.x,
              top: dragGhost.y,
              transform: `translate(${
                dragGhost.flipX ? "calc(-100% - 12px)" : "12px"
              }, ${dragGhost.flipY ? "calc(-100% - 12px)" : "12px"})`,
            }}
            aria-hidden="true"
          >
            <span className="pane-drag-ghost-label">{labelText}</span>
            {dragGhost.detach && (
              <span className="pane-drag-ghost-hint">↗ New window</span>
            )}
          </div>,
          document.body,
        )}
    </div>
  );
}
