<?php

namespace App\Services\AgentSettlement;

use App\Models\Player;
use App\Services\Player\PlayerCreditService;
use App\Support\DatabaseUniqueViolation;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class SettlementPaymentService
{
    public function __construct(
        private readonly AgentSettlementBillGuard $billGuard,
        private readonly PlayerCreditService $playerCreditService,
        private readonly PeriodCloseRebateService $periodCloseRebate,
        private readonly AgentSettlementPeriodCompletionService $periodCompletion,
    ) {}

    public function confirmBill(int $billId): void
    {
        $this->billGuard->markConfirmed($billId);
    }

    /**
     * @param  array{method?: string|null, proof?: string|null, remark?: string|null, idempotency_key?: string|null}  $meta
     */
    public function recordPayment(int $billId, int $amount, int $adminUserId, array $meta = []): void
    {
        $idempotencyKey = $meta['idempotency_key'] ?? null;

        if ($idempotencyKey !== null && $idempotencyKey !== '') {
            $existing = DB::table('payment_records')
                ->where('settlement_bill_id', $billId)
                ->where('idempotency_key', $idempotencyKey)
                ->first();
            if ($existing !== null) {
                return;
            }
        }

        try {
            DB::transaction(function () use ($billId, $amount, $adminUserId, $meta, $idempotencyKey): void {
                $this->insertPaymentRecord($billId, $amount, $adminUserId, $meta, $idempotencyKey);
            });
        } catch (QueryException $e) {
            if (
                $idempotencyKey !== null
                && $idempotencyKey !== ''
                && DatabaseUniqueViolation::matches($e)
                && $this->paymentIdempotencyExists($billId, $idempotencyKey)
            ) {
                return;
            }

            throw $e;
        }
    }

    /**
     * @param  array{method?: string|null, proof?: string|null, remark?: string|null, idempotency_key?: string|null}  $meta
     */
    private function insertPaymentRecord(
        int $billId,
        int $amount,
        int $adminUserId,
        array $meta,
        ?string $idempotencyKey,
    ): void {
        if ($idempotencyKey !== null && $idempotencyKey !== '') {
            $existing = DB::table('payment_records')
                ->where('settlement_bill_id', $billId)
                ->where('idempotency_key', $idempotencyKey)
                ->lockForUpdate()
                ->first();
            if ($existing !== null) {
                return;
            }
        }

        $bill = DB::table('settlement_bills')->where('id', $billId)->lockForUpdate()->first();
            if ($bill === null) {
                throw new \InvalidArgumentException('bill_not_found');
            }

            $this->billGuard->assertPeriodMutable($billId);
            if (! in_array((string) $bill->status, ['confirmed', 'partial_paid', 'overdue'], true)) {
                throw ValidationException::withMessages([
                    'bill' => ['not_payable'],
                ]);
            }

            $unpaid = abs((int) $bill->unpaid_amount);
            if ($amount > $unpaid) {
                throw ValidationException::withMessages([
                    'amount' => ['exceeds_unpaid'],
                ]);
            }

            $payAmount = $amount;
            if ($payAmount <= 0) {
                return;
            }

            [$payerType, $payerId, $payeeType, $payeeId] = $this->resolvePayerPayee($bill);

            DB::table('payment_records')->insert([
                'settlement_bill_id' => $billId,
                'payer_type' => $payerType,
                'payer_id' => $payerId,
                'payee_type' => $payeeType,
                'payee_id' => $payeeId,
                'amount' => $payAmount,
                'method' => $meta['method'] ?? null,
                'proof' => $meta['proof'] ?? null,
                'remark' => $meta['remark'] ?? null,
                'idempotency_key' => $idempotencyKey,
                'status' => 'confirmed',
                'created_by' => $adminUserId,
                'confirmed_by' => $adminUserId,
                'confirmed_at' => now(),
                'created_at' => now(),
                'updated_at' => now(),
            ]);

            $newPaid = (int) $bill->paid_amount + $payAmount;
            $newUnpaid = max(0, (int) $bill->unpaid_amount - $payAmount);
            $status = $newUnpaid === 0 ? 'settled' : 'partial_paid';

            DB::table('settlement_bills')->where('id', $billId)->update([
                'paid_amount' => $newPaid,
                'unpaid_amount' => $newUnpaid,
                'status' => $status,
                'updated_at' => now(),
            ]);

            if ($bill->owner_type === 'player' && (int) $bill->owner_id > 0) {
                $player = Player::query()->find((int) $bill->owner_id);
                if ($player !== null) {
                    if ((int) $bill->net_amount > 0) {
                        $this->playerCreditService->releaseFromSettlement($player, $newPaid, $billId);
                    } elseif ((int) $bill->net_amount < 0) {
                        $this->playerCreditService->applySettlementPayout($player, $newPaid, $billId);
                    }

                    if ($status === 'settled') {
                        $this->periodCloseRebate->markRebatesSettledForBill($billId);
                    }
                }
            }

            $this->periodCompletion->syncIfReady((int) $bill->settlement_period_id);
    }

    private function paymentIdempotencyExists(int $billId, string $idempotencyKey): bool
    {
        return DB::table('payment_records')
            ->where('settlement_bill_id', $billId)
            ->where('idempotency_key', $idempotencyKey)
            ->exists();
    }

    /**
     * net_amount > 0：owner 应付 counterparty；< 0：counterparty 应付 owner。
     *
     * @return array{0: string, 1: int, 2: string, 3: int}
     */
    private function resolvePayerPayee(object $bill): array
    {
        if ((int) $bill->net_amount < 0) {
            return [
                (string) $bill->counterparty_type,
                (int) $bill->counterparty_id,
                (string) $bill->owner_type,
                (int) $bill->owner_id,
            ];
        }

        return [
            (string) $bill->owner_type,
            (int) $bill->owner_id,
            (string) $bill->counterparty_type,
            (int) $bill->counterparty_id,
        ];
    }
}
