<?php

namespace App\Services\AgentSettlement;

use App\Models\AgentNode;
use Illuminate\Support\Facades\DB;

/**
 * 账期关账：回水计提 → in_bill，并写入 rebate_allocations（§7、§9.3）。
 */
final class PeriodCloseRebateService
{
    /**
     * @return array{dispatched: int, allocations: int}
     */
    public function dispatchAndAllocate(
        int $periodId,
        int $adminSiteId,
        string $periodStart,
        string $periodEnd,
    ): array {
        $rebateIds = $this->dispatchAccruedToPeriod($periodId, $adminSiteId, $periodStart, $periodEnd);
        $allocationCount = $this->buildAllocations($periodId, $rebateIds);

        return [
            'dispatched' => count($rebateIds),
            'allocations' => $allocationCount,
        ];
    }

    /**
     * @return list<int>
     */
    private function dispatchAccruedToPeriod(int $periodId, int $adminSiteId, string $periodStart, string $periodEnd): array
    {
        $siteCode = (string) DB::table('admin_sites')->where('id', $adminSiteId)->value('code');

        $ids = DB::table('rebate_records as rr')
            ->join('ticket_items as ti', 'ti.id', '=', 'rr.ticket_item_id')
            ->join('share_ledger as sl', function ($join): void {
                $join->on('sl.ticket_item_id', '=', 'ti.id')
                    ->whereNull('sl.reversal_of_id');
            })
            ->join('players as p', 'p.id', '=', 'rr.player_id')
            ->where('p.site_code', $siteCode)
            ->where('rr.status', 'accrued')
            ->whereBetween('sl.settled_at', [$periodStart, $periodEnd])
            ->pluck('rr.id')
            ->map(fn ($id): int => (int) $id)
            ->all();

        if ($ids === []) {
            return [];
        }

        DB::table('rebate_records')
            ->whereIn('id', $ids)
            ->update([
                'settlement_period_id' => $periodId,
                'status' => 'in_bill',
                'updated_at' => now(),
            ]);

        return $ids;
    }

    /**
     * @param  list<int>  $rebateIds
     */
    private function buildAllocations(int $periodId, array $rebateIds): int
    {
        if ($rebateIds === []) {
            return 0;
        }

        $playerBills = DB::table('settlement_bills')
            ->where('settlement_period_id', $periodId)
            ->where('bill_type', 'player')
            ->get()
            ->keyBy('owner_id');

        $count = 0;
        $rebates = DB::table('rebate_records')
            ->whereIn('id', $rebateIds)
            ->where('status', 'in_bill')
            ->get();

        foreach ($rebates as $rebate) {
            $playerId = (int) $rebate->player_id;
            $bill = $playerBills->get($playerId);
            $billId = $bill !== null ? (int) $bill->id : null;

            if ((string) $rebate->rebate_type === 'extra') {
                $count += $this->insertExtraAllocation($rebate, $billId);

                continue;
            }

            $count += $this->insertBasicShareAllocations($rebate, $billId);
        }

        return $count;
    }

    private function insertExtraAllocation(object $rebate, ?int $billId): int
    {
        $agentId = (int) ($rebate->owner_agent_id ?? 0);
        if ($agentId <= 0) {
            return 0;
        }

        DB::table('rebate_allocations')->insert([
            'rebate_record_id' => (int) $rebate->id,
            'settlement_bill_id' => $billId,
            'participant_type' => 'agent',
            'participant_id' => $agentId,
            'actual_share_rate' => 0,
            'allocated_amount' => (int) $rebate->rebate_amount,
            'allocation_rule' => 'owner',
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return 1;
    }

    private function insertBasicShareAllocations(object $rebate, ?int $billId): int
    {
        $ticketItemId = (int) ($rebate->ticket_item_id ?? 0);
        if ($ticketItemId <= 0) {
            return 0;
        }

        $ledger = DB::table('share_ledger')
            ->where('ticket_item_id', $ticketItemId)
            ->whereNull('reversal_of_id')
            ->orderByDesc('id')
            ->first();

        if ($ledger === null) {
            return 0;
        }

        $snapshot = $this->decodeSnapshot($ledger->share_snapshot);
        if ($snapshot === null) {
            return 0;
        }

        $amount = (int) $rebate->rebate_amount;
        if ($amount <= 0) {
            return 0;
        }

        $shares = $snapshot['actual_shares'];
        $rows = [];
        $allocatedSum = 0;
        $participants = [];

        foreach ($shares as $code => $rate) {
            if ($code === 'platform') {
                $participants[] = ['type' => 'platform', 'id' => 0, 'rate' => (float) $rate];

                continue;
            }

            $node = AgentNode::query()->where('code', (string) $code)->first();
            if ($node === null) {
                continue;
            }

            $participants[] = ['type' => 'agent', 'id' => (int) $node->id, 'rate' => (float) $rate];
        }

        foreach ($participants as $index => $p) {
            $isLast = $index === count($participants) - 1;
            $slice = $isLast
                ? $amount - $allocatedSum
                : (int) round($amount * ($p['rate'] / 100), 0, PHP_ROUND_HALF_UP);
            $allocatedSum += $slice;

            $rows[] = [
                'rebate_record_id' => (int) $rebate->id,
                'settlement_bill_id' => $billId,
                'participant_type' => (string) $p['type'],
                'participant_id' => (int) $p['id'],
                'actual_share_rate' => $p['rate'],
                'allocated_amount' => $slice,
                'allocation_rule' => 'share',
                'created_at' => now(),
                'updated_at' => now(),
            ];
        }

        if ($rows !== []) {
            DB::table('rebate_allocations')->insert($rows);
        }

        return count($rows);
    }

    /**
     * @return array{actual_shares: array<string, float>}|null
     */
    private function decodeSnapshot(mixed $raw): ?array
    {
        if ($raw === null || $raw === '') {
            return null;
        }

        $decoded = is_string($raw) ? json_decode($raw, true) : $raw;
        if (! is_array($decoded)) {
            return null;
        }

        $actual = $decoded['actual_shares'] ?? null;
        if (! is_array($actual) || $actual === []) {
            return null;
        }

        $shares = [];
        foreach ($actual as $code => $rate) {
            $shares[(string) $code] = (float) $rate;
        }

        return ['actual_shares' => $shares];
    }

    public function markRebatesSettledForBill(int $billId): void
    {
        $bill = DB::table('settlement_bills')->where('id', $billId)->first();
        if ($bill === null || (string) $bill->bill_type !== 'player') {
            return;
        }

        DB::table('rebate_records')
            ->where('player_id', (int) $bill->owner_id)
            ->where('settlement_period_id', (int) $bill->settlement_period_id)
            ->where('status', 'in_bill')
            ->update([
                'status' => 'settled',
                'updated_at' => now(),
            ]);
    }
}
