<?php

declare(strict_types=1);

namespace CalendarPlugin\Domain;

use DateTimeImmutable;
use DateTimeZone;

final class IcsService
{
    private const PRODID = '-//Calendar Plugin//EN';

    public function buildCalendar(
        array $events,
        callable $deletedKeysProvider,
        string $calendarName = 'Calendar',
        bool $redactPrivate = false
    ): string
    {
        $lines = [
            'BEGIN:VCALENDAR',
            'VERSION:2.0',
            'PRODID:' . self::PRODID,
            'CALSCALE:GREGORIAN',
            'X-WR-CALNAME:' . $this->escapeText($calendarName),
            'X-WR-TIMEZONE:Europe/London',
            'BEGIN:VTIMEZONE',
            'TZID:Europe/London',
            'X-LIC-LOCATION:Europe/London',
            'BEGIN:DAYLIGHT',
            'TZOFFSETFROM:+0000',
            'TZOFFSETTO:+0100',
            'TZNAME:BST',
            'DTSTART:19700329T010000',
            'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
            'END:DAYLIGHT',
            'BEGIN:STANDARD',
            'TZOFFSETFROM:+0100',
            'TZOFFSETTO:+0000',
            'TZNAME:GMT',
            'DTSTART:19701025T020000',
            'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
            'END:STANDARD',
            'END:VTIMEZONE',
        ];

        foreach ($events as $event) {
            $lines = array_merge(
                $lines,
                $this->eventToLines(
                    $event,
                    (array) $deletedKeysProvider((int) ($event['id'] ?? 0)),
                    $redactPrivate
                )
            );
        }

        $lines[] = 'END:VCALENDAR';

        return implode("\r\n", $this->foldLines($lines)) . "\r\n";
    }

    public function parseEventFromIcs(string $ics): ?array
    {
        $props = $this->extractVeventProperties($ics);
        if ($props === null) {
            return null;
        }

        $uid = (string) ($props['UID'][0] ?? '');
        $summary = (string) ($props['SUMMARY'][0] ?? 'Untitled');
        $description = (string) ($props['DESCRIPTION'][0] ?? '');
        $location = (string) ($props['LOCATION'][0] ?? '');
        $category = (string) ($props['CATEGORIES'][0] ?? '');
        $dtstartRaw = (string) ($props['DTSTART'][0] ?? '');
        $dtendRaw = (string) ($props['DTEND'][0] ?? '');
        if ($dtstartRaw === '' || $dtendRaw === '') {
            return null;
        }

        $allDay = str_contains((string) ($props['_DTSTART_PARAMS'][0] ?? ''), 'VALUE=DATE');
        $start = $this->parseIcsDateTime($dtstartRaw, $allDay);
        $end = $this->parseIcsDateTime($dtendRaw, $allDay);
        if ($start === null || $end === null) {
            return null;
        }

        $payload = [
            'uid' => $uid !== '' ? $uid : bin2hex(random_bytes(10)) . '@calendar-plugin',
            'title' => $summary,
            'description' => $description,
            'location' => $location,
            'category' => $category,
            'all_day_event' => $allDay,
            'start_datetime' => $start,
            'end_datetime' => $end,
            'repeat_type' => 'none',
            'repeat_interval' => 1,
            'repeat_nth_mode' => '',
            'repeat_nth_day' => null,
            'repeat_nth_pos' => null,
            'repeat_nth_weekday' => null,
            'repeat_range_mode' => 'none',
            'repeat_count' => null,
            'repeat_until' => null,
            'timezone' => 'Europe/London',
        ];
        if (isset($props['CLASS'][0])) {
            $payload['visibility'] = strtoupper((string) $props['CLASS'][0]) === 'PRIVATE' ? 'private' : 'public';
        } elseif (isset($props['X-CALENDARSERVER-ACCESS'][0])) {
            $payload['visibility'] = strtoupper((string) $props['X-CALENDARSERVER-ACCESS'][0]) === 'PRIVATE' ? 'private' : 'public';
        }

        $rrule = (string) ($props['RRULE'][0] ?? '');
        if ($rrule !== '') {
            $payload = array_merge($payload, $this->parseRrule($rrule));
        }

        $exdates = [];
        foreach (($props['EXDATE'] ?? []) as $exdateRaw) {
            $chunks = array_filter(array_map('trim', explode(',', (string) $exdateRaw)));
            foreach ($chunks as $chunk) {
                $asDate = $this->parseIcsDateTime($chunk, false);
                if ($asDate !== null) {
                    $exdates[] = $asDate;
                }
            }
        }
        $payload['deleted_occurrence_keys'] = $exdates;

        return $payload;
    }

