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

const IDLE_THRESHOLD_MS = 5000;

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.
  //
  // Filter: for WSL panes, before flagging idle we probe the backend to see
  // if any "watched" process (currently just `claude`) is running in THIS
  // pane specifically — per-pane, not per-distro. Per-pane is essential for
  // tiletopia's primary use case (multiple claude sessions across panes in
  // the same distro). The backend matches by reading `TILETOPIA_PANE_ID`
  // out of each candidate process's `/proc/<pid>/environ` (the env var is
  // injected at spawn time; see src-tauri/src/pty.rs WSLENV setup).
  //
  // PowerShell + SSH skip the probe and fall through to legacy behaviour
  // (PS has no portable `ps`; SSH processes live on a remote box).
  const lastDataTimeRef = useRef(Date.now());
  const [isIdle, setIsIdle] = useState(false);
  const isWslPane = leaf.shellKind === "wsl";
  // Captures the distro name into the interval callback. Empty string when
  // the leaf doesn't have one yet — the probe returns "not running" for
  // empty input so the pane goes idle normally.
  const wslDistro = isWslPane ? (leaf.distro ?? "") : "";
  // Backend pane id (PaneId, the u64 used inside Rust). Set by the
  // XtermPane onSpawn callback; null until the spawn round-trip completes.
  // Idle ticks before that point pass 0 — won't match any real pane's
  // TILETOPIA_PANE_ID env, so the probe returns false (no suppression).
  const paneIdRef = useRef<number | null>(null);
  const onDataReceived = useCallback(() => {
    lastDataTimeRef.current = Date.now();
    setIsIdle((cur) => {
      if (cur) orch.reportLeafIdle(leaf.id, false);
      return false;
    });
  }, [orch.reportLeafIdle, leaf.id]);
  useEffect(() => {
    // Guard against late-resolving probes after unmount or another tick
    // already shipping a fresher answer.
    let cancelled = false;
    let inFlight = false;

    const tick = () => {
      const dt = Date.now() - lastDataTimeRef.current;
      const nowIdle = dt >= IDLE_THRESHOLD_MS;

      // Transitioning out of idle is unconditional — fresh output beats
      // any probe answer.
      if (!nowIdle) {
        setIsIdle((cur) => {
          if (!cur) return cur;
          orch.reportLeafIdle(leaf.id, false);
          return false;
        });
        return;
      }

      // Transitioning into idle. Non-WSL panes: report immediately (legacy
      // behaviour). WSL panes: gate on the probe; suppress if a watched
      // process is running in the distro.
      if (!isWslPane) {
        setIsIdle((cur) => {
          if (cur) return cur;
          orch.reportLeafIdle(leaf.id, true);
          return true;
        });
        return;
      }

      // WSL path. Don't stack probes — one in flight per pane at a time.
      if (inFlight) return;
      inFlight = true;
      const paneIdForProbe = paneIdRef.current ?? 0;
      void isWatchProcessRunning(wslDistro, paneIdForProbe)
        .then((suppress) => {
          if (cancelled) return;
          // If output arrived while the probe was in flight, the next tick
          // (or onDataReceived) will reconcile; don't flip-flop here.
          if (Date.now() - lastDataTimeRef.current < IDLE_THRESHOLD_MS) return;
          if (suppress) {
            // claude (or another watched proc) is running in THIS pane —
            // treat the silence as expected; stay out of the idle set.
            setIsIdle((cur) => {
              if (!cur) return cur;
              orch.reportLeafIdle(leaf.id, false);
              return false;
            });
          } else {
            setIsIdle((cur) => {
              if (cur) return cur;
              orch.reportLeafIdle(leaf.id, true);
              return true;
            });
          }
        })
        .catch((e) => {
          // Probe IPC errored — don't flip idle either way; next tick retries.
          // The Rust side now also fails-safe to "not running" so the pane
          // will flag idle eventually if the probe stays broken.
          if (cancelled) return;
          // eslint-disable-next-line no-console
          console.debug("idle probe failed", e);
        })
        .finally(() => {
          inFlight = false;
        });
    };

    const id = window.setInterval(tick, 1000);
    return () => {
      cancelled = true;
      clearInterval(id);
    };
  }, [leaf.id, orch.reportLeafIdle, isWslPane, wslDistro]);
  // 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) => {
      paneIdRef.current = paneId;
      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 () => {
      paneIdRef.current = null;
      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);
  }, []);

  // ---- 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,
  );
  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);
    },
    [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;
      if (wasDragging) {
        document.body.style.cursor = "";
        orch.endHeaderDrag(true);
      }
    },
    [orch.endHeaderDrag],
  );

  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;
      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}
      >
        {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}
            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>
    </div>
  );
}
