<?php

namespace App\Http\Controllers;

use App\Models\Calendar;
use App\Models\Event;
use App\Models\Location;
use App\Services\Event\EventAttendeeSynchronizer;
use App\Services\Event\EventRecurrence;
use App\Services\Location\Geocoder;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

class EventController extends Controller
{
    /**
     * create a new event
     */
    public function create(Calendar $calendar, Request $request)
    {
        $this->authorize('update', $calendar);

        $instance = $calendar->instanceForUser($request->user());
        $tz = $this->displayTimezone($calendar, $request);

        // build a fresh event "shell" with meta defaults
        $event = new Event;
        $event->meta = (object) [
            'title'       => '',
            'description' => '',
            'location'    => '',
            'start_at'    => null,
            'end_at'      => null,
            'all_day'     => false,
            'category'    => '',
        ];

        // if ?date=YYYY-MM-DD is present, start that day at 9am; otherwise "now"
        $anchor = $request->query('date')
            ? Carbon::parse($request->query('date'), $tz)->startOfDay()->addHours(9)
            : Carbon::now($tz);

        $anchor->second(0);

        $start = $anchor->copy()->format('Y-m-d\TH:i');
        $end   = $anchor->copy()->addHour()->format('Y-m-d\TH:i');
        $rrule = '';

        $data = compact(
            'calendar',
            'instance',
            'event',
            'start',
            'end',
            'tz',
            'rrule',
        );
        $data = array_merge($data, $this->buildRecurrenceFormData($request, $start, $tz, $rrule));
        $data = array_merge($data, $this->buildAttendeeFormData($request));

        if ($request->header('HX-Request') === 'true') {
            return view('event.partials.form-modal', $data);
        }

        return view('event.form', $data);
    }

    /**
     * edit event
     */
    public function edit(Calendar $calendar, Event $event, Request $request, EventRecurrence $recurrence)
    {
        $this->authorize('update', $calendar);

        // ensure the event belongs to the parent calendar
        if ((int) $event->calendarid !== (int) $calendar->id) {
            abort(Response::HTTP_NOT_FOUND);
        }

        $instance = $calendar->instanceForUser($request->user());
        $tz = $this->displayTimezone($calendar, $request);

        $event->load('meta', 'attendees');

        $start = $event->meta?->start_at
            ? Carbon::parse($event->meta->start_at)->timezone($tz)->format('Y-m-d\TH:i')
            : null;

        $end = $event->meta?->end_at
            ? Carbon::parse($event->meta->end_at)->timezone($tz)->format('Y-m-d\TH:i')
            : null;

        $rrule = $event->meta?->extra['rrule']
            ?? $recurrence->extractRrule($event)
            ?? '';

        $data = compact('calendar', 'instance', 'event', 'start', 'end', 'tz', 'rrule');
        $data = array_merge($data, $this->buildRecurrenceFormData($request, $start ?? '', $tz, $rrule));
        $data = array_merge($data, $this->buildAttendeeFormData($request, $event));

        if ($request->header('HX-Request') === 'true') {
            return view('event.partials.form-modal', $data);
        }

        return view('event.form', $data);
    }

