<?php

namespace App\Services\Calendar;

use App\Services\Event\EventRecurrence;
use Carbon\Carbon;
use Illuminate\Support\Collection;

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

        $payloads = $events->flatMap(function ($e) use (
            $calendarMap,
            $uiFormat,
            $view,
            $range,
            $tz,
            $recurrence,
            $spanStartUtc,
            $spanEndUtc,
            $gridStartMinutes,
            $gridEndMinutes,
            $daytimeHours
        ) {
            $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) {
                $meta = $e->meta;
                $isAllDay = (bool) ($meta?->all_day ?? false);
                $startUtc = null;
                $endUtc = null;

                if ($isAllDay && $meta?->start_on && $meta?->end_on) {
                    $allDayTz = $meta?->tzid
                        ?? ($meta?->extra['tzid'] ?? null)
                        ?? $timezone;
                    $startUtc = Carbon::parse($meta->start_on->toDateString(), $allDayTz)
                        ->startOfDay()
                        ->utc();
                    $endUtc = Carbon::parse($meta->end_on->toDateString(), $allDayTz)
                        ->startOfDay()
                        ->utc();
                } else {
                    if ($meta?->start_at instanceof Carbon) {
                        $startUtc = $meta->start_at->copy()->utc();
                    } elseif ($meta?->start_at) {
                        $startUtc = Carbon::parse($meta->start_at, 'UTC');
                    } else {
                        $startUtc = Carbon::createFromTimestamp($e->firstoccurence, 'UTC');
                    }

                    if ($meta?->end_at instanceof Carbon) {
                        $endUtc = $meta->end_at->copy()->utc();
                    } elseif ($meta?->end_at) {
                        $endUtc = Carbon::parse($meta->end_at, 'UTC');
                    } else {
                        $endUtc = $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,
                $gridStartMinutes,
                $gridEndMinutes,
                $daytimeHours,
                $spanStartUtc,
                $spanEndUtc
            ) {
                $startUtc = $occ['start'];
                $endUtc = $occ['end'];
                $isAllDay = (bool) ($e->meta?->all_day ?? false);

                if ($endUtc->lte($spanStartUtc) || $startUtc->gte($spanEndUtc)) {
                    return null;
                }

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

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

                if ($daytimeHours && !$isAllDay) {
                    $startMinutes = ($startForGrid->hour * 60) + $startForGrid->minute;
                    $endMinutes = ($endForGrid->hour * 60) + $endForGrid->minute;

                    if ($endMinutes <= $gridStartMinutes || $startMinutes >= $gridEndMinutes) {
                        return null;
                    }

                    $displayStartMinutes = max($startMinutes, $gridStartMinutes);
                    $displayEndMinutes = min($endMinutes, $gridEndMinutes);

                    $startForGrid = $startForGrid->copy()->startOfDay()->addMinutes($displayStartMinutes);
                    $endForGrid = $endForGrid->copy()->startOfDay()->addMinutes($displayEndMinutes);
                }

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

                $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.',
                    'all_day'         => $isAllDay,
                    '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'],
                ];
            })->filter()->values();
        })->filter();

        // ensure chronological ordering across calendars for all views
        $payloads = $payloads
            ->sortBy('start')
            ->keyBy('occurrence_id');

        return $this->applyOverlapLayout($payloads, $view);
    }

    /**
     * Assemble an array of day-objects for the requested view.
     */
    public function buildCalendarGrid(
        string $view,
        array $range,
        Collection $events,
        string $tz,
        array $span
    ): array {
        ['start' => $grid_start, 'end' => $grid_end] = $span;
        $today = Carbon::today($tz)->toDateString();

        $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;
            if (!empty($ev['all_day']) && $end->gt($start)) {
                $end = $end->copy()->subSecond();
            }

            for ($d = $start->copy()->startOfDay();
                 $d->lte($end->copy()->endOfDay());
                 $d->addDay()) {
                $key = $d->toDateString();
                $events_by_day[$key][] = $ev['occurrence_id'] ?? $ev['id'];
            }
        }

        $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];
    }

    /**
     * Build the mini-month grid for day buttons.
     */
    public 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();

        $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];
    }

    /**
     * Create the time gutter for time-based views.
     */
    public function timeSlots(
        Carbon $dayStart,
        string $tz,
        string $timeFormat,
        ?array $daytimeHours = null
    ): array {
        $minutesPerSlot = 15;
        $startMinutes = 0;
        $endMinutes = 24 * 60;

        if (is_array($daytimeHours)) {
            $startMinutes = (int) $daytimeHours['start'] * 60;
            $endMinutes = (int) $daytimeHours['end'] * 60;
        }

        $slotsPerDay = intdiv(max(0, $endMinutes - $startMinutes), $minutesPerSlot);

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

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

        for ($i = 0; $i < $slotsPerDay; $i++) {
            $slots[] = [
                'iso'      => $t->toIso8601String(),
                'label'    => $t->format($format),
                'key'      => $t->format('H:i'),
                'index'    => $i,
                'minutes'  => $startMinutes + ($i * $minutesPerSlot),
                'duration' => $minutesPerSlot,
            ];

            $t->addMinutes($minutesPerSlot);
        }

        return $slots;
    }

    /**
     * Create the specific view's date headers.
     */
    public 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();

        if ($view === 'month') {
            return collect($this->weekdayHeaders($tz, $weekStart))
                ->map(fn ($h) => $h + ['is_today' => false])
                ->all();
        }

        $headers = [];
        for ($d = $start->copy()->startOfDay(); $d->lte($end); $d->addDay()) {
            $date = $d->toDateString();
            $headers[] = [
                'date' => $d->toDateString(),
                'day'  => $d->format('j'),
                'dow'  => $d->translatedFormat('l'),
                'dow_short' => $d->translatedFormat('D'),
                'month' => $d->translatedFormat('M'),
                'is_today'  => $date === $today,
            ];
        }

        return $headers;
    }

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

        for ($i = 0; $i < 7; $i++) {
            $headers[] = [
                'key'   => $i,
                'label' => $d->translatedFormat('D'),
            ];
            $d->addDay();
        }

        return $headers;
    }

    /**
     * Place the "now" indicator on the grid for time-based views.
     */
    public function nowIndicator(
        string $view,
        array $range,
        string $tz,
        int $minutesPerSlot = 15,
        int $gutterCols = 1,
        ?array $daytimeHours = null
    ): array {
        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();

        if (!$now->betweenIncluded($start, $end)) {
            return ['show' => false, 'row' => 1, 'day_col' => 1, 'col_start' => 1, 'col_end' => 2];
        }

        $minutes = ($now->hour * 60) + $now->minute;
        $gridStartMinutes = $daytimeHours ? ((int) $daytimeHours['start'] * 60) : 0;
        $gridEndMinutes = $daytimeHours ? ((int) $daytimeHours['end'] * 60) : (24 * 60);

        if ($daytimeHours && ($minutes < $gridStartMinutes || $minutes >= $gridEndMinutes)) {
            return ['show' => false, 'row' => 1, 'day_col' => 1, 'col_start' => 1, 'col_end' => 2];
        }

        $relativeMinutes = $minutes - $gridStartMinutes;
        $relativeMinutes = max(0, min($relativeMinutes, $gridEndMinutes - $gridStartMinutes));

        $row = intdiv($relativeMinutes, $minutesPerSlot) + 1;
        $offset = ($relativeMinutes % $minutesPerSlot) / $minutesPerSlot;

        if ($view === 'day') {
            $dayCol = 1;
        } else {
            $todayStart = $now->copy()->startOfDay();
            $dayCol = $start->diffInDays($todayStart) + 1;
        }

        $dayCol = (int) $dayCol;

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

    /**
     * Time-based layout slot placement.
     */
    private function slotPlacement(
        Carbon $startLocal,
        ?Carbon $endLocal,
        Carbon $rangeStart,
        string $view,
        int $minutesPerSlot = 15,
        int $gridStartMinutes = 0,
        int $gridEndMinutes = 1440
    ): array {
        $start = $startLocal->copy();
        $end   = ($endLocal ?? $startLocal)->copy();

        $startMinutes = (($start->hour * 60) + $start->minute) - $gridStartMinutes;
        $endMinutes = (($end->hour * 60) + $end->minute) - $gridStartMinutes;

        $maxStart = max(0, ($gridEndMinutes - $gridStartMinutes) - $minutesPerSlot);
        $startMinutes = $this->snapToSlot($startMinutes, $minutesPerSlot, 0, $maxStart);
        $endMinutes = $this->snapToSlot(
            $endMinutes,
            $minutesPerSlot,
            $minutesPerSlot,
            max($minutesPerSlot, $gridEndMinutes - $gridStartMinutes)
        );

        if ($endMinutes <= $startMinutes) {
            $endMinutes = min($startMinutes + $minutesPerSlot, $gridEndMinutes - $gridStartMinutes);
        }

        $startRow = intdiv($startMinutes, $minutesPerSlot) + 1;
        $rowSpan = max(1, intdiv($endMinutes - $startMinutes, $minutesPerSlot));
        $endRow  = $startRow + $rowSpan;

        $maxCols = match ($view) {
            'day'  => 1,
            'four' => 4,
            'week' => 7,
            default => 1,
        };
        $startCol = $rangeStart->copy()->startOfDay()->diffInDays($start->copy()->startOfDay()) + 1;
        $startCol = max(1, min($maxCols, $startCol));

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

    private function snapToSlot(int $minutes, int $slot, int $min, int $max): int
    {
        $rounded = (int) round($minutes / $slot) * $slot;
        return max($min, min($rounded, $max));
    }

    /**
     * Apply overlap metadata for time-based views (day/four/week).
     */
    private function applyOverlapLayout(Collection $events, string $view): Collection
    {
        if (!in_array($view, ['day', 'four', 'week'], true) || $events->isEmpty()) {
            return $events;
        }

        $items = $events->all(); // keyed by occurrence_id
        $eventsByCol = [];

        foreach ($items as $id => $event) {
            $col = (int) ($event['start_col'] ?? 1);
            $eventsByCol[$col][$id] = $event;
        }

        foreach ($eventsByCol as $group) {
            uasort($group, function (array $a, array $b) {
                $cmp = ($a['start_row'] ?? 0) <=> ($b['start_row'] ?? 0);
                if ($cmp !== 0) {
                    return $cmp;
                }
                return ($a['end_row'] ?? 0) <=> ($b['end_row'] ?? 0);
            });

            $cluster = [];
            $clusterEnd = null;

            foreach ($group as $id => $event) {
                $startRow = (int) ($event['start_row'] ?? 0);
                $endRow = (int) ($event['end_row'] ?? 0);

                if ($clusterEnd !== null && $startRow >= $clusterEnd) {
                    $this->assignOverlapCluster($cluster, $items);
                    $cluster = [];
                    $clusterEnd = null;
                }

                $cluster[$id] = $event;
                $clusterEnd = $clusterEnd === null
                    ? $endRow
                    : max($clusterEnd, $endRow);
            }

            if ($cluster) {
                $this->assignOverlapCluster($cluster, $items);
            }
        }

        return collect($items);
    }

    private function assignOverlapCluster(array $cluster, array &$items): void
    {
        if (empty($cluster)) {
            return;
        }

        $active = [];
        $availableCols = [];
        $assigned = [];
        $maxCol = 0;

        foreach ($cluster as $id => $event) {
            $startRow = (int) ($event['start_row'] ?? 0);
            $endRow = (int) ($event['end_row'] ?? 0);

            foreach ($active as $idx => $info) {
                if ($info['end'] <= $startRow) {
                    $availableCols[] = $info['col'];
                    unset($active[$idx]);
                }
            }

            sort($availableCols);

            if (!empty($availableCols)) {
                $col = array_shift($availableCols);
            } else {
                $col = $maxCol;
                $maxCol++;
            }

            $assigned[$id] = $col;
            $active[] = ['end' => $endRow, 'col' => $col];
        }

        $totalCols = max(1, $maxCol);
        $width = 100 / $totalCols;

        foreach ($cluster as $id => $event) {
            $index = $assigned[$id] ?? 0;
            $items[$id]['overlap_count'] = $totalCols;
            $items[$id]['overlap_index'] = $index;
            $items[$id]['overlap_width'] = round($width, 4);
            $items[$id]['overlap_offset'] = round($width * $index, 4);
            $items[$id]['overlap_z'] = $index + 1;
        }
    }
}
