/**
 * View placement model (ticket #22) — generalises the former single
 * `openPanel: PanelId | null` state into a per-view *slot* assignment.
 *
 * Each view (context/work/tickets/agents/…) is in exactly one placement at a
 * time — the core invariant. V1 supports:
 *   - `"closed"`   — not shown anywhere,
 *   - `"floating"` — shown in a modal {@link FloatingWindow} (the historical
 *                    behaviour),
 *   - `{ dock }`   — anchored in-flow in the left/right {@link DockRegion},
 *   - `"detached"` — popped out into its own OS window (ticket #23); nothing is
 *                    rendered for it in the main window while detached.
 *
 * One view = exactly one slot (the invariant): a view is closed, floating,
 * docked **or** detached, never two at once. Absent from the map ≡ `"closed"`,
 * so the default (nothing open) is the empty map.
 */

import type { DockSide } from "@/shared";
import type { ViewWindowSnapshot } from "@/ports";

/** A panel reachable from the menu bar. */
export type PanelId =
  | "projects"
  | "context"
  | "work"
  | "tickets"
  | "agents"
  | "templates"
  | "skills"
  | "permissions"
  | "memory"
  | "git";

/**
 * Where a view currently lives. Exactly one slot per view (the invariant).
 * `{ dock }` carries the side; `"detached"` means it lives in its own OS window.
 */
export type ViewPlacement =
  | "closed"
  | "floating"
  | { dock: DockSide }
  | "detached";

/** The placement of every open view (absent key ≡ `"closed"`). */
export type ViewPlacements = Partial<Record<PanelId, ViewPlacement>>;

/** Human titles for each panel (window title, dock header, aria-label). */
export const PANEL_TITLE: Record<PanelId, string> = {
  projects: "Projects",
  context: "Project context",
  work: "Work state",
  tickets: "Tickets",
  agents: "Agents",
  templates: "Templates",
  skills: "Skills",
  permissions: "Permissions",
  memory: "Memory",
  git: "Git",
};

/** Resolve a view's placement, treating an absent entry as `"closed"`. */
export function placementOf(
  placements: ViewPlacements,
  panel: PanelId,
): ViewPlacement {
  return placements[panel] ?? "closed";
}

/** True when the view is docked to the given side. */
export function isDockedTo(
  placement: ViewPlacement,
  side: DockSide,
): boolean {
  return typeof placement === "object" && placement.dock === side;
}

/** Every panel currently docked to `side`, in stable declaration order. */
export function panelsDockedTo(
  placements: ViewPlacements,
  side: DockSide,
): PanelId[] {
  return (Object.keys(PANEL_TITLE) as PanelId[]).filter((panel) =>
    isDockedTo(placementOf(placements, panel), side),
  );
}

/** Every panel currently floating, in stable declaration order. */
export function floatingPanels(placements: ViewPlacements): PanelId[] {
  return (Object.keys(PANEL_TITLE) as PanelId[]).filter(
    (panel) => placementOf(placements, panel) === "floating",
  );
}

/** True when the view is popped out into its own OS window (#23). */
export function isDetached(placement: ViewPlacement): boolean {
  return placement === "detached";
}

/**
 * True when `panel` is a known, detachable {@link PanelId} — every panel except
 * `projects`, which is main-window-only chrome (its submenu never offers the
 * detached-window action). Used by the startup reconciliation (#50) to accept
 * only detachable panels from a `ViewWindowSnapshot`: `projects` is excluded and
 * any unknown panel string is ignored.
 */
export function isDetachablePanel(panel: string): boolean {
  return panel !== "projects" && panel in PANEL_TITLE;
}

/**
 * Reconcile `prev` placements with the detached windows already open at the OS
 * level (#50), returning the merged placements — or `prev` unchanged when there
 * is nothing to reconcile (referential-stability so a React setter is a no-op).
 *
 * Strict, best-effort merge honouring the Architect invariants:
 *  - only `visible === true` snapshots are considered;
 *  - only known detachable panels — `projects` and unknown panel strings are
 *    dropped ({@link isDetachablePanel});
 *  - a panel is set to `"detached"` **only** when its current placement is
 *    `"closed"`/absent — an existing `floating`/dock/`detached` is never
 *    clobbered (no loss of user state or of a more specific UI restore).
 */
export function reconcileDetachedWindows(
  prev: ViewPlacements,
  snapshots: ViewWindowSnapshot[],
): ViewPlacements {
  let changed = false;
  const next: ViewPlacements = { ...prev };
  for (const item of snapshots) {
    if (!item.visible) continue;
    if (!isDetachablePanel(item.panel)) continue;
    const panel = item.panel as PanelId;
    if (placementOf(prev, panel) !== "closed") continue;
    next[panel] = "detached";
    changed = true;
  }
  return changed ? next : prev;
}
