/**
 * Pixel Office layout 저장 스키마 — workspaceState 의 `g1nation.pixelOfficeLayout` 키
 * 에 저장되는 객체의 런타임 validator + v1 → v2 migration.
 *
 * 옛 runtime.ts 의 `_isV2Snap()` heuristic 을 정식 schema 로 격상. webview 에서 받는
 * 즉시 한 번 통과시키면 깨진 데이터 / 옛 데이터 모두 안전하게 처리된다.
 *
 * 백엔드는 unknown 그대로 저장하지만, *로드 직후* 이 validator 를 적용해 정규화한다.
 */

export interface OfficeDeskCell {
    /** 안정적 식별자 — DOM dataset.role 로도 쓰임. */
    roleKey: string;
    /** 매핑된 agent id. 비어있으면 unmapped. */
    agentKey: string;
    label: string;
    charRow: number; // 0~7
    deskSprite: string;
    /** 앉은 face. */
    face: 'L' | 'R' | 'U' | 'D';
    boss: boolean;
    /** seat 에서 잠시 일어났다 가는 dock 좌표. */
    dock?: [number, number];
    /** 랜덤 roam 후보 좌표들. */
    roam?: Array<[number, number]>;
    deskX: number;
    deskY: number;
    deskW: number;
    deskRot: number;
    deskZ: number;
    seatX: number;
    seatY: number;
    charRot: number;
    charZ: number;
    /** 캐릭터를 지운 빈 책상. */
    noChar: boolean;
}

export interface OfficeProp {
    id: string;
    name: string;
    x: number;
    y: number;
    w?: number;
    rot: number;
    z: number;
}

export interface LayoutV2 {
    schema: 2;
    cells: OfficeDeskCell[];
    objs: OfficeProp[];
}

const VALID_FACES = new Set<OfficeDeskCell['face']>(['L', 'R', 'U', 'D']);

/**
 * raw 가 valid v2 layout 이면 정규화된 LayoutV2 를, 아니면 null.
 * v1 (옛 좌표만 있는 포맷) 은 별도 `migrateLayout()` 사용.
 */
export function validateLayout(raw: unknown): LayoutV2 | null {
    if (!raw || typeof raw !== 'object') return null;
    const r = raw as Record<string, unknown>;
    if (!Array.isArray(r.cells)) return null;

    const isV2 = r.schema === 2 || r.cells.some(
        (c) =>
            c && typeof c === 'object' &&
            (typeof (c as Record<string, unknown>).deskSprite === 'string'
                || typeof (c as Record<string, unknown>).agentKey === 'string'
                || typeof (c as Record<string, unknown>).charRow === 'number'),
    );
    if (!isV2) return null;

    const cells = r.cells.map((c) => _normalizeCell(c)).filter((c): c is OfficeDeskCell => c !== null);
    const objsRaw = Array.isArray(r.objs) ? r.objs : [];
    const objs = objsRaw.map((o) => _normalizeProp(o)).filter((o): o is OfficeProp => o !== null);

    return { schema: 2, cells, objs };
}

/**
 * v1 (옛 좌표 패치 포맷) → v2 (전체 station 정의) 마이그레이션. v1 은 좌표만 갖고 있어
 * default station 의 나머지 필드(charRow, deskSprite 등)를 채워줘야 한다. webview 의
 * default station 매핑이 함께 주어져야 정확. 없으면 best-effort.
 *
 * 이번 세션 stub: v1 입력이 들어오면 v2 shape 으로 일단 변환 (default 필드는 0/빈 값).
 * 다음 세션에서 default station 룩업과 결합.
 */
export function migrateLayout(raw: unknown): LayoutV2 | null {
    const asV2 = validateLayout(raw);
    if (asV2) return asV2;
    if (!raw || typeof raw !== 'object') return null;
    const r = raw as Record<string, unknown>;
    if (!Array.isArray(r.cells)) return null;

    const cells: OfficeDeskCell[] = r.cells
        .map((cRaw) => {
            if (!cRaw || typeof cRaw !== 'object') return null;
            const c = cRaw as Record<string, unknown>;
            if (typeof c.roleKey !== 'string') return null;
            return _normalizeCell({
                ...c,
                // v1 은 charRow / deskSprite 등이 없으니 안전한 기본값.
                charRow: 0,
                deskSprite: 'desk-main',
                face: 'R',
                boss: false,
                agentKey: c.roleKey, // 옛 키가 곧 agent 였음.
                label: c.roleKey,
                noChar: false,
            });
        })
        .filter((c): c is OfficeDeskCell => c !== null);

    const objs = Array.isArray(r.objs)
        ? r.objs.map((o) => _normalizeProp(o)).filter((o): o is OfficeProp => o !== null)
        : [];

    return { schema: 2, cells, objs };
}

function _normalizeCell(raw: unknown): OfficeDeskCell | null {
    if (!raw || typeof raw !== 'object') return null;
    const c = raw as Record<string, unknown>;
    if (typeof c.roleKey !== 'string' || !c.roleKey) return null;
    const face = typeof c.face === 'string' && (VALID_FACES as ReadonlySet<string>).has(c.face)
        ? (c.face as OfficeDeskCell['face'])
        : 'R';
    return {
        roleKey: c.roleKey,
        agentKey: typeof c.agentKey === 'string' ? c.agentKey : '',
        label: typeof c.label === 'string' ? c.label : c.roleKey,
        charRow: _num(c.charRow, 0),
        deskSprite: typeof c.deskSprite === 'string' ? c.deskSprite : 'desk-main',
        face,
        boss: !!c.boss,
        dock: _pair(c.dock),
        roam: Array.isArray(c.roam)
            ? (c.roam.map(_pair).filter(Boolean) as Array<[number, number]>)
            : undefined,
        deskX: _num(c.deskX, 0),
        deskY: _num(c.deskY, 0),
        deskW: _num(c.deskW, 112),
        deskRot: _num(c.deskRot, 0),
        deskZ: _num(c.deskZ, 0),
        seatX: _num(c.seatX, 0),
        seatY: _num(c.seatY, 0),
        charRot: _num(c.charRot, 0),
        charZ: _num(c.charZ, 0),
        noChar: !!c.noChar,
    };
}

function _normalizeProp(raw: unknown): OfficeProp | null {
    if (!raw || typeof raw !== 'object') return null;
    const o = raw as Record<string, unknown>;
    if (typeof o.name !== 'string') return null;
    return {
        id: typeof o.id === 'string' ? o.id : `obj_${Math.random().toString(36).slice(2, 8)}`,
        name: o.name,
        x: _num(o.x, 0),
        y: _num(o.y, 0),
        w: typeof o.w === 'number' ? o.w : undefined,
        rot: _num(o.rot, 0),
        z: _num(o.z, 0),
    };
}

function _num(v: unknown, fallback: number): number {
    return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
}

function _pair(v: unknown): [number, number] | undefined {
    if (!Array.isArray(v) || v.length !== 2) return undefined;
    const a = typeof v[0] === 'number' ? v[0] : NaN;
    const b = typeof v[1] === 'number' ? v[1] : NaN;
    if (!Number.isFinite(a) || !Number.isFinite(b)) return undefined;
    return [a, b];
}
