<?php

namespace App\Services\Admin;

use App\Models\AdminUser;
use App\Models\AuditLog;
use App\Support\AdminDataScope;
use App\Support\AdminScopeContext;
use App\Support\AdminScopeContextResolver;
use App\Models\Draw;
use App\Models\RiskPool;
use App\Models\RiskPoolLockLog;
use App\Models\SettlementBatch;
use App\Models\TicketItem;
use App\Models\TicketOrder;
use App\Models\TransferOrder;
use App\Models\WalletTxn;
use Carbon\Carbon;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\LengthAwarePaginator as PaginatorInstance;
use Illuminate\Support\Facades\DB;

/**
 * 报表中心聚合查询（模块十三）。
 */
final class AdminReportQueryService
{
    private function normalizeScope(AdminUser|AdminScopeContext|null $scope): ?AdminScopeContext
    {
        if ($scope instanceof AdminScopeContext) {
            return $scope;
        }

        if ($scope instanceof AdminUser) {
            return AdminScopeContextResolver::fromValues($scope);
        }

        return null;
    }

    /**
     * @return array{date_from: string, date_to: string}
     */
    public function resolveDateRange(?array $filters): array
    {
        $fromRaw = trim((string) ($filters['date_from'] ?? ''));
        $toRaw = trim((string) ($filters['date_to'] ?? ''));

        // 未传日期时默认近 30 个自然日（与后台报表中心默认筛选一致）。
        if ($fromRaw === '' && $toRaw === '') {
            return $this->defaultReportDateRange();
        }

        $dateFrom = $fromRaw !== '' ? $fromRaw : $toRaw;
        $dateTo = $toRaw !== '' ? $toRaw : $dateFrom;

        if ($dateFrom > $dateTo) {
            [$dateFrom, $dateTo] = [$dateTo, $dateFrom];
        }

        return ['date_from' => $dateFrom, 'date_to' => $dateTo];
    }

    /**
     * @return array{date_from: string, date_to: string}
     */
    public function resolveDashboardPeriod(string $period, ?string $dateFrom, ?string $dateTo): array
    {
        $today = now()->toDateString();

        $range = match ($period) {
            'today' => ['date_from' => $today, 'date_to' => $today],
            'last_7_days' => [
                'date_from' => now()->subDays(6)->toDateString(),
                'date_to' => $today,
            ],
            'last_30_days' => [
                'date_from' => now()->subDays(29)->toDateString(),
                'date_to' => $today,
            ],
            'this_month' => [
                'date_from' => now()->startOfMonth()->toDateString(),
                'date_to' => $today,
            ],
            'lifetime' => $this->lifetimeBusinessDateBounds(),
            'custom' => [
                'date_from' => $dateFrom !== null && $dateFrom !== '' ? $dateFrom : $today,
                'date_to' => $dateTo !== null && $dateTo !== '' ? $dateTo : $today,
            ],
            default => ['date_from' => $today, 'date_to' => $today],
        };

        $from = $range['date_from'];
        $to = $range['date_to'];
        if ($from > $to) {
            [$from, $to] = [$to, $from];
        }

        return ['date_from' => $from, 'date_to' => $to];
    }

    /**
     * @return array{date_from: string, date_to: string}
     */
    public function defaultReportDateRange(): array
    {
        $today = now()->toDateString();

        return [
            'date_from' => now()->subDays(29)->toDateString(),
            'date_to' => $today,
        ];
    }

    /**
     * @return array{date_from: string, date_to: string}
     */
    private function lifetimeBusinessDateBounds(): array
    {
        $today = now()->toDateString();
        $bounds = DB::table('draws as d')
            ->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
            ->selectRaw('MIN(d.business_date) as date_from')
            ->selectRaw('MAX(d.business_date) as date_to')
            ->first();

        $from = $this->formatBusinessDateValue($bounds?->date_from) ?? $today;
        $to = $this->formatBusinessDateValue($bounds?->date_to) ?? $today;

        return ['date_from' => $from, 'date_to' => $to];
    }

