import type { LayoutNode, LayoutPanel, LayoutGroup, PanelConfig } from './types';

let counter = 0;
/** The only way to mint a panel id. Time-stamped, so ids stay unique across page loads and dashboards. */
export const uid = () => `p-${Date.now()}-${++counter}`;

export const createDefaultLayout = (): LayoutPanel => ({
  type: 'panel',
  id: uid(),
  appType: null,
});

type LayoutChild = LayoutGroup['children'][number];

/**
 * Insert a new sibling by taking half of the target's space, not by redistributing the whole group.
 * Splitting one panel used to reset every proportion around it, so a deliberately narrow sidebar became
 * an equal column the first time anyone split anything else in the same row.
 */
function insertSplittingTarget(
  children: LayoutChild[],
  targetIdx: number,
  insertIdx: number,
  newPanel: LayoutNode,
): LayoutChild[] {
  const half = children[targetIdx]!.size / 2;
  const resized = children.map((c, i) => (i === targetIdx ? { ...c, size: half } : c));
  return [...resized.slice(0, insertIdx), { node: newPanel, size: half }, ...resized.slice(insertIdx)];
}

export function splitPanel(root: LayoutNode, panelId: string, direction: 'horizontal' | 'vertical'): LayoutNode {
  return splitInner(root, panelId, direction);
}

function splitInner(node: LayoutNode, panelId: string, direction: 'horizontal' | 'vertical'): LayoutNode {
  if (node.type === 'panel') {
    if (node.id !== panelId) return node;

    const newPanel: LayoutPanel = { type: 'panel', id: uid(), appType: null };
    const group: LayoutGroup = {
      type: 'group',
      id: uid(),
      direction,
      children: [
        { node, size: 50 },
        { node: newPanel, size: 50 },
      ],
    };
    return group;
  }

  // Check if the target panel is a direct child and the directions match — append as sibling
  const childIdx = node.children.findIndex((c) => c.node.type === 'panel' && c.node.id === panelId);
  if (childIdx !== -1 && node.direction === direction) {
    const newPanel: LayoutPanel = { type: 'panel', id: uid(), appType: null };
    return { ...node, children: insertSplittingTarget(node.children, childIdx, childIdx + 1, newPanel) };
  }

  // Recurse into children
  const newChildren = node.children.map((child) => ({
    ...child,
    node: splitInner(child.node, panelId, direction),
  }));
  if (newChildren.some((c, i) => c.node !== node.children[i]!.node)) {
    return { ...node, children: newChildren };
  }
  return node;
}

export function removePanel(root: LayoutNode, panelId: string): LayoutNode {
  if (root.type === 'panel') return root;
  if (countPanels(root) <= 1) return root;

  return removePanelInner(root);

  function removePanelInner(node: LayoutNode): LayoutNode {
    if (node.type === 'panel') return node;

    const filtered = node.children.filter((c) => !(c.node.type === 'panel' && c.node.id === panelId));

    if (filtered.length < node.children.length) {
      if (filtered.length === 0) return createDefaultLayout();
      if (filtered.length === 1) return filtered[0]!.node;
      const total = filtered.reduce((sum, c) => sum + c.size, 0);
      return {
        ...node,
        children: filtered.map((c) => ({ ...c, size: (c.size / total) * 100 })),
      };
    }

    const newChildren = node.children.map((child) => ({
      ...child,
      node: removePanelInner(child.node),
    }));

    const unwrapped = newChildren.map((child) => {
      if (child.node.type === 'group' && child.node.children.length === 1) {
        return { ...child, node: child.node.children[0]!.node };
      }
      return child;
    });

    return { ...node, children: unwrapped };
  }
}

export const ZOOM_MIN = 0.7;
export const ZOOM_MAX = 1.6;
export const ZOOM_STEP = 0.1;

export const clampZoom = (z: number) => Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(z * 10) / 10));

// Set a panel's content scale. Stores nothing at 1 — the default — so an untouched panel adds no key to
// the persisted layout and a reset returns the node to its original shape.
export function setZoom(root: LayoutNode, panelId: string, zoom: number): LayoutNode {
  if (root.type === 'panel') {
    if (root.id !== panelId) return root;
    const next = clampZoom(zoom);
    if ((root.zoom ?? 1) === next) return root;
    if (next === 1) {
      const { zoom: _drop, ...rest } = root;
      return rest;
    }
    return { ...root, zoom: next };
  }
  const newChildren = root.children.map((child) => {
    const node = setZoom(child.node, panelId, zoom);
    return node === child.node ? child : { ...child, node };
  });
  return newChildren.some((c, i) => c !== root.children[i]) ? { ...root, children: newChildren } : root;
}

