// Deterministic design-token extraction (Epic #750, Issue #752).
//
// Walks the kept (pruned) node trees and projects four token families out of structural fields:
//   color       — solid `fills`/`strokes` → normalized #RRGGBB / #RRGGBBAA (alpha only when < 1).
//   typography  — TEXT `style` → family|size|weight|lineHeight.
//   spacing     — auto-layout `itemSpacing` + paddingTop/Right/Bottom/Left → distinct numbers.
//   radius      — `cornerRadius` → distinct numbers.
//
// Token identity is the canonical value (content-free): same value → one token, regardless of how
// many nodes carry it or in what order. Every family is sorted by its canonical key before emit, so
// output never depends on traversal or map-insertion order.
import { asNode, nodeType, readArray, readNumber, readString, } from "./sourceNode.js";
import { isVisiblePaint, paintColorToHex } from "./color.js";
const collectPaintColors = (node, key, out) => {
    for (const paint of readArray(node[key])) {
        const record = asNode(paint);
        if (record === undefined || readString(record.type) !== "SOLID")
            continue;
        if (!isVisiblePaint(record))
            continue;
        const hex = paintColorToHex(record);
        if (hex !== undefined)
            out.add(hex);
    }
};
const collectTypography = (node, out) => {
    if (nodeType(node) !== "TEXT")
        return;
    const style = asNode(node.style);
    if (style === undefined)
        return;
    const fontFamily = readString(style.fontFamily) ?? "";
    const fontSize = readNumber(style.fontSize) ?? 0;
    const fontWeight = readNumber(style.fontWeight) ?? 0;
    const lineHeight = readNumber(style.lineHeightPx) ?? 0;
    const id = `typography:${fontFamily}|${String(fontSize)}|${String(fontWeight)}|${String(lineHeight)}`;
    out.set(id, { id, kind: "typography", fontFamily, fontSize, fontWeight, lineHeight });
};
const SPACING_KEYS = ["itemSpacing", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"];
const collectSpacing = (node, out) => {
    for (const key of SPACING_KEYS) {
        const value = readNumber(node[key]);
        if (value !== undefined && value > 0)
            out.add(value);
    }
};
const collectRadius = (node, out) => {
    const value = readNumber(node.cornerRadius);
    if (value !== undefined && value > 0)
        out.add(value);
};
const visit = (pruned, acc) => {
    const node = pruned.source;
    collectPaintColors(node, "fills", acc.colors);
    collectPaintColors(node, "strokes", acc.colors);
    collectTypography(node, acc.typography);
    collectSpacing(node, acc.spacing);
    collectRadius(node, acc.radius);
    for (const child of pruned.children)
        visit(child, acc);
};
const toColorTokens = (values) => [...values].sort().map((value) => ({ id: `color:${value}`, kind: "color", value }));
const toSpacingTokens = (values) => [...values]
    .sort((a, b) => a - b)
    .map((value) => ({ id: `spacing:${String(value)}`, kind: "spacing", value }));
const toRadiusTokens = (values) => [...values]
    .sort((a, b) => a - b)
    .map((value) => ({ id: `radius:${String(value)}`, kind: "radius", value }));
/** Extract deduped, stable-ordered design tokens from the pruned screen roots. */
export const extractDesignTokens = (screens) => {
    const acc = {
        colors: new Set(),
        typography: new Map(),
        spacing: new Set(),
        radius: new Set(),
    };
    for (const screen of screens)
        visit(screen, acc);
    return {
        colors: toColorTokens(acc.colors),
        typography: [...acc.typography.values()].sort((a, b) => a.id.localeCompare(b.id)),
        spacing: toSpacingTokens(acc.spacing),
        radius: toRadiusTokens(acc.radius),
    };
};
// ─── Tolerant parse of a persisted/serialised design-tokens artifact (#752 → #755) ──────
//
// The snapshot persists the tokens artifact as an opaque JSON value (like the screen IR). Design-to-
// code (#755) reads the STORED snapshot, so it must re-hydrate the typed shape defensively: a missing
// or malformed family yields an empty list (codegen emits a smaller token table) rather than crashing.
const EMPTY_DESIGN_TOKENS = { colors: [], typography: [], spacing: [], radius: [] };
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
const isString = (value) => typeof value === "string";
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
const parseColorRows = (value) => (Array.isArray(value) ? value : []).flatMap((entry) => isRecord(entry) && isString(entry.value)
    ? [{ id: `color:${entry.value}`, kind: "color", value: entry.value }]
    : []);
const parseTypographyRows = (value) => (Array.isArray(value) ? value : []).flatMap((entry) => {
    if (!isRecord(entry) ||
        !isString(entry.fontFamily) ||
        !isFiniteNumber(entry.fontSize) ||
        !isFiniteNumber(entry.fontWeight) ||
        !isFiniteNumber(entry.lineHeight)) {
        return [];
    }
    const id = `typography:${entry.fontFamily}|${String(entry.fontSize)}|${String(entry.fontWeight)}|${String(entry.lineHeight)}`;
    return [
        {
            id,
            kind: "typography",
            fontFamily: entry.fontFamily,
            fontSize: entry.fontSize,
            fontWeight: entry.fontWeight,
            lineHeight: entry.lineHeight,
        },
    ];
});
const parseScalarRows = (value, kind) => (Array.isArray(value) ? value : []).flatMap((entry) => isRecord(entry) && isFiniteNumber(entry.value)
    ? [{ id: `${kind}:${String(entry.value)}`, kind, value: entry.value }]
    : []);
/**
 * Re-hydrate a {@link DesignTokens} value from the opaque, serialised tokens artifact persisted in a
 * Figma Snapshot (#753). Total + defensive: a non-object or any malformed family degrades to an empty
 * list so design-to-code (#755) never crashes on an old or partial snapshot. Stable shape, no IO.
 */
export const parseDesignTokens = (value) => {
    if (!isRecord(value))
        return EMPTY_DESIGN_TOKENS;
    return {
        colors: parseColorRows(value.colors),
        typography: parseTypographyRows(value.typography),
        spacing: parseScalarRows(value.spacing, "spacing"),
        radius: parseScalarRows(value.radius, "radius"),
    };
};
