<?php

namespace App\Http\Controllers;

use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use App\Models\Calendar;
use App\Models\CalendarMeta;
use App\Models\CalendarInstance;
use App\Models\Event;
use App\Models\EventMeta;
use App\Models\Subscription;
use App\Services\Calendar\CreateCalendar;
use App\Services\Event\EventRecurrence;

class CalendarController extends Controller
{
    private const VIEWS = ['day', 'week', 'month', 'four'];

    /**
     * Consolidated calendar dashboard.
     *
     * Query params:
     *  view  = month | week | four   (default: month)
     *  date  = Y-m-d anchor date     (default: today, in user TZ)
     *
     * The view receives a `$payload` array:
     *  ├─ view          current view name
     *  ├─ range         ['start' => Carbon, 'end' => Carbon]
     *  ├─ calendars     keyed by calendar id (for the left-hand toggle list)
     *  └─ events        flat list of VEVENTs in that range
     */
    public function index(Request $request, EventRecurrence $recurrence)
    {
        /**
         *
         * manage parameters and core variables
         */

        // set the calendar key
        $principal = auth()->user()->principal_uri;

        // user settings
        $user = $request->user();
        $tz = $user->timezone ?? config('app.timezone');
        $timeFormat = $user->getSetting('app.time_format', '12');

        // settings
        $defaultView    = $user->getSetting('calendar.last_view', 'month');
        $defaultDate    = $user->getSetting('calendar.last_date', Carbon::today($tz)->toDateString());
        $defaultDensity = (int) $user->getSetting('calendar.last_density', 30);

        // week start preference
        $weekStartPref = $user->getSetting('calendar.week_start', 'sunday'); // 'sunday'|'monday'
        $weekStartPref = in_array($weekStartPref, ['sunday', 'monday'], true)
            ? $weekStartPref
            : 'sunday';
        $weekStart = $weekStartPref === 'monday' ? Carbon::MONDAY : Carbon::SUNDAY;
        $weekEnd = (int) (($weekStart + 6) % 7);

        // get the view and time range
        [$view, $range] = $this->resolveRange($request, $tz, $weekStart, $weekEnd, $defaultView, $defaultDate);
        $today = Carbon::today($tz)->toDateString();

        // get the display density, if present (in minutes for each step)
        $stepMinutes = (int) $request->query('density', $defaultDensity);
        if (! in_array($stepMinutes, [15, 30, 60], true)) { // lock it down
            $stepMinutes = 30;
        }
        $labelEvery = match ($stepMinutes) { // how many 15-min slots per label/row
            15 => 1,
            30 => 2,
            60 => 4,
        };

        // date range span and controls
        $span = $this->gridSpan($view, $range, $weekStart, $weekEnd);
        $nav  = $this->navDates($view, $range['start'], $tz);

        // get the user's visible calendars from the left bar
        $visible = collect($request->query('c', []));

        // keep a stable anchor date for forms that aren't the nav buttons
        $anchorDate = $request->query('date', now($tz)->toDateString());

        // persist settings
        if ($request->hasAny(['view', 'date', 'density'])) {
            $user->setSetting('calendar.last_view', $view);
            $user->setSetting('calendar.last_date', $range['start']->toDateString());
            $user->setSetting('calendar.last_density', (string) $stepMinutes);
        }

        /**
         *
         * calendars
         */

        $calendars = Calendar::query()
            ->dashboardForPrincipal($principal)
            ->get();

        $calendars = $calendars->map(function ($cal) use ($visible) {
            $cal->visible = $visible->isEmpty() || $visible->contains($cal->slug);
            return $cal;
        });

        $calendar_map = $calendars->keyBy('id');

        /**
         *
         * get events for calendars in range
         */

        // get all the events in one query
        $events = Event::forCalendarsInRange(
            $calendars->pluck('id'),
            $span['start'],
            $span['end']
        );

        $events = $this->buildEventPayloads(
            $events,
            $calendar_map,
            $timeFormat,
            $view,
            $range,
            $tz,
            $recurrence,
            $span,
        );

        /**
         *
         * mini calendar
         */

        // create the mini calendar grid based on the mini cal controls
        $mini_anchor = $request->query('mini', $range['start']->toDateString());

        // anchor is a DATE string, so create it explicitly in the user tz
        $mini_start = Carbon::createFromFormat('Y-m-d', $mini_anchor, $tz)
            ->startOfMonth();

        $mini_nav = [
            'prev'  => $mini_start->copy()->subMonth()->toDateString(),
            'next'  => $mini_start->copy()->addMonth()->toDateString(),
            'today' => Carbon::today($tz)->startOfMonth()->toDateString(),
            'label' => $mini_start->format('F Y'),
        ];
        $mini_headers = $this->weekdayHeaders($tz, $weekStart);

        // compute the mini's 42-day span (Mon..Sun, 6 rows)
        $mini_grid_start = $mini_start->copy()->startOfWeek($weekStart);
        $mini_grid_end   = $mini_start->copy()->endOfMonth()->endOfWeek($weekEnd);
        if ($mini_grid_start->diffInDays($mini_grid_end) + 1 < 42) {
            $mini_grid_end->addWeek();
        }

        // fetch events specifically for the mini-span
        $mini_events = Event::forCalendarsInRange(
            $calendars->pluck('id'),
            $mini_grid_start,
            $mini_grid_end
        );

        $mini_events = $this->buildEventPayloads(
            $mini_events,
            $calendar_map,
            $timeFormat,
            $view,
            ['start' => $mini_grid_start, 'end' => $mini_grid_end],
            $tz,
            $recurrence,
            ['start' => $mini_grid_start, 'end' => $mini_grid_end],
        );

        // now build the mini from mini_events (not from $events)
        $mini = $this->buildMiniGrid($mini_start, $mini_events, $tz, $weekStart, $weekEnd);

        /**
         *
         * main calendar grid
         */

        // create the calendar grid of days
        $grid = $this->buildCalendarGrid($view, $range, $events, $tz, $weekStart, $weekEnd);

        // get the title
        $header = $this->headerTitle($view, $range['start'], $range['end']);

        // format the data for the frontend, including separate arrays for events specifically and the big grid
        $payload = [
            'view'       => $view,
            'range'      => $range,
            'nav'        => $nav,
            'header'     => $header,
            'active'     => [
                'date'   => $range['start']->toDateString(),
                'year'   => $range['start']->format('Y'),
                'month'  => $range['start']->format("F"),
                'day'    => $range['start']->format("d"),
            ],
            'week_start' => $weekStart,
            'calendars'  => $calendars->mapWithKeys(function ($cal)
            {
                // compute colors
                $color = $cal->meta_color
                    ?? $cal->calendarcolor
                    ?? default_calendar_color();
                $colorFg = $cal->meta_color_fg
                    ?? contrast_text_color($color);

                return [
                    $cal->id => [
                        'id'        => $cal->id,
                        'slug'      => $cal->slug,
                        'name'      => $cal->displayname,
                        'color'     => $color,
                        'color_fg'  => $colorFg,
                        'visible'   => $cal->visible,
                        'is_remote' => $cal->is_remote,
                    ],
                ];
            }),
            'hgroup'       => $this->viewHeaders($view, $range, $tz, $weekStart),
            'events'       => $events,    // keyed by occurrence
            'grid'         => $grid,      // day objects hold only ID-sets
            'mini'         => $mini,      // mini calendar days with events for indicators
            'mini_nav'     => $mini_nav,  // separate mini calendar navigation
            'mini_headers' => $mini_headers,
        ];

        // time-based payload values
        $timeBased = in_array($view, ['day', 'week', 'four'], true);
        if ($timeBased)
        {
            // create the time gutter if we're in a time-based view
            $payload['slots'] = $this->timeSlots($range['start'], $tz, $timeFormat);
            $payload['time_format'] = $timeFormat; // optional, if the blade cares

            // add the now indicator
            $payload['now'] = $this->nowIndicator($view, $range, $tz, 15);
        }

        // send the density array always, even though it doesn't matter for month
        $payload['density'] = [
            'step'        => $stepMinutes,  // 15|30|60
            'label_every' => $labelEvery,   // 1|2|4
            'anchor'      => $anchorDate,
        ];

        return view('calendar.index', $payload);
    }

