<?php

declare(strict_types=1);

namespace CalendarPlugin\Domain;

use DateInterval;
use DateTimeImmutable;
use DateTimeZone;

final class RecurrenceExpander
{
    private const MAX_ITERATIONS = 512;

    public static function expand(array $event, DateTimeImmutable $windowStart, DateTimeImmutable $windowEnd, array $deletedKeys = []): array
    {
        $tz = new DateTimeZone('Europe/London');
        $start = self::parseDateTime((string) ($event['start_datetime'] ?? ''), $tz);
        $end = self::parseDateTime((string) ($event['end_datetime'] ?? ''), $tz);
        if (!$start || !$end || $end < $start) {
            return [];
        }

        $deletedMap = [];
        foreach ($deletedKeys as $key) {
            $deletedMap[(string) $key] = true;
        }

        $duration = $start->diff($end);
        $repeatType = (string) ($event['repeat_type'] ?? 'none');
        $interval = max(1, (int) ($event['repeat_interval'] ?? 1));
        $rangeMode = (string) ($event['repeat_range_mode'] ?? 'none');
        $repeatCount = isset($event['repeat_count']) ? (int) $event['repeat_count'] : null;
        $repeatUntil = null;
        if ($rangeMode === 'until' && !empty($event['repeat_until'])) {
            $repeatUntil = self::parseDateTime((string) $event['repeat_until'] . 'T23:59:59', $tz);
        }

        if ($repeatType === 'none') {
            if (self::overlaps($start, $end, $windowStart, $windowEnd)) {
                return [self::occurrence($event, $start, $end)];
            }
            return [];
        }

        $occurrences = [];
        $current = $start;
        $produced = 0;

        for ($i = 0; $i < self::MAX_ITERATIONS; $i++) {
            if ($rangeMode === 'count' && $repeatCount !== null && $produced >= $repeatCount) {
                break;
            }
            if ($repeatUntil && $current > $repeatUntil) {
                break;
            }
            $currentEnd = $current->add($duration);
            if (self::overlaps($current, $currentEnd, $windowStart, $windowEnd)) {
                $key = $current->format('c');
                if (!isset($deletedMap[$key])) {
                    $occurrences[] = self::occurrence($event, $current, $currentEnd);
                }
            }
            if ($current > $windowEnd->modify('+400 days')) {
                break;
            }
            $produced++;
            $current = self::nextStart($current, $repeatType, $interval, $event);
            if (!$current) {
                break;
            }
        }

        return $occurrences;
    }

    private static function nextStart(DateTimeImmutable $current, string $repeatType, int $interval, array $event): ?DateTimeImmutable
    {
        return match ($repeatType) {
            'daily' => $current->add(new DateInterval('P' . $interval . 'D')),
            'weekly', 'custom' => $current->add(new DateInterval('P' . $interval . 'W')),
            'monthly' => self::nextMonthlyStart($current, $interval, $event),
            'yearly' => $current->modify('+' . $interval . ' year') ?: null,
            default => null,
        };
    }

    private static function nextMonthlyStart(DateTimeImmutable $current, int $interval, array $event): ?DateTimeImmutable
    {
        $mode = (string) ($event['repeat_nth_mode'] ?? '');
        $next = $current->modify('+' . $interval . ' month');
        if (!$next) {
            return null;
        }
        if ($mode === 'day_of_month' && isset($event['repeat_nth_day']) && $event['repeat_nth_day'] !== null) {
            $day = max(1, (int) $event['repeat_nth_day']);
            $daysInMonth = (int) $next->format('t');
            return $next->setDate((int) $next->format('Y'), (int) $next->format('n'), min($day, $daysInMonth));
        }
        if ($mode === 'weekday_of_month' && isset($event['repeat_nth_pos'], $event['repeat_nth_weekday']) && $event['repeat_nth_pos'] !== null && $event['repeat_nth_weekday'] !== null) {
            $year = (int) $current->format('Y');
            $month = (int) $current->format('n');
            for ($i = 0; $i < 120; $i++) {
                [$year, $month] = self::addMonths($year, $month, max(1, $interval));
                $day = self::nthWeekdayOfMonth($year, $month, (int) $event['repeat_nth_weekday'], (int) $event['repeat_nth_pos']);
                if ($day !== null) {
                    return $current->setDate($year, $month, $day);
                }
            }
            return null;
        }
        return $next;
    }

    private static function nthWeekdayOfMonth(int $year, int $month, int $weekday, int $pos): ?int
    {
        $weekday = max(0, min(6, $weekday));
        $tz = new DateTimeZone('Europe/London');
        if ($pos === -1) {
            $last = new DateTimeImmutable(sprintf('%04d-%02d-01T00:00:00', $year, $month), $tz);
            $last = $last->modify('last day of this month');
            for ($day = (int) $last->format('j'); $day >= 1; $day--) {
                $dt = $last->setDate($year, $month, $day);
                if ((int) $dt->format('w') === $weekday) {
                    return $day;
                }
            }
            return null;
        }
        $first = new DateTimeImmutable(sprintf('%04d-%02d-01T00:00:00', $year, $month), $tz);
        $daysInMonth = (int) $first->format('t');
        $seen = 0;
        for ($day = 1; $day <= $daysInMonth; $day++) {
            $dt = $first->setDate($year, $month, $day);
            if ((int) $dt->format('w') !== $weekday) {
                continue;
            }
            $seen++;
            if ($seen === $pos) {
                return $day;
            }
        }
        return null;
    }

    private static function occurrence(array $event, DateTimeImmutable $start, DateTimeImmutable $end): array
    {
        return [
            'event_id' => (int) ($event['id'] ?? 0),
            'uid' => (string) ($event['uid'] ?? ''),
            'visibility' => (string) ($event['visibility'] ?? 'public'),
            'title' => (string) ($event['title'] ?? ''),
            'description' => (string) ($event['description'] ?? ''),
            'location' => (string) ($event['location'] ?? ''),
            'category' => (string) ($event['category'] ?? ''),
            'all_day_event' => (bool) ($event['all_day_event'] ?? false),
            'occurrence_start' => $start->format('c'),
            'occurrence_end' => $end->format('c'),
            'repeat_type' => (string) ($event['repeat_type'] ?? 'none'),
        ];
    }

    private static function overlaps(DateTimeImmutable $start, DateTimeImmutable $end, DateTimeImmutable $windowStart, DateTimeImmutable $windowEnd): bool
    {
        return $start < $windowEnd && $end > $windowStart;
    }

    private static function parseDateTime(string $value, DateTimeZone $tz): ?DateTimeImmutable
    {
        if ($value === '') {
            return null;
        }
        if (!str_contains($value, 'T') && preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
            return new DateTimeImmutable($value . 'T00:00:00', $tz);
        }
        try {
            return new DateTimeImmutable($value, $tz);
        } catch (\Throwable) {
            return null;
        }
    }

    private static function addMonths(int $year, int $month, int $delta): array
    {
        $index = ($year * 12) + ($month - 1) + $delta;
        $newYear = (int) floor($index / 12);
        $newMonth = ($index % 12) + 1;
        if ($newMonth <= 0) {
            $newMonth += 12;
            $newYear -= 1;
        }
        return [$newYear, $newMonth];
    }
}
