-- 百战成神(普通服 NS)
--
-- 玩法: 跨服 5v5 积分 PvP, 五族独立阵容 COMBAT_TYPE39~43, 5 局 3 胜。
-- 开服>=45 天; 周六 0:10~周日 23:00; 每 21 天新轮。
--
-- 数据分工:
--   跨服 DB: 积分、排行、匹配池、玩家展示(REGISTER/UPDATE_SHOW)
--   本地 human.db.baiZhanChengShen: 免费次数、战报、crossRegistered
--   human.bzcs_Match_Cache: 匹配列表/积分内存缓存(TTL 见 BZCS_MATCH_CACHE_TTL, 不持久化)
--   CommonDB.KEY_BZCS_START_TIME: 本服活动轮次标记(与跨服 WL_BZCS_ACT_START 同步)
--
-- 跨服同步策略:
--   首次完成挑战 -> 先 LW_BZCS_REGISTER 再 LW_BZCS_FIGHT_END (crossRegistered=true, 每轮仅一次)
--   阵容/战力变更 -> LW_BZCS_UPDATE_SHOW (须已注册)
--   每场战斗结束 -> LW_BZCS_FIGHT_END (不重复 REGISTER)
--
-- 战斗:
--   挑战一次依次走 CG_COMBAT_BEGIN(COMBAT_TYPE39~43); 仅首场校验次数/道具并请求跨服
--   扣次在 C2N_CanFight; onFightEnd 链式 nextCombatType 打满 5 族或 3 胜, 整场结束发 fightReward
--   机器人: combatBegin; 真人: MiddleCommonLogic_CombatBegin_LW
--
-- 文件结构:
--   本地辅助 -> 客户端请求(BZCS_*) -> 跨服通知(C2N_*) -> 战斗相关 -> 对外接口
local Config = require("Config")
local Msg = require("core.Msg")
local Lang = require("common.Lang")
local Util = require("common.Util")
local ObjHuman = require("core.ObjHuman")
local Broadcast = require("broadcast.Broadcast")
local CombatDefine = require("combat.CombatDefine")
local CombatLogic = require("combat.CombatLogic")
local CombatPosLogic = require("combat.CombatPosLogic")
local RoleDBLogic = require("role.RoleDBLogic")
local RoleHeadLogic = require("role.RoleHeadLogic")
local RoleAttr = require("role.RoleAttr")
local InnerMsg = require("core.InnerMsg")
local MailManager = require("mail.MailManager")
local MailExcel = require("excel.mail")
local Timer = require("core.Timer")
local Grid = require("bag.Grid")
local BagLogic = require("bag.BagLogic")
local CommonDB = require("common.CommonDB")
local MiddleConnect = require("middle.MiddleConnect")
local MiddleCommonLogic = require("middle.MiddleCommonLogic")
local HeroExcel = require("excel.hero")
local BaiZhanChengShenDefine = require("baiZhanChengShen.BaiZhanChengShenDefine")
local BzcsLog = require("baiZhanChengShen.BaiZhanChengShenLog")
local BzcsConfig = require("excel.baiZhanChengShen")
local SkillExcel = require("excel.skill")
local Skill = require("combat.Skill")

local HeroLogic
local MoshouLogic
local ElfLogic

local LOGTAG = "BaiZhanChengShen"

-- 战斗类型 -> 种族 camp, 用于 checkUpdatePos 限制上阵
local COMBATTYPE_2_CAMP = {
    [CombatDefine.COMBAT_TYPE39] = 1,
    [CombatDefine.COMBAT_TYPE40] = 2,
    [CombatDefine.COMBAT_TYPE41] = 3,
    [CombatDefine.COMBAT_TYPE42] = 4,
    [CombatDefine.COMBAT_TYPE43] = 5,
}

------------------------------------ 本地辅助 ------------------------------------

-- 逻辑服与跨服链路是否可用(WL_HELLO 成功后为 true)
local function isMiddleReady()
    if _G.is_middle then return true end
    return MiddleConnect.IS_MIDDLE_CONNECT == true
end

-- 发送 LW 到跨服; 断连时提示玩家并返回 false
local function sendLwToMiddle(human, msgData)
    if not isMiddleReady() then
        if human and human.fd then
            Broadcast.sendErr(human, Lang.MIDDLE_SVR_ERR_CONNECT)
        end
        return false
    end
    InnerMsg.sendMsg(0, msgData)
    return true
end

-- 当前星期(Util.getWeekDay: 1=周日, 7=周六)
local function getWDay()
    return Util.getWeekDay()
end

-- 开服天数是否满足参与条件
local function isServerEligible()
    return CommonDB.getServerOpenDay() >= BaiZhanChengShenDefine.BZCS_OPEN_SVR_DAY
end

-- 本服是否在活动期(跨服已广播 KEY_BZCS_START_TIME 且 IsRunning)
local function actStartTimeCheck()
    local wDay = getWDay()
    if wDay > BaiZhanChengShenDefine.BZCS_OPEN_WDAY_AREA[2]
        and wDay < BaiZhanChengShenDefine.BZCS_OPEN_WDAY_AREA[1] then
        return false
    end
    local startTime = CommonDB.getValueByKey(CommonDB.KEY_BZCS_START_TIME)
    if not startTime or startTime == 0 then
        return false
    end
    return IsRunning(startTime)
end

-- 新活动轮次时清空本地战报
local function clearRoundLocalData(db)
    db.warReport = {}
    db.crossRegistered = false
end

-- 清除匹配内存缓存(积分变化/新轮/强制刷新)
local function clearMatchCache(human)
    if human then
        human.bzcs_Match_Cache = nil
    end
end

-- 活动开始时间变化(新轮开启/结束)时重置本地轮次数据
local function resetRoundLocalIfNeeded(human, db)
    local actStart = CommonDB.getValueByKey(CommonDB.KEY_BZCS_START_TIME) or 0
    if (db.actStartTime or 0) ~= actStart then
        db.actStartTime = actStart
        clearRoundLocalData(db)
        clearMatchCache(human)
    end
end

-- 玩家本地百战成神数据 (不入跨服 Mongo)
--   freeTimes      今日剩余免费挑战次数(跨天由 updateDaily 重置)
--   actStartTime   已同步的活动轮开始时间(与 KEY_BZCS_START_TIME 对比清轮次数据)
--   crossRegistered 本轮是否已向跨服 REGISTER
--   warReport      本地战报 {warType,...}, 按时间尾插(最老在[1]), 下发时倒序为新在前
local function getBzcsDb(human)
    human.db.baiZhanChengShen = human.db.baiZhanChengShen or {
        freeTimes = BaiZhanChengShenDefine.BZCS_FREE_TIMES,
        actStartTime = 0,
        crossRegistered = false,
        warReport = {},
    }
    resetRoundLocalIfNeeded(human, human.db.baiZhanChengShen)
    return human.db.baiZhanChengShen
end

local function getMatchCache(human)
    return human and human.bzcs_Match_Cache
end

local function setMatchCache(human, myScore, opponentList)
    human.bzcs_Match_Cache = {
        myScore = myScore,
        matchScore = myScore,
        opponentList = opponentList or {},
        cacheTime = os.time(),
    }
end

-- 展示刷新: 保留 matchScore(匹配基准), 更新对手展示并重置 TTL
local function refreshMatchCacheDisplay(human, myScore, opponentList)
    local cache = getMatchCache(human)
    human.bzcs_Match_Cache = {
        myScore = myScore,
        matchScore = cache and cache.matchScore or myScore,
        opponentList = opponentList or {},
        cacheTime = os.time(),
    }
end

