local ADDON_NAME, RPAL = ...

-- ODYN 1.0 migration --------------------------------------------------------
-- The old addon stored its account and character data in RPAL_DB and
-- RPAL_CHAR_DB. We deliberately keep those variable names so the in-memory
-- schema does not change. A tiny legacy addon in the old folder loads the old
-- SavedVariables first; ODYN then inherits them and writes the exact same
-- globals into ODYN's own SavedVariables files on the next clean logout/reload.
--
-- Critical safety rule: never delete or rewrite source tables during the brand
-- migration. Before Core.InitializeDB performs its normal schema migrations we
-- make one recovery copy in ODYN-owned SavedVariables.

local MIGRATION_VERSION = 1

local function LegacyState()
    local value = _G.ODYN_LEGACY_BRIDGE
    return type(value) == "table" and value or nil
end

local function HasEntries(value)
    return type(value) == "table" and next(value) ~= nil
end

local function LegacyAddonExists()
    if C_AddOns and type(C_AddOns.GetAddOnInfo) == "function" then
        local ok, info = pcall(C_AddOns.GetAddOnInfo, "RPAchievementLedger")
        if ok and info ~= nil then return true end
    end
    if type(GetAddOnInfo) == "function" then
        local ok, name = pcall(GetAddOnInfo, "RPAchievementLedger")
        if ok and name ~= nil then return true end
    end
    return false
end

local function DeepCopy(value, seen)
    if type(value) ~= "table" then return value end
    seen = seen or {}
    if seen[value] then return seen[value] end
    local copy = {}
    seen[value] = copy
    for key, child in pairs(value) do
        copy[DeepCopy(key, seen)] = DeepCopy(child, seen)
    end
    return copy
end

local function Fingerprint(value)
    local stats = {tables=0, keys=0, strings=0, numbers=0, booleans=0, bytes=0}
    local seen = {}
    local function Walk(v)
        local kind = type(v)
        if kind == "table" then
            if seen[v] then return end
            seen[v] = true
            stats.tables = stats.tables + 1
            for k, child in pairs(v) do
                stats.keys = stats.keys + 1
                Walk(k)
                Walk(child)
            end
        elseif kind == "string" then
            stats.strings = stats.strings + 1
            stats.bytes = stats.bytes + #v
        elseif kind == "number" then
            stats.numbers = stats.numbers + 1
        elseif kind == "boolean" then
            stats.booleans = stats.booleans + 1
        end
    end
    Walk(value)
    return string.format("t%d:k%d:s%d:n%d:b%d:x%d", stats.tables, stats.keys, stats.strings, stats.numbers, stats.booleans, stats.bytes)
end

local function CharacterKey()
    local guid = UnitGUID and UnitGUID("player")
    if guid and guid ~= "" then return guid end
    local name, realm
    if UnitFullName then name, realm = UnitFullName("player") end
    name = name or (UnitName and UnitName("player")) or "Unknown"
    realm = realm or (GetRealmName and GetRealmName()) or ""
    return tostring(name) .. "-" .. tostring(realm)
end

local function RebrandBuiltInLibrary(library, defaults)
    if type(library) ~= "table" or type(defaults) ~= "table" then return 0 end
    local categories = {}
    for _, category in ipairs(library) do
        if type(category) == "table" and category.id then categories[category.id] = category end
    end
    local changed = 0
    for _, builtinCategory in ipairs(defaults) do
        local category = type(builtinCategory) == "table" and categories[builtinCategory.id] or nil
        if category then
            local byID = {}
            for _, badge in ipairs(type(category.badges) == "table" and category.badges or {}) do
                if type(badge) == "table" and badge.id then byID[badge.id] = badge end
            end
            for _, builtin in ipairs(type(builtinCategory.badges) == "table" and builtinCategory.badges or {}) do
                local badge = type(builtin) == "table" and byID[builtin.id] or nil
                if badge then
                    for _, field in ipairs({"name", "description", "additional"}) do
                        local current = builtin[field]
                        if type(current) == "string" and current:find("ODYN", 1, true) then
                            local legacy = current:gsub("ODYN", "RPAL")
                            if badge[field] == legacy then
                                badge[field] = current
                                changed = changed + 1
                            end
                        end
                    end
                end
            end
        end
    end
    return changed
end