    private function eventToLines(array $event, array $deletedKeys, bool $redactPrivate): array
    {
        $uid = (string) ($event['uid'] ?? '');
        $uid = $uid !== '' ? $uid : ('event-' . (string) ($event['id'] ?? 0) . '@calendar-plugin');

        $start = $this->toDateTime((string) ($event['start_datetime'] ?? ''));
        $end = $this->toDateTime((string) ($event['end_datetime'] ?? ''));
        if ($start === null || $end === null) {
            return [];
        }

        $allDay = (bool) ($event['all_day_event'] ?? false);
        $visibility = strtolower(trim((string) ($event['visibility'] ?? 'public'))) === 'private' ? 'private' : 'public';
        $isRedactedPrivate = $redactPrivate && $visibility === 'private';
        $updated = $this->toDateTime((string) ($event['updated_at'] ?? '')) ?? new DateTimeImmutable('now', new DateTimeZone('UTC'));

        $lines = [
            'BEGIN:VEVENT',
            'UID:' . $this->escapeText($uid),
            'SUMMARY:' . $this->escapeText($isRedactedPrivate ? 'Private Event' : (string) ($event['title'] ?? 'Untitled')),
            'DTSTAMP:' . $this->toUtcIcs($updated),
            'LAST-MODIFIED:' . $this->toUtcIcs($updated),
        ];
        if (!$isRedactedPrivate) {
            $icsVisibility = strtoupper($visibility === 'private' ? 'PRIVATE' : 'PUBLIC');
            $lines[] = 'CLASS:' . $icsVisibility;
            // Compatibility hint for clients that rely on CalendarServer-style access fields.
            $lines[] = 'X-CALENDARSERVER-ACCESS:' . $icsVisibility;
            $lines[] = 'DESCRIPTION:' . $this->escapeText((string) ($event['description'] ?? ''));
            $lines[] = 'LOCATION:' . $this->escapeText((string) ($event['location'] ?? ''));
            $lines[] = 'CATEGORIES:' . $this->escapeText((string) ($event['category'] ?? ''));
        }

        if ($allDay) {
            $lines[] = 'DTSTART;VALUE=DATE:' . $start->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd');
            $lines[] = 'DTEND;VALUE=DATE:' . $end->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd');
        } else {
            $lines[] = 'DTSTART;TZID=Europe/London:' . $start->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd\\THis');
            $lines[] = 'DTEND;TZID=Europe/London:' . $end->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd\\THis');
        }

        $rrule = $this->eventToRrule($event);
        if ($rrule !== null) {
            $lines[] = 'RRULE:' . $rrule;
        }

        if ($deletedKeys) {
            $parts = [];
            foreach ($deletedKeys as $key) {
                $dt = $this->toDateTime((string) $key);
                if ($dt === null) {
                    continue;
                }
                $parts[] = $dt->setTimezone(new DateTimeZone('Europe/London'))->format('Ymd\\THis');
            }
            if ($parts) {
                $lines[] = 'EXDATE;TZID=Europe/London:' . implode(',', $parts);
            }
        }

        $lines[] = 'END:VEVENT';
        return $lines;
    }

    private function eventToRrule(array $event): ?string
    {
        $type = strtolower((string) ($event['repeat_type'] ?? 'none'));
        if ($type === 'none') {
            return null;
        }

        $freq = match ($type) {
            'daily' => 'DAILY',
            'weekly', 'custom' => 'WEEKLY',
            'monthly' => 'MONTHLY',
            'yearly' => 'YEARLY',
            default => null,
        };
        if ($freq === null) {
            return null;
        }

        $interval = max(1, (int) ($event['repeat_interval'] ?? 1));
        $parts = ['FREQ=' . $freq, 'INTERVAL=' . $interval];
        if ($type === 'monthly') {
            $nthMode = (string) ($event['repeat_nth_mode'] ?? '');
            $nthDay = isset($event['repeat_nth_day']) && $event['repeat_nth_day'] !== null ? (int) $event['repeat_nth_day'] : null;
            $nthPos = isset($event['repeat_nth_pos']) && $event['repeat_nth_pos'] !== null ? (int) $event['repeat_nth_pos'] : null;
            $nthWeekday = isset($event['repeat_nth_weekday']) && $event['repeat_nth_weekday'] !== null ? (int) $event['repeat_nth_weekday'] : null;
            if ($nthMode === 'day_of_month' && $nthDay !== null) {
                $parts[] = 'BYMONTHDAY=' . max(1, min(31, $nthDay));
            } elseif ($nthMode === 'weekday_of_month' && $nthPos !== null && $nthWeekday !== null) {
                $byDay = $this->weekdayNumToToken($nthWeekday);
                if ($byDay !== null) {
                    $parts[] = 'BYDAY=' . $byDay;
                    $parts[] = 'BYSETPOS=' . ($nthPos < 0 ? -1 : max(1, min(5, $nthPos)));
                }
            }
        }

        $rangeMode = strtolower((string) ($event['repeat_range_mode'] ?? 'none'));
        if ($rangeMode === 'count' && !empty($event['repeat_count'])) {
            $parts[] = 'COUNT=' . max(1, (int) $event['repeat_count']);
        }
        if ($rangeMode === 'until' && !empty($event['repeat_until'])) {
            $until = $this->toDateTime((string) $event['repeat_until'] . 'T23:59:59');
            if ($until !== null) {
                $parts[] = 'UNTIL=' . $this->toUtcIcs($until);
            }
        }

        return implode(';', $parts);
    }

