<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Booking;
use App\Models\Customer;
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\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;

class UpsertBookingFromVeVs implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    /**
     * Raw VEVS row (already normalised by VeVsApi::normalizeList()).
     *
     * @param  array<string,mixed>  $row
     */
    public function __construct(public array $row)
    {
    }

    public function handle(): void
    {
        $r = $this->row;

        // ---------------------------------------------------------------------
        // Resolve a usable booking reference
        // ---------------------------------------------------------------------
        $ref = (string) (
            $r['ref_id']
            ?? $r['booking_id']
            ?? $r['ReferenceID']
            ?? $r['Reference']
            ?? $r['reference']
            ?? ''
        );

        if ($ref === '') {
            Log::warning('[UpsertBookingFromVeVs] No reference found in row', [
                'keys'   => array_keys($r),
                'sample' => Arr::only($r, ['id', 'booking_id', 'uuid']),
            ]);

            return;
        }

        // ---------------------------------------------------------------------
        // Find or create Booking by reference
        // ---------------------------------------------------------------------
        /** @var Booking $booking */
        $booking = Booking::firstOrNew([
            'reference' => $ref,
        ]);

        // Get actual columns on the bookings table so we don't try to write
        // to columns that don't exist (like status/source_system on old schemas).
        $bookingTable   = $booking->getTable();
        $bookingColumns = Schema::getColumnListing($bookingTable);
        $hasColumn      = fn (string $col): bool => in_array($col, $bookingColumns, true);

        // ---------------------------------------------------------------------
        // Source / brand fields (only if columns exist)
        // ---------------------------------------------------------------------
        if ($hasColumn('source_system') && $booking->source_system === null) {
            $booking->source_system = 'vevs_jimny';
        }

        if ($hasColumn('source_id') && ! $booking->source_id) {
            $booking->source_id = $booking->source_id ?: ($r['id'] ?? $r['uuid'] ?? null);
        }

        if ($hasColumn('external_reference') && ! $booking->external_reference) {
            $booking->external_reference = (string) (
                $r['booking_id']
                ?? $r['uuid']
                ?? null
            );
        }

        $sourceUpdatedAt = $this->parseDateTime(
            $this->firstNonEmpty($r, ['processed_on', 'created'])
        );
        if ($hasColumn('source_updated_at') && $sourceUpdatedAt) {
            $booking->source_updated_at = $sourceUpdatedAt;
        }

        // ---------------------------------------------------------------------
        // Status (only if bookings.status exists)
        // ---------------------------------------------------------------------
        if ($hasColumn('status') && isset($r['status'])) {
            $booking->status = (string) $r['status'];
        }

        // ---------------------------------------------------------------------
        // Dates / times: start/end + pickup/return + date-only fields
        // ---------------------------------------------------------------------
        $startRaw = $this->firstNonEmpty($r, ['from', 'start', 'pickup_date']);
        $endRaw   = $this->firstNonEmpty($r, ['to', 'end', 'actual_dropoff_datetime']);

        $start = $this->parseDateTime($startRaw);
        $end   = $this->parseDateTime($endRaw);

        if ($hasColumn('start_at') && $start) {
            $booking->start_at = $start;
        }
        if ($hasColumn('end_at') && $end) {
            $booking->end_at = $end;
        }

        // Date-only columns (casts will drop the time)
        if ($hasColumn('start_date') && $start) {
            $booking->start_date = $start;
        }
        if ($hasColumn('end_date') && $end) {
            $booking->end_date = $end;
        }

        // pickup_at / return_at – default to start/end if nothing more specific
        if ($hasColumn('pickup_at')) {
            $pickupAt = $this->parseDateTime(
                $this->firstNonEmpty($r, ['pickup_date', 'from', 'start'])
            );
            if ($pickupAt) {
                $booking->pickup_at = $pickupAt;
            }
        }

        if ($hasColumn('return_at')) {
            $returnAt = $this->parseDateTime(
                $this->firstNonEmpty($r, ['actual_dropoff_datetime', 'to', 'end'])
            );
            if ($returnAt) {
                $booking->return_at = $returnAt;
            }
        }

        // ---------------------------------------------------------------------
        // Vehicle mapping (VEVS side)
        // ---------------------------------------------------------------------
        if ($hasColumn('vevs_car_id') && isset($r['car_id'])) {
            $booking->vevs_car_id = (string) $r['car_id'];
        }

        if ($hasColumn('vehicle_name')) {
            $map   = (array) config('vevs_cars', []);
            $idStr = isset($r['car_id']) ? (string) $r['car_id'] : null;

            $vehicleName = null;

            if ($idStr && array_key_exists($idStr, $map)) {
                $vehicleName = (string) $map[$idStr];
            } elseif (! empty($r['car_name'])) {
                $vehicleName = (string) $r['car_name'];
            } elseif ($idStr) {
                $vehicleName = 'VEVS car ' . $idStr;
            }

            if ($vehicleName) {
                $booking->vehicle_name = $vehicleName;
            }
        }

        // ---------------------------------------------------------------------
        // Money fields (stored as cents in Booking) — also guarded
        // ---------------------------------------------------------------------
        $total    = $this->firstNonEmpty($r, ['total_price', 'TotalAmount', 'grand_total', 'GrandTotal', 'total']);
        $deposit  = $this->firstNonEmpty($r, ['required_deposit', 'DepositAmount', 'deposit']);
        $hold     = $this->firstNonEmpty($r, ['security_deposit', 'SecurityDeposit', 'bond', 'Bond', 'hold', 'hold_amount']);
        $currency = $r['currency']['value'] ?? $r['currency'] ?? null;

        if ($hasColumn('total_amount') && $total !== null && $total !== '') {
            $booking->total_amount = $this->moneyToCents($total);
        }

        if ($hasColumn('deposit_amount') && $deposit !== null && $deposit !== '') {
            $booking->deposit_amount = $this->moneyToCents($deposit);
        }

        if ($hasColumn('hold_amount') && $hold !== null && $hold !== '') {
            $booking->hold_amount = $this->moneyToCents($hold);
        }

        if ($hasColumn('currency') && ! $booking->currency) {
            $booking->currency = $currency ?: 'NZD';
        }

        // ---------------------------------------------------------------------
        // Customer sync
        // ---------------------------------------------------------------------
        $customer = $this->ensureCustomer($r, $ref);

        if ($customer && $hasColumn('customer_id') && ! $booking->customer_id) {
            $booking->customer_id = $customer->id;
        }

        // ---------------------------------------------------------------------
        // Billing address (top-level array column if present)
        // ---------------------------------------------------------------------
        if ($hasColumn('billing_address')) {
            $booking->billing_address = [
                'line1'   => $r['c_address'] ?? null,
                'city'    => $r['c_city'] ?? null,
                'region'  => $r['c_state'] ?? null,
                'zip'     => $r['c_zip'] ?? null,
                'country' => $r['country_title'] ?? $r['country'] ?? null,
            ];
        }

        // ---------------------------------------------------------------------
        // Meta blob (keep raw VEVS, nicely structured) — only if meta column exists
        // ---------------------------------------------------------------------
        if ($hasColumn('meta')) {
            $meta = (array) ($booking->meta ?? []);

            $meta['vevs'] = [
                'raw' => $r,

                'customer' => [
                    'title'   => $r['c_title'] ?? null,
                    'name'    => $this->firstNonEmpty($r, ['c_name', 'c_driver_name']),
                    'phone'   => $this->firstNonEmpty($r, ['c_phone', 'phone']),
                    'email'   => $this->firstNonEmpty($r, ['c_email', 'email', 'customer_email']),
                    'address' => [
                        'line1'   => $r['c_address'] ?? null,
                        'city'    => $r['c_city'] ?? null,
                        'region'  => $r['c_state'] ?? null,
                        'zip'     => $r['c_zip'] ?? null,
                        'country' => $r['country_title'] ?? $r['country'] ?? null,
                    ],
                ],

                'rental' => [
                    'rental_days'  => $r['rental_days'] ?? null,
                    'rental_hours' => $r['rental_hours'] ?? null,
                    'pickup_id'    => $r['pickup_id'] ?? null,
                    'return_id'    => $r['return_id'] ?? null,
                    'processed_on' => $r['processed_on'] ?? null,
                    'created'      => $r['created'] ?? null,
                ],

                'pricing' => [
                    'car_rental_fee'          => $r['car_rental_fee'] ?? null,
                    'extra_price'             => $r['extra_price'] ?? null,
                    'extra_mileage_fee'       => $r['extra_mileage_fee'] ?? null,
                    'home_delivery_fee'       => $r['home_delivery_fee'] ?? null,
                    'location_fee'            => $r['location_fee'] ?? null,
                    'out_of_hours_fee'        => $r['out_of_hours_fee'] ?? null,
                    'late_return_fee'         => $r['late_return_fee'] ?? null,
                    'young_driver_fee'        => $r['young_driver_fee'] ?? null,
                    'excess_price'            => $r['excess_price'] ?? null,
                    'vat'                     => $r['vat'] ?? null,
                    'vat_price'               => $r['vat_price'] ?? null,
                    'total_price_before_disc' => $r['total_price_before_discount'] ?? null,
                    'total_price'             => $r['total_price'] ?? null,
                    'required_deposit'        => $r['required_deposit'] ?? null,
                    'security_deposit'        => $r['security_deposit'] ?? null,
                    'currency'                => $currency,
                ],
            ];

            $booking->meta = $meta;
        }

        $booking->save();
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    protected function parseDateTime(?string $value): ?Carbon
    {
        if (! $value) {
            return null;
        }

        try {
            return Carbon::parse($value, config('app.timezone'));
        } catch (\Throwable) {
            return null;
        }
    }

    /**
     * Create or update a Customer model from a VEVS row (schema-aware).
     *
     * Prioritises email as the stable key and only writes columns that
     * actually exist on the customers table.
     *
     * @param  array<string,mixed>  $row
     */
    protected function ensureCustomer(array $row, string $reference): ?Customer
    {
        $email = $this->firstNonEmpty($row, ['c_email', 'email', 'customer_email']);
        $name  = $this->firstNonEmpty($row, ['c_name', 'c_driver_name', 'customer_name', 'name']);

        // Some feeds send placeholder "unknown+...@example.invalid"
        $isPlaceholder = is_string($email) && str_ends_with(strtolower((string) $email), '@example.invalid');

        if (! $email) {
            // create a stable, unique placeholder per booking to avoid collisions
            $email         = 'unknown+' . ($reference ?: uniqid('vevs_')) . '@example.invalid';
            $isPlaceholder = true;
        }

        [$first, $last] = $this->splitName($name);

        $payload = [
            'email'      => $email,
            'first_name' => $first ?: null,
            'last_name'  => $last ?: null,
            'full_name'  => $name ?: trim($first . ' ' . $last) ?: null,
            'meta'       => [
                'source'  => 'vevs',
                'phone'   => $this->firstNonEmpty($row, ['c_phone', 'phone']),
                'licence' => $this->firstNonEmpty($row, ['c_licence', 'licence']),
                'company' => $this->firstNonEmpty($row, ['c_company', 'company']),
            ],
        ];

        // Only keep keys that actually exist as columns on customers
        $cols    = Schema::getColumnListing((new Customer)->getTable());
        $payload = array_intersect_key(
            array_filter($payload, fn ($v) => $v !== null && $v !== ''),
            array_flip($cols)
        );

        /** @var Customer $customer */
        $customer = Customer::updateOrCreate(
            ['email' => $email],
            $payload
        );

        // If we later learn the real email on another sync run, update it.
        if ($isPlaceholder) {
            $realEmail = $this->firstNonEmpty($row, ['c_email', 'email', 'customer_email']);
            if ($realEmail && ! str_ends_with(strtolower($realEmail), '@example.invalid')) {
                $customer->email = $realEmail;
                $customer->save();
            }
        }

        return $customer;
    }

    /**
     * Very simple name splitter: "Tim Foxell" => ["Tim", "Foxell"]
     *
     * @return array{0:string|null,1:string|null}
     */
    protected function splitName(?string $name): array
    {
        $name = trim((string) $name);
        if ($name === '') {
            return [null, null];
        }

        $name  = preg_replace('/\s+/', ' ', $name) ?? $name;
        $parts = explode(' ', $name);

        if (count($parts) === 1) {
            return [$parts[0], null];
        }

        $first = array_shift($parts);
        $last  = implode(' ', $parts);

        return [$first ?: null, $last ?: null];
    }

    /**
     * Return the first non-empty value for any of the given keys.
     *
     * @param  array<string,mixed>  $arr
     * @param  array<int,string>    $keys
     */
    protected function firstNonEmpty(array $arr, array $keys): ?string
    {
        foreach ($keys as $k) {
            if (Arr::has($arr, $k)) {
                $v = Arr::get($arr, $k);
                if (is_string($v) && trim($v) !== '') {
                    return trim($v);
                }
                if (is_numeric($v)) {
                    return (string) $v;
                }
            }
        }

        return null;
    }

    /**
     * Convert a money string (e.g. "210.00", "$210", "210") into integer cents.
     */
    protected function moneyToCents(?string $v): int
    {
        if ($v === null || $v === '') {
            return 0;
        }

        // Normalize: strip currency symbols & commas
        $s = preg_replace('/[^0-9.\-]/', '', (string) $v);
        if ($s === '' || $s === '.' || $s === '-.' || $s === '-') {
            return 0;
        }

        // If it has a decimal point, treat as dollars; else treat as integer dollars
        if (str_contains($s, '.')) {
            $float = (float) $s;
            return (int) round($float * 100);
        }

        return (int) ((int) $s * 100);
    }
}