-- 积分变动: 清空对手列表, 重置 matchScore/TTL(从变动时刻重新计时)
local function resetMatchCacheOnScoreChange(human, newScore)
    if not human then return end
    newScore = newScore or BaiZhanChengShenDefine.BZCS_INIT_SCORE
    human.bzcs_Match_Cache = {
        myScore = newScore,
        matchScore = newScore,
        opponentList = {},
        cacheTime = os.time(),
    }
end

local function isMatchCacheFresh(human)
    local cache = getMatchCache(human)
    if not cache or not cache.opponentList or not next(cache.opponentList) then
        return false
    end
    if (cache.myScore or 0) ~= (cache.matchScore or 0) then
        return false
    end
    local ttl = BaiZhanChengShenDefine.BZCS_MATCH_CACHE_TTL or 30
    return (os.time() - (cache.cacheTime or 0)) < ttl
end

local function collectRefreshRanks(opponentList)
    local ranks = {}
    for _, opp in ipairs(opponentList or {}) do
        if opp.rank and opp.rank > 0 then
            ranks[#ranks + 1] = opp.rank
        end
    end
    return ranks
end

-- 校验 rank 是否属于当前匹配缓存(全服名次, 非列表下标 1~3)
local function resolveMatchOpponentByRank(human, rank)
    if not rank or rank < 1 then
        return nil
    end
    local cache = getMatchCache(human)
    for _, opp in ipairs(cache and cache.opponentList or {}) do
        if opp.rank == rank and opp.uuid then
            return opp
        end
    end
    return nil
end

-- 指定种族阵容是否至少上阵 1 名英雄(空位 "0"/"" 不算)
local function hasRaceLineupHero(human, race)
    local combatType = BaiZhanChengShenDefine.BZCS_RACE_COMBAT_TYPE[race]
    if not combatType then return false end
    local heroList = CombatPosLogic.getCombatHeros(human, combatType)
    if not heroList then return false end
    for _, heroUuid in pairs(heroList) do
        if heroUuid and heroUuid ~= "" and heroUuid ~= "0" then
            return true
        end
    end
    return false
end

-- 五族阵容是否均已至少上阵 1 名英雄(供 GC_BZCS_MATCH_LIST.allLineupReady)
local function isAllRaceLineupReady(human)
    for _, race in ipairs(BaiZhanChengShenDefine.BZCS_RACE_ORDER) do
        if not hasRaceLineupHero(human, race) then
            return false
        end
    end
    return true
end

-- 挑战前校验五族阵容均已上阵英雄
local function checkAllRaceLineupForFight(human)
    for _, race in ipairs(BaiZhanChengShenDefine.BZCS_RACE_ORDER) do
        if not hasRaceLineupHero(human, race) then
            local raceName = BaiZhanChengShenDefine.BZCS_RACE_NAME[race] or ""
            Broadcast.sendErr(human, Util.format(Lang.BZCS_NEED_RACE_LINEUP, raceName))
            return false
        end
    end
    return true
end

-- 组装并下发 GC_BZCS_MATCH_LIST
local function sendMatchListGC(human, myScore, opponentList)
    local db = getBzcsDb(human)
    local msgRet = Msg.gc.GC_BZCS_MATCH_LIST
    msgRet.myScore = myScore or BaiZhanChengShenDefine.BZCS_INIT_SCORE
    msgRet.freeTimes = db.freeTimes or 0
    Grid.makeItem(
        msgRet.ticketCost,
        BaiZhanChengShenDefine.BZCS_TICKET_ITEM_ID,
        BaiZhanChengShenDefine.BZCS_TICKET_COST
    )
    msgRet.allLineupReady = isAllRaceLineupReady(human) and 1 or 0
    msgRet.opponentList[0] = #(opponentList or {})
    for i, opp in ipairs(opponentList or {}) do
        local net = msgRet.opponentList[i]
        net.rank = opp.rank or 0
        net.name = opp.name
        net.body = opp.body or 0
        net.power = opp.power
        net.score = opp.score
        net.serverId = BaiZhanChengShenDefine.ToClientServerId(opp.serverId, opp.isRobot)
    end
    Msg.send(msgRet, human.fd)
end

-- 单个英雄展示字段(阵容/协议用)
local function getHeroInfo(human, heroUuid)
    HeroLogic = HeroLogic or require("hero.HeroLogic")
    local heroGrid = HeroLogic.getHeroGridByUuid(human, heroUuid)
    if not heroGrid then return end
    local heroCfg = HeroExcel.hero[heroGrid.id]
    RoleAttr.calcHeroGrid(heroGrid, nil, human)
    return {
        heroUuid = heroUuid,
        heroStar = heroGrid.star,
        heroLevel = heroGrid.lv,
        heroCamp = heroGrid.camp or heroCfg.camp,
        heroBody = heroCfg.body,
        heroIcon = heroGrid.head or heroCfg.head,
        heroPower = heroGrid.zhandouli,
        heroId = heroGrid.id,
        heroQuality = heroCfg.grade,
    }
end

-- 某战斗类型阵容内已上阵英雄展示列表
local function getRaceHeroListInfo(human, combatType)
    local heroArr = {}
    local len = 0
    local heroList = CombatPosLogic.getCombatHeros(human, combatType)
    if not heroList then
        return heroArr
    end
    for _, heroUuid in pairs(heroList) do
        if heroUuid ~= "" and heroUuid ~= "0" then
            local heroInfo = getHeroInfo(human, heroUuid)
            if heroInfo then
                len = len + 1
                heroArr[len] = heroInfo
            end
        end
    end
    return heroArr
end

-- 技能 icon(助阵凤凰 moshouSkill 用)
local function getBzcsSkillIcon(skillId)
    if not skillId or skillId < 1 then
        return ""
    end
    local skillConfig = SkillExcel.skill[skillId] or Skill.GetSkillConfig(skillId)
    return skillConfig and skillConfig.icon or ""
end

-- 填充精灵位展示(同 CombatPosLogic.Elf_Pos_Query)
local function fillBzcsElfPosArr(msgElfPosArr, elfList, elfSkillIds, human)
    ElfLogic = ElfLogic or require("elf.ElfLogic")
    msgElfPosArr[0] = CombatDefine.COMBAT_ELF_NOW_CNT
    for i = 1, CombatDefine.COMBAT_ELF_NOW_CNT do
        local net = msgElfPosArr[i]
        net.elfId = 0
        net.nowSkillDesc = ""
        net.nowSkillIcon = ""
        local elfId = elfList and elfList[i]
        if elfId and elfId > 0 then
            local skillId
            if human then
                skillId = ElfLogic.GetElfSkill(human, elfId)
            else
                skillId = elfSkillIds and elfSkillIds[i]
            end
            if skillId and skillId > 0 then
                local skillConfig = Skill.GetSkillConfig(skillId)
                if skillConfig then
                    net.elfId = elfId
                    net.nowSkillDesc = skillConfig.desc
                    net.nowSkillIcon = skillConfig.icon
                end
            end
        end
    end
end

-- 填充 BZCS_TEAM_INFO(英雄/助阵 icon/精灵展示)
local function fillBzcsTeamNet(teamNet, show, human, combatType)
    show = BaiZhanChengShenDefine.ExpandBzcsRaceShow(show) or show
    local heroList = show.heroArr or {}
    teamNet.power = BaiZhanChengShenDefine.CalcRaceTeamPower(show)
    teamNet.formation = show.formation or 1
    teamNet.heroArr[0] = #heroList
    for j, h in ipairs(heroList) do
        local net = teamNet.heroArr[j]
        net.heroBody = h.heroBody or 0
        net.heroStar = h.heroStar or 0
        net.heroLv = h.heroLevel or h.heroLv or 0
        net.heroCamp = h.heroCamp or 0
        net.heroIcon = h.heroIcon or 0
        net.heroId = h.heroId or 0
        net.heroQuality = h.heroQuality or 1
    end

    local helpSkillId = show.helpSkillId or 0
    if human and combatType and helpSkillId < 1 then
        MoshouLogic = MoshouLogic or require("moshou.MoshouLogic")
        local combatHeroDB = CombatPosLogic.getCombatHeroDB(human, combatType)
        if combatHeroDB and combatHeroDB.helpList then
            helpSkillId = MoshouLogic.getPutMoshouSkillID(human, combatType, combatHeroDB.helpList) or 0
        end
    end
    teamNet.moshouSkill = getBzcsSkillIcon(helpSkillId)

    fillBzcsElfPosArr(teamNet.elfPosArr, show.elfList, show.elfSkillIds, human)
end

-- 单种族阵容展示(跨服仅存 heroArr/阵法/助阵技能id/精灵id+技能id; 战力见各英雄 heroPower)
local function getRaceLineupShow(human, race)
    local combatType = BaiZhanChengShenDefine.BZCS_RACE_COMBAT_TYPE[race]
    local heroArr = getRaceHeroListInfo(human, combatType)
    local combatHeroDB = CombatPosLogic.getCombatHeroDB(human, combatType)
    local helpSkillId = 0
    local elfList, elfSkillIds
    if combatHeroDB then
        MoshouLogic = MoshouLogic or require("moshou.MoshouLogic")
        ElfLogic = ElfLogic or require("elf.ElfLogic")
        if combatHeroDB.helpList then
            helpSkillId = MoshouLogic.getPutMoshouSkillID(human, combatType, combatHeroDB.helpList) or 0
        end
        if combatHeroDB.elfList then
            elfList = {}
            elfSkillIds = {}
            for i = 1, CombatDefine.COMBAT_ELF_NOW_CNT do
                local elfId = combatHeroDB.elfList[i]
                if elfId and elfId > 0 then
                    elfList[i] = elfId
                    elfSkillIds[i] = ElfLogic.GetElfSkill(human, elfId) or 0
                end
            end
        end
    end
    return {
        heroArr = heroArr,
        formation = combatHeroDB and combatHeroDB.formation or 1,
        helpSkillId = helpSkillId,
        elfList = elfList,
        elfSkillIds = elfSkillIds,
    }
end

-- 跨服展示包: 外观 + 五族阵容(存入 DB.playerList[].showInfo, 仅 REGISTER 全量使用)
local function buildShowInfo(human)
    local heroArr = {}
    for _, race in ipairs(BaiZhanChengShenDefine.BZCS_RACE_ORDER) do
        heroArr[race] = getRaceLineupShow(human, race)
    end
    return {
        name = human.db.name,
        head = RoleHeadLogic.getRoleAppearance(human, RoleHeadLogic.HEAD_TYPE_1),
        headFrame = RoleHeadLogic.getRoleAppearance(human, RoleHeadLogic.HEAD_TYPE_2),
        body = RoleHeadLogic.getRoleAppearance(human, RoleHeadLogic.HEAD_TYPE_3),
        heroArr = heroArr,
    }
end

-- 按 updateType 组装增量 showInfo(仅含本次变更字段)
local function buildShowPatch(human, updateType, race)
    if updateType == BaiZhanChengShenDefine.BZCS_UPDATE_SHOW_NAME then
        return {name = human.db.name}
    elseif updateType == BaiZhanChengShenDefine.BZCS_UPDATE_SHOW_HEAD then
        return {head = RoleHeadLogic.getRoleAppearance(human, RoleHeadLogic.HEAD_TYPE_1)}
    elseif updateType == BaiZhanChengShenDefine.BZCS_UPDATE_SHOW_HEAD_FRAME then
        return {headFrame = RoleHeadLogic.getRoleAppearance(human, RoleHeadLogic.HEAD_TYPE_2)}
    elseif updateType == BaiZhanChengShenDefine.BZCS_UPDATE_SHOW_BODY then
        return {body = RoleHeadLogic.getRoleAppearance(human, RoleHeadLogic.HEAD_TYPE_3)}
    elseif updateType == BaiZhanChengShenDefine.BZCS_UPDATE_SHOW_LINEUP then
        if not race then return end
        return {heroArr = {[race] = getRaceLineupShow(human, race)}}
    end
end

-- 按排名取周期奖励道具配置
local function getRankReward(rank)
    for _, v in ipairs(BzcsConfig.rankReward) do
        if rank >= v.rankArea[1] and rank <= v.rankArea[2] then
            return v.awardList
        end
    end
end

-- 道具列表转日志串 itemId:cnt,...
local function formatAwardList(awardList)
    if not awardList or #awardList < 1 then
        return ""
    end
    local parts = {}
    for i, item in ipairs(awardList) do
        parts[i] = string.format("%s:%s", item[1] or 0, item[2] or 0)
    end
    return table.concat(parts, ",")
end

-- 整场挑战结束发放道具奖励(读 excel fightReward: 1=胜 2=负)
-- combatInfo.rewardItem 供 GC_COMBAT_FINISH 展示, 参考 BattleLogic.onFightEnd
local function grantFightReward(human, atkWin, combatInfo)
    if not human or not human.db then return end
    local rewardId = atkWin and BaiZhanChengShenDefine.BZCS_FIGHT_REWARD_WIN_ID
        or BaiZhanChengShenDefine.BZCS_FIGHT_REWARD_LOSE_ID
    local fightReward = BzcsConfig.fightReward
    local cfg = fightReward and fightReward[rewardId]
    if not cfg or not cfg.awardList or not next(cfg.awardList) then
        BzcsLog.logAction("fight_reward_miss", string.format(
            "uuid=%s atkWin=%s rewardId=%s", human.db._id, atkWin and 1 or 0, rewardId
        ))
        return
    end
    if combatInfo then
        combatInfo.rewardItem = {}
    end
    local awardList = {}
    for i, itemInfo in ipairs(cfg.awardList) do
        local itemID = itemInfo[1]
        local itemCnt = itemInfo[2]
        awardList[i] = {itemID, itemCnt}
        if combatInfo then
            combatInfo.rewardItem[i] = {itemID, itemCnt}
        end
        BagLogic.addItem(human, itemID, itemCnt, LOGTAG)
    end
    BzcsLog.logAction("fight_reward", string.format(
        "uuid=%s atkWin=%s rewardId=%s itemCnt=%s items=%s",
        human.db._id, atkWin and 1 or 0, rewardId, #awardList, formatAwardList(awardList)
    ))
end

-- 发周期结算邮件(排名写入正文)
local function sendMail(mailId, receiverUuid, itemArray, rank)
    local mailCfg = MailExcel.mail[mailId]
    if not mailCfg then
        BzcsLog.logAction("reward_mail_miss", string.format(
            "uuid=%s rank=%s mailId=%s", receiverUuid or "", rank or 0, mailId or 0
        ))
        return
    end
    if not itemArray or not next(itemArray) then
        BzcsLog.logAction("reward_mail_empty", string.format(
            "uuid=%s rank=%s mailId=%s", receiverUuid or "", rank or 0, mailId
        ))
        return
    end
    local content = mailCfg.content
    if rank then
        if rank > BaiZhanChengShenDefine.BZCS_RANK_MAX then
            rank = "100+"
        end
        content = Util.format(content, rank)
    end
    MailManager.add(MailManager.SYSTEM, receiverUuid, mailCfg.title, content, itemArray, mailCfg.senderName or "GM")
    BzcsLog.logAction("reward_mail_ok", string.format(
        "uuid=%s rank=%s mailId=%s itemCnt=%s items=%s",
        receiverUuid, rank or 0, mailId, #itemArray, formatAwardList(itemArray)
    ))
end

-- 批量发奖队列(分批发邮件, 失败重试)
local function createRewardQueue()
    local q = {playerArray = {}, insertMaxNum = 100, repeatMaxTimes = 3, repeatTb = {}}
    function q:add(info) table.insert(self.playerArray, info) end
    function q:insertDB()
        local maxNum = math.min(self.insertMaxNum, #self.playerArray)
        for _ = 1, maxNum do
            local info = table.remove(self.playerArray)
            local uuid, rank = info[1], info[2]
            local itemArray = getRankReward(rank)
            if not itemArray then
                BzcsLog.logAction("reward_cfg_miss", string.format("uuid=%s rank=%s", uuid or "", rank or 0))
            else
                local ok, err = pcall(sendMail, BaiZhanChengShenDefine.BZCS_AWARD_MAIL_ID, uuid, itemArray, rank)
                if not ok then
                    local retry = (self.repeatTb[uuid] or 0) + 1
                    if retry <= self.repeatMaxTimes then
                        self.repeatTb[uuid] = retry
                        q:add(info)
                        BzcsLog.logAction("reward_mail_fail", string.format(
                            "uuid=%s rank=%s retry=%s err=%s", uuid, rank, retry, err
                        ))
                    else
                        BzcsLog.logAction("reward_mail_giveup", string.format(
                            "uuid=%s rank=%s retry=%s err=%s", uuid, rank, retry, err
                        ))
                    end
                end
            end
        end
        if #self.playerArray > 0 then
            Timer.addLater(3, self.insertDB, self)
        end
    end
    return q
end

-- bzcs_Battle_Cache: defRank/defUuid/defServerId/defName/defScore/isRobot, raceIdx, atkW/defW, rounds

-- 战报尾插; 超上限删最老; 下发 GC 时倒序
local function addWarReport(human, report)
    local db = getBzcsDb(human)
    db.warReport = db.warReport or {}
    db.warReport[#db.warReport + 1] = report
    if #db.warReport > BaiZhanChengShenDefine.BZCS_WARREPORT_MAX then
        table.remove(db.warReport, 1)
    end
end

-- 是否仍有挑战次数(免费或道具115); 仅校验不扣
local function canChallenge(human)
    local db = getBzcsDb(human)
    if db.freeTimes > 0 then
        return true
    end
    return BagLogic.getItemCnt(human, BaiZhanChengShenDefine.BZCS_TICKET_ITEM_ID) >= BaiZhanChengShenDefine.BZCS_TICKET_COST
end

-- 优先扣免费次数, 否则扣门票; 整场仅在 C2N_CanFight 扣 1 次
local function deductChallengeTimes(human)
    local db = getBzcsDb(human)
    if db.freeTimes > 0 then
        db.freeTimes = db.freeTimes - 1
        return true
    end
    if BagLogic.getItemCnt(human, BaiZhanChengShenDefine.BZCS_TICKET_ITEM_ID) < BaiZhanChengShenDefine.BZCS_TICKET_COST then
        return false
    end
    BagLogic.delItem(human, BaiZhanChengShenDefine.BZCS_TICKET_ITEM_ID, BaiZhanChengShenDefine.BZCS_TICKET_COST, LOGTAG)
    return true
end

-- 本轮首次完成挑战时向跨服注册, 每轮仅一次
local function tryRegisterToCross(human)
    local db = getBzcsDb(human)
    if db.crossRegistered then
        return
    end
    local reg = InnerMsg.lw.LW_BZCS_REGISTER
    reg.playerInfo = {
        uuid = human.db._id,
        serverId = Config.SVR_INDEX,
        firstJoinTime = os.time(),
        showInfo = buildShowInfo(human),
    }
    InnerMsg.sendMsg(0, reg)
    BzcsLog.logAction("register_send", string.format("uuid=%s serverId=%s", human.db._id, Config.SVR_INDEX))
end

-- 按 raceIdx 开打当前族; 机器人本地战, 真人跨服
local function startRaceCombat(human, cache)
    local raceIdx = cache.raceIdx
    local combatType = BaiZhanChengShenDefine.BZCS_RACE_COMBAT_TYPE[raceIdx]
    if cache.isRobot == 1 then
        CombatLogic.combatBegin(human, nil, {cache.defUuid, raceIdx}, combatType)
    else
        MiddleCommonLogic.MiddleCommonLogic_CombatBegin_LW(human, {
            combatType = combatType,
            nServerIndex = cache.defServerId,
            param = cache.defUuid,
        })
    end
end

-- 5 局系列结束: 注册跨服(首次) -> LW_BZCS_FIGHT_END, 攻方战报, 清 cache
local function challengeFinish(human, cache)
    if not isMiddleReady() then
        BzcsLog.logAction("fight_end_middle_down", string.format("uuid=%s def=%s", human.db._id, cache.defUuid or ""))
        if human.fd then
            Broadcast.sendErr(human, Lang.MIDDLE_SVR_ERR_CONNECT)
        end
        human.bzcs_Battle_Cache = nil
        return
    end
    -- 须先于 FIGHT_END: 跨服 UpdateScore 依赖 playerList 中已有攻方
    tryRegisterToCross(human)

    local atkWin = cache.atkW >= BaiZhanChengShenDefine.BZCS_WIN_TARGET
    local msgData = InnerMsg.lw.LW_BZCS_FIGHT_END
    msgData.atkUuid = human.db._id
    msgData.atkServerId = Config.SVR_INDEX
    msgData.atkName = human.db.name
    msgData.defUuid = cache.defUuid
    msgData.defName = cache.defName
    msgData.defServerId = cache.defServerId
    msgData.atkWin = atkWin and 1 or 0
    msgData.raceResults = cache.rounds
    InnerMsg.sendMsg(0, msgData)
    BzcsLog.logAction("fight_end_send", string.format(
        "atk=%s def=%s atkWin=%s isRobot=%s atkW=%s defW=%s",
        human.db._id, cache.defUuid or "", msgData.atkWin, cache.isRobot or 0, cache.atkW, cache.defW
    ))

    local scoreChange = atkWin and BaiZhanChengShenDefine.BZCS_ATK_WIN_SCORE or BaiZhanChengShenDefine.BZCS_ATK_LOSE_SCORE
    addWarReport(human, {
        warType = atkWin and BaiZhanChengShenDefine.BZCS_WAR_TYPE_ATK_WIN or BaiZhanChengShenDefine.BZCS_WAR_TYPE_ATK_LOSE,
        oppServerId = cache.defServerId or 0,
        oppName = cache.defName or "",
        scoreChange = scoreChange,
        isRobot = cache.isRobot or 0,
    })

    human.bzcs_Battle_Cache = nil
end

------------------------------------ 客户端请求 (Handler.lua -> BZCS_*) ------------------------------------
-- 由 Handler.lua 转发; 需跨服的走 sendLwToMiddle, 仅本地数据的直接 GC

-- 请求跨服匹配列表; forceRefresh=true 时全量重匹配
local function requestMatchList(human, forceRefresh)
    if human.fd then
        ObjHuman.updateDaily(human)
    end
    getBzcsDb(human)
    if not isServerEligible() then
        return Broadcast.sendErr(human, Lang.COMMOM_NOT_ENABLED)
    end
    if not actStartTimeCheck() then
        return Broadcast.sendErr(human, Lang.COMMOM_NOT_ENABLED)
    end
    if forceRefresh then
        clearMatchCache(human)
    end
    if isMatchCacheFresh(human) then
        local cache = getMatchCache(human)
        return sendMatchListGC(human, cache.myScore, cache.opponentList)
    end
    if not isMiddleReady() then
        return Broadcast.sendErr(human, Lang.MIDDLE_SVR_ERR_CONNECT)
    end
    local cache = getMatchCache(human)
    local msgData = InnerMsg.lw.LW_BZCS_MATCH
    msgData.sourceServerId = Config.SVR_INDEX
    msgData.playerUuid = human.db._id
    msgData.refreshRanks = {}
    if cache and cache.opponentList and next(cache.opponentList)
        and (cache.myScore or 0) == (cache.matchScore or 0) then
        msgData.refreshRanks = collectRefreshRanks(cache.opponentList)
    end
    if not sendLwToMiddle(human, msgData) then
        return
    end
end

-- 匹配主界面: 下发 myScore + 3 名对手(score/name/power)
-- 内存缓存未过期直返; 过期则跨服按 rank 刷新展示; 无缓存则全量匹配
function BZCS_MatchList(human)
    requestMatchList(human, false)
end

-- 客户端刷新匹配对手(清缓存后跨服按积分±500步进重匹配3人, 回包 GC_BZCS_MATCH_LIST)
function BZCS_MatchRefresh(human)
    if not actStartTimeCheck() then
        return Broadcast.sendErr(human, Lang.COMMOM_NOT_ENABLED)
    end
    if human.bzcs_Battle_Cache then
        return Broadcast.sendErr(human, Lang.DATA_ERR)
    end
    requestMatchList(human, true)
end

-- 排行榜前 100
function BZCS_RankList(human)
    local msgData = InnerMsg.lw.LW_BZCS_RANK_LIST
    msgData.sourceServerId = Config.SVR_INDEX
    msgData.playerUuid = human.db._id
    sendLwToMiddle(human, msgData)
end

-- 对手头像/战力/积分(按全服名次 rank, 跨服实时解析该名次上的玩家)
function BZCS_OpponentInfo(human, rank)
    local opp = resolveMatchOpponentByRank(human, rank)
    if not opp then
        return Broadcast.sendErr(human, Lang.BZCS_MATCH_OPPONENT_INVALID)
    end
    local msgData = InnerMsg.lw.LW_BZCS_OPPONENT_INFO
    msgData.sourceServerId = Config.SVR_INDEX
    msgData.playerUuid = human.db._id
    msgData.targetRank = rank
    sendLwToMiddle(human, msgData)
end

-- 对手五族阵容(按全服名次 rank)
function BZCS_OpponentLineup(human, rank)
    local opp = resolveMatchOpponentByRank(human, rank)
    if not opp then
        return Broadcast.sendErr(human, Lang.BZCS_MATCH_OPPONENT_INVALID)
    end
    local msgData = InnerMsg.lw.LW_BZCS_OPPONENT_LINEUP
    msgData.sourceServerId = Config.SVR_INDEX
    msgData.playerUuid = human.db._id
    msgData.targetRank = rank
    sendLwToMiddle(human, msgData)
end

-- 己方五族阵容(仅读本地, 不访问跨服)
function BZCS_MyLineup(human)
    local msgRet = Msg.gc.GC_BZCS_MY_LINEUP
    msgRet.teamList[0] = BaiZhanChengShenDefine.BZCS_RACE_CNT
    for i, race in ipairs(BaiZhanChengShenDefine.BZCS_RACE_ORDER) do
        local combatType = BaiZhanChengShenDefine.BZCS_RACE_COMBAT_TYPE[race]
        local show = getRaceLineupShow(human, race)
        msgRet.teamList[i].race = race
        fillBzcsTeamNet(msgRet.teamList[i], show, human, combatType)
    end
    Msg.send(msgRet, human.fd)
end

-- 本地战报列表(存储最老->最新, 协议仍新记录在前)
function BZCS_WarReport(human)
    local db = getBzcsDb(human)
    local list = db.warReport or {}
    local total = #list
    local cnt = math.min(total, BaiZhanChengShenDefine.BZCS_WARREPORT_MAX, #Msg.gc.GC_BZCS_WAR_REPORT.reportList)
    local msgRet = Msg.gc.GC_BZCS_WAR_REPORT
    msgRet.reportList[0] = cnt
    for j = 1, cnt do
        local r = list[total - j + 1]
        local net = msgRet.reportList[j]
        net.warType = r.warType or 0
        local isRobot = r.isRobot or 0
        if isRobot ~= 1 and (r.oppServerId or 0) == 0 then
            local warType = r.warType or 0
            if warType == BaiZhanChengShenDefine.BZCS_WAR_TYPE_ATK_WIN
                or warType == BaiZhanChengShenDefine.BZCS_WAR_TYPE_ATK_LOSE then
                isRobot = 1
            end
        end
        net.oppServerId = BaiZhanChengShenDefine.ToClientServerId(r.oppServerId, isRobot)
        net.oppName = r.oppName or ""
        net.scoreChange = r.scoreChange or 0
    end
    Msg.send(msgRet, human.fd)
end

-- 排名奖励配置预览(读 excel)
function BZCS_RankReward(human)
    local msgRet = Msg.gc.GC_BZCS_RANK_REWARD
    msgRet.rewardList[0] = #BzcsConfig.rankReward
    for k, v in ipairs(BzcsConfig.rankReward) do
        msgRet.rewardList[k].rankLeft = v.rankArea[1]
        msgRet.rewardList[k].rankRight = v.rankArea[2]
        local items = msgRet.rewardList[k].itemList
        items[0] = #v.awardList
        for idx, itemCfg in ipairs(v.awardList) do
            Grid.makeItem(items[idx], itemCfg[1], itemCfg[2])
        end
    end
    Msg.send(msgRet, human.fd)
end

------------------------------------ 跨服通知本地服 (InnerHandler.lua -> C2N_*) ------------------------------------
-- 跨服 WL 回调, 组装 GC 或写本地数据

-- WL_BZCS_ACT_START: 记录本轮开始时间(重连补偿广播也会走到这里)
function C2N_Act_Start(msg)
    local oldStart = CommonDB.getValueByKey(CommonDB.KEY_BZCS_START_TIME) or 0
    local resetCnt = 0
    if msg.startTime and msg.startTime > 0 then
        CommonDB.updateValue(CommonDB.KEY_BZCS_START_TIME, msg.startTime)
        for uuid, human in pairs(ObjHuman.onlineUuid) do
            if human.db and human.db.baiZhanChengShen then
                local actStart = msg.startTime
                if (human.db.baiZhanChengShen.actStartTime or 0) ~= actStart then
                    human.db.baiZhanChengShen.actStartTime = actStart
                    clearRoundLocalData(human.db.baiZhanChengShen)
                    clearMatchCache(human)
                    resetCnt = resetCnt + 1
                end
            end
        end
    end
    BzcsLog.logAction("act_open", string.format(
        "svr=%s start=%s end=%s oldStart=%s online=%s resetCnt=%s",
        Config.SVR_INDEX, msg.startTime or 0, msg.endTime or 0, oldStart,
        Util.getTableCount(ObjHuman.onlineUuid), resetCnt
    ))
end

-- WL_BZCS_ACT_END: 清除活动开始时间, 并清空在线玩家本轮本地战报/匹配缓存
function C2N_Act_End(msg)
    local oldStart = CommonDB.getValueByKey(CommonDB.KEY_BZCS_START_TIME) or 0
    CommonDB.updateValue(CommonDB.KEY_BZCS_START_TIME, nil)
    local resetCnt = 0
    for uuid, human in pairs(ObjHuman.onlineUuid) do
        if human.db and human.db.baiZhanChengShen then
            clearRoundLocalData(human.db.baiZhanChengShen)
            human.db.baiZhanChengShen.actStartTime = 0
            clearMatchCache(human)
            resetCnt = resetCnt + 1
        end
    end
    BzcsLog.logAction("act_close", string.format(
        "svr=%s oldStart=%s online=%s resetCnt=%s",
        Config.SVR_INDEX, oldStart, Util.getTableCount(ObjHuman.onlineUuid), resetCnt
    ))
end

-- WL_BZCS_TIPS: 跨服业务错误(活动未开/对手无效等)
function C2N_ErrTips(msg)
    local human = ObjHuman.onlineUuid[msg.playerUuid]
    if not human then return end
    local errCode = msg.errCode
    if errCode == BaiZhanChengShenDefine.BZCS_ERR_NOT_OPEN then
        return Broadcast.sendErr(human, Lang.COMMOM_NOT_ENABLED)
    elseif errCode == BaiZhanChengShenDefine.BZCS_ERR_TARGET_INVALID then
        return Broadcast.sendErr(human, Lang.BZCS_MATCH_OPPONENT_INVALID)
    elseif errCode == BaiZhanChengShenDefine.BZCS_ERR_NO_TIMES then
        return Broadcast.sendErr(human, Lang.ITEM_NOT_ENOUGH)
    end
    Broadcast.sendErr(human, Lang.DATA_ERR)
end

-- WL_BZCS_MATCH -> GC_BZCS_MATCH_LIST, 写入内存匹配缓存
function C2N_Match(msg)
    local human = ObjHuman.onlineUuid[msg.playerUuid]
    if not human then return end
    local myScore = msg.myScore or BaiZhanChengShenDefine.BZCS_INIT_SCORE
    local opponentList = msg.opponentList or {}
    local cache = getMatchCache(human)
    local isDisplayRefresh = cache and cache.matchScore and cache.matchScore == myScore
        and cache.opponentList and next(cache.opponentList)
    if isDisplayRefresh then
        refreshMatchCacheDisplay(human, myScore, opponentList)
    else
        setMatchCache(human, myScore, opponentList)
    end
    sendMatchListGC(human, myScore, opponentList)
end

local function fillBzcsRankNet(net, info)
    info = info or {}
    net.rank = info.rank or 0
    net.name = info.name or ""
    net.head = info.head or 0
    net.headFrame = info.headFrame or 0
    net.power = info.power or 0
    net.score = info.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE
    net.serverId = BaiZhanChengShenDefine.ToClientServerId(info.serverId, info.isRobot or 0)
    net.uuid = info.uuid or ""
end

-- 未上榜/未注册跨服时, 用本地五族阵容实时战力(同 CombatPosLogic.getCombatHeroZDL)
local function calcLocalBzcsPower(human)
    local total = 0
    for _, race in ipairs(BaiZhanChengShenDefine.BZCS_RACE_ORDER) do
        local combatType = BaiZhanChengShenDefine.BZCS_RACE_COMBAT_TYPE[race]
        total = total + CombatPosLogic.getCombatHeroZDL(human, combatType)
    end
    return total
end

local function enrichMyRankInfoForClient(human, myInfo)
    myInfo = myInfo or {}
    if (myInfo.rank or 0) > 0 then
        return myInfo
    end
    return {
        rank = 0,
        uuid = human.db._id,
        name = human.db.name or "",
        head = RoleHeadLogic.getRoleAppearance(human, RoleHeadLogic.HEAD_TYPE_1),
        headFrame = RoleHeadLogic.getRoleAppearance(human, RoleHeadLogic.HEAD_TYPE_2),
        serverId = Config.SVR_INDEX,
        power = calcLocalBzcsPower(human),
        score = myInfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE,
    }
end

-- WL_BZCS_RANK_LIST -> GC_BZCS_RANK_LIST
function C2N_RankList(msg)
    local human = ObjHuman.onlineUuid[msg.playerUuid]
    if not human then return end
    local msgRet = Msg.gc.GC_BZCS_RANK_LIST
    local myInfo = enrichMyRankInfoForClient(human, msg.myRankInfo)
    fillBzcsRankNet(msgRet.myRankInfo, myInfo)
    msgRet.rankList[0] = #(msg.rankList or {})
    for i, info in ipairs(msg.rankList or {}) do
        fillBzcsRankNet(msgRet.rankList[i], info)
    end
    Msg.send(msgRet, human.fd)
end

-- WL_BZCS_OPPONENT_INFO -> GC_BZCS_OPPONENT_INFO
function C2N_OpponentInfo(msg)
    local human = ObjHuman.onlineUuid[msg.playerUuid]
    if not human then return end
    if msg.res ~= 0 then
        return Broadcast.sendErr(human, Lang.DATA_ERR)
    end
    local t = msg.targetInfo or {}
    local msgRet = Msg.gc.GC_BZCS_OPPONENT_INFO
    msgRet.name = t.name or ""
    msgRet.head = t.head or 0
    msgRet.headFrame = t.headFrame or 0
    msgRet.power = t.power or 0
    msgRet.score = t.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE
    Msg.send(msgRet, human.fd)
end

-- WL_BZCS_OPPONENT_LINEUP -> GC_BZCS_OPPONENT_LINEUP
function C2N_OpponentLineup(msg)
    local human = ObjHuman.onlineUuid[msg.playerUuid]
    if not human then return end
    local msgRet = Msg.gc.GC_BZCS_OPPONENT_LINEUP
    msgRet.teamList[0] = BaiZhanChengShenDefine.BZCS_RACE_CNT
    local heroArr = msg.showInfo and msg.showInfo.heroArr or {}
    for i, race in ipairs(BaiZhanChengShenDefine.BZCS_RACE_ORDER) do
        local show = heroArr[race] or {}
        msgRet.teamList[i].race = race
        fillBzcsTeamNet(msgRet.teamList[i], show, nil, nil)
    end
    Msg.send(msgRet, human.fd)
end

-- WL_BZCS_ISSUE_REWARD: 本服批量发周期结算邮件(rewardList={{uuid,rank},...})
function C2N_IssueReward(msg)
    local rewardList = msg.rewardList
    if not rewardList or #rewardList == 0 then
        return
    end
    BzcsLog.logAction("reward_issue_ns", string.format("cnt=%s svr=%s", #rewardList, Config.SVR_INDEX))
    local q = createRewardQueue()
    for _, info in ipairs(rewardList) do
        q:add({info[1], info[2]})
    end
    q:insertDB()
end

------------------------------------ 战斗相关 (CombatLogic / InnerHandler) ------------------------------------
-- CG_COMBAT_BEGIN / CombatLogic.moduleFn / CombatPosLogic / InnerHandler C2N 战斗回调

-- combatType -> 五族场次下标(1~5)
local function getBzcsRaceIdxByCombatType(combatType)
    for raceIdx, t in ipairs(BaiZhanChengShenDefine.BZCS_RACE_COMBAT_TYPE) do
        if t == combatType then
            return raceIdx
        end
    end
end

-- CG_COMBAT_BEGIN: 挑战一次依次调用 TYPE39~43; param=全服名次rank(须在内存匹配缓存中)
function fight(human, args, combatType)
    local raceIdx = getBzcsRaceIdxByCombatType(combatType)
    if not raceIdx then
        return Broadcast.sendErr(human, Lang.DATA_ERR)
    end

    if raceIdx == 1 then
        -- 首场: 校验次数/道具、五族阵容, 请求跨服 CAN_FIGHT(扣次在 C2N_CanFight)
        ObjHuman.updateDaily(human)
        local rank = tonumber(args[1])
        if not rank or rank < 1 then
            return Broadcast.sendErr(human, Lang.BZCS_MATCH_OPPONENT_INVALID)
        end
        if human.bzcs_Battle_Cache then
            return Broadcast.sendErr(human, Lang.BZCS_FIGHT_STATE_ERR)
        end
        if not actStartTimeCheck() then
            return Broadcast.sendErr(human, Lang.COMMOM_NOT_ENABLED)
        end
        if not isMiddleReady() then
            return Broadcast.sendErr(human, Lang.MIDDLE_SVR_ERR_CONNECT)
        end
        if not canChallenge(human) then
            return Broadcast.sendErr(human, Lang.ITEM_NOT_ENOUGH)
        end
        if not checkAllRaceLineupForFight(human) then
            return
        end
        if not resolveMatchOpponentByRank(human, rank) then
            return Broadcast.sendErr(human, Lang.BZCS_MATCH_OPPONENT_INVALID)
        end
        local msgData = InnerMsg.lw.LW_BZCS_CAN_FIGHT
        msgData.sourceServerId = Config.SVR_INDEX
        msgData.playerUuid = human.db._id
        msgData.targetRank = rank
        sendLwToMiddle(human, msgData)
        return
    end

    -- 第 2~5 场: 已有 bzcs_Battle_Cache 则直接开打, 不再请求跨服/扣次
    local cache = human.bzcs_Battle_Cache
    if not cache then
        return Broadcast.sendErr(human, Lang.BZCS_FIGHT_STATE_ERR)
    end
    if cache.atkW >= BaiZhanChengShenDefine.BZCS_WIN_TARGET
        or cache.defW >= BaiZhanChengShenDefine.BZCS_WIN_TARGET then
        return Broadcast.sendErr(human, Lang.BZCS_FIGHT_STATE_ERR)
    end
    cache.raceIdx = raceIdx
    startRaceCombat(human, cache)
end

function getCombatMonsterOutID(human, side, args)
    if side ~= CombatDefine.DEFEND_SIDE then return end
    local defUuid = args[1]
    local raceIdx = args[2]
    if not defUuid or not raceIdx then return end
    local cfg = BaiZhanChengShenDefine.GetRobotListCfg(defUuid)
    if not cfg or not cfg.monsterOutIDs then return end
    local monsterOutID = cfg.monsterOutIDs[raceIdx]
    if not monsterOutID then return end
    return monsterOutID, BaiZhanChengShenDefine.CalcMonsterOutPower(monsterOutID)
end

function getCombatObjList(human, side, args, combatType)
    if side == CombatDefine.ATTACK_SIDE then
        if not human then return end
        return CombatLogic.getHumanObjList(human, combatType)
    end
    -- 防守方: args[2]=raceIdx 为本地机器人, 返回 nil 走 getCombatMonsterOutID
    if args[2] then
        return nil
    end
    local defUuid = args[1]
    if not defUuid then return end
    local db = RoleDBLogic.getDb(defUuid)
    if not db then return end
    return CombatLogic.getHumanObjList({db = db}, combatType)
end

function onFightEnd(human, result, combatType, cbParam, combatInfo)
    local cache = human.bzcs_Battle_Cache
    if not cache then return end

    local roundResult = (result == CombatDefine.RESULT_WIN) and 1 or 2
    cache.rounds[cache.raceIdx] = roundResult
    if result == CombatDefine.RESULT_WIN then
        cache.atkW = cache.atkW + 1
    else
        cache.defW = cache.defW + 1
    end

    combatInfo.defender.name = cache.defName

    local seriesOver = cache.atkW >= BaiZhanChengShenDefine.BZCS_WIN_TARGET
        or cache.defW >= BaiZhanChengShenDefine.BZCS_WIN_TARGET
        or cache.raceIdx >= BaiZhanChengShenDefine.BZCS_RACE_CNT

    if not seriesOver then
        cache.raceIdx = cache.raceIdx + 1
        combatInfo.nextCombatType = BaiZhanChengShenDefine.BZCS_RACE_COMBAT_TYPE[cache.raceIdx]
        return
    end

    -- 整场结束: 发挑战奖励并写入 combatInfo.rewardItem(GC_COMBAT_FINISH 展示)
    local atkWin = cache.atkW >= BaiZhanChengShenDefine.BZCS_WIN_TARGET
    grantFightReward(human, atkWin, combatInfo)
    challengeFinish(human, cache)
end

function checkUpdatePos(human, msg)
    local condiCamp = COMBATTYPE_2_CAMP[msg.type]
    if not condiCamp then return false end
    HeroLogic = HeroLogic or require("hero.HeroLogic")
    local heroList = Util.split(msg.heroList, ",")
    for i = 1, CombatDefine.COMBAT_HERO_CNT do
        local uuid = heroList[i] or ""
        if uuid ~= "0" and uuid ~= "" then
            local heroGrid = HeroLogic.getHeroGridByUuid(human, uuid)
            if not heroGrid then return false end
            local heroCfg = HeroExcel.hero[heroGrid.id]
            if not heroCfg or heroCfg.camp ~= condiCamp then
                return false
            end
        end
    end
    return true
end

function onCombatPosUpdate(human, combatType)
    local race = COMBATTYPE_2_CAMP[combatType]
    if not race then return end
    UpdateShowInfo(human, BaiZhanChengShenDefine.BZCS_UPDATE_SHOW_LINEUP, race)
end

-- 百战成神阵容精灵位变更(CombatPosLogic.Elf_Pos_Update 调用)
function onElfPosUpdate(human, combatType)
    onCombatPosUpdate(human, combatType)
end

-- 该族阵容精灵位是否包含指定 elfId
local function raceLineupHasElf(elfList, elfId)
    if not elfList or not elfId or elfId < 1 then
        return false
    end
    for i = 1, CombatDefine.COMBAT_ELF_NOW_CNT do
        if elfList[i] == elfId then
            return true
        end
    end
    return false
end

-- 精灵升星后同步所有上阵了该精灵的百战成神队伍(同精灵可出现在多族阵容)
function onElfStarUp(human, elfId)
    if not elfId or elfId < 1 then return end
    if not actStartTimeCheck() or not isMiddleReady() then return end
    local db = getBzcsDb(human)
    if not db.crossRegistered then return end
    for _, race in ipairs(BaiZhanChengShenDefine.BZCS_RACE_ORDER) do
        local combatType = BaiZhanChengShenDefine.BZCS_RACE_COMBAT_TYPE[race]
        local combatHeroDB = CombatPosLogic.getCombatHeroDB(human, combatType)
        if raceLineupHasElf(combatHeroDB and combatHeroDB.elfList, elfId) then
            UpdateShowInfo(human, BaiZhanChengShenDefine.BZCS_UPDATE_SHOW_LINEUP, race)
        end
    end
end

-- 该族阵容是否包含指定英雄 uuid
local function raceLineupHasHero(heroList, heroUuid)
    if not heroUuid or heroUuid == "" or heroUuid == "0" then
        return false
    end
    if not heroList then return false end
    for _, uuid in pairs(heroList) do
        if uuid == heroUuid then
            return true
        end
    end
    return false
end

-- 收集百战成神五族中上阵了该英雄的种族列表(删除英雄前调用)
function collectBzcsRacesWithHero(human, heroUuid)
    local races = {}
    if not heroUuid or heroUuid == "" or heroUuid == "0" then
        return races
    end
    for _, race in ipairs(BaiZhanChengShenDefine.BZCS_RACE_ORDER) do
        local combatType = BaiZhanChengShenDefine.BZCS_RACE_COMBAT_TYPE[race]
        local heroList = CombatPosLogic.getCombatHeros(human, combatType)
        if raceLineupHasHero(heroList, heroUuid) then
            races[#races + 1] = race
        end
    end
    return races
end

-- 英雄数据变更后同步跨服阵容; raceList 可选(重生等删除英雄后传入删除前收集的种族)
function onHeroDataUpdate(human, heroUuid, raceList)
    if not actStartTimeCheck() or not isMiddleReady() then return end
    local db = getBzcsDb(human)
    if not db.crossRegistered then return end
    local races = raceList
    if not races or #races < 1 then
        if not heroUuid or heroUuid == "" or heroUuid == "0" then return end
        races = collectBzcsRacesWithHero(human, heroUuid)
    end
    if #races < 1 then return end
    for _, race in ipairs(races) do
        UpdateShowInfo(human, BaiZhanChengShenDefine.BZCS_UPDATE_SHOW_LINEUP, race)
    end
end

-- WL_BZCS_CAN_FIGHT: 扣次并初始化 bzcs_Battle_Cache, 开打第一族
function C2N_CanFight(msg)
    local human = ObjHuman.onlineUuid[msg.playerUuid]
    if not human then return end
    if msg.res ~= 0 then
        return Broadcast.sendErr(human, Lang.DATA_ERR)
    end
    if not deductChallengeTimes(human) then
        return Broadcast.sendErr(human, Lang.ITEM_NOT_ENOUGH)
    end
    human.bzcs_Battle_Cache = {
        defRank = msg.targetRank,
        defUuid = msg.defUuid,
        defServerId = msg.defServerId,
        defName = msg.defName,
        defScore = msg.defScore,
        isRobot = msg.isRobot,
        raceIdx = 1,
        atkW = 0,
        defW = 0,
        rounds = {},
    }
    BzcsLog.logAction("can_fight", string.format(
        "atk=%s defRank=%s def=%s defScore=%s isRobot=%s defSvr=%s",
        msg.playerUuid or "", msg.targetRank or 0, msg.defUuid or "", msg.defScore or 0, msg.isRobot or 0, msg.defServerId or 0
    ))
    startRaceCombat(human, human.bzcs_Battle_Cache)
end

-- WL_BZCS_FIGHT_END: 攻方积分变化, 重置匹配缓存与 TTL
function C2N_FightEnd(msg)
    local human = ObjHuman.onlineUuid[msg.playerUuid]
    if not human then return end
    local db = getBzcsDb(human)
    db.crossRegistered = true
    resetMatchCacheOnScoreChange(human, msg.myScore)
    BzcsLog.logAction("fight_end_ns", string.format("uuid=%s atkWin=%s scoreChange=%s myScore=%s", msg.playerUuid or "", msg.atkWin or 0, msg.scoreChange or 0, msg.myScore or 0))
end

-- WL_BZCS_DEF_NOTIFY: 防守方战报(在线/离线)
function C2N_DefNotify(msg)
    local human = ObjHuman.onlineUuid[msg.playerUuid]
    local offlineSave = false
    if not human then
        local db = RoleDBLogic.getDb(msg.playerUuid, {baiZhanChengShen = 1})
        if not db then return end
        human = {db = db}
        offlineSave = true
    end
    local defWin = msg.atkWin == 1
    addWarReport(human, {
        warType = defWin and BaiZhanChengShenDefine.BZCS_WAR_TYPE_DEF_WIN or BaiZhanChengShenDefine.BZCS_WAR_TYPE_DEF_LOSE,
        oppServerId = msg.atkServerId or 0,
        oppName = msg.atkName or "",
        scoreChange = msg.scoreChange or 0,
    })
    if human.fd then
        resetMatchCacheOnScoreChange(human, msg.myScore)
    elseif offlineSave then
        RoleDBLogic.saveRoleSset(human.db)
    end
end

------------------------------------ 对外接口 ------------------------------------
-- 供红点/活动状态/阵容展示同步等其它模块调用

-- 跨天重置免费挑战次数(由 ObjHuman.updateDaily 调用)
function updateDaily(human)
    if not human.db.baiZhanChengShen then
        return
    end
    human.db.baiZhanChengShen.freeTimes = BaiZhanChengShenDefine.BZCS_FREE_TIMES
end

local ACT_STATE_NOOPEN = 0  -- 活动未开启
local ACT_STATE_START = 2   -- 活动进行中(与 JjcActLogic.STATE_START 一致)

-- 本轮活动剩余秒数(至周日23:00)
local function calcActLeftSec()
    local startTime = CommonDB.getValueByKey(CommonDB.KEY_BZCS_START_TIME)
    if not startTime or startTime == 0 or not IsRunning(startTime) then
        return 0
    end
    local endDayOffset = BaiZhanChengShenDefine.GetOpenEndDayOffset()
    local endTime = Util.getDayStartTime(startTime) + endDayOffset * 86400
        + BaiZhanChengShenDefine.BZCS_END_SEC
    local now = os.time()
    if endTime > now then
        return endTime - now
    end
    return 0
end

-- 百战成神活动是否处于开启状态(挑战日 + 跨服已开轮且在活动期内)
function ModuleisOpen(human)
    return actStartTimeCheck()
end

-- 玩法状态与剩余时间: state 0=未开启 2=进行中; leftSec=剩余秒数
function getActState(human)
    if not actStartTimeCheck() then
        return ACT_STATE_NOOPEN, 0
    end
    return ACT_STATE_START, calcActLeftSec()
end

-- 活动红点: 可参与且仍有免费挑战次数
function isActRed(human)
    if not isServerEligible() then return false end
    if not actStartTimeCheck() then return false end
    local db = getBzcsDb(human)
    return db.freeTimes > 0
end

-- joinTime 为跨服下发的本轮开始时间; 无参时仅判断星期
function IsRunning(joinTime)
    if not joinTime then
        return true
    end
    local wDay = getWDay()
    if wDay > BaiZhanChengShenDefine.BZCS_OPEN_WDAY_AREA[2]
        and wDay < BaiZhanChengShenDefine.BZCS_OPEN_WDAY_AREA[1] then
        return false
    end
    local now = os.time()
    if now < joinTime then
        return false
    end
    local endDayOffset = BaiZhanChengShenDefine.GetOpenEndDayOffset()
    local endTime = Util.getDayStartTime(joinTime) + endDayOffset * 86400
        + BaiZhanChengShenDefine.BZCS_END_SEC
    if now > endTime then
        return false
    end
    local dayStart = Util.getDayStartTime(now)
    if wDay == BaiZhanChengShenDefine.BZCS_OPEN_WDAY_AREA[2]
        and now > (dayStart + BaiZhanChengShenDefine.BZCS_END_SEC) then
        return false
    end
    local diffDays = Util.diffDay(joinTime)
    if diffDays > BaiZhanChengShenDefine.BZCS_OPEN_DAYS then
        return false
    end
    return true
end

-- 增量同步跨服展示(LW_BZCS_UPDATE_SHOW, 须已注册)
-- updateType: BZCS_UPDATE_SHOW_NAME/HEAD/HEAD_FRAME/BODY/LINEUP; LINEUP 时 race 必填(1~5)
function UpdateShowInfo(human, updateType, race)
    if not actStartTimeCheck() or not isMiddleReady() then return end
    if not updateType then return end
    local db = getBzcsDb(human)
    if not db.crossRegistered then
        return
    end
    local patch = buildShowPatch(human, updateType, race)
    if not patch then return end
    local msgData = InnerMsg.lw.LW_BZCS_UPDATE_SHOW
    msgData.playerUuid = human.db._id
    msgData.updateType = updateType
    msgData.race = race or 0
    msgData.showInfo = patch
    InnerMsg.sendMsg(0, msgData)
    BzcsLog.logAction("update_show_send", string.format("uuid=%s type=%s race=%s", human.db._id, updateType, race or 0))
end