    /**
     * create sabre calendar + meta
     */
    public function store(Request $request, CreateCalendar $creator)
    {
        $data = $request->validate([
            'name'        => 'required|string|max:100',
            'description' => 'nullable|string|max:255',
            'timezone'    => 'required|string|max:64',
            'color'       => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/',
            'redirect'    => 'nullable|string', // where to go after creating
        ]);

        $creator->create($request->user(), $data);
        $redirect = $data['redirect'] ?? route('calendar.index');

        return redirect($redirect)->with('toast', [
            'message' => __('Calendar created!'),
            'type'    => 'success',
        ]);
    }

    /**
     * show calendar details
     */
    public function show(Calendar $calendar)
    {
        $this->authorize('view', $calendar);

        $calendar->load([
            'meta',
            'instances' => fn ($q) => $q->where('principaluri', auth()->user()->uri),
        ]);

        /* grab the single instance for convenience in the view */
        $instance = $calendar->instances->first();
        $caldavUrl = $instance?->caldavUrl();   // null-safe

        /* events + meta, newest first */
        $events = $calendar->events()
            ->with('meta')
            ->orderByDesc('lastmodified')
            ->get();

        return view(
            'calendar.show',
            compact('calendar', 'instance', 'events', 'caldavUrl')
        );
    }

