-- [ MIGRATIONS ] ------------------------------------------------------------------------------------
-- The ordered history of every shape OrbitDB has ever had. Register at file scope; Init.lua runs the chain once per gate (AccountSettings.MigrationsApplied). Profile-scope bodies MUST be idempotent — imported profiles re-run them ungated via RunProfile. A failed body stays ungated (loud error, retries next boot); bodies are self-contained snapshots — never call live helpers that may evolve past the shape they describe.
local _, Orbit = ...

Orbit.Migrations = { _list = {} }
local Migrations = Orbit.Migrations

local SCOPES = { db = true, profile = true }

function Migrations:Register(spec)
    assert(type(spec) == "table" and type(spec.id) == "string" and spec.id ~= "" and type(spec.body) == "function" and SCOPES[spec.scope], "Migrations:Register: spec needs id (string), scope ('db'|'profile'), body (function)")
    table.insert(self._list, spec)
end

local function Fail(id, err)
    geterrorhandler()(("Orbit migration %q failed: %s"):format(id, tostring(err)))
end

function Migrations:Run(db)
    local acct = db.AccountSettings
    local applied = acct.MigrationsApplied
    if not applied then applied = {}; acct.MigrationsApplied = applied end
    -- Grandfather the pre-registry sentinels so existing installs don't re-migrate.
    if acct.VisibilityPerProfileV1 then applied.VisibilityPerProfileV1 = true; acct.VisibilityPerProfileV1 = nil end
    if acct.SpecDataBySpecV1 then applied.SpecDataBySpecV1 = true; acct.SpecDataBySpecV1 = nil end

    for _, m in ipairs(self._list) do
        if not applied[m.id] then
            local ok, err
            if m.scope == "db" then
                ok, err = pcall(m.body, db)
            else
                ok = true
                if type(db.profiles) == "table" then
                    for name, profile in pairs(db.profiles) do
                        local pok, perr = pcall(m.body, profile, name)
                        if not pok then ok, err = false, perr end
                    end
                end
            end
            if ok then applied[m.id] = true else Fail(m.id, err) end
        end
    end
end

-- Ungated sweep for profiles entering from outside the gated boot path (import).
function Migrations:RunProfile(profile)
    for _, m in ipairs(self._list) do
        if m.scope == "profile" then
            local ok, err = pcall(m.body, profile)
            if not ok then Fail(m.id, err) end
        end
    end
end

-- [ SHARED SNAPSHOT HELPERS ] -----------------------------------------------------------------------
local function CopyTable(src, dest)
    if type(dest) ~= "table" then dest = {} end
    for k, v in pairs(src) do
        dest[k] = type(v) == "table" and CopyTable(v, {}) or v
    end
    return dest
end

-- [ THE CHAIN ] ---------------------------------------------------------------------------------------
Migrations:Register{ id = "ErrorLogSplitV1", scope = "db", body = function(db)
    if not db.ErrorLog then return end
    OrbitErrorLogDB = OrbitErrorLogDB or {}
    OrbitErrorLogDB.entries = db.ErrorLog
    OrbitErrorLogDB.index = db.ErrorLogIndex or 0
    db.ErrorLog, db.ErrorLogIndex = nil, nil
end }

-- Legacy "Default" layout folds into the live "Orbit" layout (Orbit wins per-key) so GetSetting reads a single layout.
Migrations:Register{ id = "DefaultLayoutFoldV1", scope = "profile", body = function(profile)
    local layouts = profile.Layouts
    if not layouts or not layouts.Default then return end
    layouts.Orbit = layouts.Orbit or {}
    for system, sysData in pairs(layouts.Default) do
        local target = layouts.Orbit[system]
        if target == nil then
            layouts.Orbit[system] = sysData
        else
            for sysIdx, idxData in pairs(sysData) do
                if target[sysIdx] == nil then
                    target[sysIdx] = idxData
                else
                    for k, v in pairs(idxData) do
                        if target[sysIdx][k] == nil then target[sysIdx][k] = v end
                    end
                end
            end
        end
    end
    layouts.Default = nil
end }

-- Two-slider roundness-era border keys rewrite to the unified BorderStyle model.
Migrations:Register{ id = "BorderStyleUnifyV1", scope = "profile", body = function(profile)
    if profile.GlobalSettings then Orbit.Constants.BorderStyle.Migrate(profile.GlobalSettings) end
end }

-- Visibility moved from account-wide roots into the per-profile blob; stale per-plugin opacity/fade keys stripped from every layout. Frames start clean; shared fade profiles copy into every profile.
Migrations:Register{ id = "VisibilityPerProfileV1", scope = "db", body = function(db)
    local LEGACY_VIS_KEYS = { "Opacity", "OutOfCombatFade", "ShowOnMouseover" }
    local rootFade = db.FadeProfiles
    if type(db.profiles) == "table" then
        for _, profile in pairs(db.profiles) do
            if type(profile) == "table" then
                local layouts = profile.Layouts
                if type(layouts) == "table" then
                    for _, sysMap in pairs(layouts) do
                        if type(sysMap) == "table" then
                            for _, idxMap in pairs(sysMap) do
                                if type(idxMap) == "table" then
                                    for _, node in pairs(idxMap) do
                                        if type(node) == "table" then
                                            for _, k in ipairs(LEGACY_VIS_KEYS) do node[k] = nil end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
                profile.Visibility = profile.Visibility or {}
                profile.Visibility.frames = {}
                profile.Visibility.fade = type(rootFade) == "table" and CopyTable(rootFade, {}) or { profiles = {}, revealAll = false, nextId = 1 }
            end
        end
    end
    db.VisibilityEngine = nil
    db.FadeProfiles = nil
end }

-- Folds legacy per-character SpecData ({ [Name-Realm] = { [specID] = ... } }) to bare specIDs; the given character's specs win, other characters fill only missing specs, numeric (already-folded) keys pass through. Public: the Account import path re-folds incoming payloads with it.
function Migrations.FoldSpecDataByChar(root, ownKey)
    local folded = {}
    for k, specs in pairs(root) do
        if type(k) == "number" then
            folded[k] = specs
        elseif type(specs) == "table" then
            for specID, node in pairs(specs) do
                if k == ownKey or folded[specID] == nil then folded[specID] = node end
            end
        end
    end
    return folded
end

-- specIDs are globally unique per class-spec, so same-class characters share config and the old class-change wipe (SpecDataMeta) is unnecessary.
Migrations:Register{ id = "SpecDataBySpecV1", scope = "db", body = function(db)
    if type(db.SpecData) == "table" then
        db.SpecData = Migrations.FoldSpecDataByChar(db.SpecData, Orbit.CHAR_KEY)
    end
    db.SpecDataMeta = nil
end }
