// src/utils/legacy-migrate.ts
/**
 * Migration utility for older nvdocument shapes.
 *
 * Exports:
 *  - migrateLegacyDocument(documentData) -> normalized DocumentData
 *  - normalizeMeshForRehydrate(mesh) -> normalized mesh object suitable for rehydrateMeshes
 */

import { deserialize } from '@ungap/structured-clone'
import { DocumentData } from '@/nvdocument' // keep types aligned with your project

// --- helpers --------------------------------------------------------------

function isNumeric(v: any): boolean {
    return typeof v === 'number' && Number.isFinite(v)
}

function stabilizeNumber(n: number, decimals = 12): number {
    if (!isNumeric(n)) {
        return n
    }
    const mag = Math.max(1, Math.abs(n))
    const eps = 1e-9 * mag
    const rounded = Number(n.toFixed(decimals))
    if (Math.abs(n - rounded) <= eps) {
        return rounded
    }
    return n
}

function stabilizeArray(arr: any[], decimals = 12): any[] {
    return arr.map((v) => {
        if (Array.isArray(v)) {
            return stabilizeArray(v, decimals)
        }
        if (isNumeric(v)) {
            return stabilizeNumber(v, decimals)
        }
        return v
    })
}

// Encode special numeric values into strings for safe JSON roundtrip
function specialNumberReplacer(_key: string, value: any): any {
    if (typeof value === 'number') {
        if (Number.isNaN(value)) {
            return 'NaN'
        }
        if (value === Infinity) {
            return 'infinity'
        }
        if (value === -Infinity) {
            return '-infinity'
        }
        return value
    }
    return value
}

/**
 * Try to extract an Array<number> from a value produced by structured-clone
 * or various typed-array serializations.
 */
export function extractNumberArray(maybe: any): number[] | undefined {
    if (maybe === undefined || maybe === null) {
        return undefined
    }
    if (Array.isArray(maybe)) {
        return maybe.slice()
    }
    // typed arrays / ArrayBufferView
    if (ArrayBuffer.isView(maybe)) {
        try {
            return Array.from(maybe as unknown as Iterable<number>)
        } catch (_) {
            /* fallthrough */
        }
    }
    if (typeof maybe === 'object') {
        if (Array.isArray((maybe as any).data)) {
            return (maybe as any).data.slice()
        }
        if (maybe.buffer && Array.isArray(maybe.buffer.data)) {
            return maybe.buffer.data.slice()
        }
        // numeric-keyed object like { "0": v0, "1": v1, length: N }
        const numericKeys = Object.keys(maybe)
            .filter((k) => /^\d+$/.test(k))
            .map((k) => Number(k))
            .sort((a, b) => a - b)
        if (numericKeys.length > 0) {
            try {
                return numericKeys.map((i) => maybe[String(i)])
            } catch (_) {
                /* ignore */
            }
        }
        for (const candidateKey of ['_data', 'source', 'values', 'elements', 'items']) {
            if (Array.isArray((maybe as any)[candidateKey])) {
                return (maybe as any)[candidateKey].slice()
            }
        }
    }
    try {
        const arr = Array.from(maybe as unknown as Iterable<number>)
        if (Array.isArray(arr)) {
            return arr
        }
    } catch (_) {
        /* ignore */
    }
    return undefined
}

// --- per-mesh normalization -----------------------------------------------

/**
 * Convert a single parsed mesh object from legacy shapes to the modern shape
 * used by rehydrateMeshes. This is intentionally defensive and non-destructive.
 */
