/**
 * `useLayouts` — view-model hook for the named-layout tab bar (L4 / #4).
 *
 * Manages the list of named layouts for a project (list / create / rename /
 * delete / set-active). Speaks exclusively to the {@link LayoutGateway} port.
 */

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

import type { GatewayError, LayoutInfo, LayoutKind, PluginLayoutOrigin } from "@/domain";
import { useGateways } from "@/app/di";

export interface LayoutsViewModel {
  /** Ordered list of named layouts. */
  layouts: LayoutInfo[];
  /** Currently active layout id. */
  activeId: string | null;
  /** Whether a request is in flight. */
  busy: boolean;
  /** Last error message, or `null`. */
  error: string | null;
  /** Switches the active layout (does NOT force a re-fetch here; the caller uses the returned activeId). */
  setActive: (layoutId: string) => Promise<void>;
  /** Creates a new layout with the given name and kind; resolves with the new layoutId. */
  create: (
    name: string,
    kind?: LayoutKind,
    pluginOrigin?: PluginLayoutOrigin,
  ) => Promise<string | null>;
  /** Renames a layout. */
  rename: (layoutId: string, name: string) => Promise<void>;
  /** Deletes a layout; refuses if it is the last one. */
  deleteLayout: (layoutId: string) => Promise<void>;
}

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

/** True when `e` is the backend `NOT_FOUND` refusal (a {@link GatewayError}). */
function isNotFound(e: unknown): boolean {
  return (
    typeof e === "object" &&
    e !== null &&
    "code" in e &&
    (e as { code: unknown }).code === "NOT_FOUND"
  );
}

export function useLayouts(projectId: string | null): LayoutsViewModel {
  const { layout: gateway, system } = useGateways();
  const [layouts, setLayouts] = useState<LayoutInfo[]>([]);
  const [activeId, setActiveId] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Re-list the layouts and reconcile the locally-remembered active id against
  // the backend's authoritative set (invariants I3/I4). Any local active id that
  // is absent from the freshly-listed layouts is *purged* — we never replay a
  // vanished id (the bug after git overwrites `layouts.json`); we fall back to
  // the active id the backend reports. The shared self-heal used on mount, on
  // `layoutChanged`, and after a NOT_FOUND from an explicit op (rename/delete).
  const refresh = useCallback(async () => {
    if (!projectId || !gateway) return;
    try {
      const { layouts: list, activeId: aid } = await gateway.listLayouts(projectId);
      setLayouts(list);
      setActiveId((prev) =>
        prev && list.some((l) => l.id === prev) ? prev : aid,
      );
    } catch (e) {
      setError(describe(e));
    }
  }, [gateway, projectId]);

  // Load the list on mount and whenever projectId changes.
  useEffect(() => {
    if (!projectId || !gateway) {
      setLayouts([]);
      setActiveId(null);
      return;
    }
    let cancelled = false;
    setBusy(true);
    gateway
      .listLayouts(projectId)
      .then(({ layouts: list, activeId: aid }) => {
        if (!cancelled) {
          setLayouts(list);
          setActiveId(aid);
        }
      })
      .catch((e: unknown) => {
        if (!cancelled) setError(describe(e));
      })
      .finally(() => {
        if (!cancelled) setBusy(false);
      });
    return () => {
      cancelled = true;
    };
  }, [gateway, projectId]);

  // Re-sync on external layout changes (e.g. git overwriting `layouts.json`,
  // reboot reconciliation): the backend emits `layoutChanged`. We re-list and
  // purge any stale local active id so the tab bar never replays a vanished id.
  useEffect(() => {
    if (!projectId || !system) return;
    let cancelled = false;
    let unsubscribe: (() => void) | undefined;
    void system
      .onDomainEvent((event) => {
        if (event.type === "layoutChanged" && event.projectId === projectId) {
          void refresh();
        }
      })
      .then((un) => {
        if (cancelled) un();
        else unsubscribe = un;
      });
    return () => {
      cancelled = true;
      unsubscribe?.();
    };
  }, [system, projectId, refresh]);

  const setActive = useCallback(
    async (layoutId: string) => {
      if (!projectId || !gateway) return;
      setBusy(true);
      setError(null);
      try {
        // I4: the backend's returned id is authoritative — adopt it instead of
        // re-imposing the requested id (which may be stale). Code defensively in
        // case an older backend still returns void: fall back to the requested id.
        const result = await gateway.setActiveLayout(projectId, layoutId);
        setActiveId(result?.activeId ?? layoutId);
      } catch (e) {
        setError(describe(e));
      } finally {
        setBusy(false);
      }
    },
    [gateway, projectId],
  );

  const create = useCallback(
    async (
      name: string,
      kind?: LayoutKind,
      pluginOrigin?: PluginLayoutOrigin,
    ): Promise<string | null> => {
      if (!projectId || !gateway) return null;
      setBusy(true);
      setError(null);
      try {
        const { layoutId } = await gateway.createLayout(projectId, name, kind, pluginOrigin);
        const updated = await gateway.listLayouts(projectId);
        setLayouts(updated.layouts);
        return layoutId;
      } catch (e) {
        setError(describe(e));
        return null;
      } finally {
        setBusy(false);
      }
    },
    [gateway, projectId],
  );

  const rename = useCallback(
    async (layoutId: string, name: string) => {
      if (!projectId || !gateway) return;
      setBusy(true);
      setError(null);
      try {
        await gateway.renameLayout(projectId, layoutId, name);
        setLayouts((prev) =>
          prev.map((l) => (l.id === layoutId ? { ...l, name } : l)),
        );
      } catch (e) {
        // NOT_FOUND on an explicit op is legitimate (the layout vanished, e.g.
        // an external overwrite) and must NOT freeze the UI: re-list and adopt
        // the backend's active id instead of surfacing a hard error.
        if (isNotFound(e)) {
          await refresh();
        } else {
          setError(describe(e));
        }
      } finally {
        setBusy(false);
      }
    },
    [gateway, projectId, refresh],
  );

  const deleteLayout = useCallback(
    async (layoutId: string) => {
      if (!projectId || !gateway) return;
      if (layouts.length <= 1) {
        setError("Cannot delete the last layout.");
        return;
      }
      setBusy(true);
      setError(null);
      try {
        const { activeId: newActiveId } = await gateway.deleteLayout(projectId, layoutId);
        setLayouts((prev) => prev.filter((l) => l.id !== layoutId));
        setActiveId(newActiveId);
      } catch (e) {
        // NOT_FOUND means the layout was already gone (e.g. external overwrite):
        // treat it as non-fatal — re-list and adopt the backend's active id.
        if (isNotFound(e)) {
          await refresh();
        } else {
          setError(describe(e));
        }
      } finally {
        setBusy(false);
      }
    },
    [gateway, projectId, layouts.length, refresh],
  );

  return { layouts, activeId, busy, error, setActive, create, rename, deleteLayout };
}