    /**
     * @return array{
     *     total_bet_minor: int,
     *     total_payout_minor: int,
     *     approx_house_gross_minor: int,
     *     draw_count: int,
     *     business_day_count: int
     * }
     */
    public function periodFinanceTotals(string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null): array
    {
        $context = $this->normalizeScope($scope);
        $rows = $this->dailyProfitRows($dateFrom, $dateTo, $context);
        $totalBet = 0;
        $totalPayout = 0;
        $totalGross = 0;
        foreach ($rows as $row) {
            $totalBet += (int) $row['total_bet_minor'];
            $totalPayout += (int) $row['total_payout_minor'];
            $totalGross += (int) $row['approx_house_gross_minor'];
        }

        $activityQuery = DB::table('draws as d')
            ->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
            ->whereDate('d.business_date', '>=', $dateFrom)
            ->whereDate('d.business_date', '<=', $dateTo);
        AdminDataScope::applyToTicketOrdersViaPlayer($activityQuery, $context?->admin, 'o');
        $activity = $activityQuery
            ->selectRaw('COUNT(DISTINCT d.id) as draw_count')
            ->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count')
            ->first();

        return [
            'total_bet_minor' => $totalBet,
            'total_payout_minor' => $totalPayout,
            'approx_house_gross_minor' => $totalGross,
            'draw_count' => (int) ($activity->draw_count ?? 0),
            'business_day_count' => (int) ($activity->business_day_count ?? 0),
        ];
    }

    /**
     * 连续业务日序列（无数据日补零），用于趋势图。
     *
     * @return list<array<string, mixed>>
     */
    public function dailyProfitSeriesFilled(string $dateFrom, string $dateTo, int $maxDays = 90, AdminUser|AdminScopeContext|null $scope = null): array
    {
        $context = $this->normalizeScope($scope);
        $from = Carbon::parse($dateFrom)->startOfDay();
        $to = Carbon::parse($dateTo)->startOfDay();
        $spanDays = (int) $from->diffInDays($to) + 1;

        $chartFrom = $dateFrom;
        $chartTo = $dateTo;
        $truncated = false;
        if ($spanDays > $maxDays) {
            $chartFrom = $to->copy()->subDays($maxDays - 1)->format('Y-m-d');
            $truncated = true;
        }

        $indexed = collect($this->dailyProfitRows($chartFrom, $chartTo, $context))->keyBy('business_date');
        $cursor = Carbon::parse($chartFrom)->startOfDay();
        $end = Carbon::parse($chartTo)->startOfDay();
        $series = [];

        while ($cursor <= $end) {
            $key = $cursor->format('Y-m-d');
            $series[] = $indexed[$key] ?? [
                'business_date' => $key,
                'total_bet_minor' => 0,
                'total_payout_minor' => 0,
                'approx_house_gross_minor' => 0,
            ];
            $cursor->addDay();
        }

        return [
            'series' => $series,
            'chart_date_from' => $chartFrom,
            'chart_date_to' => $chartTo,
            'truncated' => $truncated,
            'span_days' => $spanDays,
        ];
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function playDimensionBreakdownRows(
        string $dateFrom,
        string $dateTo,
        ?string $playCode = null,
        int $limit = 12,
        AdminUser|AdminScopeContext|null $scope = null,
    ): array {
        return $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo, $scope)
            ->orderByDesc('total_bet_minor')
            ->limit($limit)
            ->get()
            ->map(static function (object $row): array {
                return [
                    'play_code' => (string) $row->play_code,
                    'dimension' => (int) $row->dimension,
                    'total_bet_minor' => (int) $row->total_bet_minor,
                    'total_payout_minor' => (int) $row->total_payout_minor,
                    'approx_house_gross_minor' => (int) $row->approx_house_gross_minor,
                ];
            })
            ->values()
            ->all();
    }

    /**
     * 仪表盘「代理排行」：按代理子树聚合投注/派彩/盈亏（与 daily-profit 同口径）。
     *
     * @return list<array{
     *     agent_node_id: int,
     *     agent_code: string,
     *     agent_name: string,
     *     total_bet_minor: int,
     *     total_payout_minor: int,
     *     approx_house_gross_minor: int
     * }>
     */
    public function agentRankingRows(
        string $dateFrom,
        string $dateTo,
        ?string $playCode = null,
        int $limit = 200,
        AdminUser|AdminScopeContext|null $scope = null,
    ): array {
        $context = $this->normalizeScope($scope);
        $query = DB::table('ticket_items as ti')
            ->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
            ->leftJoin('players as p', 'p.id', '=', 'ti.player_id')
            ->leftJoin('agent_nodes as an', 'an.id', '=', 'p.agent_node_id')
            ->selectRaw('p.agent_node_id as agent_node_id')
            ->selectRaw('an.code as agent_code')
            ->selectRaw('an.name as agent_name')
            ->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor')
            ->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
            ->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as approx_house_gross_minor')
            ->whereDate('o.created_at', '>=', $dateFrom)
            ->whereDate('o.created_at', '<=', $dateTo)
            ->whereNotNull('p.agent_node_id')
            ->groupBy('p.agent_node_id', 'an.code', 'an.name');

        if ($playCode !== null && $playCode !== '') {
            $query->where('ti.play_code', $playCode);
        }

        AdminDataScope::applyToPlayersAlias(
            $query,
            $context?->admin,
            'p',
            $context?->effectiveRequestedAgentNodeId(),
        );

        return $query
            ->orderByDesc('total_bet_minor')
            ->limit($limit)
            ->get()
            ->map(static function (object $row): array {
                return [
                    'agent_node_id' => (int) $row->agent_node_id,
                    'agent_code' => (string) ($row->agent_code ?? ''),
                    'agent_name' => (string) ($row->agent_name ?? ''),
                    'total_bet_minor' => (int) $row->total_bet_minor,
                    'total_payout_minor' => (int) $row->total_payout_minor,
                    'approx_house_gross_minor' => (int) $row->approx_house_gross_minor,
                ];
            })
            ->values()
            ->all();
    }

