<?php

namespace App\Services\AgentSettlement;

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

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

        $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->expandPeriodToUtcDays((string) $row->period_start, (string) $row->period_end) as $day) {
                $occupiedPeriodDates[$day] = true;
            }
        }

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

        $pendingActivityDates = 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')
            ->selectRaw('DATE(sl.settled_at) as activity_day')
            ->groupBy('activity_day')
            ->orderBy('activity_day')
            ->pluck('activity_day')
            ->map(static fn ($day): string => (string) $day)
            ->values()
            ->all();

        $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->expandPeriodToUtcDays((string) $row->period_start, (string) $row->period_end) as $day) {
                $unpaidBillDates[$day] = true;
            }
        }

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

        return [
            'suggested_start' => $suggested['start'],
            'suggested_end' => $suggested['end'],
            'occupied_period_dates' => array_keys($occupiedPeriodDates),
            'pending_activity_dates' => $pendingActivityDates,
            '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): array
    {
        $lastEndDay = $lastPeriod !== null
            ? Carbon::parse((string) $lastPeriod->period_end)->utc()->startOfDay()
            : null;

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

        if ($freePending !== []) {
            $minDay = Carbon::parse($freePending[0])->utc()->startOfDay();
            $maxDay = Carbon::parse($freePending[array_key_last($freePending)])->utc()->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('UTC')->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 expandPeriodToUtcDays(string $periodStart, string $periodEnd): array
    {
        $dates = [];
        $tz = 'Asia/Shanghai';
        $cursor = Carbon::parse($periodStart)->timezone($tz)->startOfDay();
        $end = Carbon::parse($periodEnd)->timezone($tz)->startOfDay();

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

        return $dates;
    }

    /** @return array{suggested_start: string, suggested_end: string, occupied_period_dates: list<string>, pending_activity_dates: list<string>, unpaid_bill_dates: list<string>} */
    private function emptyHints(): array
    {
        return [
            'suggested_start' => '',
            'suggested_end' => '',
            'occupied_period_dates' => [],
            'pending_activity_dates' => [],
            'unpaid_bill_dates' => [],
        ];
    }
}
