<?php

namespace App\Services\AgentSettlement;

use App\Models\AgentNode;
use App\Models\Player;
use App\Services\Agent\AgentCreditAllocatedSyncService;
use App\Services\Player\PlayerCreditService;
use App\Support\AgentSettlementPeriodWindow;
use App\Support\AgentSettlementProductionGuard;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class AgentSettlementPeriodCloseService
{
    public function __construct(
        private readonly AgentPeriodAggregator $aggregator,
        private readonly SettlementBillGenerator $billGenerator,
        private readonly PeriodCloseRebateService $periodCloseRebate,
        private readonly UnsettledTicketPeriodWarning $unsettledWarning,
        private readonly PlatformRoundingAdjuster $platformRounding,
        private readonly AgentCreditAllocatedSyncService $allocatedSync,
        private readonly PlayerCreditService $playerCreditService,
    ) {}

    /**
     * @return array<string, mixed>
     */
    public function closePeriod(int $periodId): array
    {
        AgentSettlementProductionGuard::assertProductionCloseAllowed();

        return DB::transaction(function () use ($periodId): array {
            $period = DB::table('settlement_periods')->where('id', $periodId)->lockForUpdate()->first();
            if ($period === null) {
                throw ValidationException::withMessages([
                    'period' => ['period_not_found'],
                ]);
            }

            if ((string) $period->status === 'closed' || (string) $period->status === 'completed') {
                throw ValidationException::withMessages([
                    'period' => ['period_already_closed'],
                ]);
            }

            $adminSiteId = (int) $period->admin_site_id;
            [$periodStart, $periodEnd] = AgentSettlementPeriodWindow::boundStrings(
                (string) $period->period_start,
                (string) $period->period_end,
            );

            $unsettled = $this->unsettledWarning->countForSite($adminSiteId, $periodStart, $periodEnd);
            if ($unsettled['count'] > 0) {
                throw ValidationException::withMessages([
                    'period' => ['period_has_unsettled_tickets'],
                ]);
            }

            try {
                $aggregate = $this->aggregator->aggregate($adminSiteId, $periodStart, $periodEnd);
            } catch (\InvalidArgumentException $e) {
                if (str_starts_with($e->getMessage(), 'share_snapshot_missing')) {
                    throw ValidationException::withMessages([
                        'period' => ['share_snapshot_missing'],
                    ]);
                }

                throw $e;
            }

            $billIds = $this->billGenerator->generate($periodId, $adminSiteId, $aggregate);

            $roundingDiff = $this->platformRounding->apply($periodId, $aggregate);

            $rebateStats = $this->periodCloseRebate->dispatchAndAllocate($periodId, $adminSiteId, $periodStart, $periodEnd);
            $this->releasePlayerBillRebatesFromCredit($periodId);

            DB::table('settlement_periods')->where('id', $periodId)->update([
                'status' => 'closed',
                'updated_at' => now(),
            ]);

            $siteCode = (string) DB::table('admin_sites')->where('id', $adminSiteId)->value('code');

            DB::table('share_ledger')
                ->whereIn('id', function ($query) use ($siteCode, $periodStart, $periodEnd): void {
                    $query->select('sl.id')
                        ->from('share_ledger as sl')
                        ->join('players as p', 'p.id', '=', 'sl.player_id')
                        ->where('p.site_code', $siteCode)
                        ->whereNull('sl.settlement_period_id')
                        ->whereBetween('sl.settled_at', [$periodStart, $periodEnd]);
                })
                ->update(['settlement_period_id' => $periodId]);

            $this->reconcileAllocatedCreditForSite($adminSiteId);

            return [
                'period_id' => $periodId,
                'bill_ids' => $billIds,
                'player_count' => count($aggregate['players']),
                'agent_edges' => $aggregate['agent_edges'],
                'rebate_dispatched' => $rebateStats['dispatched'],
                'rebate_allocations' => $rebateStats['allocations'],
                'unsettled_ticket_count' => $unsettled['count'],
                'unsettled_ticket_sample' => $unsettled['ticket_item_ids'],
                'platform_rounding_adjustment' => $roundingDiff,
            ];
        });
    }

    /** 玩家亏损账单里的回水已抵扣应付额，应同步释放同等已用信用。 */
    private function releasePlayerBillRebatesFromCredit(int $periodId): void
    {
        $bills = DB::table('settlement_bills')
            ->where('settlement_period_id', $periodId)
            ->where('bill_type', 'player')
            ->where('owner_type', 'player')
            ->where('gross_win_loss', '>', 0)
            ->where('rebate_amount', '>', 0)
            ->get(['id', 'owner_id', 'gross_win_loss', 'rebate_amount']);

        foreach ($bills as $bill) {
            $player = Player::query()->find((int) $bill->owner_id);
            if ($player === null) {
                continue;
            }

            $rebateMinor = min((int) $bill->gross_win_loss, (int) $bill->rebate_amount);
            $this->playerCreditService->releaseRebateInBill($player, $rebateMinor, (int) $bill->id);
        }
    }

    /** 关账后按真理源重算各代理「已下发额度」，避免与直属玩家/下级代理授信脱节。 */
    private function reconcileAllocatedCreditForSite(int $adminSiteId): void
    {
        $nodes = AgentNode::query()->where('admin_site_id', $adminSiteId)->get();
        foreach ($nodes as $node) {
            $this->allocatedSync->syncForAgent($node);
        }
    }
}