    private function parseRrule(string $rrule): array
    {
        $parts = [];
        foreach (explode(';', strtoupper(trim($rrule))) as $chunk) {
            [$k, $v] = array_pad(explode('=', $chunk, 2), 2, '');
            if ($k !== '') {
                $parts[$k] = $v;
            }
        }

        $repeatType = match ($parts['FREQ'] ?? '') {
            'DAILY' => 'daily',
            'WEEKLY' => 'weekly',
            'MONTHLY' => 'monthly',
            'YEARLY' => 'yearly',
            default => 'none',
        };

        $payload = [
            'repeat_type' => $repeatType,
            'repeat_interval' => max(1, (int) ($parts['INTERVAL'] ?? 1)),
            'repeat_nth_mode' => '',
            'repeat_nth_day' => null,
            'repeat_nth_pos' => null,
            'repeat_nth_weekday' => null,
            'repeat_range_mode' => 'none',
            'repeat_count' => null,
            'repeat_until' => null,
        ];

        if ($repeatType === 'monthly') {
            if (!empty($parts['BYMONTHDAY'])) {
                $raw = trim(explode(',', (string) $parts['BYMONTHDAY'])[0]);
                if (preg_match('/^-?\d+$/', $raw)) {
                    $payload['repeat_nth_mode'] = 'day_of_month';
                    $payload['repeat_nth_day'] = max(1, min(31, (int) $raw));
                }
            } elseif (!empty($parts['BYDAY'])) {
                $byDayRaw = trim(explode(',', (string) $parts['BYDAY'])[0]);
                $pos = null;
                $token = $byDayRaw;
                if (preg_match('/^(-?\d+)([A-Z]{2})$/', $byDayRaw, $m)) {
                    $pos = (int) $m[1];
                    $token = $m[2];
                }
                $weekday = $this->weekdayTokenToNum($token);
                if ($weekday !== null) {
                    $payload['repeat_nth_mode'] = 'weekday_of_month';
                    $payload['repeat_nth_weekday'] = $weekday;
                    if (isset($parts['BYSETPOS']) && preg_match('/^-?\d+$/', (string) $parts['BYSETPOS'])) {
                        $pos = (int) $parts['BYSETPOS'];
                    }
                    $payload['repeat_nth_pos'] = $pos === null ? 1 : ($pos < 0 ? -1 : max(1, min(5, $pos)));
                }
            }
        }

        if (isset($parts['COUNT'])) {
            $payload['repeat_range_mode'] = 'count';
            $payload['repeat_count'] = max(1, (int) $parts['COUNT']);
        } elseif (isset($parts['UNTIL'])) {
            $until = $this->parseIcsDateTime($parts['UNTIL'], false);
            if ($until !== null) {
                $payload['repeat_range_mode'] = 'until';
                $payload['repeat_until'] = substr($until, 0, 10);
            }
        }

        return $payload;
    }