    /**
     * single event view handling
     */
    public function show(Request $request, Calendar $calendar, Event $event, EventRecurrence $recurrence)
    {
        if ((int) $event->calendarid !== (int) $calendar->id) {
            abort(Response::HTTP_NOT_FOUND);
        }

        $this->authorize('view', $event);

        $event->loadMissing(['meta', 'meta.venue', 'attendees']);

        $isHtmx = $request->header('HX-Request') === 'true';
        $tz = $this->displayTimezone($calendar, $request);

        $instance = $calendar->instanceForUser($request->user());
        if ($instance) {
            $instance->loadMissing('meta');
        }

        $calendarColor = calendar_color(
            ['color' => $instance?->meta?->color],
            $instance?->calendarcolor
        );
        $calendarColorFg = $instance?->meta?->color_fg
            ?? contrast_text_color($calendarColor);
        $calendarName = $instance?->displayname ?? __('common.calendar');

        // prefer occurrence when supplied (recurring events), fall back to meta, then sabre columns
        $occurrenceParam = $request->query('occurrence');
        $occurrenceStart = null;
        if ($occurrenceParam) {
            try {
                $occurrenceStart = Carbon::parse($occurrenceParam)->utc();
            } catch (\Throwable $e) {
                $occurrenceStart = null;
            }
        }
        $occurrence = $occurrenceStart
            ? $recurrence->resolveOccurrence($event, $occurrenceStart)
            : null;

        $startUtc = $occurrence['start'] ?? ($event->meta?->start_at
            ? Carbon::parse($event->meta->start_at)->utc()
            : Carbon::createFromTimestamp($event->firstoccurence, 'UTC'));

        $endUtc = $occurrence['end'] ?? ($event->meta?->end_at
            ? Carbon::parse($event->meta->end_at)->utc()
            : ($event->lastoccurence
                ? Carbon::createFromTimestamp($event->lastoccurence, 'UTC')
                : $startUtc->copy()));

        // convert for display
        $start = $startUtc->copy()->timezone($tz);
        $end   = $endUtc->copy()->timezone($tz);

        if ($event->meta?->all_day && $end->gt($start)) {
            $end = $end->copy()->subDay();
        }

        $map = $this->buildBasemapTiles($event->meta?->venue);

        $event->setAttribute('color', $calendarColor);
        $event->setAttribute('color_fg', $calendarColorFg);

        $data = compact('calendar', 'event', 'start', 'end', 'tz', 'map', 'calendarName');

        return $isHtmx
            ? view('event.partials.details', $data)
            : view('event.show', $data);
    }