export function normalizeMeshForRehydrate(meshIn: any): any {
    if (!meshIn || typeof meshIn !== 'object') {
        return meshIn
    }
    const m: any = { ...(meshIn || {}) }

    // 1) pts (vertex positions)
    if ((m.pts === undefined || m.pts === null) && (m.vertices !== undefined || m.positions !== undefined || m.verts !== undefined)) {
        const candidates = [m.vertices, m.positions, m.verts]
        for (const c of candidates) {
            const arr = extractNumberArray(c)
            if (arr && arr.length > 0) {
                m.pts = stabilizeArray(arr)
                break
            }
        }
    } else if (m.pts != null && !Array.isArray(m.pts) && ArrayBuffer.isView(m.pts)) {
        const arr = extractNumberArray(m.pts)
        if (arr) {
            m.pts = stabilizeArray(arr)
        }
    } else if (Array.isArray(m.pts)) {
        m.pts = stabilizeArray(m.pts)
    }

    // 2) tris (triangle indices)
    if ((m.tris === undefined || m.tris === null) && (m.indices !== undefined || m.faces !== undefined || m.cells !== undefined)) {
        const candidates = [m.indices, m.faces, m.cells]
        for (const c of candidates) {
            const arr = extractNumberArray(c)
            if (arr && arr.length > 0) {
                m.tris = arr
                break
            }
        }
    } else if (m.tris != null && !Array.isArray(m.tris) && ArrayBuffer.isView(m.tris)) {
        const arr = extractNumberArray(m.tris)
        if (arr) {
            m.tris = arr
        }
    }

    // 3) rgba255 (colors)
    if ((m.rgba255 === undefined || m.rgba255 === null) && (m.rgba !== undefined || m.color !== undefined || m.colors !== undefined)) {
        const candidates = [m.rgba, m.color, m.colors]
        for (const c of candidates) {
            const arr = extractNumberArray(c)
            if (arr && arr.length > 0) {
                m.rgba255 = arr
                break
            }
        }
    } else if (m.rgba255 != null && !Array.isArray(m.rgba255) && ArrayBuffer.isView(m.rgba255)) {
        const arr = extractNumberArray(m.rgba255)
        if (arr) {
            m.rgba255 = arr
        }
    }

    // 4) offsetPt0 (legacy fiber storage), fiber metadata left intact
    if (m.offsetPt0 != null && !Array.isArray(m.offsetPt0)) {
        const arr = extractNumberArray(m.offsetPt0)
        if (arr) {
            m.offsetPt0 = arr
        }
    }

    // 5) nodes/edges to plain arrays with shallow clones for objects
    if (Array.isArray(m.nodes)) {
        m.nodes = m.nodes.length > 0 && typeof m.nodes[0] === 'object' ? m.nodes.map((n: any) => ({ ...n })) : m.nodes.slice()
    }
    if (Array.isArray(m.edges)) {
        m.edges = m.edges.length > 0 && typeof m.edges[0] === 'object' ? m.edges.map((e: any) => ({ ...e })) : m.edges.slice()
    }

    // 6) normalize layers: rename legacy keys and extract values arrays
    if (Array.isArray(m.layers)) {
        m.layers = m.layers.map((layer: any) => {
            if (!layer || typeof layer !== 'object') {
                return layer
            }
            const lcopy: any = { ...(layer || {}) }

            // Normalize legacy colormap keys to modern names, but prefer any already-present modern keys.
            if ('colorMap' in lcopy) {
                if (!('colormap' in lcopy)) {
                    lcopy.colormap = lcopy.colorMap
                }
                // remove legacy key
                delete lcopy.colorMap
            }
            if ('colorMapNegative' in lcopy) {
                if (!('colormapNegative' in lcopy)) {
                    lcopy.colormapNegative = lcopy.colorMapNegative
                }
                // remove legacy key
                delete lcopy.colorMapNegative
            }

            // decode numeric strings if present (lightweight)
            for (const k of ['global_min', 'global_max', 'cal_min', 'cal_max', 'cal_minNeg', 'cal_maxNeg']) {
                if (typeof lcopy[k] === 'string') {
                    const v = lcopy[k]
                    if (v === 'infinity') {
                        lcopy[k] = Infinity
                    } else if (v === '-infinity') {
                        lcopy[k] = -Infinity
                    } else if (v === 'NaN') {
                        lcopy[k] = NaN
                    } else {
                        const n = Number(v)
                        if (!Number.isNaN(n)) {
                            lcopy[k] = n
                        }
                    }
                }
            }

            if (lcopy.values != null && !Array.isArray(lcopy.values)) {
                const ev = extractNumberArray(lcopy.values)
                if (ev) {
                    lcopy.values = stabilizeArray(ev)
                }
            } else if (Array.isArray(lcopy.values)) {
                lcopy.values = stabilizeArray(lcopy.values)
            }

            if (lcopy.atlasValues != null && !Array.isArray(lcopy.atlasValues)) {
                const ev = extractNumberArray(lcopy.atlasValues)
                if (ev) {
                    lcopy.atlasValues = ev
                }
            }

            return lcopy
        })
    }

    return m
}

// --- meshesString normalization ------------------------------------------

