<?php

namespace App\Services;

use App\Models\Holiday;
use App\Models\Pto;
use App\Models\TeamMember;
use App\Models\TeamMemberAvailability;
use App\Utilities\WorkingDaysCalculator;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use DateTimeInterface;
use Illuminate\Cache\Repository as CacheRepository;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Throwable;

class CapacityService
{
    private int $hoursPerDay = 8;

    private ?bool $redisAvailable = null;

    /**
     * Calculate how many working days exist for the supplied month (weekends and holidays excluded).
     */
    public function calculateWorkingDays(string $month): int
    {
        $holidayDates = $this->getHolidaysForMonth($month)
            ->pluck('date')
            ->map(fn (Carbon $date): string => $date->toDateString())
            ->all();

        return WorkingDaysCalculator::calculate($month, $holidayDates);
    }

    /**
     * Calculate capacity for a single team member for the requested month.
     */
    public function calculateIndividualCapacity(string $teamMemberId, string $month): array
    {
        $cacheKey = $this->buildCacheKey($month, $teamMemberId);
        $tags = $this->getCapacityCacheTags($month, "team_member:{$teamMemberId}");

        $resolver = function () use ($teamMemberId, $month): array {
            $period = $this->createMonthPeriod($month);
            $holidayDates = $this->getHolidaysForMonth($month)
                ->pluck('date')
                ->map(fn (Carbon $date): string => $date->toDateString())
                ->all();
            $holidayLookup = array_flip($holidayDates);
            $ptoDates = $this->buildPtoDates($this->getPtoForTeamMember($teamMemberId, $month), $month);
            $availabilities = $this->getAvailabilityEntries($teamMemberId, $month);
            $personDays = 0.0;
            $details = [];

            foreach ($period as $day) {
                $date = $day->toDateString();

                if (! WorkingDaysCalculator::isWorkingDay($date, $holidayLookup)) {
                    continue;
                }

                $isPto = in_array($date, $ptoDates, true);
                $hasAvailabilityOverride = $availabilities->has($date);
                $availability = $hasAvailabilityOverride
                    ? (float) $availabilities->get($date)
                    : ($isPto ? 0.0 : 1.0);

                $details[] = [
                    'date' => $date,
                    'availability' => (float) $availability,
                    'is_pto' => $isPto,
                ];

                $personDays += $availability;
            }

            $hours = (int) round($personDays * $this->hoursPerDay);

            return [
                'person_days' => round($personDays, 2),
                'hours' => $hours,
                'details' => $details,
            ];
        };

        /** @var array $capacity */
        /** @var array $capacity */
        $capacity = $this->rememberCapacity($cacheKey, now()->addHour(), $resolver, $tags);

        return $capacity;
    }

    /**
     * Calculate the combined capacity for all active team members.
     */
    public function calculateTeamCapacity(string $month): array
    {
        $cacheKey = $this->buildCacheKey($month, 'team');
        $tags = $this->getCapacityCacheTags($month, 'team');

        /** @var array $payload */
        $payload = $this->rememberCapacity($cacheKey, now()->addHour(), function () use ($month): array {
            $activeMembers = TeamMember::where('active', true)->get();
            $totalDays = 0.0;
            $totalHours = 0;
            $members = [];

            foreach ($activeMembers as $member) {
                $capacity = $this->calculateIndividualCapacity($member->id, $month);

                $totalDays += $capacity['person_days'];
                $totalHours += $capacity['hours'];

                $members[] = [
                    'id' => $member->id,
                    'name' => $member->name,
                    'person_days' => $capacity['person_days'],
                    'hours' => $capacity['hours'],
                ];
            }

            return [
                'month' => $month,
                'person_days' => round($totalDays, 2),
                'hours' => $totalHours,
                'members' => $members,
            ];
        }, $tags);

        return $payload;
    }

    /**
     * Estimate revenue by multiplying capacity hours with hourly rates.
     */
    public function calculatePossibleRevenue(string $month): float
    {
        $cacheKey = $this->buildCacheKey($month, 'revenue');
        $tags = $this->getCapacityCacheTags($month, 'revenue');

        /** @var float $revenue */
        $revenue = $this->rememberCapacity($cacheKey, now()->addHour(), function () use ($month): float {
            $activeMembers = TeamMember::where('active', true)->get();
            $revenue = 0.0;

            foreach ($activeMembers as $member) {
                $capacity = $this->calculateIndividualCapacity($member->id, $month);
                $revenue += $capacity['hours'] * (float) $member->hourly_rate;
            }

            return round($revenue, 2);
        }, $tags);

        return $revenue;
    }

    /**
     * Return all holidays in the requested month.
     */
    public function getHolidaysForMonth(string $month): Collection
    {
        $period = $this->createMonthPeriod($month);

        return Holiday::whereBetween('date', [$period->getStartDate(), $period->getEndDate()])
            ->orderBy('date')
            ->get();
    }

    /**
     * Return approved PTO records for a team member inside the requested month.
     */
    public function getPtoForTeamMember(string $teamMemberId, string $month): Collection
    {
        $period = $this->createMonthPeriod($month);

        return Pto::where('team_member_id', $teamMemberId)
            ->where('status', 'approved')
            ->where(function ($query) use ($period): void {
                $query->whereBetween('start_date', [$period->getStartDate(), $period->getEndDate()])
                    ->orWhereBetween('end_date', [$period->getStartDate(), $period->getEndDate()])
                    ->orWhere(function ($nested) use ($period): void {
                        $nested->where('start_date', '<=', $period->getStartDate())
                            ->where('end_date', '>=', $period->getEndDate());
                    });
            })
            ->get();
    }