export function updateSizes(root: LayoutNode, groupId: string, sizes: number[]): LayoutNode {
  if (root.type === 'panel') return root;
  if (root.id === groupId) {
    return {
      ...root,
      children: root.children.map((child, i) => ({ ...child, size: sizes[i] ?? child.size })),
    };
  }
  const newChildren = root.children.map((child) => ({
    ...child,
    node: updateSizes(child.node, groupId, sizes),
  }));
  return { ...root, children: newChildren };
}

/**
 * Pin every panel in a tree to an app the screen actually knows how to render.
 *
 * A locked screen's layout is persisted user state, so it outlives the code that wrote it: rename an
 * appType, drop a panel from a screen, restore an old row, and the stored tree still names something the
 * screen has no answer for. `PanelSlot` renders that as an empty bordered box with no picker — and on a
 * locked screen there is no way for the user to get out of it.
 *
 * Returns the input by reference when nothing needed fixing, which is what lets the caller tell "this
 * layout was already fine" from "this layout was repaired and should be written back".
 *
 * `null` — an empty panel — is always allowed. It is a state of the framework rather than an app, every
 * hand-rolled copy of this allowed it, and a screen that forgot to would rewrite each of its empty panels
 * to the fallback on first load.
 */
export function normalizeLayout(node: LayoutNode, allowed: readonly string[], fallback: string): LayoutNode {
  if (node.type === 'panel') {
    if (node.appType === null || allowed.includes(node.appType)) return node;
    // The config goes with the app that owned it. Whatever is in there was written by the app this panel
    // is no longer running, and handing it to the fallback is the same mistake `setApp` avoids.
    const { config: _drop, ...rest } = node;
    return { ...rest, appType: fallback };
  }
  const children = node.children.map((child) => {
    const fixed = normalizeLayout(child.node, allowed, fallback);
    return fixed === child.node ? child : { ...child, node: fixed };
  });
  return children.some((c, i) => c !== node.children[i]) ? { ...node, children } : node;
}

export function pruneEmptyPanels(root: LayoutNode): LayoutNode | null {
  if (root.type === 'panel') {
    return root.appType ? root : null;
  }

  const pruned = root.children
    .map((child) => {
      const node = pruneEmptyPanels(child.node);
      return node ? { ...child, node } : null;
    })
    .filter((c): c is NonNullable<typeof c> => c !== null);

  if (pruned.length === 0) return null;
  if (pruned.length === 1) return pruned[0]!.node;

  const total = pruned.reduce((sum, c) => sum + c.size, 0);
  return {
    ...root,
    children: pruned.map((c) => ({ ...c, size: (c.size / total) * 100 })),
  };
}

export type DropPosition = 'top' | 'bottom' | 'left' | 'right' | 'center';

export function movePanel(root: LayoutNode, sourceId: string, targetId: string, position: DropPosition): LayoutNode {
  if (sourceId === targetId) return root;
  if (position === 'center') return swapPanels(root, sourceId, targetId);

  const contents = findPanelContents(root, sourceId);
  if (!contents) return root;

  let result = removePanel(root, sourceId);
  const direction = position === 'top' || position === 'bottom' ? 'vertical' : 'horizontal';
  const before = position === 'top' || position === 'left';
  return insertPanel(result, targetId, direction, before, contents);
}

const newPanelFrom = (contents: PanelContents): LayoutPanel => ({
  type: 'panel',
  id: uid(),
  appType: contents.appType,
  ...(contents.config && Object.keys(contents.config).length > 0 ? { config: contents.config } : {}),
});

