<?php

namespace App\Services\Attendance;

use App\Models\WeekoffRule;
use App\Models\WeekoffRuleAssignment;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Illuminate\Support\Collection;

class WeekoffResolver
{
    /** @var array<int, Collection<int, WeekoffRuleAssignment>> */
    protected array $assignmentsByUser = [];

    /** @var array<int, WeekoffRule|null> */
    protected array $defaultRuleByCompany = [];

    /** @var array<string, WeekoffRule> */
    protected array $rulesById = [];

    public function dayType(int $companyId, int $userId, Carbon $date): string
    {
        $rule = $this->ruleForUser($companyId, $userId, $date);
        if (! $rule || ! $rule->isActive()) {
            return $date->isWeekend() ? WeekoffRule::DAY_WEEKLY_OFF : WeekoffRule::DAY_WORKING;
        }

        return $this->dayTypeFromRule($rule, $date);
    }

    public function isWeekoff(int $companyId, int $userId, Carbon $date): bool
    {
        return $this->dayType($companyId, $userId, $date) === WeekoffRule::DAY_WEEKLY_OFF;
    }

    public function isScheduledHalfDay(int $companyId, int $userId, Carbon $date): bool
    {
        return $this->dayType($companyId, $userId, $date) === WeekoffRule::DAY_HALF;
    }

    public function isWorkingDay(int $companyId, int $userId, Carbon $date): bool
    {
        $type = $this->dayType($companyId, $userId, $date);

        return in_array($type, [WeekoffRule::DAY_WORKING, WeekoffRule::DAY_HALF], true);
    }

    /**
     * @return array<string, string> date => day type
     */
    public function dayTypeMap(int $companyId, int $userId, Carbon $start, Carbon $end): array
    {
        $map = [];
        foreach (CarbonPeriod::create($start->copy()->startOfDay(), $end->copy()->startOfDay()) as $date) {
            /** @var Carbon $date */
            $map[$date->toDateString()] = $this->dayType($companyId, $userId, $date);
        }

        return $map;
    }

    /**
     * @param  array<int>  $userIds
     * @return array<int, array<string, string>>
     */
    public function dayTypeMapsForUsers(int $companyId, array $userIds, Carbon $start, Carbon $end): array
    {
        $this->preloadAssignments($companyId, $userIds);
        $this->preloadDefaultRule($companyId);
        $this->preloadRulesForCompany($companyId);

        $maps = [];
        foreach ($userIds as $userId) {
            $maps[(int) $userId] = $this->dayTypeMap($companyId, (int) $userId, $start, $end);
        }

        return $maps;
    }

    public function currentRuleName(int $companyId, int $userId, ?Carbon $onDate = null): ?string
    {
        $date = ($onDate ?? Carbon::today())->copy()->startOfDay();
        $rule = $this->ruleForUser($companyId, $userId, $date);

        return $rule?->name;
    }

    protected function ruleForUser(int $companyId, int $userId, Carbon $date): ?WeekoffRule
    {
        if ($companyId <= 0) {
            return null;
        }

        $assignment = $this->assignmentsForUser($companyId, $userId)
            ->first(function (WeekoffRuleAssignment $item) use ($date) {
                return $item->effective_from->lte($date);
            });

        if ($assignment) {
            return $this->ruleById((int) $assignment->weekoff_rule_id);
        }

        return $this->defaultRuleForCompany($companyId, $date);
    }

    protected function dayTypeFromRule(WeekoffRule $rule, Carbon $date): string
    {
        $schedule = $rule->schedule ?? WeekoffRule::defaultSchedule();
        $weekSlot = (string) ((($date->isoWeek - 1) % 5) + 1);
        $dayKey = strtolower($date->format('D'));
        $cell = $schedule[$weekSlot][$dayKey] ?? WeekoffRule::DAY_WORKING;

        if ($cell === WeekoffRule::DAY_HALF && ! $rule->half_day_enabled) {
            return WeekoffRule::DAY_WORKING;
        }

        if (! in_array($cell, [WeekoffRule::DAY_WORKING, WeekoffRule::DAY_HALF, WeekoffRule::DAY_WEEKLY_OFF], true)) {
            return WeekoffRule::DAY_WORKING;
        }

        return $cell;
    }

    protected function assignmentsForUser(int $companyId, int $userId): Collection
    {
        if (! isset($this->assignmentsByUser[$userId])) {
            $this->assignmentsByUser[$userId] = WeekoffRuleAssignment::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('user_id', $userId)
                ->orderByDesc('effective_from')
                ->orderByDesc('id')
                ->get();
        }

        return $this->assignmentsByUser[$userId];
    }

    protected function defaultRuleForCompany(int $companyId, Carbon $date): ?WeekoffRule
    {
        if (! array_key_exists($companyId, $this->defaultRuleByCompany)) {
            $this->defaultRuleByCompany[$companyId] = WeekoffRule::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('is_company_default', true)
                ->where('status', WeekoffRule::STATUS_ACTIVE)
                ->orderByDesc('effective_from')
                ->first();
        }

        $rule = $this->defaultRuleByCompany[$companyId];
        if (! $rule || $rule->effective_from->gt($date)) {
            return null;
        }

        return $rule;
    }

    protected function ruleById(int $ruleId): ?WeekoffRule
    {
        if (! isset($this->rulesById[$ruleId])) {
            $this->rulesById[$ruleId] = WeekoffRule::withoutGlobalScopes()->find($ruleId);
        }

        return $this->rulesById[$ruleId];
    }

    /**
     * @param  array<int>  $userIds
     */
    protected function preloadAssignments(int $companyId, array $userIds): void
    {
        $missing = array_values(array_filter($userIds, fn ($id) => ! isset($this->assignmentsByUser[(int) $id])));
        if ($missing === []) {
            return;
        }

        $grouped = WeekoffRuleAssignment::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereIn('user_id', $missing)
            ->orderByDesc('effective_from')
            ->orderByDesc('id')
            ->get()
            ->groupBy('user_id');

        foreach ($missing as $userId) {
            $this->assignmentsByUser[(int) $userId] = $grouped->get((int) $userId, collect());
        }
    }

    protected function preloadDefaultRule(int $companyId): void
    {
        if (! array_key_exists($companyId, $this->defaultRuleByCompany)) {
            $this->defaultRuleByCompany[$companyId] = WeekoffRule::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('is_company_default', true)
                ->where('status', WeekoffRule::STATUS_ACTIVE)
                ->orderByDesc('effective_from')
                ->first();
        }
    }

    protected function preloadRulesForCompany(int $companyId): void
    {
        $rules = WeekoffRule::withoutGlobalScopes()->where('company_id', $companyId)->get();
        foreach ($rules as $rule) {
            $this->rulesById[(int) $rule->id] = $rule;
        }
    }
}
