<?php

namespace App\Services\AgentSettlement;

use Carbon\Carbon;
use App\Support\AgentSettlementSiteTimezone;
use Illuminate\Support\Facades\DB;

/** 开账弹窗：建议账期与日历标记（已有账期 / 待入账 / 未结清 / 已关账漏账）。 */
final class SettlementPeriodOpenHintsService
{
    /**
     * @return array{
     *     suggested_start: string,
     *     suggested_end: string,
     *     settlement_timezone: string,
     *     occupied_period_dates: list<string>,
     *     pending_activity_dates: list<string>,
     *     orphan_activity_dates: list<string>,
     *     unpaid_bill_dates: list<string>
     * }
     */
    public function hints(int $adminSiteId): array
    {
        $site = DB::table('admin_sites')->where('id', $adminSiteId)->first(['code', 'settlement_timezone']);
        $siteCode = (string) ($site->code ?? '');
        if ($siteCode === '') {
            return $this->emptyHints();
        }
        $timezone = AgentSettlementSiteTimezone::isValid((string) ($site->settlement_timezone ?? ''))
            ? (string) $site->settlement_timezone
            : AgentSettlementSiteTimezone::forSiteId($adminSiteId);

        $periodRows = DB::table('settlement_periods')
            ->where('admin_site_id', $adminSiteId)
            ->orderBy('period_start')
            ->get(['period_start', 'period_end', 'status']);

        $occupiedPeriodDates = [];
        foreach ($periodRows as $row) {
            foreach ($this->expandPeriodToLocalDays((string) $row->period_start, (string) $row->period_end, $timezone) as $day) {
                $occupiedPeriodDates[$day] = true;
            }
        }

        $lastPeriod = DB::table('settlement_periods')
            ->where('admin_site_id', $adminSiteId)
            ->whereIn('status', ['closed', 'completed'])
            ->orderByDesc('period_end')
            ->first();

        $pendingLedgerRows = DB::table('share_ledger as sl')
            ->join('players as p', 'p.id', '=', 'sl.player_id')
            ->where('p.site_code', $siteCode)
            ->whereNull('sl.settlement_period_id')
            ->whereNull('sl.reversal_of_id')
            ->orderBy('sl.settled_at')
            ->get(['sl.settled_at']);

        $pendingActivityDates = [];
        $orphanActivityDates = [];
        foreach ($pendingLedgerRows as $row) {
            $day = AgentSettlementSiteTimezone::localDayFromUtc($row->settled_at, $timezone);
            $pendingActivityDates[$day] = true;
            if (isset($occupiedPeriodDates[$day])) {
                $orphanActivityDates[$day] = true;
            }
        }
        $pendingActivityDates = array_keys($pendingActivityDates);
        $orphanActivityDates = array_keys($orphanActivityDates);

        $unpaidPeriodRows = DB::table('settlement_periods as sp')
            ->where('sp.admin_site_id', $adminSiteId)
            ->whereIn('sp.status', ['closed', 'completed'])
            ->whereExists(function ($query): void {
                $query->selectRaw('1')
                    ->from('settlement_bills as sb')
                    ->whereColumn('sb.settlement_period_id', 'sp.id')
                    ->where('sb.unpaid_amount', '>', 0)
                    ->whereIn('sb.status', ['pending_confirm', 'confirmed', 'partial_paid', 'overdue']);
            })
            ->orderBy('sp.period_start')
            ->get(['sp.period_start', 'sp.period_end']);

        $unpaidBillDates = [];
        foreach ($unpaidPeriodRows as $row) {
            foreach ($this->expandPeriodToLocalDays((string) $row->period_start, (string) $row->period_end, $timezone) as $day) {
                $unpaidBillDates[$day] = true;
            }
        }

        $suggested = $this->suggestRange($lastPeriod, $pendingActivityDates, $occupiedPeriodDates, $timezone);

        return [
            'suggested_start' => $suggested['start'],
            'suggested_end' => $suggested['end'],
            'settlement_timezone' => $timezone,
            'occupied_period_dates' => array_keys($occupiedPeriodDates),
            'pending_activity_dates' => $pendingActivityDates,
            'orphan_activity_dates' => $orphanActivityDates,
            'unpaid_bill_dates' => array_keys($unpaidBillDates),
        ];
    }