    /**
     * insert vevent into sabre’s calendarobjects + meta row
     */
    public function store(
        Request $request,
        Calendar $calendar,
        Geocoder $geocoder,
        EventRecurrence $recurrence,
        EventAttendeeSynchronizer $attendeeSync
    ): RedirectResponse
    {
        $this->authorize('update', $calendar);

        $this->normalizeRecurrenceInputs($request);

        $rules = [
            'title'       => ['required', 'string', 'max:200'],
            'description' => ['nullable', 'string'],
            'location'    => ['nullable', 'string'],
            'all_day'     => ['sometimes', 'boolean'],
            'category'    => ['nullable', 'string', 'max:50'],
            'repeat_frequency' => [
                function (string $attribute, mixed $value, $fail) {
                    if ($value === null || $value === '') {
                        return;
                    }
                    if (!in_array($value, ['daily', 'weekly', 'monthly', 'yearly'], true)) {
                        $fail(__('calendar.event.recurrence.invalid_frequency'));
                    }
                }
            ],
            'repeat_interval' => ['nullable', 'integer', 'min:1', 'max:365'],
            'repeat_weekdays' => ['nullable', 'array'],
            'repeat_weekdays.*' => ['in:SU,MO,TU,WE,TH,FR,SA'],
            'repeat_monthly_mode' => ['nullable', 'in:days,weekday'],
            'repeat_month_days' => ['nullable', 'array'],
            'repeat_month_days.*' => ['integer', 'min:1', 'max:31'],
            'repeat_month_week' => ['nullable', 'in:first,second,third,fourth,last'],
            'repeat_month_weekday' => ['nullable', 'in:SU,MO,TU,WE,TH,FR,SA'],

            'attendees' => ['nullable', 'array'],
            'attendees.*.attendee_uri' => ['nullable', 'string', 'max:500'],
            'attendees.*.email' => ['nullable', 'email', 'max:255'],
            'attendees.*.name' => ['nullable', 'string', 'max:200'],
            'attendees.*.optional' => ['nullable', 'boolean'],
            'attendees.*.role' => ['nullable', 'string', 'max:32'],
            'attendees.*.partstat' => ['nullable', 'string', 'max:32'],
            'attendees.*.cutype' => ['nullable', 'string', 'max:32'],
            'attendees.*.rsvp' => ['nullable', 'boolean'],
            'attendees.*.is_organizer' => ['nullable', 'boolean'],
            'attendees.*.extra' => ['nullable', 'array'],

            // normalized location hints (optional)
            'loc_display_name' => ['nullable', 'string'],
            'loc_place_name'   => ['nullable', 'string'],
            'loc_street'       => ['nullable', 'string'],
            'loc_city'         => ['nullable', 'string'],
            'loc_state'        => ['nullable', 'string'],
            'loc_postal'       => ['nullable', 'string'],
            'loc_country'      => ['nullable', 'string'],
            'loc_lat'          => ['nullable'],
            'loc_lon'          => ['nullable'],
        ];

        if ($request->boolean('all_day')) {
            $rules['start_at'] = ['required', 'date'];
            $rules['end_at'] = ['required', 'date', 'after_or_equal:start_at'];
        } else {
            $rules['start_at'] = ['required', 'date_format:Y-m-d\\TH:i'];
            $rules['end_at'] = ['required', 'date_format:Y-m-d\\TH:i', 'after:start_at'];
        }

        $data = $request->validate($rules);

        $tz = $this->displayTimezone($calendar, $request);
        $isAllDay = (bool) ($data['all_day'] ?? false);

        // parse input in display tz, store in utc
        if ($isAllDay) {
            [$startOn, $endOn, $startUtc, $endUtc] = $this->deriveAllDayRange(
                $data['start_at'],
                $data['end_at'],
                $tz
            );
        } else {
            $startOn = null;
            $endOn = null;
            $startUtc = $this->parseLocalDatetimeToUtc($data['start_at'], $tz);
            $endUtc   = $this->parseLocalDatetimeToUtc($data['end_at'], $tz);
        }

        $uid = Str::uuid() . '@' . parse_url(config('app.url'), PHP_URL_HOST);
        $attendees = $this->resolveAttendeesPayload($request, null, $attendeeSync);

        $rrule = $this->buildRruleFromRequest($request, $this->parseLocalInputToTz($data['start_at'], $tz, $isAllDay));
        $extra = $this->mergeRecurrenceExtra([], $rrule, $tz, $request);

        $ical = $recurrence->buildCalendar([
            'uid'         => $uid,
            'start_utc'   => $startUtc,
            'end_utc'     => $endUtc,
            'all_day'     => $isAllDay,
            'start_date'  => $startOn,
            'end_date'    => $endOn,
            'summary'     => $data['title'],
            'description' => $data['description'] ?? '',
            'location'    => $data['location'] ?? '',
            'tzid'        => $rrule ? $tz : null,
            'rrule'       => $rrule,
            'attendees'   => $attendees,
        ]);

        $event = Event::create([
            'calendarid'    => $calendar->id,
            'uri'           => Str::uuid() . '.ics',
            'lastmodified'  => time(),
            'etag'          => md5($ical),
            'size'          => strlen($ical),
            'componenttype' => 'VEVENT',
            'uid'           => $uid,
            'calendardata'  => $ical,
        ]);

        $locationId = $this->resolveLocationId($request, $geocoder, $data);

        $event->meta()->create([
            'title'       => $data['title'],
            'description' => $data['description'] ?? null,
            'location'    => $data['location'] ?? null,
            'location_id' => $locationId,
            'all_day'     => $isAllDay,
            'category'    => $data['category'] ?? null,
            'start_at'    => $startUtc,
            'end_at'      => $endUtc,
            'start_on'    => $startOn,
            'end_on'      => $endOn,
            'tzid'        => $isAllDay ? $tz : null,
            'extra'       => $extra,
        ]);

        $attendeeSync->syncRows($event, $attendees);

        return redirect()->route('calendar.show', $calendar);
    }