    /**
     * edit calendar page
     */
    public function edit(Calendar $calendar)
    {
        $this->authorize('update', $calendar);

        $calendar->load([
        'meta',
        'instances' => fn ($q) =>
            $q->where('principaluri', auth()->user()->uri),
        ]);

        $instance = $calendar->instances->first();   // may be null but shouldn’t

        return view('calendar.edit', compact('calendar', 'instance'));
    }

    /**
     * update sabre + meta records
     */
    public function update(Request $request, Calendar $calendar)
    {
        $this->authorize('update', $calendar);

        $data = $request->validate([
            'name'        => 'required|string|max:100',
            'description' => 'nullable|string|max:255',
            'timezone'    => 'required|string',
            'color'       => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/',
        ]);

        // update the instance row
        $calendar->instances()
            ->where('principaluri', auth()->user()->uri)
            ->update([
                'displayname' => $data['name'],
                'description' => $data['description'] ?? '',
                'calendarcolor' => $data['color'] ?? null,
                'timezone'    => $data['timezone'],
            ]);

        // bump synctoken on master calendar row
        $calendar->increment('synctoken');

        // update calendar meta (our table)
        $color = calendar_color($data);
        $calendar->meta()->updateOrCreate([], [
            'color'    => $color,
            'color_fg' => contrast_text_color($color),
        ]);

        return redirect()
            ->route('calendar.show', $calendar)
            ->with('toast', __('Calendar saved successfully!'));
    }

    /**
     *
     * Delete calendar
     * @todo
     */
    public function destroy(Calendar $calendar)
    {
        $this->authorize('delete', $calendar);
        $calendar->delete();          // cascades to meta via FK
        return redirect()->route('calendar.index');
    }

    /**
     *
     * Private helpers
     */

