<?php

namespace App\Services\Player;

use App\Models\Player;
use App\Support\AgentOverdueGuard;
use App\Support\CreditAmountScale;
use App\Support\PlayerFundingMode;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class PlayerCreditService
{
    /**
     * @param  array{credit_limit?: int}  $payload
     */
    public function upsertAccount(Player $player, array $payload): void
    {
        $limit = max(0, (int) ($payload['credit_limit'] ?? 0));
        $now = now();

        // 整段包在事务中：并发下调额（先 SELECT used_credit 后 UPDATE）若不加锁，
        // 两个请求都拿到旧 used_credit 并各自通过 below_player_used 校验，导致额度被收缩到比已用更小。
        // lockForUpdate 保证同时只有一个请求能修改该玩家授信记录。
        DB::transaction(function () use ($player, $limit, $now): void {
            $row = DB::table('player_credit_accounts')
                ->where('player_id', $player->id)
                ->lockForUpdate()
                ->first();

            if ($row !== null) {
                $usedTotal = (int) $row->used_credit + (int) $row->frozen_credit;
                if ($limit < $usedTotal) {
                    throw ValidationException::withMessages([
                        'credit_limit' => ['below_player_used'],
                    ]);
                }

                DB::table('player_credit_accounts')
                    ->where('player_id', $player->id)
                    ->update([
                        'credit_limit' => $limit,
                        'updated_at' => $now,
                    ]);

                return;
            }

            DB::table('player_credit_accounts')->insert([
                'player_id' => $player->id,
                'credit_limit' => $limit,
                'used_credit' => 0,
                'frozen_credit' => 0,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
        });
    }

    /** 可用授信（主货币整数，与后台「授信额度」一致）。 */
    public function availableCredit(Player $player): int
    {
        $row = DB::table('player_credit_accounts')->where('player_id', $player->id)->first();
        if ($row === null) {
            return 0;
        }

        return max(0, (int) $row->credit_limit - (int) $row->used_credit - (int) $row->frozen_credit);
    }

    /** 可用授信（最小货币单位，供玩家端钱包/下注与钱包余额 API 对齐）。 */
    public function availableCreditMinor(Player $player, ?string $currencyCode = null): int
    {
        $currency = $currencyCode ?? (string) $player->default_currency;

        return CreditAmountScale::majorToMinor($this->availableCredit($player), $currency);
    }

    /** 逾期/代理线门禁与可用额度预检（不占额）。 */
    public function assertCreditPreflight(Player $player, int $amountMinor): void
    {
        if (! PlayerFundingMode::usesCredit($player) || $amountMinor <= 0) {
            return;
        }

        $this->assertCreditGuards($player);

        $currency = (string) $player->default_currency;
        $majorNeeded = CreditAmountScale::minorToMajor($amountMinor, $currency);
        if ($majorNeeded > $this->availableCredit($player)) {
            throw ValidationException::withMessages([
                'credit' => ['insufficient'],
            ]);
        }
    }

    public function holdForBet(Player $player, int $amountMinor): void
    {
        if ($amountMinor <= 0) {
            return;
        }

        if (! PlayerFundingMode::usesCredit($player)) {
            return;
        }

        $currency = (string) $player->default_currency;
        $majorDelta = CreditAmountScale::minorToMajor($amountMinor, $currency);
        $now = now();

        $row = DB::table('player_credit_accounts')
            ->where('player_id', $player->id)
            ->lockForUpdate()
            ->first();

        if ($row === null) {
            throw ValidationException::withMessages([
                'credit' => ['insufficient'],
            ]);
        }

        $availableMajor = max(
            0,
            (int) $row->credit_limit - (int) $row->used_credit - (int) $row->frozen_credit,
        );
        $availableMinor = CreditAmountScale::majorToMinor($availableMajor, $currency);
        if ($amountMinor > $availableMinor) {
            throw ValidationException::withMessages([
                'credit' => ['insufficient'],
            ]);
        }

        $updated = DB::table('player_credit_accounts')
            ->where('player_id', $player->id)
            ->whereRaw('credit_limit - used_credit - frozen_credit >= ?', [$majorDelta])
            ->update([
                'used_credit' => DB::raw('used_credit + '.$majorDelta),
                'updated_at' => $now,
            ]);

        if ($updated !== 1) {
            throw ValidationException::withMessages([
                'credit' => ['insufficient'],
            ]);
        }

        DB::table('credit_ledger')->insert([
            'owner_type' => 'player',
            'owner_id' => $player->id,
            'amount' => -$amountMinor,
            'reason' => 'bet_hold',
            'ref_type' => 'bet',
            'ref_id' => null,
            'created_at' => $now,
            'updated_at' => $now,
        ]);
    }

    public function applySettledLoss(Player $player, int $amountMinor, int $ticketItemId): void
    {
        if ($amountMinor <= 0) {
            return;
        }

        if (! PlayerFundingMode::usesCredit($player)) {
            return;
        }

        $currency = (string) $player->default_currency;
        $majorDelta = CreditAmountScale::minorToMajor($amountMinor, $currency);
        $now = now();

        // 先写 credit_ledger：以 (ref_type, ref_id) partial unique 索引为幂等闸门。
        // 已存在同 (ticket_item, game_settlement_loss) 的流水则直接返回，避免并发/重入场景下重复扣减 used_credit。
        try {
            DB::table('credit_ledger')->insert([
                'owner_type' => 'player',
                'owner_id' => $player->id,
                'amount' => -$amountMinor,
                'reason' => 'game_settlement_loss',
                'ref_type' => 'ticket_item',
                'ref_id' => $ticketItemId,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
        } catch (QueryException $e) {
            // 23505 (pgsql) / 23000/19 (sqlite) 唯一约束冲突即幂等成功，跳过余额更新。
            if ($this->isUniqueViolation($e)) {
                return;
            }
            throw $e;
        }

        // 余额变更在 lockForUpdate 保护下做条件 update。
        $row = DB::table('player_credit_accounts')
            ->where('player_id', $player->id)
            ->lockForUpdate()
            ->first();

        if ($row === null) {
            return;
        }

        DB::table('player_credit_accounts')
            ->where('player_id', $player->id)
            ->update([
                'used_credit' => (int) $row->used_credit + $majorDelta,
                'updated_at' => $now,
            ]);
    }

    public function applySettledWin(Player $player, int $amountMinor, int $ticketItemId): void
    {
        if ($amountMinor <= 0) {
            return;
        }

        if (! PlayerFundingMode::usesCredit($player)) {
            return;
        }

        $now = now();

        // 先写 credit_ledger：以 (ref_type, ref_id, reason) partial unique 索引为幂等闸门。
        // 已存在同 (ticket_item, game_settlement_win) 的流水则直接返回，避免并发/重入场景下重复扣减 used_credit。
        try {
            DB::table('credit_ledger')->insert([
                'owner_type' => 'player',
                'owner_id' => $player->id,
                'amount' => $amountMinor,
                'reason' => 'game_settlement_win',
                'ref_type' => 'ticket_item',
                'ref_id' => $ticketItemId,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
        } catch (QueryException $e) {
            if ($this->isUniqueViolation($e)) {
                return;
            }
            throw $e;
        }

        $this->decreaseUsedCredit($player, $amountMinor);
    }

    public function assertMayPlaceBet(Player $player, int $amountMinor): void
    {
        if (! PlayerFundingMode::usesCredit($player)) {
            return;
        }

        $this->assertCreditGuards($player);
        $this->holdForBet($player, $amountMinor);
    }

    private function assertCreditGuards(Player $player): void
    {
        if (! PlayerFundingMode::usesCredit($player)) {
            return;
        }

        $overdue = DB::table('settlement_bills')
            ->where('owner_type', 'player')
            ->where('owner_id', $player->id)
            ->where('status', 'overdue')
            ->where('unpaid_amount', '>', 0)
            ->exists();

        if ($overdue) {
            throw ValidationException::withMessages([
                'credit' => ['overdue'],
            ]);
        }

        $agentNodeId = (int) ($player->agent_node_id ?? 0);
        if ($agentNodeId > 0) {
            AgentOverdueGuard::assertAgentMayGrantCredit($agentNodeId);
            AgentOverdueGuard::assertAgentLineMayPlaceBet($agentNodeId);
        }
    }

    public function releaseBetHold(Player $player, int $amountMinor, int $ticketItemId): void
    {
        if ($amountMinor <= 0 || ! PlayerFundingMode::usesCredit($player)) {
            return;
        }

        $now = now();

        // 先写 credit_ledger：以 (ref_type, ref_id, reason) partial unique 索引为幂等闸门。
        // 已存在同 (ticket_item, bet_hold_release) 的流水则直接返回，避免并发/重入场景下重复扣减 used_credit。
        try {
            DB::table('credit_ledger')->insert([
                'owner_type' => 'player',
                'owner_id' => $player->id,
                'amount' => $amountMinor,
                'reason' => 'bet_hold_release',
                'ref_type' => 'ticket_item',
                'ref_id' => $ticketItemId,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
        } catch (QueryException $e) {
            if ($this->isUniqueViolation($e)) {
                return;
            }
            throw $e;
        }

        $this->decreaseUsedCredit($player, $amountMinor);
    }

    public function reverseBetHold(Player $player, int $amountMinor, int $ticketOrderId): void
    {
        if ($amountMinor <= 0 || ! PlayerFundingMode::usesCredit($player)) {
            return;
        }

        $now = now();

        // 先写 credit_ledger：以 (ref_type, ref_id, reason) partial unique 索引为幂等闸门。
        // 已存在同 (ticket_order, bet_hold_release) 的流水则直接返回，避免并发/重入场景下重复扣减 used_credit。
        try {
            DB::table('credit_ledger')->insert([
                'owner_type' => 'player',
                'owner_id' => $player->id,
                'amount' => $amountMinor,
                'reason' => 'bet_hold_release',
                'ref_type' => 'ticket_order',
                'ref_id' => $ticketOrderId,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
        } catch (QueryException $e) {
            if ($this->isUniqueViolation($e)) {
                return;
            }
            throw $e;
        }

        $this->decreaseUsedCredit($player, $amountMinor);
    }

    public function reverseGameSettlement(Player $player, int $gameWinLossSigned, int $ticketItemId): void
    {
        if ($gameWinLossSigned === 0 || ! PlayerFundingMode::usesCredit($player)) {
            return;
        }

        $now = now();
        $amountMinor = abs($gameWinLossSigned);

        // 先写 credit_ledger：以 (ref_type, ref_id, reason) partial unique 索引为幂等闸门。
        try {
            DB::table('credit_ledger')->insert([
                'owner_type' => 'player',
                'owner_id' => $player->id,
                'amount' => $gameWinLossSigned > 0 ? $amountMinor : -$amountMinor,
                'reason' => 'game_settlement_reversal',
                'ref_type' => 'ticket_item',
                'ref_id' => $ticketItemId,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
        } catch (QueryException $e) {
            if ($this->isUniqueViolation($e)) {
                return;
            }
            throw $e;
        }

        if ($gameWinLossSigned > 0) {
            $this->decreaseUsedCredit($player, $amountMinor);

            return;
        }

        $currency = (string) $player->default_currency;
        $majorDelta = CreditAmountScale::minorToMajor($amountMinor, $currency);

        $row = DB::table('player_credit_accounts')
            ->where('player_id', $player->id)
            ->lockForUpdate()
            ->first();

        if ($row === null) {
            return;
        }

        DB::table('player_credit_accounts')
            ->where('player_id', $player->id)
            ->update([
                'used_credit' => (int) $row->used_credit + $majorDelta,
                'updated_at' => $now,
            ]);
    }

    /**
     * @param  int  $cumulativePaidMinor  账单累计已登记收付（minor），支持部分收付多笔递增。
     */
    public function releaseFromSettlement(Player $player, int $cumulativePaidMinor, int $billId): void
    {
        if ($cumulativePaidMinor <= 0 || ! PlayerFundingMode::usesCredit($player)) {
            return;
        }

        $delta = $this->syncSettlementBillLedger(
            $player,
            $billId,
            'settlement_confirm',
            $cumulativePaidMinor,
        );
        if ($delta > 0) {
            $this->decreaseUsedCredit($player, $delta);
        }
    }

    /**
     * @param  int  $cumulativePaidMinor  账单累计已登记收付（minor），支持部分收付多笔递增。
     */
    public function applySettlementPayout(Player $player, int $cumulativePaidMinor, int $billId): void
    {
        if ($cumulativePaidMinor <= 0 || ! PlayerFundingMode::usesCredit($player)) {
            return;
        }

        $this->syncSettlementBillLedger(
            $player,
            $billId,
            'settlement_payout',
            $cumulativePaidMinor,
        );
    }

    /**
     * 同步账期收付台账：每账单每种 reason 仅一行，amount 为累计已付；返回本次应追加的 minor 增量。
     */
    private function syncSettlementBillLedger(
        Player $player,
        int $billId,
        string $reason,
        int $cumulativeMinor,
    ): int {
        $existing = DB::table('credit_ledger')
            ->where('owner_type', 'player')
            ->where('owner_id', $player->id)
            ->where('reason', $reason)
            ->where('ref_type', 'settlement_bill')
            ->where('ref_id', $billId)
            ->lockForUpdate()
            ->first();

        $recorded = $existing !== null ? (int) $existing->amount : 0;
        $delta = $cumulativeMinor - $recorded;
        if ($delta <= 0) {
            return 0;
        }

        $now = now();
        if ($existing === null) {
            DB::table('credit_ledger')->insert([
                'owner_type' => 'player',
                'owner_id' => $player->id,
                'amount' => $cumulativeMinor,
                'reason' => $reason,
                'ref_type' => 'settlement_bill',
                'ref_id' => $billId,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
        } else {
            DB::table('credit_ledger')
                ->where('id', (int) $existing->id)
                ->update([
                    'amount' => $cumulativeMinor,
                    'updated_at' => $now,
                ]);
        }

        return $delta;
    }

    private function decreaseUsedCredit(Player $player, int $amountMinor): void
    {
        if ($amountMinor <= 0) {
            return;
        }

        $playerId = (int) $player->id;
        $majorDelta = CreditAmountScale::minorToMajor($amountMinor, (string) $player->default_currency);

        $row = DB::table('player_credit_accounts')
            ->where('player_id', $playerId)
            ->lockForUpdate()
            ->first();
        if ($row === null) {
            return;
        }

        $next = max(0, (int) $row->used_credit - $majorDelta);
        DB::table('player_credit_accounts')
            ->where('player_id', $playerId)
            ->update([
                'used_credit' => $next,
                'updated_at' => now(),
            ]);
    }

    /**
     * 识别数据库唯一约束冲突（PostgreSQL SQLSTATE 23505；SQLite SQLSTATE 23000/error code 19）。
     * 兜底扫描消息中 unique 字样，兼容驱动返回的 SQLSTATE 缺失场景。
     */
    private function isUniqueViolation(QueryException $e): bool
    {
        $sqlState = (string) $e->getCode();
        $errorInfo = $e->errorInfo ?? null;
        $driverCode = is_array($errorInfo) ? (int) ($errorInfo[1] ?? 0) : 0;

        if ($sqlState === '23505') {
            return true;
        }
        if ($sqlState === '23000' || $sqlState === '19') {
            return true;
        }
        if ($driverCode === 19 || $driverCode === 1062) {
            return true;
        }
        $message = $e->getMessage();
        if (stripos($message, 'unique') !== false || stripos($message, 'UNIQUE constraint') !== false) {
            return true;
        }

        return false;
    }
}
