import { useEffect, useRef, useState } from "react";
import { Maximize2, Minimize2, RotateCw, GripVertical, Play, X } from "lucide-react";
import { Terminal } from "@xterm/xterm";
import { TerminalView } from "./TerminalView";
import { SearchBar } from "./SearchBar";
import { StatusRing } from "./StatusRing";
import { COLORS, FONT, STATE_COLOR } from "./theme";
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
import { setRatios, restartSurface, setZoom, moveSurface, attachSurface, detachSurface, closeSurfaceCmd } from "./socketBridge";

interface Props {
  workspaceId: string;
  workspaceName: string;
  layout: LayoutNode | null;
  /** surface_id -> running flag, from the latest status/events. */
  running: Record<string, boolean>;
  states: Record<string, SurfaceState>;
  surfaces: Record<string, SurfaceView>;
  focusedId: string | null;
  onFocus: (id: string) => void;
  zoomed: string | null;
  /** The surface whose scrollback search bar is open, or null. Anchored to the
   * panel it was opened on — it does NOT follow focus. */
  searchSurfaceId: string | null;
  searchNonce: number;
  onCloseSearch: () => void;
  font: { family: string; size: number } | null;
  palette: Record<string, string> | null;
}

type Edge = "left" | "right" | "top" | "bottom";
interface DropTarget { id: string; edge: Edge }

function edgeAt(clientX: number, clientY: number, r: DOMRect): Edge {
  const px = (clientX - r.left) / r.width;
  const py = (clientY - r.top) / r.height;
  const d: Record<Edge, number> = { left: px, right: 1 - px, top: py, bottom: 1 - py };
  return (Object.keys(d) as Edge[]).reduce((a, b) => (d[b] < d[a] ? b : a), "left");
}

export function LayoutEngine({ workspaceId, workspaceName, layout, running, states, surfaces, focusedId, onFocus, zoomed, searchSurfaceId, searchNonce, onCloseSearch, font, palette }: Props) {
  // Panel drag-to-reorder. Implemented with raw pointer events rather than the
  // HTML5 drag API, which is unreliable in the macOS WKWebView Tauri uses.
  const [drop, setDrop] = useState<DropTarget | null>(null);
  const dropRef = useRef<DropTarget | null>(null);
  dropRef.current = drop;

  const startPanelDrag = (srcId: string, e: React.MouseEvent) => {
    e.preventDefault();
    const startX = e.clientX, startY = e.clientY;
    let active = false;
    const prevUserSelect = document.body.style.userSelect;
    const move = (ev: MouseEvent) => {
      if (!active) {
        if (Math.abs(ev.clientX - startX) + Math.abs(ev.clientY - startY) < 5) return;
        active = true;
        document.body.style.userSelect = "none";
      }
      const el = (document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement | null)?.closest("[data-surface-id]") as HTMLElement | null;
      const tid = el?.getAttribute("data-surface-id");
      if (!el || !tid || tid === srcId) { setDrop(null); return; }
      setDrop({ id: tid, edge: edgeAt(ev.clientX, ev.clientY, el.getBoundingClientRect()) });
    };
    const up = () => {
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mouseup", up);
      document.body.style.userSelect = prevUserSelect;
      const d = dropRef.current;
      setDrop(null);
      if (active && d && d.id !== srcId) void moveSurface(srcId, d.id, d.edge);
    };
    window.addEventListener("mousemove", move);
    window.addEventListener("mouseup", up);
  };

  if (!layout) {
    return <div style={{ color: COLORS.textMuted, padding: 24 }}>Empty workspace — apply a preset to add panels.</div>;
  }
  const shared = { workspaceId, workspaceName, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag: startPanelDrag, searchSurfaceId, searchNonce, onCloseSearch, font, palette };
  if (zoomed) {
    return (
      <div style={{ width: "100%", height: "100%", padding: 12, boxSizing: "border-box" }}>
        <Node node={{ leaf: { surface_id: zoomed } }} path={[]} {...shared} />
      </div>
    );
  }
  return (
    <div style={{ width: "100%", height: "100%", padding: 12, boxSizing: "border-box" }}>
      <Node node={layout} path={[]} {...shared} />
    </div>
  );
}

