-- 百战成神(DB) 跨服数据层
--
-- 运行环境: 仅跨服进程 _G.is_middle == true
-- 存储: Mongo 集合 DB.db_bzcs
--
-- 全服积分榜 rankCache + uuid2rank(与 rankDirty 同步重建); 排序 score 降序 -> scoreTime 升序 -> uuid
-- 客户端展示榜仅前 BZCS_RANK_MAX 名, 与匹配用的全服榜不是同一展示范围
--[=[
BzcsData = {
    activityStartTime, activityEndTime,  -- 本轮活动起止时间戳
    lastResetTime,                       -- 上次开轮时间(用于 BZCS_CYCLE_DAYS 周期判定)
    rewardIssued,                        -- 本轮是否已发奖
    playerList = {
        [uuid] = {
            uuid, serverId,
            score,           -- 积分, 默认 BZCS_INIT_SCORE
            scoreTime,       -- 达到当前积分的时间戳(同分比较用, 越小名次越靠前)
            isRobot,         -- 1=机器人
            firstJoinTime,   -- 首次挑战时间, >0 才参与周期发奖
            showInfo,        -- { name,head,headFrame,body, heroArr={[race]=真人阵容|机器人{monsterOutID,racePower}} }
        },
    },
    serverList = { [serverId] = { uuid, ... } },  -- 按服索引, 发奖 WL 路由用
    pendingRewards = { [serverId] = { {uuid, rank}, ... } },  -- 逻辑服断连时待发奖队列
}
]=]
local LuaMongo = _G.lua_mongo
local DB = require("common.DB")
local Util = require("common.Util")
local CreateRole = require("role.CreateRole")
local RoleDBLogic = require("role.RoleDBLogic")
local BaiZhanChengShenDefine = require("baiZhanChengShen.BaiZhanChengShenDefine")
local BzcsLog = require("baiZhanChengShen.BaiZhanChengShenLog")
local BzcsConfig = require("excel.baiZhanChengShen")

BzcsData = BzcsData or {}

local dbUpdate = {_id = nil}
local dbUpdateField = {}

local rankCache = nil
local uuid2rank = nil
local rankDirty = true

-- 增量更新 Mongo 字段
local function updateValue(key, value)
    if not key then return end
    if value then
        dbUpdateField["$set"] = {[key] = value}
        dbUpdateField["$unset"] = nil
    else
        dbUpdateField["$set"] = nil
        dbUpdateField["$unset"] = {[key] = 1}
    end
    dbUpdate._id = BzcsData._id
    LuaMongo.update(DB.db_bzcs, dbUpdate, dbUpdateField)
end

-- 从 db_bzcs 加载或初始化空档
local function loadData()
    LuaMongo.find(DB.db_bzcs)
    local data = {}
    if LuaMongo.next(data) then
        BzcsData = data
    else
        BzcsData.activityStartTime = 0
        BzcsData.activityEndTime = 0
        BzcsData.lastResetTime = 0
        BzcsData.rewardIssued = true
        BzcsData.playerList = {}
        LuaMongo.insert(DB.db_bzcs, BzcsData)
    end
    rankDirty = true
end

-- 排行同分比较用时间(达到当前积分时刻)
local function getRankScoreTime(pinfo)
    return pinfo.scoreTime or 0
end

-- 按积分降序重建排行缓存; 同分按 scoreTime 升序(先达到该积分者靠前), 仍同则 uuid
local function sortRankCache()
    if not rankDirty then return end
    rankCache = {}
    uuid2rank = {}
    for uuid, pinfo in pairs(BzcsData.playerList or {}) do
        rankCache[#rankCache + 1] = {
            uuid = uuid,
            score = pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE,
            scoreTime = getRankScoreTime(pinfo),
        }
    end
    table.sort(rankCache, function(a, b)
        if a.score ~= b.score then
            return a.score > b.score
        end
        if a.scoreTime ~= b.scoreTime then
            return a.scoreTime < b.scoreTime
        end
        return a.uuid < b.uuid
    end)
    for rank, entry in ipairs(rankCache) do
        uuid2rank[entry.uuid] = rank
    end
    rankDirty = false
end

-- 保证 rankCache / uuid2rank 已按当前 playerList 重建
local function ensureRankIndex()
    sortRankCache()
end

-- 保证 showInfo 结构完整
local function normalizePlayer(pinfo)
    if not pinfo then return end
    pinfo.showInfo = pinfo.showInfo or {}
    pinfo.showInfo.heroArr = pinfo.showInfo.heroArr or {}
end

