<?php

namespace App\Services\Wallet;

use Carbon\Carbon;
use App\Models\AdminUser;
use App\Models\Player;
use App\Models\WalletTxn;
use Illuminate\Support\Str;
use App\Support\AdminDataScope;
use App\Support\CurrencyFormatter;
use App\Support\LimitedQuery;
use App\Support\PlayerFundingMode;
use App\Services\AgentSettlement\CreditLedgerBetFlowPresenter;
use App\Services\AgentSettlement\SettlementPartyEnrichment;
use App\Services\Player\PlayerCreditService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;

/**
 * 玩家流水：钱包玩家读 {@see wallet_txns}，信用盘玩家读 {@see credit_ledger}。
 */
final class PlayerLedgerLogsService
{
    /** PRD 对外类型 → wallet_txns.biz_type */
    private const WALLET_TYPE_TO_BIZ = [
        'transfer_in' => ['transfer_in'],
        'transfer_out' => ['transfer_out'],
        'refund' => ['transfer_out_refund'],
        'reversal' => ['reversal', 'bet_reverse'],
        'bet' => ['bet_deduct', 'bet'],
        'prize' => ['settle_payout', 'prize', 'jackpot_manual_payout'],
    ];

    /** PRD 对外类型 → credit_ledger.reason（信用盘不用钱包「派彩」口径） */
    private const CREDIT_TYPE_TO_REASON = [
        'bet' => ['bet_hold', 'game_settlement_loss'],
        'reversal' => ['bet_hold_release', 'game_settlement_reversal'],
        'refund' => ['settlement_confirm'],
        'win_credit' => ['game_settlement_win'],
        'credit_release' => ['game_settlement_win', 'settlement_confirm', 'bet_hold_release'],
        'bill_settlement' => ['settlement_payout'],
        'transfer_in' => [],
        'transfer_out' => [],
    ];

    public function __construct(
        private readonly PlayerCreditService $playerCreditService,
        private readonly CreditLedgerBetFlowPresenter $betFlowPresenter,
        private readonly SettlementPartyEnrichment $partyEnrichment,
    ) {}

    /**
     * @return array{
     *   items: list<array<string, mixed>>,
     *   total: int,
     *   page: int,
     *   per_page: int,
     *   ledger_source: string,
     *   funding_mode: string,
     *   auth_source: string|null,
     * }
     */
    public function listForPlayerApi(
        Player $player,
        int $page,
        int $perPage,
        string $currencyCode,
        string $typeFilterRaw,
    ): array {
        $meta = [
            'ledger_source' => PlayerFundingMode::usesCredit($player) ? 'credit_ledger' : 'wallet_txn',
            'funding_mode' => (string) ($player->funding_mode ?? ''),
            'auth_source' => $player->auth_source,
        ];

        if (PlayerFundingMode::usesCredit($player)) {
            $result = $this->paginateCreditLedger($player, $page, $perPage, $typeFilterRaw);

            return array_merge($result, $meta);
        }

        $result = $this->paginateWalletTxns($player, $page, $perPage, $currencyCode, $typeFilterRaw);

        return array_merge($result, $meta);
    }