    /**
     *
     * Prepare nav dates for the movement buttons in the calendar header
     *
     * @return array
     *  [
     *     'prev'  => 2026-08-19
     *     'next'  => 2026-08-21
     *     'today' => 2026-08-20
     *  ]
     */
    private function navDates(string $view, Carbon $start, string $tz): array
    {
        // always compute in the user tz so the UX is consistent
        $start = $start->copy()->tz($tz);

        return match ($view)
        {
            'day' => [
                'prev'  => $start->copy()->subDay()->toDateString(),
                'next'  => $start->copy()->addDay()->toDateString(),
                'today' => Carbon::today($tz)->toDateString(),
            ],
            'week' => [
                'prev'  => $start->copy()->subWeek()->toDateString(),
                'next'  => $start->copy()->addWeek()->toDateString(),
                'today' => Carbon::today($tz)->toDateString(),
            ],
            'four' => [
                'prev'  => $start->copy()->subDays(4)->toDateString(),
                'next'  => $start->copy()->addDays(4)->toDateString(),
                'today' => Carbon::today($tz)->toDateString(),
            ],
            default => [ // month
                'prev'  => $start->copy()->subMonth()->startOfMonth()->toDateString(),
                'next'  => $start->copy()->addMonth()->startOfMonth()->toDateString(),
                'today' => Carbon::today($tz)->toDateString(),
            ],
        };
    }

    /**
     *
     * Interpret $view and $date filters and normalize into a Carbon range
     *
     * @return array
     *  [
     *     $view,
     *     [
     *        'start' => Carbon,
     *        'end' => Carbon
     *     ]
     *  ]
     */
    private function resolveRange(
        Request $request,
        string $tz,
        int $weekStart,
        int $weekEnd,
        string $defaultView,
        string $defaultDate
    ): array {
        // get the view
        $requestView = $request->query('view', $defaultView);
        $view = in_array($requestView, self::VIEWS, true)
            ? $requestView
            : 'month';
        $date = $request->query('date', $defaultDate);

        // anchor date in the user's timezone
        $anchor = Carbon::createFromFormat('Y-m-d', $date, $tz)->startOfDay();

        // set dates based on view
        switch ($view)
        {
            case 'day':
                $start = $anchor->copy()->startOfDay();
                $end   = $anchor->copy()->endOfDay();
                break;

            case 'week':
                $start = $anchor->copy()->startOfWeek($weekStart);
                $end   = $anchor->copy()->endOfWeek($weekEnd);
                break;

            case 'four':
                // a rolling 4-day "agenda" view starting at anchor
                $start = $anchor->copy()->startOfDay();
                $end   = $anchor->copy()->addDays(3)->endOfDay();
                break;

            default: // month
                $start = $anchor->copy()->startOfMonth();
                $end   = $anchor->copy()->endOfMonth();
        }

        return [$view, ['start' => $start, 'end' => $end]];
    }

    /**
     * Create the grid for each view with pre- and post-padding if needed
     *
     *  This is different from resolveRange() as the rendered span != logical range
     *
     *  Month: start of the month's first week to end of the last week
     *  Week:  Sunday to Saturday
     *  Four:  selected day to 3 days later
     *  Day:   start of the day to the end of the day
     */
    private function gridSpan(string $view, array $range, int $weekStart, int $weekEnd): array
    {
        switch ($view)
        {
            case 'day':
                $start = $range['start']->copy()->startOfDay();
                $end   = $range['start']->copy()->endOfDay();
                break;

            case 'week':
                $start = $range['start']->copy()->startOfWeek($weekStart);
                $end   = $range['start']->copy()->endOfWeek($weekEnd);
                break;

            case 'four':
                $start = $range['start']->copy()->startOfDay();
                $end   = $range['start']->copy()->addDays(3);
                break;

            default: // month
                $start = $range['start']->copy()->startOfMonth()->startOfWeek($weekStart);
                $end   = $range['end']->copy()->endOfMonth()->endOfWeek($weekEnd);
        }

        return ['start' => $start, 'end' => $end];
    }