    public function resolvePeriodCurrencyCode(string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null): ?string
    {
        $context = $this->normalizeScope($scope);
        $currencyQuery = DB::table('ticket_orders as o')
            ->join('draws as d', 'd.id', '=', 'o.draw_id')
            ->whereDate('d.business_date', '>=', $dateFrom)
            ->whereDate('d.business_date', '<=', $dateTo);
        AdminDataScope::applyToTicketOrdersViaPlayer($currencyQuery, $context?->admin, 'o');
        $currencyCode = (string) ($currencyQuery->orderByDesc('o.id')->value('o.currency_code') ?? '');

        return $currencyCode !== '' ? $currencyCode : null;
    }

    public function dailyProfitPaginated(string $dateFrom, string $dateTo, int $page, int $perPage, AdminUser|AdminScopeContext|null $scope = null): LengthAwarePaginator
    {
        $rows = $this->dailyProfitRows($dateFrom, $dateTo, $scope);
        $total = count($rows);
        $offset = max(0, ($page - 1) * $perPage);
        $items = array_slice($rows, $offset, $perPage);

        return new PaginatorInstance($items, $total, $perPage, $page, [
            'path' => PaginatorInstance::resolveCurrentPath(),
        ]);
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function dailyProfitRows(string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null): array
    {
        $context = $this->normalizeScope($scope);
        $betSub = DB::table('ticket_orders as o')
            ->selectRaw('o.draw_id, SUM(o.total_actual_deduct) as total_bet_minor')
            ->groupBy('o.draw_id');
        AdminDataScope::applyToTicketOrdersViaPlayer($betSub, $context?->admin, 'o');

        $payoutSub = DB::table('ticket_items as ti')
            ->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
            ->selectRaw('ti.draw_id, SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
            ->groupBy('ti.draw_id');
        AdminDataScope::applyToTicketOrdersViaPlayer($payoutSub, $context?->admin, 'o');

        return DB::table('draws as d')
            ->whereDate('d.business_date', '>=', $dateFrom)
            ->whereDate('d.business_date', '<=', $dateTo)
            ->leftJoinSub($betSub, 'b', 'b.draw_id', '=', 'd.id')
            ->leftJoinSub($payoutSub, 'p', 'p.draw_id', '=', 'd.id')
            ->groupBy('d.business_date')
            ->havingRaw('COALESCE(SUM(b.total_bet_minor), 0) > 0 OR COALESCE(SUM(p.total_payout_minor), 0) > 0')
            ->orderBy('d.business_date')
            ->get([
                'd.business_date',
                DB::raw('COALESCE(SUM(b.total_bet_minor), 0) as total_bet_minor'),
                DB::raw('COALESCE(SUM(p.total_payout_minor), 0) as total_payout_minor'),
                DB::raw('COALESCE(SUM(b.total_bet_minor), 0) - COALESCE(SUM(p.total_payout_minor), 0) as approx_house_gross_minor'),
            ])
            ->map(static function (object $row): array {
                $businessDate = $row->business_date instanceof Carbon
                    ? $row->business_date->format('Y-m-d')
                    : substr((string) $row->business_date, 0, 10);

                return [
                    'business_date' => $businessDate,
                    'total_bet_minor' => (int) $row->total_bet_minor,
                    'total_payout_minor' => (int) $row->total_payout_minor,
                    'approx_house_gross_minor' => (int) $row->approx_house_gross_minor,
                ];
            })
            ->values()
            ->all();
    }

    /**
     * 全平台历史累计投注/派彩/盈亏（与 daily-profit 同口径，不限业务日）。
     *
     * @return array{
     *     currency_code: ?string,
     *     total_bet_minor: int,
     *     total_payout_minor: int,
     *     approx_house_gross_minor: int,
     *     draw_count: int,
     *     business_day_count: int,
     *     date_from: ?string,
     *     date_to: ?string
     * }
     */
    public function platformLifetimeTotals(AdminUser|AdminScopeContext|null $scope = null): array
    {
        $context = $this->normalizeScope($scope);
        $betQuery = DB::table('ticket_orders as o');
        AdminDataScope::applyToTicketOrdersViaPlayer($betQuery, $context?->admin, 'o');
        $totalBetMinor = (int) $betQuery->sum('o.total_actual_deduct');

        $payoutQuery = DB::table('ticket_items as ti')
            ->join('ticket_orders as o', 'o.id', '=', 'ti.order_id');
        AdminDataScope::applyToTicketOrdersViaPlayer($payoutQuery, $context?->admin, 'o');
        $payoutAgg = $payoutQuery
            ->selectRaw('COALESCE(SUM(ti.win_amount), 0) as win_minor, COALESCE(SUM(ti.jackpot_win_amount), 0) as jackpot_minor')
            ->first();
        $totalWinMinor = (int) ($payoutAgg->win_minor ?? 0);
        $totalJackpotMinor = (int) ($payoutAgg->jackpot_minor ?? 0);
        $totalPayoutMinor = $totalWinMinor + $totalJackpotMinor;

        $activityQuery = DB::table('draws as d')
            ->join('ticket_orders as o', 'o.draw_id', '=', 'd.id');
        AdminDataScope::applyToTicketOrdersViaPlayer($activityQuery, $context?->admin, 'o');
        $activity = $activityQuery
            ->selectRaw('COUNT(DISTINCT d.id) as draw_count')
            ->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count')
            ->selectRaw('MIN(d.business_date) as date_from')
            ->selectRaw('MAX(d.business_date) as date_to')
            ->first();

        $drawCount = (int) ($activity->draw_count ?? 0);
        $businessDayCount = (int) ($activity->business_day_count ?? 0);

        $dateFrom = $this->formatBusinessDateValue($activity?->date_from);
        $dateTo = $this->formatBusinessDateValue($activity?->date_to);

        $currencyQuery = DB::table('ticket_orders as o');
        AdminDataScope::applyToTicketOrdersViaPlayer($currencyQuery, $context?->admin, 'o');
        $currencyCode = (string) ($currencyQuery->orderByDesc('o.id')->value('o.currency_code') ?? '');

        $orderCountQuery = DB::table('ticket_orders as o');
        AdminDataScope::applyToTicketOrdersViaPlayer($orderCountQuery, $context?->admin, 'o');
        $itemCountQuery = DB::table('ticket_items as ti')
            ->join('ticket_orders as o', 'o.id', '=', 'ti.order_id');
        AdminDataScope::applyToTicketOrdersViaPlayer($itemCountQuery, $context?->admin, 'o');

        return [
            'currency_code' => $currencyCode !== '' ? $currencyCode : null,
            'total_bet_minor' => $totalBetMinor,
            'total_win_minor' => $totalWinMinor,
            'total_jackpot_minor' => $totalJackpotMinor,
            'total_payout_minor' => $totalPayoutMinor,
            'approx_house_gross_minor' => $totalBetMinor - $totalPayoutMinor,
            'order_count' => (int) $orderCountQuery->count(),
            'ticket_item_count' => (int) $itemCountQuery->count('ti.id'),
            'draw_count' => $drawCount,
            'business_day_count' => $businessDayCount,
            'date_from' => $dateFrom,
            'date_to' => $dateTo,
        ];
    }

    public function playerWinLossPaginated(
        ?int $playerId,
        string $dateFrom,
        string $dateTo,
        int $page,
        int $perPage,
        AdminUser|AdminScopeContext|null $scope = null,
    ): LengthAwarePaginator {
        $query = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo, $scope);

        return $query->paginate($perPage, ['*'], 'page', $page);
    }

    public function playDimensionPaginated(
        ?string $playCode,
        string $dateFrom,
        string $dateTo,
        int $page,
        int $perPage,
        AdminUser|AdminScopeContext|null $scope = null,
    ): LengthAwarePaginator {
        $query = $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo, $scope);

        return $query->paginate($perPage, ['*'], 'page', $page);
    }

