/**
 * Layout schema migrators. Runs at every read boundary so the in-memory model
 * is always at the current `LAYOUT_VERSION`. v1 layouts are upgraded
 * transparently and never persisted as v1 again — the next save writes v2.
 *
 * v1 → v2 (2026-04-25):
 *   The standalone `screenshot` layer type is retired. App-screen content is
 *   folded onto `FrameLayer.screen` so a device mockup is exactly one layer.
 *
 *   Per layer:
 *     - `frame`         → kept as-is; if a v1 screenshot is bound to it via
 *                         `frameId`, that screenshot's source + transform fields
 *                         (storagePath, fit, offsetX/Y, zoom, crop, adjust,
 *                         localePaths) are copied onto the frame's new `screen`
 *                         field. The screenshot layer is then dropped.
 *     - `screenshot` w/ frameId pointing to a sibling frame → consumed (above).
 *     - `screenshot` w/ frameId orphan (frame missing) → demoted to `image`,
 *                                                       frameId stripped.
 *     - `screenshot` standalone (no frameId)            → demoted to `image`.
 *     - other types → unchanged.
 *
 *   Frame-level transforms (skewX/Y, tiltX/Y, flipX/Y) are NOT inherited from
 *   the absorbed screenshot. v1 had no UI to tilt a frame, only its bound
 *   screenshot — and a tilted screenshot looks wrong inside an upright bezel.
 *   Migrating the screenshot's tilt onto the frame would also tilt the bezel,
 *   so we drop those fields on the absorbed screenshot and let the user
 *   re-author at the frame level if they want.
 */

import {
    type FrameScreen,
    type ImageAdjust,
    type ImageCrop,
    type ImageLayer,
    type Layer,
    type Layout,
    LAYOUT_VERSION,
} from './layout';

type AnyRecord = Record<string, unknown>;

function isRecord(v: unknown): v is AnyRecord {
    return typeof v === 'object' && v !== null && !Array.isArray(v);
}

function asNumber(v: unknown): number | undefined {
    return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
}

function asString(v: unknown): string | undefined {
    return typeof v === 'string' ? v : undefined;
}

function asBoolean(v: unknown): boolean | undefined {
    return typeof v === 'boolean' ? v : undefined;
}

function asImageCrop(v: unknown): ImageCrop | undefined {
    if (!isRecord(v)) return undefined;
    const out: ImageCrop = {};
    const top = asNumber(v.top);
    const right = asNumber(v.right);
    const bottom = asNumber(v.bottom);
    const left = asNumber(v.left);
    if (top !== undefined) out.top = top;
    if (right !== undefined) out.right = right;
    if (bottom !== undefined) out.bottom = bottom;
    if (left !== undefined) out.left = left;
    return Object.keys(out).length ? out : undefined;
}

function asImageAdjust(v: unknown): ImageAdjust | undefined {
    if (!isRecord(v)) return undefined;
    const out: ImageAdjust = {};
    const brightness = asNumber(v.brightness);
    const contrast = asNumber(v.contrast);
    const saturation = asNumber(v.saturation);
    const hue = asNumber(v.hue);
    const blur = asNumber(v.blur);
    const grayscale = asBoolean(v.grayscale);
    const invert = asBoolean(v.invert);
    if (brightness !== undefined) out.brightness = brightness;
    if (contrast !== undefined) out.contrast = contrast;
    if (saturation !== undefined) out.saturation = saturation;
    if (hue !== undefined) out.hue = hue;
    if (blur !== undefined) out.blur = blur;
    if (grayscale !== undefined) out.grayscale = grayscale;
    if (invert !== undefined) out.invert = invert;
    if (isRecord(v.tint)) {
        const color = asString(v.tint.color);
        const amount = asNumber(v.tint.amount);
        if (color && amount !== undefined) out.tint = { color, amount };
    }
    return Object.keys(out).length ? out : undefined;
}

function asLocalePaths(v: unknown): FrameScreen['localePaths'] {
    if (!isRecord(v)) return undefined;
    const out: Record<string, string> = {};
    for (const [k, val] of Object.entries(v)) {
        if (typeof val === 'string') out[k] = val;
    }
    return Object.keys(out).length ? out : undefined;
}

function asFit(v: unknown): 'cover' | 'contain' | 'fill' | undefined {
    return v === 'cover' || v === 'contain' || v === 'fill' ? v : undefined;
}

/**
 * Build a v2 `FrameScreen` from a legacy v1 screenshot layer's source+raster
 * fields. Tilt/skew/flip are intentionally dropped — see migrator preamble.
 */