    /**
     *
     * Prepare the calendar title based on the view
     *
     * @return array
     *  [
     *     'strong' => 'August 20',
     *     'span'   => '2026',
     *  ]
     */
    private function headerTitle(string $view, Carbon $start, Carbon $end): array
    {
        $sameDay   = $start->isSameDay($end);
        $sameMonth = $start->isSameMonth($end);
        $sameYear  = $start->isSameYear($end);

        // month default
        $strong = $start->format('F');
        $span   = $start->format('Y');

        if ($view === 'day' || $sameDay) {
            return [
                'strong' => $start->format('F j'),
                'span'   => $start->format('Y'),
            ];
        }

        if (in_array($view, ['week', 'four'], true)) {
            if ($sameMonth && $sameYear) {
                return [
                    'strong' => $start->format('F j') . ' to ' . $end->format('j'),
                    'span'   => $start->format('Y'),
                ];
            }

            if ($sameYear) {
                return [
                    'strong' => $start->format('F') . ' to ' . $end->format('F'),
                    'span'   => $start->format('Y'),
                ];
            }

            return [
                'strong' => $start->format('F Y') . ' to ' . $end->format('F Y'),
                'span'   => null,
            ];
        }

        return ['strong' => $strong, 'span' => $span];
    }

    /**
     *
     * Create the time gutter for time-based views
     */
    private function timeSlots(Carbon $dayStart, string $tz, string $timeFormat): array
    {
        $minutesPerSlot = 15;
        $slotsPerDay = intdiv(24 * 60, $minutesPerSlot); // 96

        $format = $timeFormat === '24' ? 'H:i' : 'g:i a';

        $slots = [];
        $t = $dayStart->copy()->tz($tz)->startOfDay();

        for ($i = 0; $i < $slotsPerDay; $i++) {
            $slots[] = [
                'iso'      => $t->toIso8601String(),
                'label'    => $t->format($format),
                'key'      => $t->format('H:i'),   // stable "machine" value
                'index'    => $i,                  // 0..95
                'minutes'  => $i * $minutesPerSlot,
                'duration' => $minutesPerSlot,     // handy for styling math
            ];

            $t->addMinutes($minutesPerSlot);
        }

        return $slots;
    }

    /**
     *
     * Time-based layout slot placement
     *
     * Placements object:
     *  [
     *    'start_line' => '24',
     *    'end_line'   => '32',
     *    'span'       => '4',
     *    'duration'   => '60',
     *  ]
     **/
    private function slotPlacement(
        Carbon $startLocal,
        ?Carbon $endLocal,
        Carbon $rangeStart,
        string $view,
        int $minutesPerSlot = 15
    ): array
    {
        $start = $startLocal->copy();
        $end   = ($endLocal ?? $startLocal)->copy();

        // get the real duration in minutes
        $durationMinutes = max(0, $start->diffInMinutes($end, false));

        // duration for display purposes
        $displayMinutes = $durationMinutes > 0 ? $durationMinutes : $minutesPerSlot;

         // row placement (96 rows when minutesPerSlot=15)
        $startMinutesFromMidnight = ($start->hour * 60) + $start->minute;
        $startRow = intdiv($startMinutesFromMidnight, $minutesPerSlot) + 1;

        $rowSpan = max(1, (int) ceil($displayMinutes / $minutesPerSlot));
        $endRow  = $startRow + $rowSpan;

        // column placement
        $maxCols = match ($view) {
            'day'  => 1,
            'four' => 4,
            'week' => 7,
            default => 1, // month won’t use this
        };
        // rangeStart is already the "first day column" for week/four/day
        $startCol = $rangeStart->copy()->startOfDay()->diffInDays($start->copy()->startOfDay()) + 1;

        // clamp to view columns
        $startCol = max(1, min($maxCols, $startCol));

        return [
            'start_row' => $startRow,
            'end_row'   => $endRow,
            'row_span'  => $rowSpan,
            'duration'  => $durationMinutes,
            'start_col' => $startCol,
        ];
    }

