<?php

namespace App\Jobs;

use App\Models\EventMeta;
use App\Models\Location;
use App\Services\Location\Geocoder;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;

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

    // process up to n records this run; null = no cap
    public function __construct(
        public ?int $limit = null
    ) {}

    // convenience helpers
    public static function push(?int $limit = null): void {
        dispatch(new static($limit));
    }
    public static function runNow(?int $limit = null): void {
        dispatch_sync(new static($limit));
    }

    // handle geocoding
    public function handle(Geocoder $geocoder): void
    {
        // working counters
        $stats = [
            'locations' => [
                'processed' => 0,
                'updated'   => 0,
                'skipped'   => 0,
                'failed'    => 0,
            ],
            'events' => [
                'processed' => 0,
                'created'   => 0,
                'updated'   => 0,
                'skipped'   => 0,
                'failed'    => 0,
            ],
        ];
        $handled = 0;

        Log::info('GeocodeEventLocations: start', ['limit' => $this->limit]);

        // first, geocode any location rows missing coordinates
        $locations = Location::query()
            ->whereNotNull('raw_address')
            ->where('raw_address', '<>', '')
            ->where(function ($q) {
                $q->whereNull('lat')->orWhereNull('lon');
            })
            ->orderBy('id');

        $stop = false;

        $locations->chunkById(200, function ($chunk) use ($geocoder, &$stats, &$handled, &$stop) {
            foreach ($chunk as $loc) {
                if ($stop) {
                    return false;
                }

                if ($this->limit !== null && $handled >= $this->limit) {
                    $stop = true;
                    return false;
                }

                $handled++;
                $stats['locations']['processed']++;

                try {
                    $norm = $geocoder->forward($loc->raw_address);

                    if (!$norm || !is_numeric($norm['lat'] ?? null) || !is_numeric($norm['lon'] ?? null)) {
                        $stats['locations']['skipped']++;
                        continue;
                    }

                    $changed = false;
                    if ($loc->lat === null && is_numeric($norm['lat'])) {
                        $loc->lat = $norm['lat'];
                        $changed = true;
                    }
                    if ($loc->lon === null && is_numeric($norm['lon'])) {
                        $loc->lon = $norm['lon'];
                        $changed = true;
                    }

                    foreach (['street', 'city', 'state', 'postal', 'country'] as $field) {
                        if (empty($loc->{$field}) && !empty($norm[$field])) {
                            $loc->{$field} = $norm[$field];
                            $changed = true;
                        }
                    }

                    if ($changed) {
                        $loc->save();
                        $stats['locations']['updated']++;
                    } else {
                        $stats['locations']['skipped']++;
                    }
                } catch (\Throwable $e) {
                    $stats['locations']['failed']++;
                    Log::warning('GeocodeEventLocations: location failed', [
                        'location_id' => $loc->id,
                        'raw_address' => $loc->raw_address,
                        'error'       => $e->getMessage(),
                    ]);
                }
            }
        }, 'id');

        if ($stop) {
            Log::info('GeocodeEventLocations: done', $stats);
            return;
        }

        // events that have a non-empty location string but no linked location row yet
        $todo = EventMeta::query()
            ->whereNull('location_id')
            ->whereNotNull('location')
            ->where('location', '<>', '')
            ->orderBy('event_id'); // important for chunkById

        // log total to process (before limit)
        $total = (clone $todo)->count();
        Log::info('[geo] starting GeocodeEventLocations', ['total' => $total, 'limit' => $this->limit]);

        // chunk through event_meta rows
        $todo->chunkById(200, function ($chunk) use ($geocoder, &$stats, &$handled, &$stop) {
            foreach ($chunk as $meta) {
                if ($stop) {
                    return false; // stop further chunking
                }

                // respect limit if provided
                if ($this->limit !== null && $handled >= $this->limit) {
                    $stop = true;
                    return false;
                }

                try {
                    // geocode the free-form location string; prefer an existing location match
                    $query = $meta->location;
                    $location = Location::where('display_name', $meta->location)
                        ->orWhere('raw_address', $meta->location)
                        ->first();

                    if (!$location) {
                        // soft match on prefix when there is exactly one candidate
                        $matches = Location::where('display_name', 'like', $meta->location . '%')
                            ->limit(2)
                            ->get();
                        if ($matches->count() === 1) {
                            $location = $matches->first();
                        }
                    }

                    if ($location) {
                        // if we already have coords, just link and move on
                        if (is_numeric($location->lat) && is_numeric($location->lon)) {
                            $meta->location_id = $location->id;
                            $meta->save();
                            $handled++;
                            $stats['events']['processed']++;
                            $stats['events']['updated']++;
                            continue;
                        }

                        if ($location->raw_address) {
                            $query = $location->raw_address;
                        }
                    }

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

                    // skip obvious non-address labels or unresolved queries
                    if (!$norm || (!$norm['lat'] && !$norm['street'])) {
                        $stats['events']['skipped']++;
                        $handled++;
                        $stats['events']['processed']++;
                        Log::info('GeocodeEventLocations: skipped', [
                            'event_id' => $meta->event_id,
                            'location' => $meta->location,
                            'query' => $query,
                        ]);
                        continue;
                    }

                    // normalized match key to reduce duplicates
                    $lookup = [
                        'display_name' => $norm['display_name'],
                        'street'       => $norm['street'],
                        'city'         => $norm['city'],
                        'state'        => $norm['state'],
                        'postal'       => $norm['postal'],
                        'country'      => $norm['country'],
                    ];

                    // try to match an existing location (by normalized fields)
                    $existing = Location::where($lookup)->first();

                    // fall back to raw string match against any pre-seeded label rows
                    if (!$existing && $meta->location) {
                        $existing = Location::where('display_name', $meta->location)
                            ->orWhere('raw_address', $meta->location)
                            ->first();
                    }

                    // reuse existing location or create a new one with coords
                    $loc = $existing ?? Location::firstOrCreate(
                        $lookup,
                        [
                            'raw_address' => $norm['raw_address'],
                            'lat'         => $norm['lat'],
                            'lon'         => $norm['lon'],
                        ]
                    );

                    // if we matched an existing row missing coords, backfill once
                    if ($existing && (is_null($existing->lat) || is_null($existing->lon))) {
                        $existing->lat = $norm['lat'];
                        $existing->lon = $norm['lon'];
                        $existing->raw_address ??= $norm['raw_address'];
                        $existing->save();
                        $stats['events']['updated']++;
                    }

                    if ($loc->wasRecentlyCreated) {
                        $stats['events']['created']++;
                    }

                    // link event_meta → locations
                    $meta->location_id = $loc->id;
                    $meta->save();

                    $handled++;
                    $stats['events']['processed']++;

                } catch (\Throwable $e) {
                    $stats['events']['failed']++;
                    $handled++;
                    $stats['events']['processed']++;
                    Log::warning('GeocodeEventLocations: failed', [
                        'event_id' => $meta->event_id,
                        'location' => $meta->location,
                        'error'    => $e->getMessage(),
                    ]);
                }
            }
        }, 'event_id');

        Log::info('GeocodeEventLocations: done', $stats);
    }
}
