import type { PenNode, ContainerProps } from '@zseven-w/pen-types';
import { isOverlayNode } from '../node-helpers.js';
import { inferLayout } from './engine.js';

/**
 * Normalize layout state across a node tree (mutates in place).
 *
 * Two fixes applied recursively to every frame:
 *
 * 1. When a frame has children but no explicit `layout`, write one:
 *    - First try `inferLayout()` (horizontal signals: gap, padding,
 *      fill_container children).
 *    - If that returns undefined and the frame has 2+ children, fall back
 *      to `vertical` — BUT only when no non-overlay child carries explicit
 *      `x`/`y`. Any such coordinate is treated as a deliberate signal that
 *      the frame is an absolute-positioning container (phone mockups, hero
 *      images with floating overlays, etc.) and we leave it alone.
 *
 * 2. When a frame has an active layout (`vertical` or `horizontal`), strip
 *    `x`/`y` from every non-overlay child. Overlay children (opt-in via
 *    `role: 'overlay'`) keep their absolute coordinates.
 *
 * Used as a post-generation pass after an AI model produces a subtree. It
 * corrects two common model mistakes:
 *   - Forgetting to set `layout` on a container (children would otherwise
 *     stack at (0,0) because `computeLayoutPositions` skips layout-less
 *     parents).
 *   - Leaving stale `x`/`y` on children of an auto-layout frame (causes
 *     visible misalignment when the layout engine also tries to position
 *     them).
 *
 * Ordering requirement: MUST run AFTER any role-based resolution pass that
 * populates `layout` from semantic roles (e.g. navbar → 'horizontal').
 * Running this first would write the generic 'vertical' fallback onto a
 * navbar frame, and the later role resolver — which only fills undefined
 * fields — would refuse to overwrite it. Treat this function as the last
 * safety net, not the first opinion.
 */
export function normalizeTreeLayout(node: PenNode): void {
  if (node.type === 'frame' && 'children' in node && Array.isArray(node.children)) {
    const c = node as PenNode & ContainerProps;
    const children = node.children;

    // (1) Ensure an explicit layout when children exist.
    // Only fill in when `layout` is missing — an explicit `'none'` is
    // intentional (absolute positioning) and must be preserved.
    if (c.layout == null && children.length > 0) {
      const inferred = inferLayout(node);
      if (inferred) {
        c.layout = inferred;
      } else if (children.length >= 2 && !hasAbsolutePositionedChild(children)) {
        // Safe to treat as a "model forgot layout" case: nobody carries x/y,
        // so there's no absolute-positioning intent to destroy.
        c.layout = 'vertical';
      }
    }

    // (2) Strip x/y from non-overlay children of active-layout frames.
    if (c.layout === 'vertical' || c.layout === 'horizontal') {
      for (const child of children) {
        if (!isOverlayNode(child)) {
          if ('x' in child) delete (child as { x?: number }).x;
          if ('y' in child) delete (child as { y?: number }).y;
        }
      }
    }
  }

  // Recurse into children regardless of node type (groups/pages may nest frames).
  if ('children' in node && Array.isArray(node.children)) {
    for (const child of node.children) {
      normalizeTreeLayout(child);
    }
  }
}

/**
 * Decide whether a layout-less parent should be treated as an absolute-
 * positioning container (skip the `vertical` fallback) instead of being
 * verticalized.
 *
 * Two signals count as "this is an absolute-positioning container":
 *
 *   1. A non-overlay child has a numeric `x` or `y` (including 0). Models
 *      that explicitly position children clearly intend absolute layout.
 *
 *   2. EVERY non-overlay child is a "composition primitive" — currently
 *      `frame`, `ellipse`, or `path`. AI models routinely emit structured
 *      compositions at (0,0) without writing x/y at all: nested-frame
 *      badge stacks, hero overlays, rings built from concentric
 *      ellipses, icon compositions built from paths. The previous check
 *      only counted frame children, so an ellipse-only or path-only
 *      composition (e.g. 3 concentric ellipses forming a progress ring)
 *      fell through to the vertical fallback and rendered as a broken
 *      list.
 *
 *      The rule intentionally does NOT treat `rectangle` as a
 *      composition primitive: plain rectangles are by far the most
 *      common children in content stacks (nav buttons, list items,
 *      card backgrounds) and defaulting those to overlay would silently
 *      break many more designs than it fixes. When you really want a
 *      rectangle overlay composition, declare x/y explicitly.
 *
 *      Mixed content stacks (frame + text, shape + icon_font, etc.)
 *      still get the vertical fallback, because those are typically
 *      content stacks where verticalization is the right call.
 *
 * Overlay nodes (opt-in via `role: 'overlay'`, detected by `isOverlayNode`)
 * are excluded from the count — they legitimately carry x/y inside
 * auto-layout frames and shouldn't tip the heuristic.
 *
 * This is intentionally conservative: it accepts a few false negatives
 * (some genuinely vertical all-shape stacks will be left un-normalized,
 * forcing the AI to declare layout explicitly) to eliminate the
 * catastrophic false-positive of silently verticalizing an overlay
 * composition.
 */
const COMPOSITION_PRIMITIVE_TYPES = new Set(['frame', 'ellipse', 'path']);

function hasAbsolutePositionedChild(children: PenNode[]): boolean {
  // Signal 1: explicit numeric x/y on any non-overlay child.
  for (const child of children) {
    if (isOverlayNode(child)) continue;
    const c = child as PenNode & { x?: number; y?: number };
    if (typeof c.x === 'number' || typeof c.y === 'number') return true;
  }

  // Signal 2: every non-overlay child is a composition primitive
  // (>= 2 such children).
  let nonOverlayCount = 0;
  let primitiveCount = 0;
  for (const child of children) {
    if (isOverlayNode(child)) continue;
    nonOverlayCount++;
    if (COMPOSITION_PRIMITIVE_TYPES.has(child.type)) primitiveCount++;
  }
  if (nonOverlayCount >= 2 && primitiveCount === nonOverlayCount) return true;

  return false;
}