    /**
     * Expand events (including recurrence) into view-ready payloads.
     */
    private function buildEventPayloads(
        Collection $events,
        Collection $calendarMap,
        string $timeFormat,
        string $view,
        array $range,
        string $tz,
        EventRecurrence $recurrence,
        array $span
    ): Collection {
        $uiFormat = $timeFormat === '24' ? 'H:i' : 'g:ia';
        $spanStartUtc = $span['start']->copy()->utc();
        $spanEndUtc = $span['end']->copy()->utc();

        return $events->flatMap(function ($e) use (
            $calendarMap,
            $uiFormat,
            $view,
            $range,
            $tz,
            $recurrence,
            $spanStartUtc,
            $spanEndUtc
        ) {
            $cal = $calendarMap[$e->calendarid];
            $timezone = $cal->timezone ?? config('app.timezone');

            $color = $cal['meta_color']
                ?? $cal['calendarcolor']
                ?? default_calendar_color();
            $colorFg = $cal['meta_color_fg']
                ?? contrast_text_color($color);

            $occurrences = [];
            $isRecurring = $recurrence->isRecurring($e);

            if ($isRecurring) {
                $occurrences = $recurrence->expand($e, $spanStartUtc, $spanEndUtc);
            }

            if (empty($occurrences) && !$isRecurring) {
                $startUtc = $e->meta?->start_at
                    ? Carbon::parse($e->meta->start_at)->utc()
                    : Carbon::createFromTimestamp($e->firstoccurence, 'UTC');
                $endUtc = $e->meta?->end_at
                    ? Carbon::parse($e->meta->end_at)->utc()
                    : ($e->lastoccurence
                        ? Carbon::createFromTimestamp($e->lastoccurence, 'UTC')
                        : $startUtc->copy());

                $occurrences[] = [
                    'start' => $startUtc,
                    'end' => $endUtc,
                    'recurrence_id' => null,
                ];
            }

            return collect($occurrences)->map(function ($occ) use (
                $e,
                $cal,
                $uiFormat,
                $view,
                $range,
                $tz,
                $timezone,
                $color,
                $colorFg
            ) {
                $startUtc = $occ['start'];
                $endUtc = $occ['end'];

                $startLocal = $startUtc->copy()->timezone($timezone);
                $endLocal = $endUtc->copy()->timezone($timezone);

                $startForGrid = $startUtc->copy()->tz($tz);
                $endForGrid = $endUtc->copy()->tz($tz);

                $placement = $this->slotPlacement(
                    $startForGrid,
                    $endForGrid,
                    $range['start']->copy()->tz($tz),
                    $view,
                    15
                );

                $occurrenceId = $occ['recurrence_id']
                    ? ($e->id . ':' . $occ['recurrence_id'])
                    : (string) $e->id;

                return [
                    'id'              => $e->id,
                    'occurrence_id'   => $occurrenceId,
                    'occurrence'      => $occ['recurrence_id']
                        ? $startUtc->toIso8601String()
                        : null,
                    'calendar_id'     => $e->calendarid,
                    'calendar_slug'   => $cal->slug,
                    'title'           => $e->meta->title ?? 'No title',
                    'description'     => $e->meta->description ?? 'No description.',
                    'start'           => $startUtc->toIso8601String(),
                    'end'             => $endUtc->toIso8601String(),
                    'start_ui'        => $startLocal->format($uiFormat),
                    'end_ui'          => $endLocal->format($uiFormat),
                    'timezone'        => $timezone,
                    'visible'         => $cal->visible,
                    'color'           => $color,
                    'color_fg'        => $colorFg,
                    'start_row'       => $placement['start_row'],
                    'end_row'         => $placement['end_row'],
                    'row_span'        => $placement['row_span'],
                    'start_col'       => $placement['start_col'],
                    'duration'        => $placement['duration'],
                ];
            });
        })->keyBy('occurrence_id');
    }

