/**
 * `useLayout` — view-model hook for the terminal layout grid (L4).
 *
 * Owns the loaded {@link LayoutTree} for a project and exposes the actions the
 * grid UI triggers (split/merge/resize/move). It consumes the
 * {@link LayoutGateway} exclusively — never `invoke()` — so the component layer
 * stays testable with a mock gateway (ARCHITECTURE §1.3). Each action calls the
 * backend, which persists `.ideai/layout.json` and returns the new tree.
 */

import { useCallback, useEffect, useState } from "react";

import type {
  Direction,
  GatewayError,
  LayoutOperation,
  LayoutTree,
} from "@/domain";
import { useGateways } from "@/app/di";
import { leaves, splitOp } from "./layout";

/** What the layout grid UI needs from this hook. */
export interface LayoutViewModel {
  /** The current layout tree, or `null` until loaded. */
  layout: LayoutTree | null;
  /**
   * Bumped every time a mutation (split/merge/resize/move/…) commits a new
   * tree, INCLUDING the initial load (ticket #61). `LayoutGrid` forwards it to
   * every mounted `TerminalView` as `refitSignal` so surviving terminals refit
   * after a structural change (split/merge) instead of waiting for a manual
   * window resize — their own `ResizeObserver` doesn't always fire a useful
   * event when a SIBLING cell appears or disappears.
   */
  layoutVersion: number;
  /** Last error message, or `null`. */
  error: string | null;
  /** Whether a request is in flight. */
  busy: boolean;
  /** Splits a leaf into two cells in the given direction. */
  split: (target: string, direction: Direction) => Promise<void>;
  /** Collapses a split container back to the child at `keepIndex`. */
  merge: (container: string, keepIndex: number) => Promise<void>;
  /** Reassigns a split's child weights (separator drag). */
  resize: (container: string, weights: number[]) => Promise<void>;
  /** Moves the session from one cell to another (empty) cell. */
  move: (from: string, to: string) => Promise<void>;
  /** Records (or clears) the session hosted by a cell. */
  setSession: (target: string, session: string | null) => Promise<void>;
  /** Pins or clears an agent on a cell (persisted in layout). */
  setCellAgent: (target: string, agent: string | null) => Promise<void>;
  /** Shows an already-running agent session in a cell without respawning it. */
  attachLiveAgentToCell: (
    target: string,
    agent: string,
    session: string,
  ) => Promise<void>;
  /**
   * Records (or clears) the persistent CLI conversation id on a cell (T4b). Used
   * to persist the id assigned at first launch so the next open resumes it.
   */
  setCellConversation: (target: string, conversationId: string | null) => Promise<void>;
}

function describe(e: unknown): string {
  if (e && typeof e === "object" && "message" in e) {
    return String((e as GatewayError).message);
  }
  return String(e);
}