    /**
     * update vevent + meta
     */
    public function update(
        Request $request,
        Calendar $calendar,
        Event $event,
        EventRecurrence $recurrence,
        EventAttendeeSynchronizer $attendeeSync
    ): RedirectResponse
    {
        $this->authorize('update', $calendar);

        if ((int) $event->calendarid !== (int) $calendar->id) {
            abort(Response::HTTP_NOT_FOUND);
        }

        $this->normalizeRecurrenceInputs($request);

        $rules = [
            'title'       => ['required', 'string', 'max:200'],
            'description' => ['nullable', 'string'],
            'location'    => ['nullable', 'string'],
            'all_day'     => ['sometimes', 'boolean'],
            'category'    => ['nullable', 'string', 'max:50'],
            'repeat_frequency' => [
                function (string $attribute, mixed $value, $fail) {
                    if ($value === null || $value === '') {
                        return;
                    }
                    if (!in_array($value, ['daily', 'weekly', 'monthly', 'yearly'], true)) {
                        $fail(__('calendar.event.recurrence.invalid_frequency'));
                    }
                }
            ],
            'repeat_interval' => ['nullable', 'integer', 'min:1', 'max:365'],
            'repeat_weekdays' => ['nullable', 'array'],
            'repeat_weekdays.*' => ['in:SU,MO,TU,WE,TH,FR,SA'],
            'repeat_monthly_mode' => ['nullable', 'in:days,weekday'],
            'repeat_month_days' => ['nullable', 'array'],
            'repeat_month_days.*' => ['integer', 'min:1', 'max:31'],
            'repeat_month_week' => ['nullable', 'in:first,second,third,fourth,last'],
            'repeat_month_weekday' => ['nullable', 'in:SU,MO,TU,WE,TH,FR,SA'],

            'attendees' => ['nullable', 'array'],
            'attendees.*.attendee_uri' => ['nullable', 'string', 'max:500'],
            'attendees.*.email' => ['nullable', 'email', 'max:255'],
            'attendees.*.name' => ['nullable', 'string', 'max:200'],
            'attendees.*.optional' => ['nullable', 'boolean'],
            'attendees.*.role' => ['nullable', 'string', 'max:32'],
            'attendees.*.partstat' => ['nullable', 'string', 'max:32'],
            'attendees.*.cutype' => ['nullable', 'string', 'max:32'],
            'attendees.*.rsvp' => ['nullable', 'boolean'],
            'attendees.*.is_organizer' => ['nullable', 'boolean'],
            'attendees.*.extra' => ['nullable', 'array'],
        ];

        if ($request->boolean('all_day')) {
            $rules['start_at'] = ['required', 'date'];
            $rules['end_at'] = ['required', 'date', 'after_or_equal:start_at'];
        } else {
            $rules['start_at'] = ['required', 'date_format:Y-m-d\\TH:i'];
            $rules['end_at'] = ['required', 'date_format:Y-m-d\\TH:i', 'after:start_at'];
        }

        $data = $request->validate($rules);

        $tz = $this->displayTimezone($calendar, $request);
        $isAllDay = (bool) ($data['all_day'] ?? false);

        if ($isAllDay) {
            [$startOn, $endOn, $startUtc, $endUtc] = $this->deriveAllDayRange(
                $data['start_at'],
                $data['end_at'],
                $tz
            );
        } else {
            $startOn = null;
            $endOn = null;
            $startUtc = $this->parseLocalDatetimeToUtc($data['start_at'], $tz);
            $endUtc   = $this->parseLocalDatetimeToUtc($data['end_at'], $tz);
        }

        $uid = $event->uid;
        $attendees = $this->resolveAttendeesPayload($request, $event, $attendeeSync);

        $rrule = $this->buildRruleFromRequest($request, $this->parseLocalInputToTz($data['start_at'], $tz, $isAllDay));
        $extra = $event->meta?->extra ?? [];
        $extra = $this->mergeRecurrenceExtra($extra, $rrule, $tz, $request);
        $rruleForIcs = $rrule ?? ($extra['rrule'] ?? $recurrence->extractRrule($event));

        $ical = $recurrence->buildCalendar([
            'uid'         => $uid,
            'start_utc'   => $startUtc,
            'end_utc'     => $endUtc,
            'all_day'     => $isAllDay,
            'start_date'  => $startOn,
            'end_date'    => $endOn,
            'summary'     => $data['title'],
            'description' => $data['description'] ?? '',
            'location'    => $data['location'] ?? '',
            'tzid'        => $rruleForIcs ? $tz : null,
            'rrule'       => $rruleForIcs,
            'exdate'      => $extra['exdate'] ?? [],
            'rdate'       => $extra['rdate'] ?? [],
            'attendees'   => $attendees,
        ]);

        $event->update([
            'calendardata' => $ical,
            'etag'         => md5($ical),
            'lastmodified' => time(),
        ]);

        $event->meta()->updateOrCreate([], [
            'title'       => $data['title'],
            'description' => $data['description'] ?? null,
            'location'    => $data['location'] ?? null,
            'all_day'     => $isAllDay,
            'category'    => $data['category'] ?? null,
            'start_at'    => $startUtc,
            'end_at'      => $endUtc,
            'start_on'    => $startOn,
            'end_on'      => $endOn,
            'tzid'        => $isAllDay ? $tz : null,
            'extra'       => $extra,
        ]);

        $attendeeSync->syncRows($event, $attendees);

        return redirect()->route('calendar.show', $calendar);
    }