-- 按 serverId 重建 serverList(仅真人, 机器人不参与发奖路由)
local function rebuildServerList()
    BzcsData.serverList = BzcsData.serverList or {}
    Util.cleanTable(BzcsData.serverList)
    for uuid, pinfo in pairs(BzcsData.playerList or {}) do
        if pinfo.isRobot ~= 1 then
            local sid = pinfo.serverId
            if sid then
                BzcsData.serverList[sid] = BzcsData.serverList[sid] or {}
                table.insert(BzcsData.serverList[sid], uuid)
            end
        end
    end
end

-- 机器单族仅存 monsterOutID + racePower(由 monsterOut 算出); 展示由 ExpandBzcsRaceShow 用时展开
local function genRobotRaceShow(robotListIdx, race)
    local cfg = BzcsConfig.robotList[robotListIdx]
    if not cfg or not cfg.monsterOutIDs then return {} end
    local monsterOutID = cfg.monsterOutIDs[race]
    if not monsterOutID then return {} end
    return {
        monsterOutID = monsterOutID,
        formation = 1,
        racePower = BaiZhanChengShenDefine.CalcMonsterOutPower(monsterOutID),
    }
end

local function getRobotScore(robotListIdx)
    local cfg = BzcsConfig.robotList[robotListIdx]
    return (cfg and cfg.score) or BaiZhanChengShenDefine.BZCS_INIT_SCORE
end

-- 生成机器人池: 数量 = robotList 条数, bzcs_robot_i 对应 robotList[i]
function GenerateRobots(cnt)
    cnt = cnt or BaiZhanChengShenDefine.GetRobotListCount()
    if cnt < 1 then return end
    BzcsData.playerList = BzcsData.playerList or {}
    local now = os.time()
    for i = 1, cnt do
        local uuid = "bzcs_robot_" .. i
        if not BzcsData.playerList[uuid] then
            local heroArr = {}
            for _, race in ipairs(BaiZhanChengShenDefine.BZCS_RACE_ORDER) do
                heroArr[race] = genRobotRaceShow(i, race)
            end
            BzcsData.playerList[uuid] = {
                uuid = uuid,
                isRobot = 1,
                score = getRobotScore(i),
                scoreTime = now + i,
                firstJoinTime = now,
                showInfo = {
                    name = CreateRole.getRandomName(),
                    head = CreateRole.getRandomHead(),
                    headFrame = CreateRole.getRandomHeadFrame(),
                    body = CreateRole.getRandomBody(),
                    heroArr = heroArr,
                },
            }
        end
    end
    rankDirty = true
    rebuildServerList()
    updateValue("playerList", BzcsData.playerList)
    BzcsLog.logAction("robot_gen", string.format("cnt=%s robotList=%s", cnt, BaiZhanChengShenDefine.GetRobotListCount()))
end

-- 逻辑服断连期间缓存的周期奖励, 重连后补发
function AddPendingReward(serverId, uuid, rank)
    if not serverId or not uuid then return end
    AddPendingRewards(serverId, {{uuid, rank}})
end