interface NodeProps {
  workspaceId: string; workspaceName: string; node: LayoutNode; path: number[];
  running: Record<string, boolean>; states: Record<string, SurfaceState>;
  surfaces: Record<string, SurfaceView>; focusedId: string | null; onFocus: (id: string) => void;
  zoomed: string | null;
  drop: DropTarget | null;
  onStartPanelDrag: (srcId: string, e: React.MouseEvent) => void;
  searchSurfaceId: string | null;
  searchNonce: number;
  onCloseSearch: () => void;
  font: { family: string; size: number } | null;
  palette: Record<string, string> | null;
}

function Node({ node, path, ...rest }: NodeProps) {
  if ("leaf" in node) {
    return <Leaf id={node.leaf.surface_id} {...rest} />;
  }
  return <SplitView split={node.split} path={path} {...rest} />;
}

const NERD_FALLBACK_LE = "'Symbols Nerd Font Mono'";
const fontStackLE = (family: string | null) =>
  family ? `'${family}', ${NERD_FALLBACK_LE}, monospace`
         : `'JetBrains Mono Variable', 'JetBrains Mono', ${NERD_FALLBACK_LE}, monospace`;

function xtermThemeLE(p: Record<string, string>) {
  return {
    background: p["term-bg"] ?? p["bg-panel"],
    foreground: p["text-primary"],
    cursor: p["text-primary"],
    selectionBackground: p["search-match"],
  };
}

function StoppedSnapshot({ surfaceId, font, palette }: { surfaceId: string; font: { family: string; size: number } | null; palette: Record<string, string> | null }) {
  const hostRef = useRef<HTMLDivElement | null>(null);
  useEffect(() => {
    const host = hostRef.current;
    if (!host) return;
    const term = new Terminal({
      fontFamily: fontStackLE(font?.family ?? null),
      fontSize: font?.size ?? 13,
      theme: palette ? xtermThemeLE(palette) : undefined,
      allowTransparency: true, // term-bg may be transparent under a background theme
      cursorBlink: false,
      disableStdin: true,
      scrollback: 0,
    });
    term.open(host);
    let disposed = false;
    void attachSurface(surfaceId, () => {}).then((res) => {
      if (!disposed && res.snapshot) term.write(res.snapshot);
    });
    return () => { disposed = true; term.dispose(); void detachSurface(surfaceId); };
  }, [surfaceId, font, palette]); // eslint-disable-line react-hooks/exhaustive-deps
  return <div ref={hostRef} style={{ position: "absolute", inset: 0, opacity: 0.45, pointerEvents: "none" }} />;
}