    /**
     * pick display timezone: calendar instance -> user -> utc
     */
    private function displayTimezone(Calendar $calendar, Request $request): string
    {
        $instanceTz = $calendar->instanceForUser($request->user())?->timezone;
        $userTz = $request->user()?->timezone;

        return $instanceTz ?: ($userTz ?: 'UTC');
    }

    /**
     * parse datetime-local in tz and return utc carbon
     */
    private function parseLocalDatetimeToUtc(string $value, string $tz): Carbon
    {
        // datetime-local: 2026-01-21T09:00
        $local = Carbon::createFromFormat('Y-m-d\TH:i', $value, $tz)->seconds(0);
        return $local->utc();
    }

    /**
     * derive all-day date range (exclusive end) and utc bounds
     */
    private function deriveAllDayRange(string $start, string $end, string $tz): array
    {
        $startLocal = $this->parseAllDayInput($start, $tz);
        $endLocal = $this->parseAllDayInput($end, $tz);

        $startOn = $startLocal->toDateString();
        $endOn = $endLocal->toDateString();

        if (Carbon::parse($endOn, $tz)->lte(Carbon::parse($startOn, $tz))) {
            $endOn = Carbon::parse($startOn, $tz)->addDay()->toDateString();
        }

        $startUtc = Carbon::parse($startOn, $tz)->startOfDay()->utc();
        $endUtc = Carbon::parse($endOn, $tz)->startOfDay()->utc();

        return [$startOn, $endOn, $startUtc, $endUtc];
    }

    private function parseAllDayInput(string $value, string $tz): Carbon
    {
        if (preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $value) === 1) {
            return Carbon::createFromFormat('Y-m-d', $value, $tz)->startOfDay();
        }

