<?php

declare(strict_types=1);

namespace App\Module\ProjectManagement\Infrastructure\Service;

use App\Module\ProjectManagement\Domain\Entity\Task;
use App\Module\ProjectManagement\Domain\Entity\TaskRecurrence;
use App\Module\ProjectManagement\Domain\Enum\RecurrenceType;
use DateTimeImmutable;

final class RecurrenceCalculator
{
    public function getNextDate(Task $task): ?DateTimeImmutable
    {
        $recurrence     = $task->getRecurrence();
        $scheduledStart = $task->getScheduledStart();

        if (null === $recurrence || null === $scheduledStart) {
            return null;
        }

        if ($this->hasReachedEnd($recurrence)) {
            return null;
        }

        $type     = $recurrence->getType();
        $interval = $recurrence->getInterval();

        return match ($type) {
            RecurrenceType::Daily   => $this->nextDaily($scheduledStart, $interval),
            RecurrenceType::Weekly  => $this->nextWeekly($scheduledStart, $interval, $recurrence->getDaysOfWeek() ?? []),
            RecurrenceType::Monthly => $this->nextMonthly($scheduledStart, $interval, $recurrence),
            RecurrenceType::Yearly  => $this->nextYearly($scheduledStart, $interval),
            default                 => null,
        };
    }

    public function getNextEnd(Task $task, DateTimeImmutable $nextStart): ?DateTimeImmutable
    {
        $scheduledStart = $task->getScheduledStart();
        $scheduledEnd   = $task->getScheduledEnd();

        if (null === $scheduledEnd || null === $scheduledStart) {
            return null;
        }

        $duration = $scheduledStart->diff($scheduledEnd);

        return $nextStart->add($duration);
    }

    public function getNextDeadline(Task $task, DateTimeImmutable $nextStart): ?DateTimeImmutable
    {
        $scheduledStart = $task->getScheduledStart();
        $deadline       = $task->getDeadline();

        if (null === $deadline || null === $scheduledStart) {
            return null;
        }

        $offset = $scheduledStart->diff($deadline);

        return $nextStart->add($offset);
    }

    public function hasReachedEnd(TaskRecurrence $recurrence): bool
    {
        $maxOccurrences = $recurrence->getMaxOccurrences();

        if (null !== $maxOccurrences && $recurrence->getOccurrenceCount() >= $maxOccurrences) {
            return true;
        }

        $endDate = $recurrence->getEndDate();

        if (null !== $endDate) {
            $today = new DateTimeImmutable('today');

            if ($endDate < $today) {
                return true;
            }
        }

        return false;
    }

    private function nextDaily(DateTimeImmutable $start, int $interval): DateTimeImmutable
    {
        return $start->modify(sprintf('+%d days', $interval));
    }

    private function nextWeekly(DateTimeImmutable $start, int $interval, array $daysOfWeek): DateTimeImmutable
    {
        $candidate = $start->modify(sprintf('+%d weeks', $interval));

        if ([] === $daysOfWeek) {
            return $candidate;
        }

        $dayNumberMap = $this->getDayNumberMap();

        // Collect target day numbers
        $targetDayNumbers = [];
        foreach ($daysOfWeek as $day) {
            if (isset($dayNumberMap[$day])) {
                $targetDayNumbers[] = $dayNumberMap[$day];
            }
        }

        if ([] === $targetDayNumbers) {
            return $candidate;
        }

        sort($targetDayNumbers);

        // Find the first matching day in the week starting from candidate
        $weekStart       = (int) $candidate->format('N'); // 1=Mon, 7=Sun
        $candidateDayNum = $weekStart;

        foreach ($targetDayNumbers as $targetDay) {
            if ($targetDay >= $candidateDayNum) {
                $diff = $targetDay - $candidateDayNum;

                return $candidate->modify(sprintf('+%d days', $diff));
            }
        }

        // Wrap to next week's first matching day
        $diff = 7 - $candidateDayNum + $targetDayNumbers[0];

        return $candidate->modify(sprintf('+%d days', $diff));
    }