    /**
     * @return list<array<int, string|int|float|null>>
     */
    public function reportRows(string $reportType, ?array $filterJson, AdminUser|AdminScopeContext|null $scope = null): array
    {
        $range = $this->resolveDateRange($filterJson);
        $dateFrom = $range['date_from'];
        $dateTo = $range['date_to'];

        return match ($reportType) {
            'draw_profit_summary' => $this->drawProfitExportRows($filterJson, $scope),
            'daily_profit_summary' => $this->dailyProfitExportRows($dateFrom, $dateTo, $scope),
            'player_win_loss' => $this->playerWinLossExportRows($filterJson, $dateFrom, $dateTo, $scope),
            'play_dimension_report' => $this->playDimensionExportRows($filterJson, $dateFrom, $dateTo, $scope),
            'audit_operation_report' => $this->auditExportRows($filterJson, $dateFrom, $dateTo),
            'wallet_transfer_report', 'transfer_orders_daily' => $this->transferOrdersExportRows($filterJson, $dateFrom, $dateTo, $scope),
            'wallet_txns_daily' => $this->walletTxnsExportRows($filterJson, $dateFrom, $dateTo, $scope),
            'hot_number_risk_report' => $this->hotNumberRiskExportRows($filterJson),
            'sold_out_number_report' => $this->soldOutNumberExportRows($filterJson),
            default => [
                ['报表类型', '开始日期', '结束日期'],
                [$this->reportLabel($reportType), $dateFrom, $dateTo],
            ],
        };
    }