local function RebrandPersistedBuiltIns()
    local changed = 0
    if type(_G.RPAL_DB) == "table" then
        changed = changed + RebrandBuiltInLibrary(_G.RPAL_DB.accountLibrary, RPAL.DEFAULT_ACCOUNT_LIBRARY or {})
    end
    if type(_G.RPAL_CHAR_DB) == "table" then
        changed = changed + RebrandBuiltInLibrary(_G.RPAL_CHAR_DB.library, RPAL.DEFAULT_LIBRARY or {})
        for _, scope in pairs(type(_G.RPAL_CHAR_DB.trpProfileScopes) == "table" and _G.RPAL_CHAR_DB.trpProfileScopes or {}) do
            if type(scope) == "table" then
                changed = changed + RebrandBuiltInLibrary(scope.library, RPAL.DEFAULT_LIBRARY or {})
            end
        end
    end
    return changed
end

local function EnsureMeta()
    if type(_G.ODYN_META) ~= "table" then _G.ODYN_META = {} end
    if type(_G.ODYN_CHAR_META) ~= "table" then _G.ODYN_CHAR_META = {} end
    _G.ODYN_META.characterMigrations = _G.ODYN_META.characterMigrations or {}
    return _G.ODYN_META, _G.ODYN_CHAR_META
end

function RPAL.BeginODYNMigration()
    local accountMeta, charMeta = EnsureMeta()
    local legacy = LegacyState()
    local legacyLoaded = legacy and legacy.loaded == true or false

    -- A fresh CurseForge install can legitimately have the optional Legacy Data
    -- Bridge present but disabled. Do not block ODYN in that case. Instead keep
    -- migration pending until the bridge is actually loaded, so a real legacy
    -- RPAL user can still enable it later and import the untouched old save.
    local accountVersionPending = math.floor(tonumber(accountMeta.migrationVersion) or 0) < MIGRATION_VERSION
    local characterVersionPending = math.floor(tonumber(charMeta.migrationVersion) or 0) < MIGRATION_VERSION
    local legacyInstalled = LegacyAddonExists()
    local accountBridgePending = accountVersionPending and legacyInstalled and not legacyLoaded
    local characterBridgePending = characterVersionPending and legacyInstalled and not legacyLoaded

    accountMeta.migrationBlocked = nil
    accountMeta.migrationBlockedReason = nil
    accountMeta.migrationDeferred = accountBridgePending or nil
    charMeta.migrationDeferred = characterBridgePending or nil

    local legacyAccount = legacyLoaded and legacy.accountHasData == true
    local legacyCharacter = legacyLoaded and legacy.characterHasData == true
    local accountNeedsMigration = legacyAccount and math.floor(tonumber(accountMeta.migrationVersion) or 0) < MIGRATION_VERSION
    local characterNeedsMigration = legacyCharacter and math.floor(tonumber(charMeta.migrationVersion) or 0) < MIGRATION_VERSION

    -- On the first ODYN run, the legacy snapshot is authoritative. Restore it
    -- into the live globals before any existing Core schema migration runs.
    -- This also protects against an accidentally-created empty ODYN save file.
    if accountNeedsMigration and type(legacy.accountSource) == "table" then
        _G.RPAL_DB = DeepCopy(legacy.accountSource)
    end
    if characterNeedsMigration and type(legacy.characterSource) == "table" then
        _G.RPAL_CHAR_DB = DeepCopy(legacy.characterSource)
    end

    RPAL._odynMigrationState = {
        legacyLoaded = legacyLoaded,
        legacyInstalled = legacyInstalled,
        accountBridgePending = accountBridgePending,
        characterBridgePending = characterBridgePending,
        legacyAccount = legacyAccount,
        legacyCharacter = legacyCharacter,
        accountBefore = Fingerprint(_G.RPAL_DB),
        characterBefore = Fingerprint(_G.RPAL_CHAR_DB),
        accountBackupCreated = false,
        characterBackupCreated = false,
        accountNeedsMigration = accountNeedsMigration,
        characterNeedsMigration = characterNeedsMigration,
    }

    -- Account migration runs once. If ODYN_META already has this version, the
    -- ODYN SavedVariables file has been seen before and is authoritative.
    if accountNeedsMigration then
        if type(_G.ODYN_MIGRATION_BACKUP) ~= "table" then
            _G.ODYN_MIGRATION_BACKUP = DeepCopy(_G.RPAL_DB)
            RPAL._odynMigrationState.accountBackupCreated = true
        end
        accountMeta.sourceAddon = "RPAchievementLedger"
        accountMeta.sourceFingerprint = RPAL._odynMigrationState.accountBefore
        accountMeta.startedAt = time and time() or 0
    end

    -- Character data is a separate SavedVariablesPerCharacter file, so every
    -- old RPAL character must be allowed to load the legacy bridge once.
    if characterNeedsMigration then
        if type(_G.ODYN_MIGRATION_CHAR_BACKUP) ~= "table" then
            _G.ODYN_MIGRATION_CHAR_BACKUP = DeepCopy(_G.RPAL_CHAR_DB)
            RPAL._odynMigrationState.characterBackupCreated = true
        end
        charMeta.sourceAddon = "RPAchievementLedger"
        charMeta.sourceFingerprint = RPAL._odynMigrationState.characterBefore
        charMeta.startedAt = time and time() or 0
    end

    return true
