<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
namespace app\dice\logic\reward;

use app\dice\helper\AdminScopeHelper;
use app\dice\helper\ConfigScopeEditHelper;
use app\dice\model\reward\DiceReward;
use app\dice\model\reward_config\DiceRewardConfig;
use plugin\saiadmin\exception\ApiException;
use support\think\Db;

/**
 * 奖励对照逻辑层（DiceReward）
 * 权重 1-10000，区分顺时针/逆时针，修改后刷新 DiceReward 缓存
 */
class DiceRewardLogic
{
    private const WEIGHT_MIN = 1;
    private const WEIGHT_MAX = 10000;

    /** 档位键 */
    private const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'];

    /**
     * 分页列表（按方向筛选，关联 dice_reward_config 展示 grid_number、ui_text、real_ev、remark）
     * @param int $direction 0=顺时针 1=逆时针
     * @param array{tier?: string, orderField?: string, orderType?: string} $where tier 档位筛选
     * @param int $page
     * @param int $limit
     * @return array{total: int, per_page: int, current_page: int, data: array}
     */
    public function getListWithConfig(
        int $direction,
        array $where,
        int $page = 1,
        int $limit = 10,
        ?array $adminInfo = null,
        $requestDeptId = null
    ): array {
        $tier = isset($where['tier']) ? trim((string) $where['tier']) : '';
        $orderField = isset($where['orderField']) && $where['orderField'] !== '' ? (string) $where['orderField'] : 'r.tier';
        $orderType = isset($where['orderType']) && strtoupper((string) $where['orderType']) === 'DESC' ? 'desc' : 'asc';

        $keepIds = $this->resolveDedupedRewardIdsByGrid($direction, $tier, $adminInfo, $requestDeptId);
        if ($keepIds === []) {
            return [
                'total'         => 0,
                'per_page'      => $limit,
                'current_page'  => $page,
                'data'          => [],
            ];
        }

        $query = DiceReward::alias('r')
            ->whereIn('r.id', $keepIds)
            ->field('r.id,r.tier,r.direction,r.end_index,r.weight,r.grid_number,r.start_index,r.ui_text,r.real_ev,r.remark,r.type,r.create_time,r.update_time')
            ->order($orderField, $orderType)
            ->order('r.grid_number', 'asc');

        $paginator = $query->paginate($limit, false, ['page' => $page]);
        $arr = $paginator->toArray();
        $data = isset($arr['data']) ? $arr['data'] : $arr['records'] ?? [];
        $total = (int) ($arr['total'] ?? 0);
        $perPage = (int) ($arr['per_page'] ?? $limit);
        $currentPage = (int) ($arr['current_page'] ?? $page);
        foreach ($data as $i => $row) {
            if (isset($row['id']) && $row['id'] !== '' && $row['id'] !== null) {
                $data[$i]['id'] = (int) $row['id'];
            } else {
                $data[$i]['id'] = isset($row['end_index']) ? (int) $row['end_index'] : 0;
            }
            $data[$i]['start_index'] = isset($row['start_index']) && $row['start_index'] !== '' && $row['start_index'] !== null
                ? (int) $row['start_index']
                : 0;
        }
        return [
            'total'         => $total,
            'per_page'      => $perPage,
            'current_page'  => $currentPage,
            'data'          => $data,
        ];
    }

    /**
     * 列表去重：每个方向、每个色子点数(5-30)仅保留一条（取 id 最大），避免历史重复数据导致 104 条
     * @return int[]
     */
    private function resolveDedupedRewardIdsByGrid(
        int $direction,
        string $tier,
        ?array $adminInfo,
        $requestDeptId
    ): array {
        $dedupeQuery = DiceReward::alias('rd')
            ->field('MAX(rd.id) AS keep_id')
            ->where('rd.direction', $direction)
            ->whereBetween('rd.grid_number', [5, 30]);

        if ($adminInfo !== null) {
            AdminScopeHelper::applyConfigScope($dedupeQuery, $adminInfo, $requestDeptId, 'rd.dept_id');
        }

        if ($tier !== '') {
            $dedupeQuery->where('rd.tier', $tier);
        }

        $rows = $dedupeQuery->group('rd.grid_number')->select()->toArray();
        $ids = [];
        foreach ($rows as $row) {
            $id = isset($row['keep_id']) ? (int) $row['keep_id'] : 0;
            if ($id > 0) {
                $ids[] = $id;
            }
        }

        return $ids;
    }