function insertPanel(
  node: LayoutNode,
  targetId: string,
  direction: 'horizontal' | 'vertical',
  before: boolean,
  contents: PanelContents,
): LayoutNode {
  if (node.type === 'panel') {
    if (node.id !== targetId) return node;
    const newPanel = newPanelFrom(contents);
    const children = before
      ? [
          { node: newPanel, size: 50 },
          { node, size: 50 },
        ]
      : [
          { node, size: 50 },
          { node: newPanel, size: 50 },
        ];
    return { type: 'group', id: uid(), direction, children };
  }

  const childIdx = node.children.findIndex((c) => c.node.type === 'panel' && c.node.id === targetId);
  if (childIdx !== -1 && node.direction === direction) {
    const insertIdx = before ? childIdx : childIdx + 1;
    return { ...node, children: insertSplittingTarget(node.children, childIdx, insertIdx, newPanelFrom(contents)) };
  }

  const newChildren = node.children.map((child) => ({
    ...child,
    node: insertPanel(child.node, targetId, direction, before, contents),
  }));
  if (newChildren.some((c, i) => c.node !== node.children[i]!.node)) {
    return { ...node, children: newChildren };
  }
  return node;
}

// Swap and move exchange the panel's *contents*, not its node: ids are positions in the tree and stay
// where they are. What moves is `{ appType, config }` together — an app and the settings it was given
// are one thing, and carrying only the app type would silently reset a configured panel to defaults on
// a drag. (That is what used to happen: the two mutators below were written when `appType` was the
// whole of a panel's identity.)
type PanelContents = { appType: string | null; config?: PanelConfig };

export function swapPanels(root: LayoutNode, idA: string, idB: string): LayoutNode {
  const a = findPanelContents(root, idA);
  const b = findPanelContents(root, idB);
  if (!a || !b) return root;
  return setContents(setContents(root, idA, b), idB, a);
}

export function setApp(root: LayoutNode, panelId: string, appType: string | null): LayoutNode {
  // Changing the app clears its config with it — config is owned by whichever app the panel is running,
  // and the next occupant would read the previous one's settings as its own. Only a no-op set (the same
  // app, e.g. re-picking what is already there) keeps them.
  const current = findPanelContents(root, panelId);
  if (!current) return root;
  return setContents(root, panelId, { appType, config: appType === current.appType ? current.config : undefined });
}

/**
 * Replace an app's opaque settings blob. Storing `undefined` or an empty object drops the key entirely,
 * so an unconfigured panel adds nothing to the persisted layout.
 */
export function setPanelConfig(root: LayoutNode, panelId: string, config: PanelConfig | undefined): LayoutNode {
  const current = findPanelContents(root, panelId);
  if (!current) return root;
  return setContents(root, panelId, { appType: current.appType, config });
}

/**
 * Which app a panel is running right now. `null` is a real answer (an empty panel); `undefined` means
 * there is no panel with that id. Callers that only want to know whether something changed can compare
 * the result directly, since both absences are distinct from any app key.
 */
export function findPanelApp(node: LayoutNode, panelId: string): string | null | undefined {
  return findPanelContents(node, panelId)?.appType;
}

/** Every configured panel in the tree, by id — the reactive view apps read through `usePanelConfig`. */
export function collectPanelConfigs(node: LayoutNode): Record<string, PanelConfig> {
  const out: Record<string, PanelConfig> = {};
  walk(node);
  return out;

  function walk(n: LayoutNode): void {
    if (n.type === 'panel') {
      if (n.config) out[n.id] = n.config;
      return;
    }
    for (const child of n.children) walk(child.node);
  }
}

function setContents(root: LayoutNode, panelId: string, contents: PanelContents): LayoutNode {
  if (root.type === 'panel') {
    if (root.id !== panelId) return root;
    const { config: _drop, ...rest } = root;
    const next: LayoutPanel = { ...rest, appType: contents.appType };
    if (contents.config && Object.keys(contents.config).length > 0) next.config = contents.config;
    return next;
  }
  const newChildren = root.children.map((child) => {
    const node = setContents(child.node, panelId, contents);
    return node === child.node ? child : { ...child, node };
  });
  return newChildren.some((c, i) => c !== root.children[i]) ? { ...root, children: newChildren } : root;
}

function findPanelContents(node: LayoutNode, panelId: string): PanelContents | undefined {
  if (node.type === 'panel') return node.id === panelId ? { appType: node.appType, config: node.config } : undefined;
  for (const child of node.children) {
    const result = findPanelContents(child.node, panelId);
    if (result !== undefined) return result;
  }
  return undefined;
}

export function countPanels(node: LayoutNode): number {
  if (node.type === 'panel') return 1;
  return node.children.reduce((sum, child) => sum + countPanels(child.node), 0);
}

export function hasAnyApp(node: LayoutNode): boolean {
  if (node.type === 'panel') return node.appType !== null;
  return node.children.some((child) => hasAnyApp(child.node));
}