    public function resolveOutputPathSuffix(string $reportType, ?array $filterJson, string $dateFrom, string $dateTo): string
    {
        if (in_array($reportType, ['draw_profit_summary', 'hot_number_risk_report', 'sold_out_number_report'], true)) {
            $draw = $this->resolveDrawForReport($filterJson);
            if ($draw !== null) {
                $suffix = (string) $draw->draw_no;
                $number = $this->normalizedNumberFromFilters($filterJson);
                if ($reportType === 'hot_number_risk_report' && $number !== null) {
                    return $suffix.'_'.$number;
                }

                return $suffix;
            }
        }

        return $dateFrom.'_'.$dateTo;
    }

    public function reportLabel(string $reportType): string
    {
        return match ($reportType) {
            'draw_profit_summary' => '期号盈亏',
            'daily_profit_summary' => '每日盈亏汇总',
            'player_win_loss' => '玩家输赢报表',
            'wallet_transfer_report', 'wallet_txns_daily', 'transfer_orders_daily' => '玩家转入转出报表',
            'hot_number_risk_report' => '热门号码风险报表',
            'play_dimension_report' => '玩法维度报表',
            'sold_out_number_report' => '售罄号码报表',
            'audit_operation_report' => '后台操作审计报表',
            default => $reportType,
        };
    }

    /**
     * @return list<array<int, string|int|float|null>>
     */
    private function dailyProfitExportRows(string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null): array
    {
        $rows = [
            ['日期', '下注', '派彩', '盈亏'],
        ];
        foreach ($this->dailyProfitRows($dateFrom, $dateTo, $scope) as $row) {
            $rows[] = [
                $row['business_date'],
                $row['total_bet_minor'],
                $row['total_payout_minor'],
                $row['approx_house_gross_minor'],
            ];
        }

        return $rows;
    }

    /**
     * @return list<array<int, string|int|float|null>>
     */
    private function playerWinLossExportRows(?array $filterJson, string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null): array
    {
        $playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
        $rows = [
            ['玩家ID', '用户名', '下注', '派彩', '净输赢'],
        ];
        $items = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo, $scope)->get();
        foreach ($items as $row) {
            $rows[] = [
                (int) $row->player_id,
                (string) $row->username,
                (int) $row->total_bet_minor,
                (int) $row->total_payout_minor,
                (int) $row->net_win_loss_minor,
            ];
        }