function frameScreenFromLegacyScreenshot(legacy: AnyRecord): FrameScreen | undefined {
    const storagePath = asString(legacy.storagePath);
    if (!storagePath) return undefined;
    const screen: FrameScreen = { storagePath };
    const fit = asFit(legacy.fit);
    if (fit) screen.fit = fit;
    const offsetX = asNumber(legacy.offsetX);
    const offsetY = asNumber(legacy.offsetY);
    const zoom = asNumber(legacy.zoom);
    if (offsetX !== undefined) screen.offsetX = offsetX;
    if (offsetY !== undefined) screen.offsetY = offsetY;
    if (zoom !== undefined) screen.zoom = zoom;
    const crop = asImageCrop(legacy.crop);
    if (crop) screen.crop = crop;
    const adjust = asImageAdjust(legacy.adjust);
    if (adjust) screen.adjust = adjust;
    const localePaths = asLocalePaths(legacy.localePaths);
    if (localePaths) screen.localePaths = localePaths;
    return screen;
}

/**
 * Demote a legacy screenshot layer to an `image` layer. Used when the
 * screenshot has no frame binding, or its bound frame is missing. Preserves
 * raster fields (transform, crop, adjust) since image already understands
 * them; drops `frameId` and `localePaths` (image has no per-locale source —
 * v1 screenshots almost never used it anyway).
 */
function legacyScreenshotToImage(legacy: AnyRecord): ImageLayer {
    const fit = asFit(legacy.fit) ?? 'cover';
    const out = { ...legacy } as AnyRecord;
    out.type = 'image';
    out.fit = fit;
    delete out.frameId;
    delete out.localePaths;
    return out as unknown as ImageLayer;
}

type LegacyLayout = {
    version?: number;
    canvas: unknown;
    layers?: unknown;
    meta?: unknown;
};

/**
 * Migrate a v1 layout (or any earlier-version layout) to the current
 * `LAYOUT_VERSION`. Idempotent: passing a current-version layout returns it
 * unchanged. Unknown future versions are returned as-is — callers that care
 * should reject them at the boundary.
 */
export function migrateLayout(input: LegacyLayout | Layout): Layout {
    const version = (input as LegacyLayout).version;

    if (version === LAYOUT_VERSION) {
        return input as Layout;
    }

    // v1 (or missing version) → v2
    if (version === undefined || version === 1) {
        const legacyLayers = Array.isArray((input as LegacyLayout).layers)
            ? ((input as LegacyLayout).layers as AnyRecord[])
            : [];

        // Index frames by id so we can fold bound screenshots onto them.
        const frameIds = new Set<string>();
        for (const layer of legacyLayers) {
            if (isRecord(layer) && layer.type === 'frame' && typeof layer.id === 'string') {
                frameIds.add(layer.id);
            }
        }

        // Pre-pass: collect screenshot → frame bindings. The first screenshot
        // bound to a given frame wins (matches v1 render behavior — frame.tsx
        // only ever read one bound screenshot). Subsequent bindings demote to
        // image so no content is lost.
        const screensByFrame = new Map<string, FrameScreen>();
        const consumedScreenshotIds = new Set<string>();
        for (const layer of legacyLayers) {
            if (!isRecord(layer)) continue;
            if (layer.type !== 'screenshot') continue;
            const frameId = asString(layer.frameId);
            if (!frameId || !frameIds.has(frameId)) continue;
            if (screensByFrame.has(frameId)) continue;
            const screen = frameScreenFromLegacyScreenshot(layer);
            if (!screen) continue;
            screensByFrame.set(frameId, screen);
            if (typeof layer.id === 'string') consumedScreenshotIds.add(layer.id);
        }

        const migratedLayers: Layer[] = [];
        for (const layer of legacyLayers) {
            if (!isRecord(layer)) continue;

            if (layer.type === 'frame') {
                const id = asString(layer.id);
                const next = { ...layer } as AnyRecord;
                if (id && screensByFrame.has(id)) {
                    next.screen = screensByFrame.get(id);
                }
                migratedLayers.push(next as unknown as Layer);
                continue;
            }

            if (layer.type === 'screenshot') {
                const id = asString(layer.id);
                if (id && consumedScreenshotIds.has(id)) {
                    continue;
                }
                migratedLayers.push(legacyScreenshotToImage(layer));
                continue;
            }

            migratedLayers.push(layer as unknown as Layer);
        }

        return {
            version: LAYOUT_VERSION,
            canvas: (input as LegacyLayout).canvas as Layout['canvas'],
            layers: migratedLayers,
            meta: (input as LegacyLayout).meta as Layout['meta'],
        };
    }

    // v2 → v3: structural no-op stamp. v3 added `CalloutLayer` to the Layer
    // union; existing v2 layouts have no callouts and are valid v3 layouts
    // as-is, just need the version field bumped so equality checks pass.
    if (version === 2) {
        return {
            ...(input as Layout),
            version: LAYOUT_VERSION,
        };
    }

    // Unknown version — return as-is. Callers may reject if strict.
    return input as Layout;
}

