-- HousingDecorGuide/Core/HDG_Migration.lua
-- Idempotent, version-gated SavedVariables migrations.
--
-- Each migration is a function {key = "vN-name", up = function(state) ... end}.
-- Migrations run in order on Store load, BEFORE the rest of the addon
-- initialises. Each migration sets state.migrations[key] = true so it never
-- runs twice. Migrations should be:
--   • Forward-only: leave a backup (don't blow away the source) until a
--     later release deletes it.
--   • Idempotent: rerunnable if the flag is missing for any reason.
--   • Pure data: no UI, no API calls, no events.
--
-- Adding a migration:
--   1. Append to MIGRATIONS list below (don't reorder — keys are stable
--      identifiers, the runner relies on the array order for sequencing).
--   2. Bump no version anywhere — the per-migration flag IS the version.

local addonName, privateDB = ...
local M = {}

local function DeepCopy(orig)
    if type(orig) ~= "table" then return orig end
    local copy = {}
    for k, v in pairs(orig) do copy[k] = DeepCopy(v) end
    return copy
end
M.DeepCopy = DeepCopy

-- ---------------------------------------------------------------------------
-- Migration: v1-collections — unify customStyles + projects.crates under a
-- single state.collections table with type discriminator. Foundation for
-- the shared-Decor-Collection model used by Styles, Crates, and (later)
-- Shipping Crates / Library / Plans.
--
-- After:
--   state.collections["style:<styleId>"] = {id, type="style", name, members, tier, tags, ...}
--   state.collections["crate:<roomID>:<shortID>"] = {id, type="crate", parent=roomID, name, members, ...}
--   state.collections["library:<libID>"] = {id, type="library", name, members, ...}
--
-- Source tables (state.customStyles, state.customStyleOrder, state.projects.crates)
-- are PRESERVED as backup until a later "v2-collections-cleanup" migration
-- deletes them after a release of soak time.
-- ---------------------------------------------------------------------------
local function migrate_v1_collections(state)
    state.collections = state.collections or {}

    if state.customStyles then
        for styleId, style in pairs(state.customStyles) do
            if type(style) == "table" then
                local collID = "style:" .. tostring(styleId)
                if not state.collections[collID] then
                    -- Shallow-copy the style record into the collection slot.
                    -- Keep ALL fields verbatim (members, tier, tags, theme,
                    -- etc.) — the StyleEngine will read whatever it expects.
                    local coll = { id = collID, type = "style" }
                    for k, v in pairs(style) do coll[k] = v end
                    -- Normalise: ensure members is a table (some legacy
                    -- styles may have it nil for empty collections).
                    coll.members = coll.members or {}
                    state.collections[collID] = coll
                end
            end
        end
    end

    if state.projects and state.projects.crates then
        for crateID, crate in pairs(state.projects.crates) do
            if type(crate) == "table" then
                local collID = "crate:" .. tostring(crateID)
                if not state.collections[collID] then
                    local coll = { id = collID, type = "crate", parent = crateID:match("^(house:[^:]+:f%d+:r[^:]+)") }
                    for k, v in pairs(crate) do coll[k] = v end
                    coll.members = coll.members or {}
                    state.collections[collID] = coll
                end
            end
        end
    end
end

-- ---------------------------------------------------------------------------
-- Migration: v2-customstyles-cleanup — delete the legacy state.customStyles
-- and state.customStyleOrder source tables now that all reads/writes flow
-- through state.collections.
--
-- TODO(soak): this migration is staged but inert. After at least one full
-- release of soak time on v1-collections (so any users who skipped the
-- intermediate version still get migrated when they finally update), flip
-- the body to actually nil out the legacy fields and re-add the entry to
-- the MIGRATIONS list. Do NOT enable this without a soak release in
-- between — once the source data is gone, a botched v1 migration is
-- unrecoverable.
-- ---------------------------------------------------------------------------
local function migrate_v2_customstyles_cleanup(state) -- luacheck: no unused
    -- state.customStyles = nil
    -- state.customStyleOrder = nil
end

-- ---------------------------------------------------------------------------
-- Migration: v3-curated-library — seed Vamoose-curated crate templates
-- into state.collections under "lib_curated:<key>" IDs. Player deletes
-- stick: state.curatedSeeded[key]=true blocks re-import on update. New
-- curated keys added in future versions DO seed (they aren't tracked yet).
-- ---------------------------------------------------------------------------
local function migrate_v3_curated_library(state)
    state.collections   = state.collections   or {}
    state.curatedSeeded = state.curatedSeeded or {}

    local lib = privateDB and privateDB.HDG_CrateLibrary
    if not (lib and lib.GetEntries) then return end

    local entries = lib.GetEntries() or {}
    for _, entry in ipairs(entries) do
        local key   = entry.key
        local libID = "lib_curated:" .. tostring(key)
        if key and not state.curatedSeeded[key] and not state.collections[libID] then
            local memberMap = {}
            for _, decorID in ipairs(entry.members or {}) do
                memberMap[decorID] = true
            end
            state.collections[libID] = {
                id        = libID,
                type      = "library",
                name      = entry.name or "(curated template)",
                hint      = entry.hint,
                source    = "curated",
                origin    = entry.origin,
                members   = memberMap,
                createdAt = time(),
            }
            state.curatedSeeded[key] = true
        end
    end
end

-- ---------------------------------------------------------------------------
-- Pre-load migration: v2-capture-fallthroughs
-- Runs on raw HDG_DB/HDG_Settings BEFORE Store:LoadFromSavedVariables.
-- NON-DESTRUCTIVE: captures the 3 HDG_Settings fallthrough scalars
-- (recentActivityCap, altsViewMode, localeOverride) into HDG_DB top-level
-- so the proxy can be retired, and stamps schemaVersion = 2.
-- HDG_DB.config / HDG_DB.ui are DELIBERATELY left untouched -- post-migration
-- they ossify as a frozen pre-v2.41 snapshot (no production code writes to
-- them anymore). Save preserves them via pass-through. /hdgrollback restores
-- by clearing schemaVersion so Load takes the legacy MergeTable path.
-- Idempotency: rawDB.schemaVersion == 2 gates re-runs.
-- ---------------------------------------------------------------------------
local function migrate_v2_capture_fallthroughs(rawDB, rawSettings)
    if rawDB.schemaVersion == 2 then return end

    local rawSets = rawSettings or {}

    -- Capture localeOverride from HDG_Settings if HDG_DB doesn't already have it
    if rawDB.localeOverride == nil and type(rawSets.localeOverride) == "string" then
        rawDB.localeOverride = rawSets.localeOverride
    end

    -- Capture altsViewMode from HDG_Settings ONLY if HDG_DB doesn't already
    -- have a value. Mirrors the localeOverride guard above; without this,
    -- a stale HDG_Settings value clobbers a newer HDG_DB value (e.g. from a
    -- prior partial migration or a downgrade-and-reupgrade cycle).
    if rawDB.altsViewMode == nil and type(rawSets.altsViewMode) == "string" then
        rawDB.altsViewMode = rawSets.altsViewMode
    else
        rawDB.altsViewMode = rawDB.altsViewMode or "skills"
    end

    -- Capture recentActivity.sessionCap as a flat scalar
    local oldRA = type(rawSets.recentActivity) == "table" and rawSets.recentActivity or {}
    rawDB.recentActivityCap = rawDB.recentActivityCap
        or (type(oldRA.sessionCap) == "number" and oldRA.sessionCap)
        or 20

    rawDB.schemaVersion = 2
    -- DELIBERATELY do NOT touch rawDB.config / rawDB.ui. They become a frozen
    -- rollback snapshot that Save preserves via pass-through.
end

local MIGRATIONS = {
    { key = "v1-collections",     up = migrate_v1_collections },
    -- { key = "v2-customstyles-cleanup", up = migrate_v2_customstyles_cleanup },
    { key = "v3-curated-library", up = migrate_v3_curated_library },
}

local PRE_LOAD_MIGRATIONS = {
    { key = "v2-capture-fallthroughs", up = migrate_v2_capture_fallthroughs },
}

-- ---------------------------------------------------------------------------
-- Called by InitSettingsProxy BEFORE HDG.Store:LoadFromSavedVariables.
-- Operates on raw globals, not Store state. Idempotency via schemaVersion.
-- ---------------------------------------------------------------------------
function M.RunPreLoad(rawDB, rawSettings)
    if type(rawDB) ~= "table" then return end
    for _, mig in ipairs(PRE_LOAD_MIGRATIONS) do
        local ok, err = pcall(mig.up, rawDB, rawSettings)
        if not ok then
            if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
                DEFAULT_CHAT_FRAME:AddMessage(string.format(
                    "|cffff6666[HDG-migration]|r %s (pre-load) failed: %s",
                    mig.key, tostring(err)))
            end
        end
    end
end

-- ---------------------------------------------------------------------------
-- Public entry point: HDG.Store:LoadFromSavedVariables() calls this after
-- merging SavedVariables into self.state but before any module init.
-- ---------------------------------------------------------------------------
function M.RunAll(state)
    if type(state) ~= "table" then return end
    state.migrations = state.migrations or {}
    for _, mig in ipairs(MIGRATIONS) do
        if not state.migrations[mig.key] then
            local ok, err = pcall(mig.up, state)
            if ok then
                state.migrations[mig.key] = true
            else
                -- Don't mark failed migrations as done; they'll retry next
                -- load. Print so the user sees the failure.
                if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
                    DEFAULT_CHAT_FRAME:AddMessage(string.format(
                        "|cffff6666[HDG-migration]|r %s failed: %s",
                        mig.key, tostring(err)))
                end
            end
        end
    end
end

-- Expose for tests + Store integration
if privateDB then privateDB.HDG_Migration = M end
return M