    /**
     *
     * Assemble an array of day-objects for the requested view.
     *
     * Day object shape:
     *  [
     *    'date'      => '2025-07-14',
     *    'label'     => '14',            // two-digit day number
     *    'in_month'  => true|false,      // helpful for grey-out styling
     *    'events'    => [ …event payloads… ]
     *  ]
     *
     * For the "month" view the return value also contains
     *   'weeks' => [ [7 day-objs], [7 day-objs], … ]
     */
    private function buildCalendarGrid(
        string $view,
        array $range,
        Collection $events,
        string $tz,
        int $weekStart,
        int $weekEnd): array
    {
        // use the same span the events were fetched for (month padded to full weeks, etc.)
        ['start' => $grid_start, 'end' => $grid_end] = $this->gridSpan($view, $range, $weekStart, $weekEnd);

        // today checks
        $today = Carbon::today($tz)->toDateString();

        // index events by YYYY-MM-DD for quick lookup
        $events_by_day = [];
        foreach ($events as $ev)
        {
            $evTz = $ev['timezone'] ?? $tz;
            $start = Carbon::parse($ev['start'])->tz($evTz);
            $end = $ev['end'] ? Carbon::parse($ev['end'])->tz($evTz) : $start;

            for ($d = $start->copy()->startOfDay();
                 $d->lte($end->copy()->endOfDay());
                 $d->addDay()) {

                $key = $d->toDateString();
                $events_by_day[$key][] = $ev['occurrence_id'] ?? $ev['id'];
            }
        }

        // view span bounds and build day objects
        $days = [];
        for ($day = $grid_start->copy(); $day->lte($grid_end); $day->addDay()) {
             $iso = $day->toDateString();

             $days[] = [
                 'date'     => $iso,
                 'label'    => $day->format('j'),
                 'in_month' => $day->month === $range['start']->month,
                 'is_today' => $day->isSameDay($today),
                 'events'   => array_fill_keys($events_by_day[$iso] ?? [], true),
             ];
        }

        return $view === 'month'
            ? ['days' => $days, 'weeks' => array_chunk($days, 7)]
            : ['days' => $days];
    }

    /**
     *
     * Create the specific view's date headers
     *
     *  For day/week/four, this is an array of number dates and their day name,
     *  for month it's the user's preferred weekdays
     */
    private function viewHeaders(string $view, array $range, string $tz, int $weekStart): array
    {
        $start = $range['start']->copy()->tz($tz);
        $end   = $range['end']->copy()->tz($tz);
        $today = Carbon::today($tz)->toDateString();

        // month view: weekday headers (Sunday..Saturday for now)
        if ($view === 'month') {
            return collect($this->weekdayHeaders($tz, $weekStart))
                ->map(fn ($h) => $h + ['is_today' => false])
                ->all();
        }

        // day/week/four: column headers for each day in range
        $headers = [];
        for ($d = $start->copy()->startOfDay(); $d->lte($end); $d->addDay()) {
            $date = $d->toDateString();
            $headers[] = [
                'date' => $d->toDateString(),             // 2026-01-31
                'day'  => $d->format('j'),                // 31
                'dow'  => $d->translatedFormat('l'),      // Saturday (localized)
                'dow_short' => $d->translatedFormat('D'), // Sat (localized)
                'month' => $d->translatedFormat('M'),     // Jan (localized)
                'is_today'  => $date === $today,          // flag for viewing real today
            ];
        }

        return $headers;
    }

    /**
     *
     * Specific headers for month views (full and mini)
     */
    private function weekdayHeaders(string $tz, int $weekStart): array
    {
        $headers = [];
        $d = Carbon::now($tz)->startOfWeek($weekStart);

        for ($i = 0; $i < 7; $i++) {
            $headers[] = [
                // stable key (0..6 from the start day)
                'key'   => $i,
                // Sun, Mon...
                'label' => $d->translatedFormat('D'),
            ];
            $d->addDay();
        }

        return $headers;
    }