        return $rows;
    }

    /**
     * @return list<array<int, string|int|float|null>>
     */
    private function playDimensionExportRows(?array $filterJson, string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null): array
    {
        $playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null;
        $rows = [
            ['玩法', '维度', '下注', '派彩', '盈亏'],
        ];
        $items = $this->playDimensionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo, $scope)->get();
        foreach ($items as $row) {
            $rows[] = [
                (string) $row->play_code,
                (int) $row->dimension,
                (int) $row->total_bet_minor,
                (int) $row->total_payout_minor,
                (int) $row->approx_house_gross_minor,
            ];
        }

        return $rows;
    }

    /**
     * @return list<array<int, string|int|float|null>>
     */
    private function auditExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
    {
        $operatorId = isset($filterJson['operator_id']) ? (int) $filterJson['operator_id'] : null;
        $rows = [
            ['ID', '操作者类型', '操作者ID', '模块', '操作', 'IP', '时间'],
        ];

        $q = AuditLog::query()->orderByDesc('id');
        if ($operatorId !== null && $operatorId > 0) {
            $q->where('operator_id', $operatorId);
        }
        $q->whereDate('created_at', '>=', $dateFrom)
            ->whereDate('created_at', '<=', $dateTo);

        $limited = \App\Support\LimitedQuery::get($q, 5000);
        foreach ($limited['rows'] as $log) {
            $rows[] = [
                (int) $log->id,
                (string) $log->operator_type,
                (int) $log->operator_id,
                (string) $log->module_code,
                (string) $log->action_code,
                (string) ($log->ip ?? ''),
                $log->created_at?->toIso8601String() ?? '',
            ];
        }

        if ($limited['truncated']) {
            $rows[] = ['警告', '审计日志已截断至 5000 条，请缩小日期范围后重试'];
        }

        return $rows;
    }

    /** @return \Illuminate\Database\Query\Builder */
    private function playerWinLossBaseQuery(
        ?int $playerId,
        string $dateFrom,
        string $dateTo,
        AdminUser|AdminScopeContext|null $scope = null,
    ) {
        $context = $this->normalizeScope($scope);
        $query = DB::table('ticket_items as ti')
            ->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
            ->join('draws as d', 'd.id', '=', 'o.draw_id')
            ->leftJoin('players as p', 'p.id', '=', 'ti.player_id')
            ->leftJoin('agent_nodes as an', 'an.id', '=', 'p.agent_node_id')
            ->selectRaw('ti.player_id')
            ->selectRaw('p.username as username')
            ->selectRaw('p.agent_node_id as agent_node_id')
            ->selectRaw('an.code as agent_code')
            ->selectRaw('an.name as agent_name')
            ->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor')
            ->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
            ->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as net_win_loss_minor')
            ->whereDate('d.business_date', '>=', $dateFrom)
            ->whereDate('d.business_date', '<=', $dateTo)
            ->groupBy('ti.player_id', 'p.username', 'p.agent_node_id', 'an.code', 'an.name')
            ->orderByDesc('net_win_loss_minor');

        if ($playerId !== null && $playerId > 0) {
            $query->where('ti.player_id', $playerId);
        }

        AdminDataScope::applyToPlayersAlias(
            $query,
            $context?->admin,
            'p',
            $context?->effectiveRequestedAgentNodeId(),
        );

        return $query;
    }

    /** @return \Illuminate\Database\Query\Builder */
    private function playDimensionBaseQuery(?string $playCode, string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null)
    {
        $context = $this->normalizeScope($scope);
        $query = DB::table('ticket_items as ti')
            ->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
            ->join('draws as d', 'd.id', '=', 'o.draw_id')
            ->selectRaw('ti.play_code')
            ->selectRaw('ti.dimension')
            ->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor')
            ->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
            ->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as approx_house_gross_minor')
            ->whereDate('d.business_date', '>=', $dateFrom)
            ->whereDate('d.business_date', '<=', $dateTo)
            ->groupBy('ti.play_code', 'ti.dimension')
            ->orderBy('ti.play_code')
            ->orderBy('ti.dimension');

        if ($playCode !== null && $playCode !== '') {
            $query->where('ti.play_code', $playCode);
        }

        AdminDataScope::applyToTicketOrdersViaPlayer($query, $context?->admin, 'o');

        return $query;
    }

    /**
     * @return list<array<int, string|int|float|null>>
     */
    private function drawProfitExportRows(?array $filterJson, AdminUser|AdminScopeContext|null $scope = null): array
    {
        $context = $this->normalizeScope($scope);
        $draw = $this->resolveDrawForReport($filterJson);
        if ($draw === null) {
            return [['提示', '请提供 draw_id 或 draw_no']];
        }

        $drawId = (int) $draw->id;
        $orderQuery = TicketOrder::query()->where('draw_id', $drawId);
        $itemQuery = TicketItem::query()->where('draw_id', $drawId);
        if ($context !== null) {
            AdminDataScope::applyEloquentViaPlayer($orderQuery, $context->admin);
            AdminDataScope::applyEloquentViaPlayer($itemQuery, $context->admin);
        }

        $totalBetMinor = (int) $orderQuery->sum('total_actual_deduct');
        $orderCount = (int) $orderQuery->count();
        $itemCount = (int) $itemQuery->count();
        $currencyCode = (string) ((clone $orderQuery)->value('currency_code') ?? '');
        $totalWinMinor = (int) $itemQuery->sum('win_amount');
        $totalJackpotWinMinor = (int) (clone $itemQuery)->sum('jackpot_win_amount');
        $totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor;
        $approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor;

        $rows = [
            [
                '行类型', '期号', '状态', '币种', '订单数', '注单数', '下注', '派彩', '平台盈亏',
                '结算批次ID', '结算状态', '批次数', '中奖数', '批次派彩', '批次Jackpot', '完成时间',
            ],
            [
                'summary',
                $draw->draw_no,
                $draw->status,
                $currencyCode !== '' ? $currencyCode : null,
                $orderCount,
                $itemCount,
                $totalBetMinor,
                $totalPayoutMinor,
                $approxHouseGrossMinor,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
            ],
        ];

        $batches = SettlementBatch::query()
            ->where('draw_id', $drawId)
            ->orderByDesc('id')
            ->limit(100)
            ->get();

        foreach ($batches as $batch) {
            $rows[] = [
                'settlement_batch',
                $draw->draw_no,
                $draw->status,
                $currencyCode !== '' ? $currencyCode : null,
                null,
                null,
                null,
                null,
                null,
                (int) $batch->id,
                $batch->status,
                (int) $batch->total_ticket_count,
                (int) $batch->total_win_count,
                (int) $batch->total_payout_amount,
                (int) $batch->total_jackpot_payout_amount,
                $batch->finished_at?->toIso8601String(),
            ];
        }

        return $rows;
    }

    /**
     * @return list<array<int, string|int|float|null>>
     */
    private function soldOutNumberExportRows(?array $filterJson): array
    {
        $draw = $this->resolveDrawForReport($filterJson);
        if ($draw === null) {
            return [['提示', '请提供 draw_id 或 draw_no']];
        }

        $rows = [
            ['期号', '号码', '封顶', '已占用', '剩余', '售罄', '使用率%'],
        ];

        $pools = RiskPool::query()
            ->where('draw_id', $draw->id)
            ->where('sold_out_status', 1)
            ->orderBy('normalized_number')
            ->limit(10000)
            ->get();

        foreach ($pools as $pool) {
            $cap = (int) $pool->total_cap_amount;
            $locked = (int) $pool->locked_amount;
            $rows[] = [
                $draw->draw_no,
                $pool->normalized_number,
                $cap,
                $locked,
                (int) $pool->remaining_amount,
                '是',
                $this->riskUsageRatioPercent($cap, $locked),
            ];
        }

        return $rows;
    }

    /**
     * @return list<array<int, string|int|float|null>>
     */
    private function hotNumberRiskExportRows(?array $filterJson): array
    {
        $draw = $this->resolveDrawForReport($filterJson);
        if ($draw === null) {
            return [['提示', '请提供 draw_id 或 draw_no']];
        }

        $number = $this->normalizedNumberFromFilters($filterJson);
        $rows = [
            [
                '行类型', '期号', '号码', '封顶', '已占用', '剩余', '售罄', '使用率%',
                '日志ID', '动作', '金额', '玩法', '注单号', '玩家ID', '原因', '时间',
            ],
        ];

        if ($number !== null) {
            $pool = RiskPool::query()
                ->where('draw_id', $draw->id)
                ->where('normalized_number', $number)
                ->first();

            if ($pool === null) {
                $rows[] = ['pool', $draw->draw_no, $number, null, null, null, null, null, null, null, null, null, null, null, null, null];

                return $rows;
            }

            $cap = (int) $pool->total_cap_amount;
            $locked = (int) $pool->locked_amount;
            $rows[] = [
                'pool',
                $draw->draw_no,
                $pool->normalized_number,
                $cap,
                $locked,
                (int) $pool->remaining_amount,
                (int) $pool->sold_out_status === 1 ? '是' : '否',
                $this->riskUsageRatioPercent($cap, $locked),
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
            ];

            $logs = RiskPoolLockLog::query()
                ->where('draw_id', $draw->id)
                ->where('normalized_number', $number)
                ->with(['ticketItem:id,ticket_no,play_code,player_id'])
                ->orderByDesc('id')
                ->limit(5000)
                ->get();

            foreach ($logs as $log) {
                $rows[] = [
                    'lock_log',
                    $draw->draw_no,
                    $number,
                    null,
                    null,
                    null,
                    null,
                    null,
                    (int) $log->id,
                    $log->action_type,
                    (int) $log->amount,
                    $log->ticketItem?->play_code,
                    $log->ticketItem?->ticket_no,
                    $log->ticketItem?->player_id,
                    $log->source_reason,
                    $log->created_at?->toIso8601String(),
                ];
            }

            return $rows;
        }

        $pools = RiskPool::query()
            ->where('draw_id', $draw->id)
            ->orderByDesc('locked_amount')
            ->limit(10000)
            ->get();

        foreach ($pools as $pool) {
            $cap = (int) $pool->total_cap_amount;
            $locked = (int) $pool->locked_amount;
            $rows[] = [
                'pool',
                $draw->draw_no,
                $pool->normalized_number,
                $cap,
                $locked,
                (int) $pool->remaining_amount,
                (int) $pool->sold_out_status === 1 ? '是' : '否',
                $this->riskUsageRatioPercent($cap, $locked),
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
            ];
        }

        return $rows;
    }

    /**
     * @return list<array<int, string|int|float|null>>
     */
    private function transferOrdersExportRows(?array $filterJson, string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null): array
    {
        $context = $this->normalizeScope($scope);
        $rows = [
            ['转账单号', '玩家ID', '用户名', '昵称', '方向', '币种', '金额', '状态', '外部单号', '失败原因', '创建时间', '完成时间'],
        ];

        $query = TransferOrder::query()
            ->with(['player:id,username,nickname'])
            ->orderByDesc('id');

        if ($context !== null) {
            AdminDataScope::applyEloquentViaPlayer($query, $context->admin);
        }

        $playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
        if ($playerId !== null && $playerId > 0) {
            $query->where('player_id', $playerId);
        }

        $query->where('created_at', '>=', $dateFrom.' 00:00:00')
            ->where('created_at', '<=', $dateTo.' 23:59:59');

        foreach ($query->limit(10000)->get() as $order) {
            $player = $order->player;
            $rows[] = [
                $order->transfer_no,
                (int) $order->player_id,
                $player?->username,
                $player?->nickname,
                $order->direction,
                $order->currency_code,
                (int) $order->amount,
                $order->status,
                $order->external_ref_no,
                $order->fail_reason,
                $order->created_at?->toIso8601String(),
                $order->finished_at?->toIso8601String(),
            ];
        }

        return $rows;
    }

    /**
     * @return list<array<int, string|int|float|null>>
     */
    private function walletTxnsExportRows(?array $filterJson, string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null): array
    {
        $context = $this->normalizeScope($scope);
        $rows = [
            ['流水号', '玩家ID', '用户名', '业务类型', '业务单号', '方向', '金额', '变动前余额', '变动后余额', '状态', '外部单号', '备注', '创建时间'],
        ];

        $query = WalletTxn::query()
            ->with(['player:id,username'])
            ->orderByDesc('id');

        if ($context !== null) {
            AdminDataScope::applyEloquentViaPlayer($query, $context->admin);
        }

        $playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
        if ($playerId !== null && $playerId > 0) {
            $query->where('player_id', $playerId);
        }

        $query->where('created_at', '>=', $dateFrom.' 00:00:00')
            ->where('created_at', '<=', $dateTo.' 23:59:59');

        foreach ($query->limit(10000)->get() as $txn) {
            $rows[] = [
                $txn->txn_no,
                (int) $txn->player_id,
                $txn->player?->username,
                $txn->biz_type,
                $txn->biz_no,
                (int) $txn->direction,
                (int) $txn->amount,
                (int) $txn->balance_before,
                (int) $txn->balance_after,
                $txn->status,
                $txn->external_ref_no,
                $txn->remark,
                $txn->created_at?->toIso8601String(),
            ];
        }

        return $rows;
    }

    private function resolveDrawForReport(?array $filterJson): ?Draw
    {
        if (! is_array($filterJson)) {
            return null;
        }

        if (! empty($filterJson['draw_id'])) {
            return Draw::query()->find((int) $filterJson['draw_id']);
        }

        $drawNo = trim((string) ($filterJson['draw_no'] ?? ''));
        if ($drawNo !== '') {
            return Draw::query()->where('draw_no', $drawNo)->first();
        }

        return null;
    }

    private function normalizedNumberFromFilters(?array $filterJson): ?string
    {
        if (! is_array($filterJson)) {
            return null;
        }

        $number = trim((string) ($filterJson['normalized_number'] ?? ''));
        if (preg_match('/^[0-9]{4}$/', $number) === 1) {
            return $number;
        }

        return null;
    }

    private function riskUsageRatioPercent(int $cap, int $locked): ?string
    {
        if ($cap <= 0) {
            return null;
        }

        return (string) round($locked / $cap * 100, 2);
    }

    private function formatBusinessDateValue(mixed $value): ?string
    {
        if ($value === null) {
            return null;
        }

        if ($value instanceof Carbon) {
            return $value->format('Y-m-d');
        }

        $raw = trim((string) $value);
        if ($raw === '') {
            return null;
        }

        if (preg_match('/^\d{4}-\d{2}-\d{2}/', $raw, $m) === 1) {
            return substr($m[0], 0, 10);
        }

        return $raw;
    }
}