    private function nextMonthly(DateTimeImmutable $start, int $interval, TaskRecurrence $recurrence): DateTimeImmutable
    {
        $dayOfMonth  = $recurrence->getDayOfMonth();
        $weekOfMonth = $recurrence->getWeekOfMonth();
        $daysOfWeek  = $recurrence->getDaysOfWeek() ?? [];

        if (null !== $dayOfMonth) {
            return $this->nextMonthlyByDayOfMonth($start, $interval, $dayOfMonth);
        }

        if (null !== $weekOfMonth && [] !== $daysOfWeek) {
            return $this->nextMonthlyByWeekOfMonth($start, $interval, $weekOfMonth, $daysOfWeek[0]);
        }

        // Fallback: same day of month, interval months ahead
        return $this->nextMonthlyByDayOfMonth($start, $interval, (int) $start->format('j'));
    }

    private function nextMonthlyByDayOfMonth(DateTimeImmutable $start, int $interval, int $dayOfMonth): DateTimeImmutable
    {
        $year  = (int) $start->format('Y');
        $month = (int) $start->format('n');

        $month += $interval;

        while ($month > 12) {
            $month -= 12;
            ++$year;
        }

        // Handle month overflow (e.g. dayOfMonth=31 in a 30-day month)
        $daysInMonth = (int) new DateTimeImmutable(sprintf('%d-%02d-01', $year, $month))->format('t');
        $day         = min($dayOfMonth, $daysInMonth);

        return new DateTimeImmutable(sprintf(
            '%d-%02d-%02d %s',
            $year,
            $month,
            $day,
            $start->format('H:i:s'),
        ));
    }

    private function nextMonthlyByWeekOfMonth(DateTimeImmutable $start, int $interval, int $weekOfMonth, string $dayName): DateTimeImmutable
    {
        $year  = (int) $start->format('Y');
        $month = (int) $start->format('n');

        $month += $interval;

        while ($month > 12) {
            $month -= 12;
            ++$year;
        }

        $dayNumberMap = $this->getDayNumberMap();
        $targetDayNum = $dayNumberMap[$dayName] ?? 1;

        // Find the Nth occurrence of the target weekday in the target month
        $firstOfMonth = new DateTimeImmutable(sprintf('%d-%02d-01', $year, $month));
        $firstDayNum  = (int) $firstOfMonth->format('N'); // 1=Mon, 7=Sun

        // Days until first occurrence of target weekday
        $daysToFirst = ($targetDayNum - $firstDayNum + 7) % 7;
        $dayOfMonth  = 1 + $daysToFirst + ($weekOfMonth - 1) * 7;

        // Handle overflow (e.g. 5th occurrence that doesn't exist)
        $daysInMonth = (int) $firstOfMonth->format('t');

        if ($dayOfMonth > $daysInMonth) {
            // Fall back to last occurrence
            $dayOfMonth -= 7;
        }

        return new DateTimeImmutable(sprintf(
            '%d-%02d-%02d %s',
            $year,
            $month,
            $dayOfMonth,
            $start->format('H:i:s'),
        ));
    }

    private function nextYearly(DateTimeImmutable $start, int $interval): DateTimeImmutable
    {
        $year  = (int) $start->format('Y') + $interval;
        $month = (int) $start->format('n');
        $day   = (int) $start->format('j');

        // Handle leap year: Feb 29 → Feb 28
        $daysInMonth = (int) new DateTimeImmutable(sprintf('%d-%02d-01', $year, $month))->format('t');
        $day         = min($day, $daysInMonth);

        return new DateTimeImmutable(sprintf(
            '%d-%02d-%02d %s',
            $year,
            $month,
            $day,
            $start->format('H:i:s'),
        ));
    }

    /** @return array<string, int> */
    private function getDayNumberMap(): array
    {
        return [
            'monday'    => 1,
            'tuesday'   => 2,
            'wednesday' => 3,
            'thursday'  => 4,
            'friday'    => 5,
            'saturday'  => 6,
            'sunday'    => 7,
        ];
    }
}