function Leaf({ id, workspaceId, workspaceName, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag, searchSurfaceId, searchNonce, onCloseSearch, font, palette }: Omit<NodeProps, "node" | "path"> & { id: string }) {
  const focused = focusedId === id;
  const dropEdge = drop && drop.id === id ? drop.edge : null;

  const card = (inner: React.ReactNode) => (
    <div
      data-surface-id={id}
      onMouseDown={() => onFocus(id)}
      style={{
        position: "relative", display: "flex", flexDirection: "column", width: "100%", height: "100%",
        background: "transparent",
        borderRadius: 8, overflow: "hidden",
        // Constant 2px border, color-only on focus. A width change (1px<->2px)
        // would resize the inner content box, fire ResizeObserver -> fit -> PTY
        // SIGWINCH, making zsh/powerlevel10k reprint its prompt on every focus
        // switch (the "stacked prompts" bug).
        border: `2px solid ${focused ? COLORS.accent : COLORS.borderSubtle}`,
        boxSizing: "border-box",
      }}
    >
      {/* Glass fill + blur as a layer BEHIND the content. The terminal's transparent
          cells show this through. Crucially the terminal canvas is NOT a descendant
          of a backdrop-filter element — under WKWebView that clips/smears the WebGL
          canvas (first-glyph clip at column 0, smearing on scroll). With "none" the
          glass is the solid bg-panel so the classic look is unchanged. */}
      <div style={{ position: "absolute", inset: 0, zIndex: 0, background: COLORS.panelGlass, backdropFilter: COLORS.panelBlur, WebkitBackdropFilter: COLORS.panelBlur, pointerEvents: "none" }} />
      <div style={{ position: "relative", zIndex: 1, flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>
        {inner}
      </div>
      {dropEdge && <DropIndicator edge={dropEdge} />}
    </div>
  );

  if (running[id] === false) {
    return card(
      <div style={{ position: "relative", height: "100%", width: "100%" }}>
        <StoppedSnapshot surfaceId={id} font={font} palette={palette} />
        <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 10, color: COLORS.textSecondary, background: "rgba(0,0,0,0.35)" }}>
          <div style={{ fontFamily: FONT.mono, fontSize: 13 }}>Stopped</div>
          <div style={{ display: "flex", gap: 8 }}>
            <button onClick={() => void restartSurface(id, true)}
              style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: COLORS.accent, color: COLORS.bgApp, border: "none", borderRadius: 7, fontSize: 12, fontWeight: 600 }}>
              <Play size={13} /> Resume
            </button>
            <button onClick={() => void restartSurface(id, false)}
              style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: COLORS.bgElevated, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
              <RotateCw size={13} /> Restart fresh
            </button>
            <button onClick={() => void closeSurfaceCmd(id)}
              style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: "transparent", color: COLORS.textSecondary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
              <X size={13} /> Close
            </button>
            {zoomed === id && (
              <button onClick={() => void setZoom(workspaceId, null)}
                style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: "transparent", color: COLORS.textSecondary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
                <Minimize2 size={13} /> Exit zoom
              </button>
            )}
          </div>
        </div>
      </div>
    );
  }

  const spec = surfaces[id]?.spec;
  const agent = spec?.agent_label ?? "shell";
  const state = states[id] ?? "idle";
  return card(
    <>
      <div
        onMouseDown={(e) => { onFocus(id); onStartPanelDrag(id, e); }}
        title="Drag to move this panel"
        style={{ display: "flex", alignItems: "center", gap: 8, height: 30, flex: "0 0 30px", padding: "0 10px", background: COLORS.elevatedGlass, borderBottom: `1px solid ${COLORS.borderSubtle}`, cursor: "grab" }}
      >
        <GripVertical size={13} color={COLORS.textMuted} />
        <StatusRing state={state} running={true} />
        <span style={{ fontFamily: FONT.mono, fontSize: 12, fontWeight: 600, color: COLORS.textPrimary }}>{agent}</span>
        {workspaceName && <span style={{ fontFamily: FONT.mono, fontSize: 11, color: COLORS.textMuted, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{workspaceName}</span>}
        <span style={{ flex: 1 }} />
        <span style={{ display: "flex", alignItems: "center", height: 16, padding: "0 7px", borderRadius: 8, background: "#000", fontFamily: FONT.mono, fontSize: 10, fontWeight: 600, color: STATE_COLOR[state] }}>
          {state}
        </span>
        {zoomed === id
          ? <Minimize2 size={13} color={COLORS.textSecondary} style={{ cursor: "pointer" }} aria-label="Unzoom"
              onMouseDown={(e) => { e.stopPropagation(); void setZoom(workspaceId, null); }} />
          : <Maximize2 size={13} color={COLORS.textMuted} style={{ cursor: "pointer" }} aria-label="Zoom"
              onMouseDown={(e) => { e.stopPropagation(); onFocus(id); void setZoom(workspaceId, id); }} />}
        <X size={13} color={COLORS.textMuted} style={{ cursor: "pointer" }} aria-label="Close panel"
          onMouseDown={(e) => { e.stopPropagation(); void closeSurfaceCmd(id); }}
          onMouseEnter={(e) => { e.currentTarget.style.color = COLORS.stError; }}
          onMouseLeave={(e) => { e.currentTarget.style.color = COLORS.textMuted; }} />
      </div>
      <div style={{ flex: 1, minHeight: 0 }}>
        <TerminalView key={id} surfaceId={id} font={font} palette={palette} focused={focused} />
      </div>
      {searchSurfaceId === id && (
        <SearchBar surfaceId={id} reopenNonce={searchNonce} onClose={onCloseSearch} />
      )}
    </>
  );
}

function DropIndicator({ edge }: { edge: Edge }) {
  const base: React.CSSProperties = { position: "absolute", background: `${COLORS.accent}55`, border: `2px solid ${COLORS.accent}`, pointerEvents: "none", boxSizing: "border-box", zIndex: 5 };
  const map: Record<Edge, React.CSSProperties> = {
    left: { ...base, top: 0, bottom: 0, left: 0, width: "50%" },
    right: { ...base, top: 0, bottom: 0, right: 0, width: "50%" },
    top: { ...base, left: 0, right: 0, top: 0, height: "50%" },
    bottom: { ...base, left: 0, right: 0, bottom: 0, height: "50%" },
  };
  return <div style={map[edge]} />;
}

function SplitView({ split, path, ...rest }: Omit<NodeProps, "node"> & { split: Extract<LayoutNode, { split: unknown }>["split"] }) {
  const { orient, ratios, children } = split;
  const { workspaceId } = rest;
  const dir = orient === "h" ? "row" : "column";
  const containerRef = useRef<HTMLDivElement>(null);
  const [live, setLive] = useState<number[] | null>(null);
  // Drop any local override once the authoritative ratios arrive from the daemon.
  useEffect(() => { setLive(null); }, [ratios.join(",")]);
  const effective = live ?? ratios;

  const startDrag = (i: number, e: React.MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();
    const container = containerRef.current;
    if (!container) return;
    const total = orient === "h" ? container.clientWidth : container.clientHeight;
    if (total <= 0) return;
    const start = orient === "h" ? e.clientX : e.clientY;
    const base = [...ratios];
    const sum = base.reduce((a, b) => a + b, 0) || 1;
    const move = (ev: MouseEvent) => {
      const cur = orient === "h" ? ev.clientX : ev.clientY;
      // Accumulated delta from drag start — not an incremental step — so the
      // panel tracks the pointer 1:1 instead of crawling one echo at a time.
      const deltaFrac = ((cur - start) / total) * sum;
      const next = [...base];
      next[i] = Math.max(0.05, base[i] + deltaFrac);
      next[i + 1] = Math.max(0.05, (base[i + 1] ?? 1) - deltaFrac);
      setLive(next);
    };
    const up = () => {
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mouseup", up);
      setLive((cur) => { if (cur) void setRatios(workspaceId, path, cur); return cur; });
    };
    window.addEventListener("mousemove", move);
    window.addEventListener("mouseup", up);
  };

  return (
    <div ref={containerRef} style={{ display: "flex", flexDirection: dir, width: "100%", height: "100%" }}>
      {children.map((child, i) => (
        <div key={i} style={{ flexGrow: effective[i] ?? 1, flexBasis: 0, minWidth: 0, minHeight: 0, overflow: "visible", position: "relative", display: "flex" }}>
          <Node node={child} path={[...path, i]} {...rest} />
          {i < children.length - 1 && (
            <div onMouseDown={(e) => startDrag(i, e)}
              style={{
                position: "absolute", zIndex: 4,
                ...(orient === "h"
                  ? { top: 0, bottom: 0, right: -5, width: 10, cursor: "col-resize" }
                  : { left: 0, right: 0, bottom: -5, height: 10, cursor: "row-resize" }),
              }} />
          )}
        </div>
      ))}
    </div>
  );
}
