<?php

namespace App\Jobs;

use App\Models\Calendar;
use App\Models\CalendarInstance;
use App\Models\Event;
use App\Models\EventMeta;
use App\Models\Subscription;
use App\Services\Event\EventAttendeeSynchronizer;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Sabre\VObject\ParseException;
use Sabre\VObject\Reader;

class SyncSubscription implements ShouldQueue
{
    use Dispatchable, Queueable, InteractsWithQueue, SerializesModels;

    public Subscription $subscription;

    public function __construct(Subscription $subscription)
    {
        $this->subscription = $subscription;
    }

    public function handle(EventAttendeeSynchronizer $attendeeSync): void
    {
        // normalize the source a bit so comparisons/logging are consistent
        $source = rtrim(trim((string) $this->subscription->source), '/');

        // 1) download the remote feed
        try {
            $resp = Http::retry(3, 5000)
                ->timeout(30)
                ->withHeaders([
                    'User-Agent' => 'Kithkin CalDAV Bot',
                    'Accept'     => 'text/calendar, text/plain;q=0.9, */*;q=0.8',
                ])
                ->get($source);

            $resp->throw();

            $body = $resp->body();
            $contentType = (string) $resp->header('Content-Type');
        }
        catch (ConnectionException | RequestException | \Throwable $e)
        {
            $this->markSyncFailed('fetch_failed', $e->getMessage());

            Log::warning('Feed fetch failed', [
                'sub'  => $this->subscription->id,
                'url'  => $source,
                'msg'  => $e->getMessage(),
            ]);

            $this->fail($e);
            return;
        }

        // 2) ensure we actually got an ICS payload, not HTML or other content
        if (! $this->looksLikeIcs($body))
        {
            $this->markSyncFailed('not_ical', 'remote feed did not return vcALENDAR data');

            Log::warning('Feed did not return iCalendar data', [
                'sub'          => $this->subscription->id,
                'url'          => $source,
                'status'       => $resp->status(),
                'content_type' => $contentType ?? null,
                'starts_with'  => substr(ltrim($body), 0, 200),
            ]);

            $this->fail(new \RuntimeException('Remote feed did not return VCALENDAR data.'));
            return;
        }

        // 3) get or create mirror calendar
        $meta = $this->subscription->meta()->first() ?? $this->subscription->meta()->create();

        if (! $meta->calendar_id) {
            $meta->calendar_id = $this->createMirrorCalendar($meta, $source);
            $meta->save();
        }

        $calendarId = (int) $meta->calendar_id;

        // 4) parse the VCALENDAR
        try {
            $vcalendar = Reader::read($body);
        }
        catch (ParseException | \Throwable $e)
        {
            $this->markSyncFailed('parse_failed', $e->getMessage());

            Log::warning('ICS parse failed', [
                'sub'         => $this->subscription->id,
                'url'         => $source,
                'msg'         => $e->getMessage(),
                'starts_with' => substr(ltrim($body), 0, 200),
            ]);

            $this->fail($e);
            return;
        }

        Log::info('Syncing subscription', [
            'sub' => $this->subscription->id,
            'url' => $source,
        ]);

        // 5) upsert events
        foreach (($vcalendar->VEVENT ?? []) as $vevent) {
            $uid = isset($vevent->UID) ? (string) $vevent->UID : null;
            $dtStart = $vevent->DTSTART ?? null;

            // skip malformed events (rare, but it happens)
            if (! $uid || ! $dtStart) {
                continue;
            }

            $now  = now()->timestamp;
            $blob = (string) $vevent->serialize();

            $object = Event::updateOrCreate(
                ['uid' => $uid, 'calendarid' => $calendarId],
                [
                    'uri'           => Str::uuid().'.ics',
                    'lastmodified'  => $now,
                    'etag'          => md5($blob),
                    'size'          => strlen($blob),
                    'componenttype' => 'VEVENT',
                    'calendardata'  => $blob,
                ]
            );

            // sabre gives DateTime objects here; Carbon::instance is safest
            $start = Carbon::instance($dtStart->getDateTime())->utc();
            $end   = isset($vevent->DTEND)
                ? Carbon::instance($vevent->DTEND->getDateTime())->utc()
                : $start;
            $isAllDay = $dtStart->isFloating();
            $startOn = null;
            $endOn = null;
            $tzid = null;

            if ($isAllDay) {
                $startOn = $start->toDateString();
                $endOn = $end->toDateString();
                if (Carbon::parse($endOn)->lte(Carbon::parse($startOn))) {
                    $endOn = Carbon::parse($startOn)->addDay()->toDateString();
                }
            }

            EventMeta::upsertForEvent($object->id, [
                'title'       => (string) ($vevent->SUMMARY ?? 'Untitled'),
                'description' => (string) ($vevent->DESCRIPTION ?? ''),
                'location'    => (string) ($vevent->LOCATION ?? ''),
                'all_day'     => $isAllDay,
                'start_at'    => $start,
                'end_at'      => $end,
                'start_on'    => $startOn,
                'end_on'      => $endOn,
                'tzid'        => $tzid,
            ]);

            $attendeeSync->syncFromCalendarData($object, $blob);
        }

        // sync is ok
        $this->markSyncOk();

        Log::info('Syncing subscription complete', [
            'sub' => $this->subscription->id,
        ]);
    }

    private function looksLikeIcs(string $body): bool
    {
        // cheap + effective: an .ics must include BEGIN:VCALENDAR
        // allow leading whitespace/bom
        $haystack = ltrim($body);
        return str_contains($haystack, 'BEGIN:VCALENDAR');
    }

    private function mirrorDescription(string $source): string
    {
        return 'Remote feed: '.$source;
    }

    /**
     * Lazily builds the shadow calendar + instance when missing.
     */
    private function createMirrorCalendar($meta, string $source): int
    {
        if ($meta->calendar_id) {
            return (int) $meta->calendar_id;
        }

        // create a new master calendar in `calendars`
        $calendar = Calendar::create([
            'synctoken'  => 1,
            'components' => 'VEVENT',
        ]);

        // set the color
        $color = calendar_color([], $meta->color);

        // create the per-user instance (description is display-only; never used for lookup)
        CalendarInstance::create([
            'calendarid'    => $calendar->id,
            'principaluri'  => $this->subscription->principaluri,
            'uri'           => (string) Str::uuid(),
            'displayname'   => $this->subscription->displayname,
            'description'   => $this->mirrorDescription($source),
            'calendarcolor' => $color,
            'timezone'      => config('app.timezone', 'UTC'),
        ]);

        return (int) $calendar->id;
    }

    /**
     * sync helpers
     */
    private function markSyncOk(): void
    {
        $this->subscription->forceFill([
            'last_sync_status' => 'ok',
            'last_sync_error'  => null,
            'last_sync_at'     => now(),
            'lastmodified'     => now()->timestamp, // sabre uses int timestamps
        ])->save();
    }

    private function markSyncFailed(string $status, string $message): void
    {
        $this->subscription->forceFill([
            'last_sync_status' => $status,
            'last_sync_error'  => mb_substr($message, 0, 5000),
            'last_sync_at'     => now(),
            'lastmodified'     => now()->timestamp,
        ])->save();
    }
}
