<?php

namespace App\Services\AgentSettlement;

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

/** 坏账核销：原账单保留，记 bad_debt 调整与归档单（§2、§21.1）。 */
final class AgentSettlementBadDebtService
{
    public function __construct(
        private readonly AgentSettlementPeriodCompletionService $periodCompletion,
        private readonly PlayerCreditService $playerCreditService,
    ) {}

    public function writeOff(
        int $originalBillId,
        ?string $reason,
        int $adminUserId,
        ?string $idempotencyKey = null,
    ): int {
        if ($idempotencyKey !== null && $idempotencyKey !== '') {
            $existingArchiveId = $this->findArchiveBillByIdempotency($originalBillId, $idempotencyKey);
            if ($existingArchiveId !== null) {
                return $existingArchiveId;
            }
        }

        try {
            return (int) DB::transaction(function () use ($originalBillId, $reason, $adminUserId, $idempotencyKey): int {
                return $this->insertBadDebtWriteOff($originalBillId, $reason, $adminUserId, $idempotencyKey);
            });
        } catch (QueryException $e) {
            if (
                $idempotencyKey !== null
                && $idempotencyKey !== ''
                && DatabaseUniqueViolation::matches($e)
            ) {
                $existingArchiveId = $this->findArchiveBillByIdempotency($originalBillId, $idempotencyKey);
                if ($existingArchiveId !== null) {
                    return $existingArchiveId;
                }
            }

            throw $e;
        }
    }

    private function insertBadDebtWriteOff(
        int $originalBillId,
        ?string $reason,
        int $adminUserId,
        ?string $idempotencyKey,
    ): int {
        /** @var object|null $original */
        $original = DB::table('settlement_bills')->where('id', $originalBillId)->lockForUpdate()->first();
        if ($original === null) {
            throw new \InvalidArgumentException('bill_not_found');
        }

        $meta = $this->decodeMeta($original->meta_json);
        $existingArchiveId = (int) ($meta['bad_debt_bill_id'] ?? 0);
        if ($existingArchiveId > 0) {
            return $existingArchiveId;
        }

        if ($idempotencyKey !== null && $idempotencyKey !== '') {
            $existingArchiveId = $this->findArchiveBillByIdempotency($originalBillId, $idempotencyKey);
            if ($existingArchiveId !== null) {
                return $existingArchiveId;
            }
        }

        if ($this->periodCompletion->isPeriodReadOnly((int) $original->settlement_period_id)) {
            throw ValidationException::withMessages([
                'period' => ['completed'],
            ]);
        }

        if (! in_array((string) $original->status, ['confirmed', 'partial_paid', 'overdue'], true)) {
            throw ValidationException::withMessages([
                'bill' => ['not_eligible'],
            ]);
        }

        $unpaid = (int) $original->unpaid_amount;
        if ($unpaid <= 0) {
            throw ValidationException::withMessages([
                'bill' => ['no_unpaid'],
            ]);
        }

        if (in_array((string) $original->bill_type, ['adjustment', 'reversal', 'bad_debt'], true)) {
            throw ValidationException::withMessages([
                'bill' => ['not_eligible'],
            ]);
        }

        $now = now();
        $periodId = (int) $original->settlement_period_id;

        $archiveBillId = (int) DB::table('settlement_bills')->insertGetId([
            'settlement_period_id' => $periodId,
            'bill_type' => 'bad_debt',
            'owner_type' => (string) $original->owner_type,
            'owner_id' => (int) $original->owner_id,
            'counterparty_type' => (string) $original->counterparty_type,
            'counterparty_id' => (int) $original->counterparty_id,
            'gross_win_loss' => 0,
            'rebate_amount' => 0,
            'adjustment_amount' => -$unpaid,
            'platform_rounding_adjustment' => 0,
            'net_amount' => 0,
            'paid_amount' => 0,
            'unpaid_amount' => 0,
            'status' => 'settled',
            'reversed_bill_id' => $originalBillId,
            'meta_json' => json_encode([
                'original_bill_id' => $originalBillId,
                'written_off_amount' => $unpaid,
                'original_net_amount' => (int) $original->net_amount,
            ]),
            'locked_at' => $now,
            'confirmed_at' => $now,
            'created_at' => $now,
            'updated_at' => $now,
        ]);

        DB::table('settlement_adjustments')->insert([
            'settlement_period_id' => $periodId,
            'original_bill_id' => $originalBillId,
            'result_bill_id' => $archiveBillId,
            'adjustment_type' => 'bad_debt',
            'amount' => $unpaid,
            'reason' => $reason,
            'idempotency_key' => $idempotencyKey,
            'created_by' => $adminUserId > 0 ? $adminUserId : null,
            'created_at' => $now,
            'updated_at' => $now,
        ]);

        DB::table('settlement_bills')->where('id', $originalBillId)->update([
            'unpaid_amount' => 0,
            'status' => 'settled',
            'meta_json' => json_encode(array_merge(
                $meta,
                [
                    'bad_debt_bill_id' => $archiveBillId,
                    'written_off_amount' => $unpaid,
                ],
            )),
            'updated_at' => $now,
        ]);

        if ((string) $original->owner_type === 'player' && (int) $original->owner_id > 0 && (int) $original->net_amount > 0) {
            $player = Player::query()->find((int) $original->owner_id);
            if ($player !== null) {
                $cumulativePaid = (int) $original->paid_amount + $unpaid;
                $this->playerCreditService->releaseFromSettlement($player, $cumulativePaid, $originalBillId);
            }
        }

        $this->periodCompletion->syncIfReady($periodId);

        return $archiveBillId;
    }

    private function findArchiveBillByIdempotency(int $originalBillId, string $idempotencyKey): ?int
    {
        $resultBillId = DB::table('settlement_adjustments')
            ->where('original_bill_id', $originalBillId)
            ->where('adjustment_type', 'bad_debt')
            ->where('idempotency_key', $idempotencyKey)
            ->value('result_bill_id');

        return $resultBillId !== null ? (int) $resultBillId : null;
    }

    /**
     * @return array<string, mixed>
     */
    private function decodeMeta(mixed $metaJson): array
    {
        if ($metaJson === null || $metaJson === '') {
            return [];
        }

        if (is_array($metaJson)) {
            return $metaJson;
        }

        $decoded = json_decode((string) $metaJson, true);

        return is_array($decoded) ? $decoded : [];
    }
}
