<?php

namespace App\Http\Controllers;

use App\Models\Calendar;
use App\Models\Event;
use App\Models\Location;
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 Illuminate\Support\Facades\Log;
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 = '';

        return view('event.form', compact(
            'calendar',
            'instance',
            'event',
            'start',
            'end',
            'tz',
            'rrule',
        ));
    }

    /**
     * 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');

        $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)
            ?? '';

        return view('event.form', compact('calendar', 'instance', 'event', 'start', 'end', 'tz', 'rrule'));
    }

    /**
     * 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->load(['meta', 'meta.venue']);

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

        // 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);

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

        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): RedirectResponse
    {
        $this->authorize('update', $calendar);

        $data = $request->validate([
            'title'       => ['required', 'string', 'max:200'],
            'start_at'    => ['required', 'date_format:Y-m-d\TH:i'],
            'end_at'      => ['required', 'date_format:Y-m-d\TH:i', 'after:start_at'],
            'description' => ['nullable', 'string'],
            'location'    => ['nullable', 'string'],
            'all_day'     => ['sometimes', 'boolean'],
            'category'    => ['nullable', 'string', 'max:50'],
            'rrule'       => ['nullable', 'string', 'max:255'],

            // 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'],
        ]);

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

        // parse input in display tz, store in utc
        $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);

        $rrule = $this->normalizeRrule($request);
        $extra = $this->mergeRecurrenceExtra([], $rrule, $tz, $request);

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

        $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'     => (bool) ($data['all_day'] ?? false),
            'category'    => $data['category'] ?? null,
            'start_at'    => $startUtc,
            'end_at'      => $endUtc,
            'extra'       => $extra,
        ]);

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

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

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

        $data = $request->validate([
            'title'       => ['required', 'string', 'max:200'],
            'start_at'    => ['required', 'date_format:Y-m-d\TH:i'],
            'end_at'      => ['required', 'date_format:Y-m-d\TH:i', 'after:start_at'],
            'description' => ['nullable', 'string'],
            'location'    => ['nullable', 'string'],
            'all_day'     => ['sometimes', 'boolean'],
            'category'    => ['nullable', 'string', 'max:50'],
            'rrule'       => ['nullable', 'string', 'max:255'],
        ]);

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

        $startUtc = $this->parseLocalDatetimeToUtc($data['start_at'], $tz);
        $endUtc   = $this->parseLocalDatetimeToUtc($data['end_at'], $tz);

        $uid = $event->uid;

        $rrule = $this->normalizeRrule($request);
        $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,
            'summary'     => $data['title'],
            'description' => $data['description'] ?? '',
            'location'    => $data['location'] ?? '',
            'tzid'        => $rruleForIcs ? $tz : null,
            'rrule'       => $rruleForIcs,
            'exdate'      => $extra['exdate'] ?? [],
            'rdate'       => $extra['rdate'] ?? [],
        ]);

        $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'     => (bool) ($data['all_day'] ?? false),
            'category'    => $data['category'] ?? null,
            'start_at'    => $startUtc,
            'end_at'      => $endUtc,
            'extra'       => $extra,
        ]);

        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();
    }

    /**
     * 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 normalizeRrule(Request $request): ?string
    {
        if (! $request->has('rrule')) {
            return null;
        }

        $rrule = trim((string) $request->input('rrule'));
        return $rrule === '' ? '' : $rrule;
    }

    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)));
    }

    /**
     * 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;
    }
}