export function useLayout(
  projectId: string | null,
  layoutId?: string,
): LayoutViewModel {
  const { layout: gateway, terminal } = useGateways();
  const [layout, setLayout] = useState<LayoutTree | null>(null);
  const [layoutVersion, setLayoutVersion] = useState(0);
  const [error, setError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);

  // Commits a freshly-loaded/mutated tree AND bumps the refit signal (ticket
  // #61) in one place, so every caller below (initial load, `mutate`,
  // `mutateChain`) stays a one-liner instead of repeating the pair.
  const applyTree = useCallback((tree: LayoutTree) => {
    setLayout(tree);
    setLayoutVersion((v) => v + 1);
  }, []);

  useEffect(() => {
    // No project, or a DI subset without the layout gateway (e.g. a focused
    // test) → render nothing rather than crash the tab (mirrors TerminalView).
    if (!projectId || !gateway) {
      setLayout(null);
      return;
    }
    let cancelled = false;
    setBusy(true);
    gateway
      .loadLayout(projectId, layoutId)
      .then((tree) => {
        if (!cancelled) applyTree(tree);
      })
      .catch((e: unknown) => {
        if (!cancelled) setError(describe(e));
      })
      .finally(() => {
        if (!cancelled) setBusy(false);
      });
    return () => {
      cancelled = true;
    };
  }, [gateway, projectId, layoutId, applyTree]);

  const mutate = useCallback(
    async (operation: LayoutOperation) => {
      if (!projectId || !gateway) return;
      setBusy(true);
      setError(null);
      try {
        applyTree(await gateway.mutateLayout(projectId, operation, layoutId));
      } catch (e) {
        setError(describe(e));
      } finally {
        setBusy(false);
      }
    },
    [gateway, projectId, layoutId, applyTree],
  );

  // Applies several operations in series, persisting each, but commits a single
  // `setLayout` with the FINAL tree. This avoids an intermediate render between
  // two related mutations — critical for `setCellAgent`, where a render showing
  // the new agent while the old session is still set would remount TerminalView
  // and make it reattach to the wrong (old) PTY instead of launching the agent.
  const mutateChain = useCallback(
    async (operations: LayoutOperation[]) => {
      if (!projectId || !gateway || operations.length === 0) return;
      setBusy(true);
      setError(null);
      try {
        let tree: LayoutTree | null = null;
        for (const op of operations) {
          tree = await gateway.mutateLayout(projectId, op, layoutId);
        }
        if (tree) applyTree(tree);
      } catch (e) {
        setError(describe(e));
      } finally {
        setBusy(false);
      }
    },
    [gateway, projectId, layoutId, applyTree],
  );

  const split = useCallback(
    (target: string, direction: Direction) => mutate(splitOp(target, direction)),
    [mutate],
  );
  const merge = useCallback(
    async (container: string, keepIndex: number) => {
      // Closing a cell is a VIEW operation: it collapses the layout, but the
      // dropped cell's PTY keeps running. TerminalView cleanup detaches the local
      // stream; the agent/session can later be re-opened in a cell via IdeA.
      await mutate({ type: "merge", container, keepIndex });
    },
    [mutate],
  );
  const resize = useCallback(
    (container: string, weights: number[]) =>
      mutate({ type: "resize", container, weights }),
    [mutate],
  );
  const move = useCallback(
    (from: string, to: string) => mutate({ type: "move", from, to }),
    [mutate],
  );
  const setSession = useCallback(
    (target: string, session: string | null) =>
      mutate({ type: "setSession", target, session }),
    [mutate],
  );
  const setCellAgent = useCallback(
    async (target: string, agent: string | null) => {
      // Find the cell's current agent + session. Changing the agent must reset
      // the session so the remounted TerminalView opens/attaches for the new
      // agent instead of reattaching to the old PTY. If the previous session was
      // an agent session, it is only detached into the background; a cell is a
      // view, not the process owner.
      const cell = layout ? leaves(layout).find((l) => l.id === target) : undefined;
      const oldAgent = cell?.agent ?? null;
      const oldSession = cell?.session ?? null;
      if (agent === oldAgent) return; // no-op: same agent, keep the session/PTY.

      // Changing the agent also drops the previous conversation id: it belongs to
      // the old agent's CLI and must never be passed to the new one (T4b).
      await mutateChain([
        { type: "setCellAgent", target, agent },
        { type: "setSession", target, session: null },
        { type: "setCellConversation", target, conversationId: null },
      ]);

      // Best-effort: tear down a previous plain terminal so no orphan shell
      // lingers. Agent sessions keep running in the background.
      if (oldSession && !oldAgent && terminal) {
        try {
          await terminal.closeTerminal(oldSession);
        } catch {
          /* already gone — ignore */
        }
      }
    },
    [layout, mutateChain, terminal],
  );
  const attachLiveAgentToCell = useCallback(
    async (target: string, agent: string, session: string) => {
      const cell = layout ? leaves(layout).find((l) => l.id === target) : undefined;
      const oldAgent = cell?.agent ?? null;
      const oldSession = cell?.session ?? null;

      const operations: LayoutOperation[] = [];
      if (agent !== oldAgent) {
        operations.push(
          { type: "setCellAgent", target, agent },
          { type: "setCellConversation", target, conversationId: null },
        );
      }
      if (session !== oldSession) {
        operations.push({ type: "setSession", target, session });
      }
      await mutateChain(operations);

      if (oldSession && oldSession !== session && !oldAgent && terminal) {
        try {
          await terminal.closeTerminal(oldSession);
        } catch {
          /* already gone — ignore */
        }
      }
    },
    [layout, mutateChain, terminal],
  );

  const setCellConversation = useCallback(
    (target: string, conversationId: string | null) =>
      mutate({ type: "setCellConversation", target, conversationId }),
    [mutate],
  );

  return {
    layout,
    layoutVersion,
    error,
    busy,
    split,
    merge,
    resize,
    move,
    setSession,
    setCellAgent,
    attachLiveAgentToCell,
    setCellConversation,
  };
}