-- 逻辑服未连接时缓存整批待发奖
function AddPendingRewards(serverId, rewardList)
    if not serverId or not rewardList or #rewardList == 0 then return end
    BzcsData.pendingRewards = BzcsData.pendingRewards or {}
    local list = BzcsData.pendingRewards[serverId]
    if not list then
        list = {}
        BzcsData.pendingRewards[serverId] = list
    end
    local addCnt, skipCnt = 0, 0
    for _, info in ipairs(rewardList) do
        local uuid, rank = info[1], info[2]
        if uuid and uuid ~= "" and rank and rank > 0 then
            list[#list + 1] = {uuid, rank}
            addCnt = addCnt + 1
        else
            skipCnt = skipCnt + 1
            BzcsLog.logAction("reward_pending_skip", string.format(
                "serverId=%s uuid=%s rank=%s", serverId, uuid or "", rank or 0
            ))
        end
    end
    if addCnt < 1 then
        return
    end
    updateValue("pendingRewards." .. serverId, list)
    BzcsLog.logAction("reward_pending", string.format(
        "serverId=%s add=%s skip=%s total=%s", serverId, addCnt, skipCnt, #list
    ))
end

-- 清空全部待发奖缓存(放弃上轮发奖开新轮时用)
function ClearAllPendingRewards()
    if not BzcsData.pendingRewards or not next(BzcsData.pendingRewards) then
        return
    end
    BzcsData.pendingRewards = {}
    updateValue("pendingRewards", BzcsData.pendingRewards)
    BzcsLog.logAction("reward_pending_clear", "all")
end

-- 取出并清空某逻辑服待发奖列表, 返回 {{uuid, rank}, ...}
function TakePendingRewards(serverId)
    BzcsData.pendingRewards = BzcsData.pendingRewards or {}
    local list = BzcsData.pendingRewards[serverId]
    if not list or #list == 0 then
        return {}
    end
    BzcsData.pendingRewards[serverId] = nil
    updateValue("pendingRewards." .. serverId, nil)
    return list
end

-- 跨服启动: 加载数据, 无玩家则生成机器人池, 并向已连接逻辑服同步活动状态
function initAfterStart()
    if _G.is_middle ~= true then return end
    loadData()
    if not BzcsData.playerList or not next(BzcsData.playerList) then
        GenerateRobots()
    end
    rebuildServerList()
    BzcsLog.logAction("db_init", string.format("playerCnt=%s", Util.getTableCount(BzcsData.playerList or {})))
    local BaiZhanChengShenCS = require("baiZhanChengShen.BaiZhanChengShenCS")
    BaiZhanChengShenCS.syncActStateToAllConnected()
end

--------------------------------------------------------------------------------
-- 活动周期
--------------------------------------------------------------------------------

-- 获取本轮活动起止时间戳
function GetActivityTimes()
    return BzcsData.activityStartTime or 0, BzcsData.activityEndTime or 0
end

-- 获取上次开新轮时间(BZCS_CYCLE_DAYS 周期判定)
function GetLastResetTime()
    return BzcsData.lastResetTime or 0
end

-- 本轮周期奖励是否已发放
function IsRewardIssued()
    return BzcsData.rewardIssued == true
end

-- 设置活动时间并清除发奖标记(开新轮)
function SetActivityTimes(startTime, endTime)
    BzcsData.activityStartTime = startTime
    BzcsData.activityEndTime = endTime
    BzcsData.lastResetTime = startTime
    BzcsData.rewardIssued = false
    updateValue("activityStartTime", startTime)
    updateValue("activityEndTime", endTime)
    updateValue("lastResetTime", startTime)
    updateValue("rewardIssued", false)
end

-- 标记本轮是否已发奖
function SetRewardIssued(flag)
    BzcsData.rewardIssued = flag
    updateValue("rewardIssued", flag)
end

-- 按 uuid 查询跨服玩家/机器人
function GetPlayer(uuid)
    local pinfo = BzcsData.playerList and BzcsData.playerList[uuid]
    if pinfo then
        normalizePlayer(pinfo)
    end
    return pinfo
end

--------------------------------------------------------------------------------
-- 玩家数据
--------------------------------------------------------------------------------

-- 新增或合并玩家跨服数据
function UpsertPlayer(uuid, data)
    BzcsData.playerList = BzcsData.playerList or {}
    local old = BzcsData.playerList[uuid]
    if old then
        for k, v in pairs(data) do
            old[k] = v
        end
        data = old
    else
        BzcsData.playerList[uuid] = data
    end
    data.uuid = uuid
    normalizePlayer(data)
    rankDirty = true
    rebuildServerList()
    updateValue("playerList." .. uuid, data)
    return data
end

-- 增减积分并落库(积分变化时刷新 scoreTime 供同分排序)
function UpdateScore(uuid, delta)
    local pinfo = GetPlayer(uuid)
    if not pinfo then
        BzcsLog.logAction("score_miss", string.format("uuid=%s delta=%s", uuid or "", delta or 0))
        return
    end
    pinfo.score = (pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE) + delta
    pinfo.scoreTime = os.time()
    rankDirty = true
    updateValue("playerList." .. uuid .. ".score", pinfo.score)
    updateValue("playerList." .. uuid .. ".scoreTime", pinfo.scoreTime)
    BzcsLog.logAction("score", string.format("uuid=%s delta=%s score=%s isRobot=%s", uuid, delta, pinfo.score, pinfo.isRobot or 0))
    return pinfo.score
end

--------------------------------------------------------------------------------
-- 排行与匹配
--------------------------------------------------------------------------------

-- 查询玩家当前全服名次, 0=未上榜
function GetRankByUuid(uuid)
    if not uuid then return 0 end
    ensureRankIndex()
    return uuid2rank[uuid] or 0
end

-- 按全服名次取玩家(含机器人), rank 从 1 起
function GetPlayerByRank(rank)
    if not rank or rank < 1 then return nil end
    ensureRankIndex()
    local entry = rankCache and rankCache[rank]
    if not entry then return nil end
    return GetPlayer(entry.uuid)
end

-- 单条排行榜展示结构(榜单与 myRankInfo 共用)
function BuildRankInfoEntry(pinfo, rank)
    if not pinfo then return nil end
    local si = pinfo.showInfo or {}
    return {
        rank = rank or 0,
        uuid = pinfo.uuid,
        name = si.name or "",
        head = si.head or 0,
        headFrame = si.headFrame or 0,
        serverId = pinfo.serverId or 0,
        power = BaiZhanChengShenDefine.CalcPlayerPower(si),
        score = pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE,
        isRobot = pinfo.isRobot,
    }
end

-- 指定玩家排行展示(未注册跨服时仅 rank/score 等默认值)
function BuildPlayerRankInfo(uuid)
    local rank = GetRankByUuid(uuid)
    local pinfo = GetPlayer(uuid)
    local info = BuildRankInfoEntry(pinfo, rank)
    if info then return info end
    return {
        rank = rank,
        uuid = uuid or "",
        name = "",
        head = 0,
        headFrame = 0,
        serverId = 0,
        power = 0,
        score = BaiZhanChengShenDefine.BZCS_INIT_SCORE,
    }
end

-- 对手详情 WL 回包(仅 GC_BZCS_OPPONENT_INFO 所需字段)
function BuildOpponentInfoSnapshot(pinfo)
    if not pinfo then return nil end
    local si = pinfo.showInfo or {}
    return {
        name = si.name or "",
        head = si.head or 0,
        headFrame = si.headFrame or 0,
        power = BaiZhanChengShenDefine.CalcPlayerPower(si),
        score = pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE,
    }
end

-- 客户端展示榜前 limit 名(默认100), 含 rank/uuid/展示字段
function GetRankList(limit)
    ensureRankIndex()
    limit = limit or BaiZhanChengShenDefine.BZCS_RANK_MAX
    local ret = {}
    for i = 1, math.min(limit, #(rankCache or {})) do
        local entry = rankCache[i]
        local pinfo = GetPlayer(entry.uuid)
        local info = BuildRankInfoEntry(pinfo, i)
        if info then
            ret[#ret + 1] = info
        end
    end
    return ret
end

local function getOpponentBody(uuid, pinfo)
    local si = pinfo.showInfo or {}
    local body = si.body or 0
    if body ~= 0 or pinfo.isRobot == 1 then
        return body
    end
    local db = RoleDBLogic.getDb(uuid, "body")
    return db and db.body or 0
end

local function makeMatchOpponentEntry(uuid, pinfo)
    local si = pinfo.showInfo or {}
    local score = pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE
    return {
        rank = uuid2rank[uuid] or 0,
        uuid = uuid,
        serverId = pinfo.serverId or 0,
        name = si.name,
        body = getOpponentBody(uuid, pinfo),
        power = BaiZhanChengShenDefine.CalcPlayerPower(si),
        score = score,
        isRobot = pinfo.isRobot,
    }
end

-- 收集当前积分窗口内可匹配候选(已选/排除名单不入列)
local function collectWindowCandidates(minScore, maxScore, excludeTb)
    local candidates = {}
    for uuid, pinfo in pairs(BzcsData.playerList or {}) do
        if not excludeTb[uuid] then
            local score = pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE
            if score >= minScore and score <= maxScore then
                local oppRank = uuid2rank[uuid] or 0
                if oppRank > 0 then
                    candidates[#candidates + 1] = {uuid = uuid, pinfo = pinfo}
                end
            end
        end
    end
    return candidates
end

-- 从候选中随机抽取至多 need 名加入结果
local function pickRandomFromCandidates(selected, excludeTb, candidates, need)
    if need <= 0 or #candidates < 1 then
        return
    end
    table.shuffle(candidates)
    for i = 1, #candidates do
        if need <= 0 then
            break
        end
        local c = candidates[i]
        excludeTb[c.uuid] = true
        selected[#selected + 1] = makeMatchOpponentEntry(c.uuid, c.pinfo)
        need = need - 1
    end
end

-- 步进匹配不足时: 按与己方积分差升序补满(真人与机器人一视同仁, 可超出步进窗口)
local function fillMatchOpponentsFallback(selected, excludeTb, myScore)
    local need = BaiZhanChengShenDefine.BZCS_OPPONENT_CNT - #selected
    if need <= 0 then
        return
    end
    local candidates = {}
    for uuid, pinfo in pairs(BzcsData.playerList or {}) do
        if not excludeTb[uuid] then
            local oppRank = uuid2rank[uuid] or 0
            if oppRank > 0 then
                local score = pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE
                candidates[#candidates + 1] = {
                    uuid = uuid,
                    pinfo = pinfo,
                    scoreDist = math.abs(score - myScore),
                    rank = oppRank,
                }
            end
        end
    end
    table.sort(candidates, function(a, b)
        if a.scoreDist ~= b.scoreDist then
            return a.scoreDist < b.scoreDist
        end
        return a.rank < b.rank
    end)
    for _, c in ipairs(candidates) do
        if need <= 0 then
            break
        end
        excludeTb[c.uuid] = true
        selected[#selected + 1] = makeMatchOpponentEntry(c.uuid, c.pinfo)
        need = need - 1
    end
end

-- 按积分±step*500 步进扩大(±500/±1000/±1500...); 每步窗口内随机抽选; 不足3人时按积分差兜底补满
-- 返回 {rank,uuid,serverId,name,power,score,isRobot}[], rank 为全服积分榜名次(匹配标识)
function GetMatchOpponents(myUuid, myScore, excludeTb)
    excludeTb = excludeTb or {}
    excludeTb[myUuid] = true
    ensureRankIndex()
    local selected = {}
    local need = BaiZhanChengShenDefine.BZCS_OPPONENT_CNT
    local step = 1
    while need > 0 and step <= BaiZhanChengShenDefine.BZCS_MATCH_MAX_STEP do
        local minScore = myScore - step * BaiZhanChengShenDefine.BZCS_MATCH_STEP
        local maxScore = myScore + step * BaiZhanChengShenDefine.BZCS_MATCH_STEP
        local candidates = collectWindowCandidates(minScore, maxScore, excludeTb)
        pickRandomFromCandidates(selected, excludeTb, candidates, need)
        need = BaiZhanChengShenDefine.BZCS_OPPONENT_CNT - #selected
        step = step + 1
    end
    fillMatchOpponentsFallback(selected, excludeTb, myScore)
    table.sort(selected, function(a, b)
        return (a.rank or 0) < (b.rank or 0)
    end)
    return selected
end

-- 按全服名次列表取对手摘要(仅刷新展示, 不重算匹配)
function GetMatchOpponentsByRanks(ranks)
    if not ranks or #ranks == 0 then
        return {}
    end
    ensureRankIndex()
    local ret = {}
    for _, rank in ipairs(ranks) do
        local pinfo = GetPlayerByRank(rank)
        if pinfo then
            ret[#ret + 1] = makeMatchOpponentEntry(pinfo.uuid, pinfo)
        end
    end
    table.sort(ret, function(a, b)
        return (a.rank or 0) < (b.rank or 0)
    end)
    return ret
end

-- 新周期: 清除真人跨服数据(删号/合服后避免残留), 重置机器人池与发奖标记
function ResetForNewRound(newStart, newEnd)
    newStart = newStart or os.time()
    local robotIdx = 0
    local removedCnt = 0
    for uuid, pinfo in pairs(BzcsData.playerList or {}) do
        if pinfo.isRobot == 1 then
            robotIdx = robotIdx + 1
            local listIdx = BaiZhanChengShenDefine.GetRobotListIndex(uuid)
            pinfo.score = getRobotScore(listIdx)
            pinfo.scoreTime = newStart + robotIdx
            pinfo.firstJoinTime = 0
        else
            BzcsData.playerList[uuid] = nil
            removedCnt = removedCnt + 1
        end
    end
    rankDirty = true
    BzcsData.activityStartTime = newStart
    BzcsData.activityEndTime = newEnd
    BzcsData.lastResetTime = newStart
    BzcsData.rewardIssued = false
    if not BzcsData.playerList or not next(BzcsData.playerList) then
        GenerateRobots()
    end
    rebuildServerList()
    dbUpdate._id = BzcsData._id
    LuaMongo.update(DB.db_bzcs, dbUpdate, BzcsData)
    BzcsLog.logAction("round_reset", string.format("start=%s end=%s robotCnt=%s removedReal=%s", newStart, newEnd, robotIdx, removedCnt))
end

-- 周期发奖列表: 仅 firstJoinTime>0 且非机器人, 返回 {uuid, rank, serverId}
function GetAllPlayersForReward()
    ensureRankIndex()
    local ret = {}
    for rank, entry in ipairs(rankCache or {}) do
        if rank > BaiZhanChengShenDefine.BZCS_REWARD_RANK_MAX then
            break
        end
        local pinfo = GetPlayer(entry.uuid)
        if pinfo and (pinfo.firstJoinTime or 0) > 0 and pinfo.isRobot ~= 1 then
            ret[#ret + 1] = {entry.uuid, rank, pinfo.serverId}
        end
    end
    return ret
end