    /**
     * @param  list<string>  $pendingActivityDates  UTC `Y-m-d`
     * @param  array<string, true>  $occupiedPeriodDates
     * @return array{start: string, end: string}
     */
    private function suggestRange(
        ?object $lastPeriod,
        array $pendingActivityDates,
        array $occupiedPeriodDates,
        string $timezone,
    ): array
    {
        $lastEndDay = $lastPeriod !== null
            ? Carbon::parse((string) $lastPeriod->period_end, 'UTC')->timezone($timezone)->startOfDay()
            : null;

        $freePending = array_values(array_filter(
            $pendingActivityDates,
            static fn (string $day): bool => ! isset($occupiedPeriodDates[$day]),
        ));

        if ($freePending !== []) {
            $minDay = Carbon::parse($freePending[0])->startOfDay();
            $maxDay = Carbon::parse($freePending[array_key_last($freePending)])->startOfDay();
            $startDay = $lastEndDay !== null
                ? ($lastEndDay->copy()->addDay()->lessThanOrEqualTo($minDay) ? $lastEndDay->copy()->addDay() : $minDay)
                : $minDay;

            $candidate = [
                'start' => $startDay->format('Y-m-d'),
                'end' => $maxDay->format('Y-m-d'),
            ];

            return $this->withoutOccupiedOverlap($candidate, $occupiedPeriodDates);
        }

        if ($lastEndDay !== null) {
            $startDay = $lastEndDay->copy()->addDay();
            $endDay = Carbon::now($timezone)->subDay()->startOfDay();
            if ($endDay->lessThan($startDay)) {
                return ['start' => '', 'end' => ''];
            }

            return $this->withoutOccupiedOverlap([
                'start' => $startDay->format('Y-m-d'),
                'end' => $endDay->format('Y-m-d'),
            ], $occupiedPeriodDates);
        }

        return ['start' => '', 'end' => ''];
    }

    /**
     * @param  array{start: string, end: string}  $candidate
     * @param  array<string, true>  $occupiedPeriodDates
     * @return array{start: string, end: string}
     */
    private function withoutOccupiedOverlap(array $candidate, array $occupiedPeriodDates): array
    {
        if ($candidate['start'] === '' || $candidate['end'] === '') {
            return ['start' => '', 'end' => ''];
        }

        if ($this->rangeOverlapsOccupied($candidate['start'], $candidate['end'], $occupiedPeriodDates)) {
            return ['start' => '', 'end' => ''];
        }

        return $candidate;
    }

    /**
     * @param  array<string, true>  $occupiedPeriodDates
     */
    private function rangeOverlapsOccupied(string $startYmd, string $endYmd, array $occupiedPeriodDates): bool
    {
        $cursor = Carbon::parse($startYmd)->utc()->startOfDay();
        $end = Carbon::parse($endYmd)->utc()->startOfDay();

        while ($cursor->lessThanOrEqualTo($end)) {
            if (isset($occupiedPeriodDates[$cursor->format('Y-m-d')])) {
                return true;
            }
            $cursor->addDay();
        }

        return false;
    }

    /** @return list<string> 站点本地日历 `Y-m-d` */
    private function expandPeriodToLocalDays(string $periodStart, string $periodEnd, string $timezone): array
    {
        $dates = [];
        $cursor = Carbon::parse($periodStart, 'UTC')->timezone($timezone)->startOfDay();
        $end = Carbon::parse($periodEnd, 'UTC')->timezone($timezone)->startOfDay();

        while ($cursor->lessThanOrEqualTo($end)) {
            $dates[] = $cursor->format('Y-m-d');
            $cursor->addDay();
        }

        return $dates;
    }

    /** @return array{suggested_start: string, suggested_end: string, settlement_timezone: string, occupied_period_dates: list<string>, pending_activity_dates: list<string>, orphan_activity_dates: list<string>, unpaid_bill_dates: list<string>} */
    private function emptyHints(): array
    {
        return [
            'suggested_start' => '',
            'suggested_end' => '',
            'settlement_timezone' => (string) config('lottery.settlement.default_timezone', 'UTC'),
            'occupied_period_dates' => [],
            'pending_activity_dates' => [],
            'orphan_activity_dates' => [],
            'unpaid_bill_dates' => [],
        ];
    }
}
