<?php

declare(strict_types=1);

namespace App\Services;

use App\Enums\TransferStatus;
use App\Models\Branch;
use App\Models\Member;
use App\Models\MemberTransfer;
use App\Models\Region;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

/**
 * The whole /dashboard payload is one cached aggregate per scope per
 * docs/05-performance-and-scale.md §4 — never computed per-request.
 */
class DashboardService
{
    public function __construct(private readonly BranchAccessService $branchAccess) {}

    /**
     * @return array<string, mixed>
     */
    public function forUser(User $user): array
    {
        $branchIds = $this->branchAccess->accessibleBranchIds($user);
        $cacheKey = 'dashboard:'.$this->scopeKey($user, $branchIds);

        return Cache::remember($cacheKey, now()->addMinutes(10), fn () => $this->compute($branchIds));
    }

    /**
     * Called after member create/import completion so cached counts don't
     * lag by up to 10 minutes for the scopes that just changed.
     */
    public function bustForBranch(int $branchId): void
    {
        Cache::forget('dashboard:all');
        Cache::forget("dashboard:branch:{$branchId}");

        $regionId = Branch::query()->where('id', $branchId)->value('region_id');

        if ($regionId !== null) {
            Cache::forget("dashboard:region:{$regionId}");
        }
    }

    /**
     * @param  array<int, int>|null  $branchIds
     * @return array<string, mixed>
     */
    private function compute(?array $branchIds): array
    {
        $scoped = fn () => $branchIds === null ? Member::query() : Member::query()->whereIn('branch_id', $branchIds);

        $totalMembers = $scoped()->count();

        $byStatus = $scoped()
            ->select('membership_status', DB::raw('COUNT(*) as count'))
            ->groupBy('membership_status')
            ->pluck('count', 'membership_status');

        $newThisMonth = $scoped()
            ->where('created_at', '>=', Carbon::now()->startOfMonth())
            ->count();

        $breakdown = $branchIds === null
            ? $this->byRegionBreakdown()
            : $this->byBranchBreakdown($branchIds);

        // Filtering the generated date_of_birth_month_day column directly
        // (instead of wrapping date_of_birth in DATE_FORMAT()) keeps this on
        // an index — see docs/02-database-schema.md.
        $upcomingBirthdaysCount = $scoped()
            ->whereIn('date_of_birth_month_day', $this->next30DaysMonthDays())
            ->count();

        $pendingTransfersQuery = MemberTransfer::query()->where('status', TransferStatus::Pending);

        if ($branchIds !== null) {
            $pendingTransfersQuery->whereIn('to_branch_id', $branchIds);
        }

        $payload = [
            'total_members' => $totalMembers,
            'by_status' => $byStatus,
            'new_this_month' => $newThisMonth,
            'upcoming_birthdays_count' => $upcomingBirthdaysCount,
            'pending_transfers_count' => $pendingTransfersQuery->count(),
        ];

        $payload[$branchIds === null ? 'by_region' : 'by_branch'] = $breakdown;

        return $payload;
    }

    /**
     * @return array<int, array{id: int, name: string, count: int}>
     */
    private function byRegionBreakdown(): array
    {
        $counts = Member::query()
            ->join('branches', 'branches.id', '=', 'members.branch_id')
            ->select('branches.region_id', DB::raw('COUNT(*) as count'))
            ->groupBy('branches.region_id')
            ->pluck('count', 'branches.region_id');

        return Region::query()->orderBy('name')->get(['id', 'name'])
            ->map(fn (Region $region) => [
                'id' => $region->id,
                'name' => $region->name,
                'count' => (int) ($counts[$region->id] ?? 0),
            ])
            ->all();
    }

    /**
     * @param  array<int, int>  $branchIds
     * @return array<int, array{id: int, name: string, count: int}>
     */
    private function byBranchBreakdown(array $branchIds): array
    {
        $counts = Member::query()
            ->whereIn('branch_id', $branchIds)
            ->select('branch_id', DB::raw('COUNT(*) as count'))
            ->groupBy('branch_id')
            ->pluck('count', 'branch_id');

        return Branch::query()->whereIn('id', $branchIds)->orderBy('name')->get(['id', 'name'])
            ->map(fn (Branch $branch) => [
                'id' => $branch->id,
                'name' => $branch->name,
                'count' => (int) ($counts[$branch->id] ?? 0),
            ])
            ->all();
    }

    /**
     * @return array<int, string>
     */
    private function next30DaysMonthDays(): array
    {
        $days = [];
        $cursor = Carbon::today();

        // Inclusive of both endpoints: today through 30 days from now.
        for ($i = 0; $i <= 30; $i++) {
            $days[] = $cursor->format('m-d');
            $cursor = $cursor->copy()->addDay();
        }

        return array_values(array_unique($days));
    }

    /**
     * @param  array<int, int>|null  $branchIds
     */
    private function scopeKey(User $user, ?array $branchIds): string
    {
        if ($branchIds === null) {
            return 'all';
        }

        return $user->region_id !== null ? "region:{$user->region_id}" : "branch:{$user->branch_id}";
    }
}