    /**
     * 按单方向批量更新权重（仅更新当前方向的 weight，并刷新缓存）
     * @param int $direction 0=顺时针 1=逆时针
     * @param array<int, array{id: int, weight: int}> $items id 为 end_index（DiceRewardConfig.id）
     */
    public function batchUpdateWeightsByDirection(int $direction, array $items, ?int $deptId = null): void
    {
        if (empty($items)) {
            return;
        }
        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }
            $id = isset($item['id']) ? (int) $item['id'] : 0;
            $weight = isset($item['weight']) ? (int) $item['weight'] : self::WEIGHT_MIN;
            if ($id <= 0) {
                throw new ApiException('Invalid config ID exists');
            }
            $weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $weight));

            $configQuery = DiceRewardConfig::where('id', $id);
            if ($deptId !== null) {
                ConfigScopeEditHelper::applyDeptIdWhere($configQuery, $deptId);
            }
            $tier = $configQuery->value('tier');
            if ($tier === null || $tier === '') {
                throw new ApiException(\app\api\util\ApiLang::translateParams('配置ID %s 不存在或档位为空', [$id]));
            }
            $tier = (string) $tier;

            $rewardQuery = DiceReward::where('tier', $tier)
                ->where('direction', $direction)
                ->where('end_index', $id);
            if ($deptId !== null) {
                ConfigScopeEditHelper::applyDeptIdWhere($rewardQuery, $deptId);
            }
            $affected = $rewardQuery->update(['weight' => $weight]);
            if ($affected === 0) {
                $m = new DiceReward();
                $m->tier = $tier;
                $m->direction = $direction;
                $m->end_index = $id;
                $m->weight = $weight;
                if ($deptId !== null && $deptId > 0) {
                    $m->dept_id = $deptId;
                }
                $m->save();
            }
        }
        DiceReward::refreshCache($deptId);
    }

    /**
     * 按档位+单方向返回列表（用于权重编辑弹窗：当前方向下按档位分组的配置+权重）
     * @param int $direction 0=顺时针 1=逆时针
     * @return array<string, array> 键 T1|T2|...|BIGWIN，值为该档位下带 weight 的行数组
     */
    public function getListGroupedByTierForDirection(int $direction, ?int $deptId = null): array
    {
        $configInstance = DiceRewardConfig::getCachedInstance($deptId);
        $byTier = $configInstance['by_tier'] ?? [];
        $rewardInstance = DiceReward::getCachedInstance($deptId);
        $byTierDirection = $rewardInstance['by_tier_direction'] ?? [];

        $result = [];
        foreach (self::TIER_KEYS as $tier) {
            $result[$tier] = [];
            $rows = $byTier[$tier] ?? [];
            $dirRows = $byTierDirection[$tier][$direction] ?? [];
            $weightMap = [];
            foreach ($dirRows as $r) {
                $eid = isset($r['end_index']) ? (int) $r['end_index'] : 0;
                $weightMap[$eid] = isset($r['weight']) ? max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $r['weight'])) : 1;
            }
            foreach ($rows as $row) {
                $id = isset($row['id']) ? (int) $row['id'] : 0;
                $result[$tier][] = [
                    'id'          => $id,
                    'grid_number' => $row['grid_number'] ?? 0,
                    'ui_text'      => $row['ui_text'] ?? '',
                    'real_ev'     => $row['real_ev'] ?? 0,
                    'remark'      => $row['remark'] ?? '',
                    'tier'        => $tier,
                    'weight'      => $weightMap[$id] ?? 1,
                ];
            }
        }
        return $result;
    }

    /**
     * 按档位+方向返回 DiceReward 列表（用于权重配比弹窗），直接读 dice_reward 表，不依赖 config
     * 每行含 reward_id(DiceReward 主键，用于按 id 更新权重)、id(end_index 展示用)、grid_number、ui_text、real_ev、remark、weight
     *
     * @return array<string, array{0: array, 1: array}>
     */
    public function getListGroupedByTierWithDirection(?int $deptId = null): array
    {
        $rewardInstance = DiceReward::getCachedInstance($deptId);
        $byTierDirection = $rewardInstance['by_tier_direction'] ?? [];

        $result = [];
        foreach (self::TIER_KEYS as $tier) {
            $result[$tier] = [0 => [], 1 => []];
            foreach ([0, 1] as $direction) {
                $rows = $byTierDirection[$tier][$direction] ?? [];
                foreach ($rows as $r) {
                    $result[$tier][$direction][] = [
                        'reward_id'   => isset($r['id']) ? (int) $r['id'] : 0,
                        'id'          => isset($r['end_index']) ? (int) $r['end_index'] : 0,
                        'grid_number' => isset($r['grid_number']) ? (int) $r['grid_number'] : 0,
                        'ui_text'     => (string) ($r['ui_text'] ?? ''),
                        'real_ev'     => $r['real_ev'] ?? 0,
                        'remark'      => (string) ($r['remark'] ?? ''),
                        'weight'      => isset($r['weight']) ? max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $r['weight'])) : self::WEIGHT_MIN,
                    ];
                }
            }
        }
        return $result;
    }

    /**
     * 批量更新权重：直接按 DiceReward 主键 id 更新 weight，不依赖 direction/grid_number
     *
     * @param array<int, array{id: int, weight: int}> $items 每项 id 为 dice_reward 表主键，weight 为 1-10000
     * @throws ApiException
     */
    public function batchUpdateWeights(array $items, ?int $deptId = null): void
    {
        if (empty($items)) {
            return;
        }
        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }
            $id = isset($item['id']) ? (int) $item['id'] : 0;
            if ($id <= 0) {
                $id = isset($item['reward_id']) ? (int) $item['reward_id'] : 0;
            }
            if ($id <= 0) {
                throw new ApiException('Invalid DiceReward id exists');
            }
            $weight = isset($item['weight']) ? (int) $item['weight'] : self::WEIGHT_MIN;
            $weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $weight));
            $query = DiceReward::where('id', $id);
            if ($deptId !== null) {
                if (AdminScopeHelper::isTemplateDeptId($deptId)) {
                    $query->where(function ($q) {
                        $q->where('dept_id', AdminScopeHelper::DEFAULT_TEMPLATE_DEPT)
                            ->whereOr('dept_id', null);
                    });
                } else {
                    $query->where('dept_id', $deptId);
                }
            }
            $model = $query->find();
            if ($model !== null) {
                $model->weight = $weight;
                $model->save();
            }
        }
        DiceReward::refreshCache($deptId);
    }

    /** BIGWIN 权重范围：0=0% 中奖，10000=100% 中奖；grid_number=5/30 固定 100% 不可改 */
    private const BIGWIN_WEIGHT_MAX = 10000;

    /**
     * 按 grid_number 获取 BIGWIN 档位权重（取顺时针方向，用于编辑展示）
     * 若 DiceReward 无该点数则 5/30 返回 10000，其余返回 0
     */
    public function getBigwinWeightByGridNumber(int $gridNumber, ?int $deptId = null): int
    {
        $inst = DiceReward::getCachedInstance($deptId);
        $rows = $inst['by_tier_direction']['BIGWIN'][DiceReward::DIRECTION_CLOCKWISE] ?? [];
        foreach ($rows as $row) {
            if ((int) ($row['grid_number'] ?? 0) === $gridNumber) {
                return min(self::BIGWIN_WEIGHT_MAX, (int) ($row['weight'] ?? self::BIGWIN_WEIGHT_MAX));
            }
        }
        return in_array($gridNumber, [5, 30], true) ? self::BIGWIN_WEIGHT_MAX : 0;
    }

    /**
     * 更新 BIGWIN 档位某点数的权重（顺/逆时针同时更新）；0=0% 中奖，10000=100% 中奖
     * 表 dice_reward 唯一键为 (direction, grid_number)，同一点数同一方向仅一条记录，故先按该键查找再更新，避免重复插入
     */
    public function updateBigwinWeight(int $gridNumber, int $weight, ?int $deptId = null): void
    {
        $weight = min(self::BIGWIN_WEIGHT_MAX, max(0, $weight));
        if ($deptId === null) {
            $deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
        }
        $configQuery = DiceRewardConfig::where('tier', 'BIGWIN')->where('grid_number', $gridNumber);
        ConfigScopeEditHelper::applyDeptIdWhere($configQuery, $deptId);
        $config = $configQuery->find();
        if (! $config) {
            return;
        }
        $configArr = $config->toArray();
        foreach ([DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE] as $direction) {
            // 按唯一键 (direction, grid_number) 查找，存在则更新，不存在则插入
            $rowQuery = DiceReward::where('direction', $direction)->where('grid_number', $gridNumber);
            ConfigScopeEditHelper::applyDeptIdWhere($rowQuery, $deptId);
            $row = $rowQuery->find();
            if ($row) {
                $row->tier        = 'BIGWIN';
                $row->weight      = $weight > 0 ? $weight : self::WEIGHT_MIN;
                $row->start_index = (int) ($configArr['id'] ?? $row->start_index);
                $row->end_index   = (int) ($configArr['id'] ?? $row->end_index);
                $row->ui_text     = (string) ($configArr['ui_text'] ?? $row->ui_text);
                $row->real_ev     = (float) ($configArr['real_ev'] ?? $row->real_ev);
                $row->remark      = (string) ($configArr['remark'] ?? $row->remark);
                $row->type        = $configArr['type'] ?? $row->type;
                $row->save();
            } else {
                $m = new DiceReward();
                $m->tier        = 'BIGWIN';
                $m->direction   = $direction;
                $m->grid_number = (int) $gridNumber;
                $m->start_index = (int) ($configArr['id'] ?? 0);
                $m->end_index   = (int) ($configArr['id'] ?? 0);
                $m->ui_text     = (string) ($configArr['ui_text'] ?? '');
                $m->real_ev     = (float) ($configArr['real_ev'] ?? 0);
                $m->remark      = (string) ($configArr['remark'] ?? '');
                $m->type        = $configArr['type'] ?? null;
                $m->weight      = $weight > 0 ? $weight : self::WEIGHT_MIN;
                if (!AdminScopeHelper::isTemplateDeptId($deptId)) {
                    $m->dept_id = $deptId;
                }
                $m->save();
            }
        }
        DiceReward::refreshCache($deptId);
    }

    /** 盘面格数（用于顺时针/逆时针计算 end_index） */
    private const BOARD_SIZE = 26;

    /** 点数摇取范围：5-30，顺时针与逆时针均需创建 */
    private const GRID_NUMBER_MIN = 5;
    private const GRID_NUMBER_MAX = 30;

    /**
     * 预览：按当前 dice_reward_config 计算将要生成的 dice_reward（不写库）
     * 若当前 dice_reward 与计算结果完全一致，则标记 unchanged=true，并返回现有权重（导入时将复用旧权重）
     *
     * @return array{unchanged: bool, skipped: int, preview: array<string, array{0: array, 1: array}>}
     * @throws ApiException
     */
    public function createRewardReferencePreviewFromConfig(?int $deptId = null): array
    {
        $normalizedDeptId = $deptId;
        $list = $this->loadConfigListForReference($normalizedDeptId);
        $computed = $this->computeReferenceRowsFromConfigList($list, $normalizedDeptId);

        $existing = $this->loadExistingRewardRowsForReference($normalizedDeptId);
        $compare = $this->compareReferenceRows($computed['rows'], $existing);
        $unchanged = $compare['unchanged'];

        $specialGrids = [5, 10, 15, 20, 25, 30];
        $previewRows = [];
        foreach ($computed['rows'] as $row) {
            $key = $row['direction'] . ':' . $row['grid_number'];
            $gridNumber = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
            $weight = $unchanged
                ? self::WEIGHT_MIN
                : (in_array($gridNumber, $specialGrids, true) ? self::WEIGHT_MIN : 100);
            $oldStart = null;
            $oldEnd = null;
            $oldTier = null;
            $oldWeight = null;
            if (isset($existing[$key])) {
                $oldStart = isset($existing[$key]['start_index']) ? (int) $existing[$key]['start_index'] : null;
                $oldEnd = isset($existing[$key]['end_index']) ? (int) $existing[$key]['end_index'] : null;
                $oldTier = isset($existing[$key]['tier']) ? (string) $existing[$key]['tier'] : null;
                $oldWeight = isset($existing[$key]['weight']) ? (int) $existing[$key]['weight'] : null;
            }
            // 映射未变化时：通常复用旧权重；但若旧权重为 1 且非特殊点数，则按新默认建议展示为 100（方便管理员快速落配置）
            if ($unchanged && $oldWeight !== null) {
                $oldWeight = (int) $oldWeight;
                $weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $oldWeight));
                if ($weight === self::WEIGHT_MIN && !in_array($gridNumber, $specialGrids, true)) {
                    $weight = 100;
                }
            }

            $diffChanged = false;
            $diffFields = [];
            if ($oldStart === null || $oldEnd === null || $oldTier === null) {
                $diffChanged = true;
                $diffFields[] = 'new';
            } else {
                if ((int) $oldStart !== (int) ($row['start_index'] ?? 0)) {
                    $diffChanged = true;
                    $diffFields[] = 'start_index';
                }
                if ((int) $oldEnd !== (int) ($row['end_index'] ?? 0)) {
                    $diffChanged = true;
                    $diffFields[] = 'end_index';
                }
                if (trim((string) $oldTier) !== trim((string) ($row['tier'] ?? ''))) {
                    $diffChanged = true;
                    $diffFields[] = 'tier';
                }
            }

            $previewRows[] = array_merge($row, [
                'weight' => $weight,
                'old_start_index' => $oldStart,
                'old_end_index' => $oldEnd,
                'old_tier' => $oldTier,
                'old_weight' => $oldWeight,
                'diff_changed' => $diffChanged,
                'diff_fields' => $diffFields,
            ]);
        }

        return [
            'unchanged' => $unchanged,
            'skipped'   => $computed['skipped'],
            'preview'   => $this->groupReferenceRowsByTierWithDirection($previewRows),
        ];
    }

    /**
     * 创建奖励对照：先清空 dice_reward 表，再按两种方向为点数 5-30 生成记录。
     *
     * DiceReward 记录数据规则（与 config 通过 end_index 关联）：
     * - 方向：direction = 0（顺时针）/ 1（逆时针）
     * - 摇取点数：grid_number
     * - 起始索引：start_index = DiceRewardConfig::where('grid_number', $grid_number)->first()->id
     * - 结束索引（顺时针）：end_index = ($start_index + $grid_number) % 26（对 26 取余）
     * - 结束索引（逆时针）：end_index = ($start_index - $grid_number >= 0) ? ($start_index - $grid_number) : (26 + $start_index - $grid_number)
     * - 奖励档位：tier = DiceRewardConfig::where('id', $end_index)->first()->tier
     * - 显示ui：ui_text = DiceRewardConfig::where('id', $end_index)->first()->ui_text
     * - 实际中奖：real_ev = DiceRewardConfig::where('id', $end_index)->first()->real_ev
     * - 备注：remark = DiceRewardConfig::where('id', $end_index)->first()->remark
     * - 类型：type = DiceRewardConfig::where('id', $end_index)->first()->type（-2=唯一惩罚,-1=抽水,0=回本,1=再来一次,2=小赚,3=大奖格）
     * - weight 默认 1，后续在权重编辑弹窗设置
     *
     * 例如顺时针摇取点数为 5 时：start_index = 配置中 grid_number=5 对应格位的 id，
     * 结束位置 = (起始位置 + grid_number) % 26，再取该位置的 config 的 id 作为 end_index。
     * 使用「按 id 排序后的盘面位置 0-25」做环形计算，避免 config.id 非连续时取模结果找不到；
     * 唯一键为 (direction, grid_number)，保证每个点数、每个方向各一条记录，不因 end_index 相同而覆盖。
     *
     * @return array{created_clockwise: int, created_counterclockwise: int, updated_clockwise: int, updated_counterclockwise: int, skipped: int}
     * @throws ApiException
     */
    public function createRewardReferenceFromConfig(?int $deptId = null): array
    {
        $normalizedDeptId = $deptId;
        $list = $this->loadConfigListForReference($normalizedDeptId);
        $computed = $this->computeReferenceRowsFromConfigList($list, $normalizedDeptId);

        $existing = $this->loadExistingRewardRowsForReference($normalizedDeptId);
        $compare = $this->compareReferenceRows($computed['rows'], $existing);
        if ($compare['unchanged']) {
            return [
                'created_clockwise'         => 0,
                'created_counterclockwise' => 0,
                'updated_clockwise'         => 0,
                'updated_counterclockwise' => 0,
                'skipped'                   => $computed['skipped'],
                'unchanged'                 => true,
            ];
        }

        $table = (new DiceReward())->getTable();
        if ($normalizedDeptId === null) {
            Db::table($table)->whereNull('dept_id')->delete();
        } else {
            Db::table($table)->where('dept_id', $normalizedDeptId)->delete();
        }

        $createdCw = 0;
        $createdCcw = 0;
        foreach ($computed['rows'] as $row) {
            $m = new DiceReward();
            $m->tier = $row['tier'];
            $m->direction = (int) $row['direction'];
            $m->end_index = (int) $row['end_index'];
            $m->weight = self::WEIGHT_MIN;
            $m->grid_number = (int) $row['grid_number'];
            $m->start_index = (int) $row['start_index'];
            $m->ui_text = (string) ($row['ui_text'] ?? '');
            $m->real_ev = $row['real_ev'] ?? null;
            $m->remark = (string) ($row['remark'] ?? '');
            $m->type = isset($row['type']) ? (int) $row['type'] : 0;
            if ($normalizedDeptId !== null) {
                $m->dept_id = $normalizedDeptId;
            }
            $m->save();
            if ((int) $row['direction'] === DiceReward::DIRECTION_CLOCKWISE) {
                $createdCw++;
            } else {
                $createdCcw++;
            }
        }

        DiceReward::refreshCache($normalizedDeptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
        return [
            'created_clockwise'         => $createdCw,
            'created_counterclockwise' => $createdCcw,
            'updated_clockwise'         => 0,
            'updated_counterclockwise' => 0,
            'skipped'                   => $computed['skipped'],
            'unchanged'                 => false,
        ];
    }

    /**
     * 读取奖励配置（按 id asc），并把模板 dept 转为 null
     * @return array<int, array<string, mixed>>
     */
    private function loadConfigListForReference(?int &$deptId): array
    {
        $configQuery = DiceRewardConfig::order('id', 'asc');
        if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
            $templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
            $configQuery->where(function ($q) use ($templateId) {
                $q->where('dept_id', $templateId)->whereOr('dept_id', 'null');
            });
            $deptId = null;
        } else {
            $configQuery->where('dept_id', $deptId);
        }
        $list = $configQuery->select()->toArray();
        if (empty($list)) {
            throw new ApiException('Reward config is empty, please maintain dice_reward_config first');
        }
        if (count($list) < self::BOARD_SIZE) {
            throw new ApiException(
                \app\api\util\ApiLang::translateParams(
                    '奖励配置需覆盖 26 个格位（id 0-25 或 1-26），当前仅 %s 条，无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照',
                    [count($list)]
                )
            );
        }
        return $list;
    }

    /**
     * 计算 5-30 两个方向的对照行（不含权重）
     * @param array<int, array<string, mixed>> $list
     * @return array{rows: array<int, array<string, mixed>>, skipped: int}
     */
    private function computeReferenceRowsFromConfigList(array $list, ?int $deptId): array
    {
        // 按 id 排序后，盘面位置 0..25 对应 $list[$pos]
        $gridToPosition = [];
        foreach ($list as $pos => $row) {
            $gn = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
            if ($gn >= self::GRID_NUMBER_MIN && $gn <= self::GRID_NUMBER_MAX && !isset($gridToPosition[$gn])) {
                $gridToPosition[$gn] = $pos;
            }
        }

        $rows = [];
        $skipped = 0;
        for ($gridNumber = self::GRID_NUMBER_MIN; $gridNumber <= self::GRID_NUMBER_MAX; $gridNumber++) {
            if (!isset($gridToPosition[$gridNumber])) {
                $skipped++;
                continue;
            }
            $startPos = $gridToPosition[$gridNumber];
            $startRow = $list[$startPos];
            $startId = isset($startRow['id']) ? (int) $startRow['id'] : 0;

            $endPosCw = ($startPos + $gridNumber) % self::BOARD_SIZE;
            $endPosCcw = $startPos - $gridNumber >= 0 ? $startPos - $gridNumber : self::BOARD_SIZE + $startPos - $gridNumber;

            $configCw = $list[$endPosCw] ?? null;
            $configCcw = $list[$endPosCcw] ?? null;
            if ($configCw !== null) {
                $tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
                if ($tier !== '') {
                    $rows[] = $this->buildReferenceRowFromLandingConfig(
                        $tier,
                        $configCw,
                        DiceReward::DIRECTION_CLOCKWISE,
                        $gridNumber,
                        $startId
                    );
                }
            }
            if ($configCcw !== null) {
                $tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
                if ($tier !== '') {
                    $rows[] = $this->buildReferenceRowFromLandingConfig(
                        $tier,
                        $configCcw,
                        DiceReward::DIRECTION_COUNTERCLOCKWISE,
                        $gridNumber,
                        $startId
                    );
                }
            }
        }
        return ['rows' => $rows, 'skipped' => $skipped];
    }

    /**
     * 对照表落点行：档位按结算金额推断，备注与奖励配置页规则一致
     *
     * @param array<string, mixed> $landingConfig
     * @return array<string, mixed>
     */
    private function buildReferenceRowFromLandingConfig(
        string $tier,
        array $landingConfig,
        int $direction,
        int $gridNumber,
        int $startId
    ): array {
        $realEv = isset($landingConfig['real_ev']) ? (float) $landingConfig['real_ev'] : 0.0;
        if ($tier !== 'BIGWIN') {
            $inferred = $this->inferTierFromRealEv($realEv);
            if ($inferred !== '') {
                $tier = $inferred;
            }
        }
        return [
            'tier'        => $tier,
            'direction'   => $direction,
            'weight'      => self::WEIGHT_MIN,
            'grid_number' => $gridNumber,
            'start_index' => $startId,
            'end_index'   => isset($landingConfig['id']) ? (int) $landingConfig['id'] : 0,
            'ui_text'     => $landingConfig['ui_text'] ?? '',
            'real_ev'     => $landingConfig['real_ev'] ?? null,
            'remark'      => $this->defaultRemarkForTier($tier),
            'type'        => isset($landingConfig['type']) ? (int) $landingConfig['type'] : 0,
        ];
    }

    /**
     * 按结算金额推断档位（与前端 generateIndexByRules 一致）
     */
    private function inferTierFromRealEv(float $realEv): string
    {
        if ($realEv > 2) {
            return 'T1';
        }
        if ($realEv > 1) {
            return 'T2';
        }
        if ($realEv > 0) {
            return 'T3';
        }
        if ($realEv < 0) {
            return 'T4';
        }
        return 'T5';
    }

    /**
     * 档位默认备注
     */
    private function defaultRemarkForTier(string $tier): string
    {
        return match ($tier) {
            'T1', 'BIGWIN' => '大奖',
            'T2' => '小赚',
            'T3' => '抽水',
            'T4' => '惩罚',
            'T5' => '再来一次',
            default => '',
        };
    }

    /**
     * 读出当前 dice_reward（用于对比/复用权重）。key = "direction:grid_number"
     * @return array<string, array<string, mixed>>
     */
    private function loadExistingRewardRowsForReference(?int $deptId): array
    {
        $query = DiceReward::whereIn('grid_number', range(self::GRID_NUMBER_MIN, self::GRID_NUMBER_MAX))
            ->whereIn('direction', [DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE]);
        if ($deptId === null) {
            $query->whereNull('dept_id');
        } else {
            $query->where('dept_id', $deptId);
        }
        $rows = $query->select()->toArray();
        $map = [];
        foreach ($rows as $r) {
            $dir = isset($r['direction']) ? (int) $r['direction'] : 0;
            $gn = isset($r['grid_number']) ? (int) $r['grid_number'] : 0;
            $map[$dir . ':' . $gn] = $r;
        }
        return $map;
    }

    /**
     * 对比 computed 与 existing 是否完全一致（忽略权重）
     * @param array<int, array<string, mixed>> $computedRows
     * @param array<string, array<string, mixed>> $existingMap
     * @return array{unchanged: bool}
     */
    private function compareReferenceRows(array $computedRows, array $existingMap): array
    {
        if (empty($computedRows)) {
            return ['unchanged' => false];
        }
        foreach ($computedRows as $row) {
            $key = $row['direction'] . ':' . $row['grid_number'];
            if (!isset($existingMap[$key])) {
                return ['unchanged' => false];
            }
            $ex = $existingMap[$key];
            $same =
                ((int) ($ex['start_index'] ?? 0) === (int) ($row['start_index'] ?? 0)) &&
                ((int) ($ex['end_index'] ?? 0) === (int) ($row['end_index'] ?? 0)) &&
                (trim((string) ($ex['tier'] ?? '')) === trim((string) ($row['tier'] ?? '')));
            if (!$same) {
                return ['unchanged' => false];
            }
        }
        return ['unchanged' => true];
    }

    /**
     * 将行按 tier -> {0:[],1:[]} 组织，便于前端展示（与 weightRatioList 输出结构一致）
     * @param array<int, array<string, mixed>> $rows
     * @return array<string, array{0: array, 1: array}>
     */
    private function groupReferenceRowsByTierWithDirection(array $rows): array
    {
        $result = [];
        foreach (self::TIER_KEYS as $tier) {
            $result[$tier] = [0 => [], 1 => []];
        }
        foreach ($rows as $r) {
            $tier = isset($r['tier']) ? trim((string) $r['tier']) : '';
            if ($tier === '' || !isset($result[$tier])) {
                continue;
            }
            $dir = isset($r['direction']) ? (int) $r['direction'] : 0;
            $dir = $dir === 1 ? 1 : 0;
            $result[$tier][$dir][] = [
                'reward_id'   => 0,
                'id'          => isset($r['end_index']) ? (int) $r['end_index'] : 0,
                'grid_number' => isset($r['grid_number']) ? (int) $r['grid_number'] : 0,
                'ui_text'     => (string) ($r['ui_text'] ?? ''),
                'real_ev'     => $r['real_ev'] ?? 0,
                'remark'      => (string) ($r['remark'] ?? ''),
                'weight'      => isset($r['weight']) ? max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $r['weight'])) : self::WEIGHT_MIN,
                'start_index' => isset($r['start_index']) ? (int) $r['start_index'] : 0,
                'tier'        => (string) ($r['tier'] ?? ''),
                'old_start_index' => $r['old_start_index'] ?? null,
                'old_end_index'   => $r['old_end_index'] ?? null,
                'old_tier'        => $r['old_tier'] ?? null,
                'old_weight'      => $r['old_weight'] ?? null,
                'diff_changed'    => (bool) ($r['diff_changed'] ?? false),
                'diff_fields'     => $r['diff_fields'] ?? [],
            ];
        }
        return $result;
    }
}