    private function extractVeventProperties(string $ics): ?array
    {
        $lines = preg_split('/\r\n|\n|\r/', $ics) ?: [];
        $unfolded = [];
        foreach ($lines as $line) {
            if ($line === '') {
                continue;
            }
            if (($line[0] ?? '') === ' ' && $unfolded) {
                $unfolded[count($unfolded) - 1] .= substr($line, 1);
                continue;
            }
            $unfolded[] = $line;
        }

        $in = false;
        $props = [];
        foreach ($unfolded as $line) {
            $upper = strtoupper($line);
            if ($upper === 'BEGIN:VEVENT') {
                $in = true;
                continue;
            }
            if ($upper === 'END:VEVENT') {
                break;
            }
            if (!$in) {
                continue;
            }
            [$left, $value] = $this->splitContentLine($line);
            if ($left === '') {
                continue;
            }
            [$name, $params] = array_pad(explode(';', $left, 2), 2, '');
            $name = strtoupper(trim($name));
            if ($name === '') {
                continue;
            }
            $props[$name][] = $this->unescapeText(trim($value));
            if ($name === 'DTSTART') {
                $props['_DTSTART_PARAMS'][] = strtoupper(trim($params));
            }
        }

        return $in ? $props : null;
    }

    private function splitContentLine(string $line): array
    {
        $inQuotes = false;
        $len = strlen($line);
        for ($i = 0; $i < $len; $i++) {
            $ch = $line[$i];
            if ($ch === '"') {
                $inQuotes = !$inQuotes;
                continue;
            }
            if ($ch === ':' && !$inQuotes) {
                return [substr($line, 0, $i), substr($line, $i + 1)];
            }
        }
        return [$line, ''];
    }

    private function parseIcsDateTime(string $value, bool $dateOnly): ?string
    {
        $value = trim($value);
        if ($value === '') {
            return null;
        }

        try {
            if ($dateOnly && preg_match('/^\d{8}$/', $value)) {
                $dt = DateTimeImmutable::createFromFormat('Ymd H:i:s', $value . ' 00:00:00', new DateTimeZone('Europe/London'));
                if ($dt instanceof DateTimeImmutable) {
                    return $dt->format('Y-m-d\\T00:00:00P');
                }
            }

            if (preg_match('/^\d{8}T\d{6}Z$/', $value)) {
                $dt = DateTimeImmutable::createFromFormat('Ymd\\THis\\Z', $value, new DateTimeZone('UTC'));
                if ($dt instanceof DateTimeImmutable) {
                    return $dt->setTimezone(new DateTimeZone('Europe/London'))->format('c');
                }
            }

            if (preg_match('/^\d{8}T\d{6}$/', $value)) {
                $dt = DateTimeImmutable::createFromFormat('Ymd\\THis', $value, new DateTimeZone('Europe/London'));
                if ($dt instanceof DateTimeImmutable) {
                    return $dt->format('c');
                }
            }

            $dt = new DateTimeImmutable($value, new DateTimeZone('Europe/London'));
            return $dt->format('c');
        } catch (\Throwable) {
            return null;
        }
    }

    private function toDateTime(string $value): ?DateTimeImmutable
    {
        if ($value === '') {
            return null;
        }
        try {
            return new DateTimeImmutable($value, new DateTimeZone('Europe/London'));
        } catch (\Throwable) {
            return null;
        }
    }

    private function toUtcIcs(DateTimeImmutable $dt): string
    {
        return $dt->setTimezone(new DateTimeZone('UTC'))->format('Ymd\\THis\\Z');
    }

    private function escapeText(string $value): string
    {
        return str_replace(
            ["\\", ";", ",", "\r\n", "\n", "\r"],
            ["\\\\", "\\;", "\\,", "\\n", "\\n", "\\n"],
            $value
        );
    }

    private function unescapeText(string $value): string
    {
        return str_replace(
            ["\\n", "\\N", "\\,", "\\;", "\\\\"],
            ["\n", "\n", ",", ";", "\\"],
            $value
        );
    }

    private function weekdayNumToToken(int $weekday): ?string
    {
        return match ($weekday) {
            0 => 'SU',
            1 => 'MO',
            2 => 'TU',
            3 => 'WE',
            4 => 'TH',
            5 => 'FR',
            6 => 'SA',
            default => null,
        };
    }

    private function weekdayTokenToNum(string $token): ?int
    {
        return match (strtoupper(trim($token))) {
            'SU' => 0,
            'MO' => 1,
            'TU' => 2,
            'WE' => 3,
            'TH' => 4,
            'FR' => 5,
            'SA' => 6,
            default => null,
        };
    }

    private function foldLines(array $lines): array
    {
        $out = [];
        foreach ($lines as $line) {
            if ($line === '') {
                $out[] = $line;
                continue;
            }
            while (strlen($line) > 73) {
                $out[] = substr($line, 0, 73);
                $line = ' ' . substr($line, 73);
            }
            $out[] = $line;
        }
        return $out;
    }
}