    /**
     * 后台玩家详情「钱包流水」：信用盘玩家返回 credit_ledger，字段形状对齐 wallet_txns 列表。
     *
     * @return array{items: list<array<string, mixed>>, total: int, page: int, per_page: int}
     */
    public function listForAdminPlayer(
        Player $player,
        int $page,
        int $perPage,
        ?string $bizType = null,
    ): array {
        if (! PlayerFundingMode::usesCredit($player)) {
            return ['items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage];
        }

        $currency = (string) $player->default_currency;
        $limited = LimitedQuery::get($this->creditLedgerQuery($player->id, [
            'bet_hold',
            'bet_hold_release',
            'game_settlement_loss',
            'game_settlement_win',
            'game_settlement_reversal',
            'settlement_confirm',
            'settlement_payout',
        ]), 5000);
        $rawRows = $limited['rows']->all();

        $enriched = array_map(function (object $row) use ($player): object {
            return (object) [
                'id' => (int) $row->id,
                'amount' => (int) $row->amount,
                'reason' => (string) $row->reason,
                'ref_type' => $row->ref_type ?? null,
                'ref_id' => $row->ref_id ?? null,
                'created_at' => $row->created_at ?? null,
                'updated_at' => $row->updated_at ?? null,
                'player_id' => (int) $player->id,
                'site_code' => $player->site_code,
                'site_player_id' => $player->site_player_id,
                'username' => $player->username,
                'nickname' => $player->nickname,
                'agent_node_id' => $player->agent_node_id,
                'funding_mode' => $player->funding_mode,
                'auth_source' => $player->auth_source,
                'default_currency' => $player->default_currency,
                'direct_agent_id' => null,
                'direct_agent_code' => null,
                'direct_agent_name' => null,
                'parent_agent_id' => null,
                'parent_agent_code' => null,
                'parent_agent_name' => null,
            ];
        }, $rawRows);

        $ticketIds = [];
        foreach ($enriched as $row) {
            if ((string) ($row->ref_type ?? '') === 'ticket_item') {
                $ticketId = (int) ($row->ref_id ?? 0);
                if ($ticketId > 0) {
                    $ticketIds[] = $ticketId;
                }
            }
        }

        $ticketRefs = $this->partyEnrichment->loadTicketRefs(array_values(array_unique($ticketIds)));

        $simplified = $this->betFlowPresenter->simplifyCreditRows(
            $enriched,
            $ticketRefs,
            fn (object $row): array => $this->formatAdminCreditRow($row, $player, $currency, 0),
            function (object $row) use ($player, $currency): array {
                $formatted = $this->formatAdminCreditRow($row, $player, $currency, 0);
                $ticketId = (int) ($row->ref_id ?? 0);
                if ($ticketId > 0) {
                    $formatted['txn_no'] = 'CLS-T'.$ticketId;
                }

                return $formatted;
            },
        );

        if ($bizType !== null && $bizType !== '') {
            $filterType = trim($bizType);
            $simplified = array_values(array_filter(
                $simplified,
                static fn (array $item): bool => ($item['biz_type'] ?? '') === $filterType
                    || ($filterType === 'bet_hold' && ($item['biz_type'] ?? '') === CreditLedgerBetFlowPresenter::DISPLAY_BET_HOLD)
                    || ($filterType === 'game_settlement' && ($item['biz_type'] ?? '') === CreditLedgerBetFlowPresenter::DISPLAY_GAME_SETTLEMENT),
            ));
        }

        $total = count($simplified);
        $offset = max(0, ($page - 1) * $perPage);
        $pageRows = array_slice($simplified, $offset, $perPage);

        $runningMinor = $this->playerCreditService->availableCreditMinor($player, $currency);
        foreach (array_slice($simplified, 0, $offset) as $priorRow) {
            if (! $this->adminCreditRowAffectsAvailableBalance($priorRow)) {
                continue;
            }
            $runningMinor -= $this->adminCreditRowSignedDelta($priorRow);
        }
        $items = [];
        foreach ($pageRows as $formatted) {
            if (! $this->adminCreditRowAffectsAvailableBalance($formatted)) {
                $items[] = array_merge($formatted, [
                    'balance_after' => null,
                    'balance_after_formatted' => null,
                    'balance_before' => null,
                    'balance_before_formatted' => null,
                ]);

                continue;
            }

            $signed = $this->adminCreditRowSignedDelta($formatted);
            $items[] = array_merge($formatted, [
                'balance_after' => $runningMinor,
                'balance_after_formatted' => CurrencyFormatter::fromMinor($runningMinor),
                'balance_before' => $signed >= 0
                    ? max(0, $runningMinor - $signed)
                    : $runningMinor + abs($signed),
            ]);
            $runningMinor -= $signed;
        }

        return [
            'items' => $items,
            'total' => $total,
            'page' => $page,
            'per_page' => $perPage,
            'truncated' => $limited['truncated'],
        ];
    }

    /**
     * 结算中心：站点下全部信用盘玩家的 {@see credit_ledger} 流水。
     *
     * @return array{
     *   items: list<array<string, mixed>>,
     *   total: int,
     *   page: int,
     *   per_page: int,
     *   ledger_source: string,
     * }
     */
    public function listForAdminCreditIndex(
        AdminUser $admin,
        string $siteCode,
        int $page,
        int $perPage,
        ?int $settlementPeriodId = null,
        ?int $playerId = null,
        ?string $reason = null,
        ?string $createdFrom = null,
        ?string $createdTo = null,
    ): array {
        $query = DB::table('credit_ledger as cl')
            ->join('players as p', function ($join): void {
                $join->on('p.id', '=', 'cl.owner_id')
                    ->where('cl.owner_type', '=', 'player');
            })
            ->where('p.site_code', $siteCode)
            ->where('p.funding_mode', PlayerFundingMode::CREDIT)
            ->select([
                'cl.id',
                'cl.amount',
                'cl.reason',
                'cl.ref_type',
                'cl.ref_id',
                'cl.created_at',
                'cl.updated_at',
                'p.id as player_id',
                'p.site_code',
                'p.site_player_id',
                'p.username',
                'p.nickname',
                'p.funding_mode',
                'p.auth_source',
                'p.default_currency',
            ])
            ->orderByDesc('cl.id');

        AdminDataScope::applyToPlayersAlias($query, $admin, 'p');

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

        if ($reason !== null && $reason !== '') {
            $query->where('cl.reason', $reason);
        }

        $range = $this->resolveCreatedRange($settlementPeriodId, $createdFrom, $createdTo);
        if ($range !== null) {
            $query->whereBetween('cl.created_at', $range);
        }

        /** @var LengthAwarePaginator $paginator */
        $paginator = $query->paginate($perPage, ['*'], 'page', $page);
        $items = $paginator->getCollection()
            ->map(fn (object $row): array => $this->formatAdminCreditIndexRow($row))
            ->values()
            ->all();

        return [
            'items' => $items,
            'total' => $paginator->total(),
            'page' => $paginator->currentPage(),
            'per_page' => $paginator->perPage(),
            'ledger_source' => 'credit_ledger',
        ];
    }

    /**
     * @return array{0: Carbon, 1: Carbon}|null
     */
    private function resolveCreatedRange(
        ?int $settlementPeriodId,
        ?string $createdFrom,
        ?string $createdTo,
    ): ?array {
        if ($settlementPeriodId !== null && $settlementPeriodId > 0) {
            $period = DB::table('settlement_periods')->where('id', $settlementPeriodId)->first();
            if ($period === null) {
                return null;
            }

            return [
                Carbon::parse($period->period_start)->startOfDay(),
                Carbon::parse($period->period_end)->endOfDay(),
            ];
        }

        $from = $createdFrom !== null && $createdFrom !== ''
            ? Carbon::parse($createdFrom)->startOfDay()
            : null;
        $to = $createdTo !== null && $createdTo !== ''
            ? Carbon::parse($createdTo)->endOfDay()
            : null;

        if ($from === null && $to === null) {
            return null;
        }

        return [
            $from ?? Carbon::parse('1970-01-01')->startOfDay(),
            $to ?? Carbon::now()->endOfDay(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function formatAdminCreditIndexRow(object $row): array
    {
        $amount = (int) $row->amount;
        $amountAbs = abs($amount);
        $currency = (string) ($row->default_currency ?? '');

        return [
            'id' => (int) $row->id,
            'txn_no' => 'CL-'.$row->id,
            'player_id' => (int) $row->player_id,
            'site_code' => (string) $row->site_code,
            'site_player_id' => $row->site_player_id,
            'username' => $row->username,
            'nickname' => $row->nickname,
            'biz_type' => (string) $row->reason,
            'type' => $this->creditReasonToPublicType((string) $row->reason),
            'biz_no' => $this->creditRefLabel($row),
            'direction' => $amount >= 0 ? 1 : 2,
            'amount' => $amountAbs,
            'amount_formatted' => CurrencyFormatter::fromMinor($amountAbs),
            'signed_amount' => $amount,
            'currency_code' => $currency,
            'status' => 'posted',
            'created_at' => $this->isoTimestamp($row->created_at ?? null),
            'updated_at' => $this->isoTimestamp($row->updated_at ?? null),
            'ledger_source' => 'credit_ledger',
            'funding_mode' => (string) ($row->funding_mode ?? PlayerFundingMode::CREDIT),
            'auth_source' => $row->auth_source,
        ];
    }

    /**
     * @return array{items: list<array<string, mixed>>, total: int, page: int, per_page: int}
     */
    private function paginateWalletTxns(
        Player $player,
        int $page,
        int $perPage,
        string $currencyCode,
        string $typeFilterRaw,
    ): array {
        $bizFilter = $this->resolveWalletBizFilter($typeFilterRaw);

        if (is_array($bizFilter) && $bizFilter === []) {
            return ['items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage];
        }

        $query = WalletTxn::query()
            ->where('player_id', $player->id)
            ->with('wallet')
            ->orderByDesc('id');

        if ($currencyCode !== '') {
            $query->whereHas('wallet', fn ($q) => $q->where('currency_code', $currencyCode));
        }

        if ($bizFilter !== null) {
            $query->whereIn('biz_type', $bizFilter);
        }

        $paginator = $query->paginate($perPage, ['*'], 'page', $page);
        $items = $paginator->getCollection()
            ->map(fn (WalletTxn $txn) => $this->formatWalletTxnRow($txn))
            ->values()
            ->all();

        return [
            'items' => $items,
            'total' => $paginator->total(),
            'page' => $paginator->currentPage(),
            'per_page' => $paginator->perPage(),
        ];
    }

    /**
     * @return array{items: list<array<string, mixed>>, total: int, page: int, per_page: int}
     */
    private function paginateCreditLedger(
        Player $player,
        int $page,
        int $perPage,
        string $typeFilterRaw,
    ): array {
        $reasonFilter = $this->resolveCreditReasonFilter($typeFilterRaw);

        if (is_array($reasonFilter) && $reasonFilter === []) {
            return ['items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage];
        }

        $paginator = $this->creditLedgerQuery((int) $player->id, $reasonFilter)
            ->paginate($perPage, ['*'], 'page', $page);

        $currency = (string) $player->default_currency;
        $skipRows = max(0, ($paginator->currentPage() - 1) * $paginator->perPage());
        $runningMinor = $this->advanceCreditLedgerRunningMinor(
            (int) $player->id,
            $reasonFilter,
            $this->playerCreditService->availableCreditMinor($player, $currency),
            $skipRows,
        );
        $items = $paginator->getCollection()
            ->map(function (object $row) use (&$runningMinor, $player, $currency): array {
                $amount = (int) $row->amount;
                $reason = (string) $row->reason;
                $affectsBalance = $this->creditReasonAffectsAvailableBalance($reason);
                $formatted = $this->formatPlayerCreditRow(
                    $row,
                    $player,
                    $currency,
                    $affectsBalance ? $runningMinor : null,
                    $affectsBalance,
                );
                if ($affectsBalance) {
                    $runningMinor -= $amount;
                }

                return $formatted;
            })
            ->values()
            ->all();

        return [
            'items' => $items,
            'total' => $paginator->total(),
            'page' => $paginator->currentPage(),
            'per_page' => $paginator->perPage(),
        ];
    }

    /**
     * @param  list<string>|null  $reasonFilter
     */
    private function creditLedgerQuery(int $playerId, ?array $reasonFilter)
    {
        $query = DB::table('credit_ledger')
            ->where('owner_type', 'player')
            ->where('owner_id', $playerId)
            ->orderByDesc('id');

        if ($reasonFilter !== null) {
            $query->whereIn('reason', $reasonFilter);
        }

        return $query;
    }

    /**
     * @return list<string>|null
     */
    private function resolveWalletBizFilter(string $raw): ?array
    {
        return $this->resolveTypeFilterMap($raw, self::WALLET_TYPE_TO_BIZ);
    }

    /**
     * @return list<string>|null
     */
    private function resolveCreditReasonFilter(string $raw): ?array
    {
        return $this->resolveTypeFilterMap($raw, self::CREDIT_TYPE_TO_REASON);
    }

    /**
     * @param  array<string, list<string>>  $map
     * @return list<string>|null
     */
    private function resolveTypeFilterMap(string $raw, array $map): ?array
    {
        $raw = trim($raw);
        if ($raw === '') {
            return null;
        }

        $parts = array_filter(array_map('trim', explode(',', $raw)));
        if ($parts === []) {
            return null;
        }

        $resolved = [];
        foreach ($parts as $part) {
            $key = Str::lower($part);
            if (! isset($map[$key])) {
                continue;
            }
            foreach ($map[$key] as $value) {
                $resolved[] = $value;
            }
        }

        return array_values(array_unique($resolved));
    }

    /**
     * @return array<string, mixed>
     */
    private function formatWalletTxnRow(WalletTxn $txn): array
    {
        $currency = $txn->wallet?->currency_code ?? '';
        $amount = (int) $txn->amount;
        $balanceAfter = (int) $txn->balance_after;

        return [
            'log_id' => $txn->txn_no,
            'type' => $this->walletBizToPublicType((string) $txn->biz_type),
            'biz_type' => $txn->biz_type,
            'amount' => $this->signedWalletAmount($txn),
            'amount_formatted' => CurrencyFormatter::fromMinor($amount),
            'amount_abs' => $amount,
            'amount_abs_formatted' => CurrencyFormatter::fromMinor($amount),
            'direction' => (int) $txn->direction === 1 ? 'in' : 'out',
            'currency_code' => $currency,
            'balance_after' => $balanceAfter,
            'balance_after_formatted' => CurrencyFormatter::fromMinor($balanceAfter),
            'ref_id' => $txn->biz_no,
            'idempotent_key' => $txn->idempotent_key,
            'external_ref_no' => $txn->external_ref_no,
            'status' => $txn->status,
            'remark' => $txn->remark,
            'created_at' => $txn->created_at?->toIso8601String(),
            'ledger_source' => 'wallet_txn',
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function formatPlayerCreditRow(
        object $row,
        Player $player,
        string $currency,
        ?int $balanceAfterMinor,
        ?bool $affectsAvailableBalance = null,
    ): array {
        $amount = (int) $row->amount;
        $amountAbs = abs($amount);
        $reason = (string) $row->reason;
        $publicType = $this->creditReasonToPublicType($reason);
        $affectsBalance = $affectsAvailableBalance ?? $this->creditReasonAffectsAvailableBalance($reason);

        return [
            'log_id' => 'CL-'.$row->id,
            'type' => $publicType,
            'biz_type' => $reason,
            'amount' => $amount,
            'amount_formatted' => CurrencyFormatter::fromMinor($amountAbs),
            'amount_abs' => $amountAbs,
            'amount_abs_formatted' => CurrencyFormatter::fromMinor($amountAbs),
            'direction' => $amount >= 0 ? 'in' : 'out',
            'currency_code' => $currency,
            'balance_after' => $affectsBalance ? $balanceAfterMinor : null,
            'balance_after_formatted' => $affectsBalance && $balanceAfterMinor !== null
                ? CurrencyFormatter::fromMinor($balanceAfterMinor)
                : null,
            'affects_available_credit' => $affectsBalance,
            'ref_id' => $this->creditRefLabel($row),
            'idempotent_key' => null,
            'external_ref_no' => null,
            'status' => 'posted',
            'remark' => null,
            'created_at' => $this->isoTimestamp($row->created_at ?? null),
            'ledger_source' => 'credit_ledger',
            'funding_mode' => (string) ($player->funding_mode ?? PlayerFundingMode::CREDIT),
            'auth_source' => $player->auth_source,
        ];
    }

    /** 账期收付记账不改变 player_credit_accounts，不参与可用信用倒推。 */
    private function creditReasonAffectsAvailableBalance(string $reason): bool
    {
        return $reason !== 'settlement_payout';
    }

    /**
     * @param  list<string>|null  $reasonFilter
     */
    private function advanceCreditLedgerRunningMinor(
        int $playerId,
        ?array $reasonFilter,
        int $runningMinor,
        int $skipRows,
    ): int {
        if ($skipRows <= 0) {
            return $runningMinor;
        }

        $priorRows = $this->creditLedgerQuery($playerId, $reasonFilter)
            ->limit($skipRows)
            ->get();

        foreach ($priorRows as $row) {
            if (! $this->creditReasonAffectsAvailableBalance((string) $row->reason)) {
                continue;
            }
            $runningMinor -= (int) $row->amount;
        }

        return $runningMinor;
    }

    /**
     * @param  array<string, mixed>  $formatted
     */
    private function adminCreditRowAffectsAvailableBalance(array $formatted): bool
    {
        return ($formatted['biz_type'] ?? '') !== 'settlement_payout';
    }

    /**
     * @param  array<string, mixed>  $formatted
     */
    private function adminCreditRowSignedDelta(array $formatted): int
    {
        return (int) ($formatted['direction'] === 1 ? $formatted['amount'] : -$formatted['amount']);
    }

    /**
     * @return array<string, mixed>
     */
    private function formatAdminCreditRow(
        object $row,
        Player $player,
        string $currency,
        int $balanceAfterMinor,
    ): array {
        $amount = (int) $row->amount;
        $amountAbs = abs($amount);
        $balanceBefore = $amount >= 0
            ? max(0, $balanceAfterMinor - $amount)
            : $balanceAfterMinor + $amountAbs;

        return [
            'id' => (int) $row->id,
            'txn_no' => 'CL-'.$row->id,
            'player_id' => (int) $player->id,
            'site_code' => $player->site_code,
            'site_player_id' => $player->site_player_id,
            'username' => $player->username,
            'nickname' => $player->nickname,
            'wallet_id' => null,
            'biz_type' => (string) $row->reason,
            'biz_no' => $this->creditRefLabel($row),
            'direction' => $amount >= 0 ? 1 : 2,
            'amount' => $amountAbs,
            'amount_formatted' => CurrencyFormatter::fromMinor($amountAbs),
            'balance_before' => $balanceBefore,
            'balance_before_formatted' => CurrencyFormatter::fromMinor($balanceBefore),
            'balance_after' => $balanceAfterMinor,
            'balance_after_formatted' => CurrencyFormatter::fromMinor($balanceAfterMinor),
            'status' => 'posted',
            'external_ref_no' => null,
            'idempotent_key' => null,
            'remark' => null,
            'created_at' => $this->isoTimestamp($row->created_at ?? null),
            'updated_at' => $this->isoTimestamp($row->updated_at ?? null),
            'ledger_source' => 'credit_ledger',
            'funding_mode' => (string) ($player->funding_mode ?? PlayerFundingMode::CREDIT),
            'auth_source' => $player->auth_source,
        ];
    }

    private function creditReasonToPublicType(string $reason): string
    {
        return match ($reason) {
            'bet_hold', 'game_settlement_loss' => 'bet',
            'bet_hold_release', 'game_settlement_reversal' => 'reversal',
            'settlement_confirm' => 'refund',
            'game_settlement_win' => 'win_credit',
            'settlement_payout' => 'bill_settlement',
            default => $reason,
        };
    }

    private function walletBizToPublicType(string $biz): string
    {
        return match ($biz) {
            'transfer_out_refund' => 'refund',
            'bet_deduct', 'bet' => 'bet',
            'bet_reverse' => 'reversal',
            'settle_payout', 'prize', 'jackpot_manual_payout' => 'prize',
            'reversal' => 'reversal',
            default => $biz,
        };
    }

    private function signedWalletAmount(WalletTxn $txn): int
    {
        $a = (int) $txn->amount;

        return (int) $txn->direction === 1 ? $a : -$a;
    }

    private function creditRefLabel(object $row): ?string
    {
        if ($row->ref_type === null || $row->ref_id === null) {
            return null;
        }

        return (string) $row->ref_type.'#'.$row->ref_id;
    }

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

        return Carbon::parse($value)->toIso8601String();
    }
}