    /**
     * Clear redis cache for a specific month and team member.
     */
    public function forgetCapacityCacheForTeamMember(string $teamMemberId, array $months): void
    {
        $useRedis = $this->redisAvailable();

        foreach ($months as $month) {
            $tags = $this->getCapacityCacheTags($month, "team_member:{$teamMemberId}");
            $key = $this->buildCacheKey($month, $teamMemberId);

            // Always forget from array store (used in tests and as fallback)
            Cache::store('array')->forget($key);

            if ($useRedis) {
                $this->flushCapacityTags($tags);
            }
        }
    }

    /**
     * Clear redis cache for a month across all team members.
     */
    public function forgetCapacityCacheForMonth(string $month): void
    {
        // Always forget from array store (used in tests and as fallback)
        foreach (TeamMember::pluck('id') as $teamMemberId) {
            Cache::store('array')->forget($this->buildCacheKey($month, $teamMemberId));
        }
        Cache::store('array')->forget($this->buildCacheKey($month, 'team'));
        Cache::store('array')->forget($this->buildCacheKey($month, 'revenue'));

        if ($this->redisAvailable()) {
            $this->flushCapacityTags($this->getCapacityCacheTags($month));
        }
    }

    /**
     * Build the cache key used for storing individual capacity data.
     */
    private function buildCacheKey(string $month, string $teamMemberId): string
    {
        return "capacity:{$month}:{$teamMemberId}";
    }

    private function getCapacityCacheTags(string $month, ?string $context = null): array
    {
        $tags = ['capacity', "capacity:month:{$month}"];

        if ($context) {
            $tags[] = "capacity:{$context}";
        }

        return $tags;
    }

    private function flushCapacityTags(array $tags): void
    {
        if (! $this->redisAvailable()) {
            return;
        }

        try {
            /** @var CacheRepository $store */
            $store = Cache::store('redis');
            $store->tags($tags)->flush();
        } catch (Throwable) {
            // Ignore cache failures when Redis is unavailable.
        }
    }

    /**
     * Load availability entries for the team member within the month, keyed by date.
     */
    private function getAvailabilityEntries(string $teamMemberId, string $month): Collection
    {
        $period = $this->createMonthPeriod($month);

        return TeamMemberAvailability::where('team_member_id', $teamMemberId)
            ->whereBetween('date', [$period->getStartDate(), $period->getEndDate()])
            ->get()
            ->mapWithKeys(fn (TeamMemberAvailability $entry) => [$entry->date->toDateString() => (float) $entry->availability]);
    }

    public function upsertTeamMemberAvailability(string $teamMemberId, string $date, float $availability): TeamMemberAvailability
    {
        $entry = TeamMemberAvailability::updateOrCreate(
            ['team_member_id' => $teamMemberId, 'date' => $date],
            ['availability' => $availability]
        );

        $month = Carbon::createFromFormat('Y-m-d', $date)->format('Y-m');

        $this->forgetCapacityCacheForTeamMember($teamMemberId, [$month]);
        $this->forgetCapacityCacheForMonth($month);

        return $entry;
    }

    public function batchUpsertAvailability(array $updates, string $month): int
    {
        $count = 0;

        foreach ($updates as $update) {
            TeamMemberAvailability::updateOrCreate(
                ['team_member_id' => $update['team_member_id'], 'date' => $update['date']],
                ['availability' => $update['availability']]
            );
            $count++;
        }

        $this->forgetCapacityCacheForMonth($month);

        return $count;
    }

    /**
     * Create a CarbonPeriod for the given month.
     */
    private function createMonthPeriod(string $month): CarbonPeriod
    {
        $start = Carbon::createFromFormat('Y-m', $month)->startOfMonth();
        $end = $start->copy()->endOfMonth();

        return CarbonPeriod::create($start, $end);
    }

    /**
     * Expand PTO records into a unique list of dates inside the requested month.
     */
    private function buildPtoDates(Collection $ptos, string $month): array
    {
        $period = $this->createMonthPeriod($month);
        $dates = [];

        foreach ($ptos as $pto) {
            $ptoStart = Carbon::create($pto->start_date)->max($period->getStartDate());
            $ptoEnd = Carbon::create($pto->end_date)->min($period->getEndDate());

            if ($ptoStart->greaterThan($ptoEnd)) {
                continue;
            }

            foreach (CarbonPeriod::create($ptoStart, $ptoEnd) as $day) {
                $dates[] = $day->toDateString();
            }
        }

        return array_unique($dates);
    }

    private function rememberCapacity(string $key, DateTimeInterface|int $ttl, callable $callback, array $tags = []): mixed
    {
        if (! $this->redisAvailable()) {
            return Cache::store('array')->remember($key, $ttl, $callback);
        }

        try {
            /** @var CacheRepository $store */
            $store = Cache::store('redis');

            if (! empty($tags)) {
                $store = $store->tags($tags);
            }

            return $store->remember($key, $ttl, $callback);
        } catch (Throwable) {
            return Cache::store('array')->remember($key, $ttl, $callback);
        }
    }

    private function forgetCapacity(string $key): void
    {
        if (! $this->redisAvailable()) {
            return;
        }

        try {
            Cache::store('redis')->forget($key);
        } catch (Throwable) {
            // Ignore cache failures when Redis is unavailable.
        }
    }

    private function redisAvailable(): bool
    {
        if ($this->redisAvailable !== null) {
            return $this->redisAvailable;
        }

        if (! config('cache.stores.redis')) {
            return $this->redisAvailable = false;
        }

        $client = config('database.redis.client', 'phpredis');

        if ($client === 'predis') {
            return $this->redisAvailable = class_exists('\Predis\Client');
        }

        return $this->redisAvailable = extension_loaded('redis');
    }
}