        try {
            return Carbon::createFromFormat('Y-m-d\\TH:i', $value, $tz)->seconds(0);
        } catch (\Throwable $e) {
            return Carbon::parse($value, $tz)->startOfDay();
        }
    }

    /**
     * minimal ics escaping for text properties
     */
    private function escapeIcsText(string $text): string
    {
        $text = str_replace(["\r\n", "\r", "\n"], "\\n", $text);
        $text = str_replace(["\\", ";", ","], ["\\\\", "\;", "\,"], $text);
        return $text;
    }

    private function normalizeRecurrenceInputs(Request $request): void
    {
        if ($request->has('repeat_interval') && $request->input('repeat_interval') === '') {
            $request->merge(['repeat_interval' => null]);
        }
    }

    private function buildRruleFromRequest(Request $request, Carbon $startLocal): ?string
    {
        if (! $request->has('repeat_frequency')) {
            return null;
        }

        $freq = strtolower((string) $request->input('repeat_frequency', ''));
        if ($freq === '') {
            return '';
        }

        $interval = max(1, (int) $request->input('repeat_interval', 1));
        $parts = ['FREQ=' . strtoupper($freq)];
        if ($interval > 1) {
            $parts[] = 'INTERVAL=' . $interval;
        }

        $weekdayMap = [
            'sun' => 'SU',
            'mon' => 'MO',
            'tue' => 'TU',
            'wed' => 'WE',
            'thu' => 'TH',
            'fri' => 'FR',
            'sat' => 'SA',
        ];
        $defaultWeekday = $weekdayMap[strtolower($startLocal->format('D'))] ?? 'MO';

        if ($freq === 'weekly') {
            $days = (array) $request->input('repeat_weekdays', []);
            $days = array_values(array_filter($days, fn ($d) => preg_match('/^(SU|MO|TU|WE|TH|FR|SA)$/', $d)));
            if (empty($days)) {
                $days = [$defaultWeekday];
            }
            $parts[] = 'BYDAY=' . implode(',', $days);
        }

        if ($freq === 'monthly') {
            $mode = $request->input('repeat_monthly_mode', 'days');
            if ($mode === 'weekday') {
                $week = $request->input('repeat_month_week', 'first');
                $weekday = $request->input('repeat_month_weekday', $defaultWeekday);

                $weekMap = [
                    'first' => 1,
                    'second' => 2,
                    'third' => 3,
                    'fourth' => 4,
                    'last' => -1,
                ];
                $bysetpos = $weekMap[$week] ?? 1;
                $parts[] = 'BYDAY=' . $weekday;
                $parts[] = 'BYSETPOS=' . $bysetpos;
            } else {
                $days = (array) $request->input('repeat_month_days', []);
                $days = array_values(array_filter($days, fn ($d) => is_numeric($d) && (int) $d >= 1 && (int) $d <= 31));
                if (empty($days)) {
                    $days = [(int) $startLocal->format('j')];
                }
                $parts[] = 'BYMONTHDAY=' . implode(',', $days);
            }
        }

        if ($freq === 'yearly') {
            $parts[] = 'BYMONTH=' . $startLocal->format('n');
            $parts[] = 'BYMONTHDAY=' . $startLocal->format('j');
        }

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

    private function parseLocalInputToTz(string $value, string $tz, bool $isAllDay): Carbon
    {
        return $isAllDay
            ? $this->parseAllDayInput($value, $tz)
            : Carbon::createFromFormat('Y-m-d\\TH:i', $value, $tz)->seconds(0);
    }

    private function buildRecurrenceFormData(Request $request, string $startValue, string $tz, ?string $rrule): array
    {
        $rruleValue = trim((string) ($rrule ?? ''));
        $rruleParts = [];
        foreach (array_filter(explode(';', $rruleValue)) as $chunk) {
            if (!str_contains($chunk, '=')) {
                continue;
            }
            [$key, $value] = explode('=', $chunk, 2);
            $rruleParts[strtoupper($key)] = $value;
        }

        $freq = strtolower($rruleParts['FREQ'] ?? '');
        $interval = (int) ($rruleParts['INTERVAL'] ?? 1);
        if ($interval < 1) {
            $interval = 1;
        }

        $byday = array_filter(explode(',', $rruleParts['BYDAY'] ?? ''));
        $bymonthday = array_filter(explode(',', $rruleParts['BYMONTHDAY'] ?? ''));
        $bysetpos = $rruleParts['BYSETPOS'] ?? null;

        $startDate = null;
        if ($startValue !== '') {
            try {
                $startDate = Carbon::parse($startValue, $tz);
            } catch (\Throwable $e) {
                $startDate = null;
            }
        }
        $startDate = $startDate ?? Carbon::now($tz);

        $weekdayMap = [
            'Sun' => 'SU',
            'Mon' => 'MO',
            'Tue' => 'TU',
            'Wed' => 'WE',
            'Thu' => 'TH',
            'Fri' => 'FR',
            'Sat' => 'SA',
        ];
        $defaultWeekday = $weekdayMap[$startDate->format('D')] ?? 'MO';
        $defaultMonthDay = (int) $startDate->format('j');

        $weekMap = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth'];
        $startWeek = $startDate->copy();
        $isLastWeek = $startWeek->copy()->addWeek()->month !== $startWeek->month;
        $defaultMonthWeek = $isLastWeek ? 'last' : ($weekMap[$startDate->weekOfMonth] ?? 'first');

        $monthMode = 'days';
        if (!empty($bymonthday)) {
            $monthMode = 'days';
        } elseif (!empty($byday) && $bysetpos) {
            $monthMode = 'weekday';
        }

        $setposMap = ['1' => 'first', '2' => 'second', '3' => 'third', '4' => 'fourth', '-1' => 'last'];

        $repeatFrequency = $request->old('repeat_frequency', $freq ?: '');
        $repeatInterval = $request->old('repeat_interval', $interval);
        $repeatWeekdays = $request->old('repeat_weekdays', $byday ?: [$defaultWeekday]);
        $repeatMonthDays = $request->old('repeat_month_days', $bymonthday ?: [$defaultMonthDay]);
        $repeatMonthMode = $request->old('repeat_monthly_mode', $monthMode);
        $repeatMonthWeek = $request->old('repeat_month_week', $setposMap[(string) $bysetpos] ?? $defaultMonthWeek);
        $repeatMonthWeekday = $request->old('repeat_month_weekday', $byday[0] ?? $defaultWeekday);

        $rruleOptions = [
            'daily' => __('calendar.event.recurrence.daily'),
            'weekly' => __('calendar.event.recurrence.weekly'),
            'monthly' => __('calendar.event.recurrence.monthly'),
            'yearly' => __('calendar.event.recurrence.yearly'),
        ];

        $weekdayOptions = [
            'SU' => __('calendar.event.recurrence.weekdays.sun_short'),
            'MO' => __('calendar.event.recurrence.weekdays.mon_short'),
            'TU' => __('calendar.event.recurrence.weekdays.tue_short'),
            'WE' => __('calendar.event.recurrence.weekdays.wed_short'),
            'TH' => __('calendar.event.recurrence.weekdays.thu_short'),
            'FR' => __('calendar.event.recurrence.weekdays.fri_short'),
            'SA' => __('calendar.event.recurrence.weekdays.sat_short'),
        ];
        $weekdayLong = [
            'SU' => __('calendar.event.recurrence.weekdays.sun'),
            'MO' => __('calendar.event.recurrence.weekdays.mon'),
            'TU' => __('calendar.event.recurrence.weekdays.tue'),
            'WE' => __('calendar.event.recurrence.weekdays.wed'),
            'TH' => __('calendar.event.recurrence.weekdays.thu'),
            'FR' => __('calendar.event.recurrence.weekdays.fri'),
            'SA' => __('calendar.event.recurrence.weekdays.sat'),
        ];

        return compact(
            'repeatFrequency',
            'repeatInterval',
            'repeatWeekdays',
            'repeatMonthDays',
            'repeatMonthMode',
            'repeatMonthWeek',
            'repeatMonthWeekday',
            'rruleOptions',
            'weekdayOptions',
            'weekdayLong'
        );
    }

    private function buildAttendeeFormData(Request $request, ?Event $event = null): array
    {
        $attendees = $request->old('attendees');

        if (!is_array($attendees)) {
            $attendees = $event?->attendees
                ? $event->attendees->map(function ($attendee) {
                    return [
                        'attendee_uri' => $attendee->attendee_uri,
                        'email' => $attendee->email,
                        'name' => $attendee->name,
                        'optional' => strtoupper((string) $attendee->role) === 'OPT-PARTICIPANT',
                        'rsvp' => $attendee->rsvp,
                        'verified' => !empty($attendee->attendee_user_id),
                    ];
                })->values()->all()
                : [];
        }

        $attendees = array_values(array_map(function ($attendee) {
            $row = is_array($attendee) ? $attendee : [];
            $optional = array_key_exists('optional', $row)
                ? (bool) $row['optional']
                : strtoupper((string) ($row['role'] ?? '')) === 'OPT-PARTICIPANT';

            return [
                'attendee_uri' => $row['attendee_uri'] ?? null,
                'email' => $row['email'] ?? null,
                'name' => $row['name'] ?? null,
                'optional' => $optional,
                'rsvp' => array_key_exists('rsvp', $row) ? (bool) $row['rsvp'] : true,
                'verified' => (bool) ($row['verified'] ?? false),
            ];
        }, $attendees));

        return compact('attendees');
    }

    private function mergeRecurrenceExtra(array $extra, ?string $rrule, string $tz, Request $request): array
    {
        if ($rrule === null) {
            return $extra; // no change requested
        }

        if ($rrule === '') {
            unset($extra['rrule'], $extra['exdate'], $extra['rdate'], $extra['tzid']);
            return $extra;
        }

        $extra['rrule'] = $rrule;
        $extra['tzid'] = $tz;

        $extra['exdate'] = $this->normalizeDateList($request->input('exdate', $extra['exdate'] ?? []), $tz);
        $extra['rdate']  = $this->normalizeDateList($request->input('rdate', $extra['rdate'] ?? []), $tz);

        return $extra;
    }

    private function normalizeDateList(mixed $value, string $tz): array
    {
        if (is_string($value)) {
            $value = array_filter(array_map('trim', explode(',', $value)));
        }

        if (! is_array($value)) {
            return [];
        }

        return array_values(array_filter(array_map(function ($item) use ($tz) {
            if (! $item) {
                return null;
            }

            return Carbon::parse($item, $tz)->utc()->toIso8601String();
        }, $value)));
    }

    private function resolveAttendeesPayload(
        Request $request,
        ?Event $event,
        EventAttendeeSynchronizer $attendeeSync
    ): array {
        if ($request->exists('attendees_present')) {
            return $this->normalizeRequestAttendees((array) $request->input('attendees', []));
        }

        if (!$event) {
            return [];
        }

        return $attendeeSync->extractFromCalendarData($event->calendardata);
    }

    private function normalizeRequestAttendees(array $attendees): array
    {
        $rows = [];

        foreach ($attendees as $attendee) {
            if (!is_array($attendee)) {
                continue;
            }

            $isOptional = (bool) ($attendee['optional'] ?? false);

            $rows[] = [
                'attendee_uri' => $attendee['attendee_uri'] ?? null,
                'email' => $attendee['email'] ?? null,
                'name' => $attendee['name'] ?? null,
                'role' => $isOptional ? 'OPT-PARTICIPANT' : 'REQ-PARTICIPANT',
                'partstat' => 'NEEDS-ACTION',
                'cutype' => 'INDIVIDUAL',
                'rsvp' => array_key_exists('rsvp', $attendee) ? (bool) $attendee['rsvp'] : true,
                'is_organizer' => false,
                'extra' => is_array($attendee['extra'] ?? null) ? $attendee['extra'] : null,
            ];
        }

        return $rows;
    }

    /**
     * resolve location_id from hints or geocoding
     */
    private function resolveLocationId(Request $request, Geocoder $geocoder, array $data): ?int
    {
        $raw = $data['location'] ?? null;
        if (!$raw) return null;

        $hasNormHints = $request->filled('loc_display_name') ||
            $request->filled('loc_place_name') ||
            $request->filled('loc_street') ||
            $request->filled('loc_city') ||
            $request->filled('loc_state') ||
            $request->filled('loc_postal') ||
            $request->filled('loc_country') ||
            $request->filled('loc_lat') ||
            $request->filled('loc_lon');

        if ($hasNormHints) {
            $norm = [
                'display_name' => $request->input('loc_display_name') ?: $raw,
                'place_name'   => $request->input('loc_place_name'),
                'raw_address'  => $raw,
                'street'       => $request->input('loc_street'),
                'city'         => $request->input('loc_city'),
                'state'        => $request->input('loc_state'),
                'postal'       => $request->input('loc_postal'),
                'country'      => $request->input('loc_country'),
                'lat'          => $request->filled('loc_lat') ? (float) $request->input('loc_lat') : null,
                'lon'          => $request->filled('loc_lon') ? (float) $request->input('loc_lon') : null,
            ];

            return Location::findOrCreateNormalized($norm, $raw)->id;
        }

        $norm = $geocoder->forward($raw);

        if ($norm) {
            return Location::findOrCreateNormalized($norm, $raw)->id;
        }

        return Location::labelOnly($raw)->id;
    }

    /**
     * build static basemap tiles for an event location
     */
    private function buildBasemapTiles(?Location $venue): array
    {
        $map = [
            'enabled' => false,
            'needs_key' => false,
            'url' => null,
            'zoom' => (int) config('services.geocoding.arcgis.basemap_zoom', 14),
        ];

        if (!$venue || !is_numeric($venue->lat) || !is_numeric($venue->lon)) {
            return $map;
        }

        $token = config('services.geocoding.arcgis.api_key');
        if (!$token) {
            $map['needs_key'] = true;
            return $map;
        }

        $style = config('services.geocoding.arcgis.basemap_style', 'arcgis/light-gray');
        $zoom = max(0, (int) $map['zoom']);

        $lat = max(min((float) $venue->lat, 85.05112878), -85.05112878);
        $lon = (float) $venue->lon;
        $n = 2 ** $zoom;

        $x = (int) floor(($lon + 180.0) / 360.0 * $n);
        $latRad = deg2rad($lat);
        $y = (int) floor((1.0 - log(tan($latRad) + (1 / cos($latRad))) / M_PI) / 2.0 * $n);

        $base = 'https://static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/v1';

        $tx = $x % $n;
        if ($tx < 0) {
            $tx += $n;
        }
        $ty = max(0, min($n - 1, $y));

        $map['url'] = "{$base}/{$style}/static/tile/{$zoom}/{$ty}/{$tx}?token={$token}";

        $map['enabled'] = true;
        $map['zoom'] = $zoom;

        return $map;
    }
}