    /**
     *
     * Build the mini-month grid for day buttons in the bottom left of the UI
     *
     * Returns ['days' => [
     *   [
     *     'date'      => '2025-06-30',
     *     'label'     => '30',
     *     'in_month'  => false,
     *     'events'    => [id, id …]
     *   ], …
     * ]]
     */
    private function buildMiniGrid(
        Carbon $monthStart,
        Collection $events,
        string $tz,
        int $weekStart,
        int $weekEnd): array
    {
        $monthStart = $monthStart->copy()->tz($tz);
        $monthEnd   = $monthStart->copy()->endOfMonth();

        $gridStart = $monthStart->copy()->startOfWeek($weekStart);
        $gridEnd   = $monthEnd->copy()->endOfWeek($weekEnd);

        if ($gridStart->diffInDays($gridEnd) + 1 < 42) {
            $gridEnd->addWeek();
        }

        $today = Carbon::today($tz)->toDateString();

        // map event ids by yyyy-mm-dd in USER tz (so indicators match what user sees)
        $byDay = [];
        foreach ($events as $ev) {
            $s = Carbon::parse($ev['start'])->tz($tz);
            $e = $ev['end'] ? Carbon::parse($ev['end'])->tz($tz) : $s;

            for ($d = $s->copy()->startOfDay(); $d->lte($e->copy()->endOfDay()); $d->addDay()) {
                $byDay[$d->toDateString()][] = $ev['occurrence_id'] ?? $ev['id'];
            }
        }

        $days = [];
        for ($d = $gridStart->copy(); $d->lte($gridEnd); $d->addDay()) {
            $iso = $d->toDateString();
            $days[] = [
                'date'     => $iso,
                'label'    => $d->format('j'),
                'in_month' => $d->between($monthStart, $monthEnd),
                'is_today' => $iso === $today,
                'events'   => $byDay[$iso] ?? [],
            ];
        }

        return ['days' => $days];
    }

    /**
     * Place the "now" indicator on the grid if we're in a time-based view and on the current day
     *
     * Returns:
     *  [
     *    'show' => bool,
     *    'row' => int,        // 1..96 (15-min grid line)
     *    'col_start' => int,  // grid column start
     *    'col_end' => int,    // grid column end
     *  ]
     */
    private function nowIndicator(string $view, array $range, string $tz, int $minutesPerSlot = 15, int $gutterCols = 1): array
    {
        // only meaningful for time-based views
        if (!in_array($view, ['day', 'week', 'four'], true)) {
            return ['show' => false, 'row' => 1, 'day_col' => 1, 'col_start' => 1, 'col_end' => 2];
        }

        $now = Carbon::now($tz);

        $start = $range['start']->copy()->tz($tz)->startOfDay();
        $end   = $range['end']->copy()->tz($tz)->endOfDay();

        // show only if "now" is inside the visible range
        if (! $now->betweenIncluded($start, $end)) {
            return ['show' => false, 'row' => 1, 'day_col' => 1, 'col_start' => 1, 'col_end' => 2];
        }

        // row: minutes since midnight, snapped down to slot size
        $minutes = ($now->hour * 60) + $now->minute;
        $snapped = intdiv($minutes, $minutesPerSlot) * $minutesPerSlot;
        $row = intdiv($snapped, $minutesPerSlot) + 1; // 1-based

        // column: 1..N where 1 is the first day column in the events grid
        if ($view === 'day') {
            $dayCol = 1;
        } else {
            // IMPORTANT: compare dates at midnight to avoid fractional diffs
            $todayStart = $now->copy()->startOfDay();
            $dayCol = $start->diffInDays($todayStart) + 1; // 1..7 or 1..4
        }

        $dayCol = (int) $dayCol;

        return [
            'show'      => true,
            'row'       => (int) $row,
            'day_col'   => $dayCol,
            'col_start' => $dayCol,
            'col_end'   => $dayCol + 1,
        ];
    }
}
