<?php
// =============================================================
//  include/IcsGenerator.php
//
//  Minimal RFC 5545 (iCalendar) writer — hand-rolled rather than a
//  Composer dependency, same rationale as include/Totp.php: this is
//  self-contained enough not to need one, and the format is simple and
//  well-specified enough to get right without an external library.
//  Correctness (escaping, line folding, DTEND being exclusive) is
//  verified in tests/IcsGeneratorTest.php against real calendar apps'
//  actual expectations, not just "looks right."
// =============================================================

final class IcsGenerator
{
    /**
     * @param string $calendarName  Shown as the calendar's name in most apps.
     * @param array  $events        Each: ['uid'=>string,'summary'=>string,'start'=>'Y-m-d',
     *                               'end'=>'Y-m-d' (inclusive, last day the event covers),
     *                               'description'=>?string,'location'=>?string]
     */
    public static function build(string $calendarName, array $events): string
    {
        $lines = [
            'BEGIN:VCALENDAR',
            'VERSION:2.0',
            'PRODID:-//STH NRMS//Rotation Schedule//EN',
            'CALSCALE:GREGORIAN',
            'METHOD:PUBLISH',
            self::fold('X-WR-CALNAME:' . self::escapeText($calendarName)),
        ];

        $stamp = gmdate('Ymd\THis\Z');

        foreach ($events as $event) {
            $startCompact = str_replace('-', '', $event['start']);
            // DTEND is exclusive per RFC 5545 — the day AFTER the last day the event
            // covers. Get this wrong and every multi-day block shows one day short
            // in Google Calendar / Outlook / Apple Calendar.
            $endExclusive  = date('Y-m-d', strtotime($event['end'] . ' +1 day'));
            $endCompact    = str_replace('-', '', $endExclusive);

            $lines[] = 'BEGIN:VEVENT';
            $lines[] = self::fold('UID:' . $event['uid']);
            $lines[] = 'DTSTAMP:' . $stamp;
            $lines[] = 'DTSTART;VALUE=DATE:' . $startCompact;
            $lines[] = 'DTEND;VALUE=DATE:' . $endCompact;
            $lines[] = self::fold('SUMMARY:' . self::escapeText($event['summary']));
            if (!empty($event['description'])) {
                $lines[] = self::fold('DESCRIPTION:' . self::escapeText($event['description']));
            }
            if (!empty($event['location'])) {
                $lines[] = self::fold('LOCATION:' . self::escapeText($event['location']));
            }
            $lines[] = 'END:VEVENT';
        }

        $lines[] = 'END:VCALENDAR';

        return implode("\r\n", $lines) . "\r\n";
    }

    /** RFC 5545 TEXT escaping — backslash, semicolon, comma, then newlines. Order matters. */
    private static function escapeText(string $text): string
    {
        $text = str_replace('\\', '\\\\', $text);
        $text = str_replace(';', '\\;', $text);
        $text = str_replace(',', '\\,', $text);
        $text = str_replace(["\r\n", "\n", "\r"], '\\n', $text);
        return $text;
    }

    /** RFC 5545 line folding — lines over 75 octets get continued on the next line with a leading space. */
    private static function fold(string $line): string
    {
        if (strlen($line) <= 75) return $line;

        $folded = '';
        $first  = true;
        while (strlen($line) > 0) {
            $chunkLen = $first ? 75 : 74; // continuation lines lose 1 byte to the leading space
            $folded  .= ($first ? '' : "\r\n ") . substr($line, 0, $chunkLen);
            $line     = substr($line, $chunkLen);
            $first    = false;
        }
        return $folded;
    }
}