end

function RPAL.CompleteODYNMigration()
    local accountMeta, charMeta = EnsureMeta()
    local state = RPAL._odynMigrationState or {}
    local key = CharacterKey()
    local rebrandedBuiltIns = RebrandPersistedBuiltIns()

    -- Only stamp a migration version once the optional bridge has either loaded
    -- or is not installed. If it is merely present-but-disabled we keep the
    -- migration pending without preventing ODYN from running.
    if state.accountBridgePending ~= true then
        accountMeta.migrationVersion = MIGRATION_VERSION
        accountMeta.migrationDeferred = nil
    else
        accountMeta.migrationDeferred = true
    end
    accountMeta.brand = "ODYN"
    accountMeta.addonVersion = "1.8.1"
    accountMeta.addonBuild = "Stable"
    accountMeta.lastLoadedAddonName = ADDON_NAME or "ODYN"
    accountMeta.lastCompletedAt = time and time() or 0
    accountMeta.accountFingerprint = Fingerprint(_G.RPAL_DB)
    accountMeta.rebrandedBuiltInFields = (tonumber(accountMeta.rebrandedBuiltInFields) or 0) + rebrandedBuiltIns

    if state.characterBridgePending ~= true then
        charMeta.migrationVersion = MIGRATION_VERSION
        charMeta.migrationDeferred = nil
    else
        charMeta.migrationDeferred = true
    end
    charMeta.brand = "ODYN"
    charMeta.addonVersion = "1.8.1"
    charMeta.addonBuild = "Stable"
    charMeta.lastCompletedAt = time and time() or 0
    charMeta.characterFingerprint = Fingerprint(_G.RPAL_CHAR_DB)

    if state.characterBridgePending ~= true then
        accountMeta.characterMigrations[key] = {
            completedAt = charMeta.lastCompletedAt,
            legacySourceLoaded = state.legacyLoaded == true,
            legacyCharacterDataFound = state.legacyCharacter == true,
            backupCreated = state.characterBackupCreated == true,
            sourceFingerprint = state.characterBefore,
            finalFingerprint = charMeta.characterFingerprint,
        }
    end

    state.completed = true
    state.characterKey = key
    state.accountAfter = accountMeta.accountFingerprint
    state.characterAfter = charMeta.characterFingerprint

    if state.accountNeedsMigration or state.characterNeedsMigration then
        RPAL._odynMigrationNotice = "Legacy RPAL data was loaded into ODYN and a recovery backup was prepared. Keep the ODYN Legacy Data Bridge enabled until every character that used RPAL has logged in to ODYN at least once."
    end
    return true
end

function RPAL.GetODYNMigrationStatusLines()
    local accountMeta, charMeta = EnsureMeta()
    local state = RPAL._odynMigrationState or {}
    local legacy = LegacyState()
    local key = CharacterKey()
    local record = accountMeta.characterMigrations and accountMeta.characterMigrations[key]
    local lines = {
        "ODYN migration version: " .. tostring(accountMeta.migrationVersion or 0),
        "Legacy bridge loaded: " .. tostring(legacy and legacy.loaded == true or false),
        "Legacy bridge pending (non-blocking): " .. tostring(accountMeta.migrationDeferred == true or charMeta.migrationDeferred == true),
        "Legacy account data detected this load: " .. tostring(legacy and legacy.accountHasData == true or false),
        "Legacy character data detected this load: " .. tostring(legacy and legacy.characterHasData == true or false),
        "Account recovery backup: " .. tostring(type(_G.ODYN_MIGRATION_BACKUP) == "table"),
        "Character recovery backup: " .. tostring(type(_G.ODYN_MIGRATION_CHAR_BACKUP) == "table"),
        "Current character migrated: " .. tostring(type(record) == "table" and tonumber(charMeta.migrationVersion) == MIGRATION_VERSION),
        "Current character key: " .. tostring(key),
        "Account fingerprint: " .. tostring(accountMeta.accountFingerprint or state.accountAfter or "pending"),
        "Character fingerprint: " .. tostring(charMeta.characterFingerprint or state.characterAfter or "pending"),
        "Important: keep the Legacy Data Bridge until each old RPAL character has logged in once.",
    }
    return lines
end

function RPAL.GetODYNMigrationNotice()
    return RPAL._odynMigrationNotice
end