function normalizeMeshesString(meshesString: any): string | undefined {
    if (typeof meshesString !== 'string') {
        return undefined
    }
    const trimmed = meshesString.trim()
    if (trimmed.length === 0) {
        return meshesString
    }
    try {
        const parsed = JSON.parse(meshesString)

        // Detect structured-clone serialized form: array of small objects with numeric keys 0 and 1
        const looksLikeStructuredClone = Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'object' && parsed[0] !== null && '0' in parsed[0] && '1' in parsed[0]

        if (looksLikeStructuredClone) {
            // Use deserialize to reconstruct original objects, then normalize them
            try {
                const des = deserialize(parsed as unknown as any) as any[] // reconstructs typed arrays / objects
                if (Array.isArray(des)) {
                    const normalized = des.map((mx: any) => normalizeMeshForRehydrate(mx))
                    return JSON.stringify(normalized, specialNumberReplacer)
                }
                // fallback: if deserialize produced something unexpected, continue below
            } catch (deserErr) {
                // if deserialize fails, fall through to legacy parsing below
                console.warn('legacy-migrate: structured-clone deserialize failed', deserErr)
            }
        }

        // If not structured-clone or deserialize didn't work, handle the usual JSON array case
        if (!Array.isArray(parsed)) {
            // try to locate inner array if serialize() produced wrapper
            const maybeArrayCandidates = ['value', 'data', '0']
            for (const c of maybeArrayCandidates) {
                if (parsed && Array.isArray((parsed as any)[c])) {
                    const arr = (parsed as any)[c]
                    const norm = arr.map((mx: any) => normalizeMeshForRehydrate(mx))
                    return JSON.stringify(norm, specialNumberReplacer)
                }
            }
            return meshesString
        }
        const normalized = parsed.map((mx: any) => normalizeMeshForRehydrate(mx))
        return JSON.stringify(normalized, specialNumberReplacer)
    } catch (e) {
        return meshesString
    }
}

// --- document-level migration -------------------------------------------

/**
 * Top-level migrateLegacyDocument function.
 * Takes a DocumentData-ish object and returns a normalized DocumentData object (shallow clone).
 */
export function migrateLegacyDocument(input: DocumentData | any | undefined): DocumentData {
    if (!input || typeof input !== 'object') {
        return input as DocumentData
    }

    const doc: any = { ...(input || {}) }

    // normalize root-level legacy colormap names
    if ('colorMap' in doc && !('colormap' in doc)) {
        doc.colormap = doc.colorMap
    }
    if ('colorMapNegative' in doc && !('colormapNegative' in doc)) {
        doc.colormapNegative = doc.colorMapNegative
    }

    // imageOptionsArray: convert typed-array-ish colormap fields and decode numeric strings
    if (Array.isArray(doc.imageOptionsArray)) {
        doc.imageOptionsArray = doc.imageOptionsArray.map((io: any) => {
            if (!io || typeof io !== 'object') {
                return io
            }
            const copy = { ...(io || {}) }
            for (const k of ['cal_min', 'cal_max', 'cal_minNeg', 'cal_maxNeg']) {
                if (typeof copy[k] === 'string') {
                    if (copy[k] === 'infinity') {
                        copy[k] = Infinity
                    } else if (copy[k] === '-infinity') {
                        copy[k] = -Infinity
                    } else if (copy[k] === 'NaN') {
                        copy[k] = NaN
                    } else {
                        const n = Number(copy[k])
                        if (!Number.isNaN(n)) {
                            copy[k] = n
                        }
                    }
                }
            }
            if (copy.colormap && typeof copy.colormap !== 'string' && !Array.isArray(copy.colormap)) {
                const arr = extractNumberArray(copy.colormap)
                if (arr) {
                    copy.colormap = arr
                }
            }
            if (copy.colormapNegative && typeof copy.colormapNegative !== 'string' && !Array.isArray(copy.colormapNegative)) {
                const arr = extractNumberArray(copy.colormapNegative)
                if (arr) {
                    copy.colormapNegative = arr
                }
            }
            return copy
        })
    }

    // meshesString normalization
    if (typeof doc.meshesString === 'string' && doc.meshesString.trim().length > 0) {
        const normalized = normalizeMeshesString(doc.meshesString)
        if (typeof normalized === 'string') {
            doc.meshesString = normalized
        }
    }

    // Ensure labels/connectomes/encodedImageBlobs are plain arrays where possible
    if (doc.labels && !Array.isArray(doc.labels)) {
        try {
            doc.labels = Array.from(doc.labels)
        } catch (_) {
            /* leave as-is */
        }
    }
    if (doc.connectomes && !Array.isArray(doc.connectomes)) {
        try {
            doc.connectomes = Array.from(doc.connectomes)
        } catch (_) {
            /* leave as-is */
        }
    }
    if (doc.encodedImageBlobs && !Array.isArray(doc.encodedImageBlobs)) {
        try {
            doc.encodedImageBlobs = Array.from(doc.encodedImageBlobs)
        } catch (_) {
            /* leave as-is */
        }
    }

    return doc as DocumentData
}

export default migrateLegacyDocument
