<?php

namespace Fleetbase\FleetOps\Models;

use Fleetbase\Casts\Json;
use Fleetbase\FleetOps\Events\EntityActivityChanged;
use Fleetbase\FleetOps\Events\EntityCompleted;
use Fleetbase\FleetOps\Events\WaypointActivityChanged;
use Fleetbase\FleetOps\Events\WaypointCompleted;
use Fleetbase\FleetOps\Flow\Activity;
use Fleetbase\FleetOps\Http\Resources\v1\Payload as PayloadResource;
use Fleetbase\FleetOps\Support\Utils;
use Fleetbase\LaravelMysqlSpatial\Types\Point;
use Fleetbase\Models\Model;
use Fleetbase\Traits\HasApiModelBehavior;
use Fleetbase\Traits\HasMetaAttributes;
use Fleetbase\Traits\HasPublicId;
use Fleetbase\Traits\HasUuid;
use Fleetbase\Traits\TracksApiCredential;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class Payload extends Model
{
    use HasUuid;
    use HasPublicId;
    use HasApiModelBehavior;
    use TracksApiCredential;
    use HasMetaAttributes;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'payloads';

    /**
     * The type of public Id to generate.
     *
     * @var string
     */
    protected $publicIdType = 'payload';

    /**
     * Delegate a HTTP resource to use for this model.
     *
     * @var string
     */
    protected $httpResource = PayloadResource::class;

    /**
     * These attributes that can be queried.
     *
     * @var array
     */
    protected $searchableColumns = [];

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        '_key',
        'company_uuid',
        'pickup_uuid',
        'pickup_tracking_number_uuid',
        'dropoff_uuid',
        'dropoff_tracking_number_uuid',
        'return_uuid',
        'return_tracking_number_uuid',
        'current_waypoint_uuid',
        'meta',
        'payment_method',
        'cod_amount',
        'cod_currency',
        'cod_payment_method',
        'type',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'meta' => Json::class,
    ];

    /**
     * Relations to load with the model.
     *
     * @var array
     */
    protected $with = ['entities', 'waypoints']; // 'pickup', 'dropoff', 'return',

    /**
     * Dynamic attributes that are appended to object.
     *
     * @var array
     */
    protected $appends = ['pickup_name', 'dropoff_name', 'return_name'];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [];

    /**
     * Address/name of the dropoff location.
     */
    public function getDropoffNameAttribute()
    {
        $dropoff = $this->getDropoffOrLastWaypoint();

        return $dropoff->address ?? $dropoff->name ?? $dropoff->street1 ?? null;
    }

    /**
     * Address/name of the pickup location.
     */
    public function getPickupNameAttribute()
    {
        $pickup = $this->getPickupOrCurrentWaypoint();

        return $pickup->address ?? $pickup->name ?? $pickup->street1 ?? null;
    }

    /**
     * Address/name of the return location.
     */
    public function getReturnNameAttribute()
    {
        return $this->return ? ($this->return->address ?? $this->return->name ?? $this->return->street1) : null;
    }

    /**
     * Entities in the payload.
     */
    public function entities()
    {
        return $this->hasMany(Entity::class);
    }

    /**
     * Waypoint records in the payload.
     */
    public function waypointMarkers()
    {
        return $this->hasMany(Waypoint::class)->with(['place']);
    }

    /**
     * The first waypoint marker for lightweight index fallback.
     */
    public function firstWaypointMarker()
    {
        return $this->hasOne(Waypoint::class, 'payload_uuid', 'uuid')
            ->oldestOfMany('order')
            ->with(['place']);
    }

    /**
     * The last waypoint marker for lightweight index fallback.
     */
    public function lastWaypointMarker()
    {
        return $this->hasOne(Waypoint::class, 'payload_uuid', 'uuid')
            ->latestOfMany('order')
            ->with(['place']);
    }

    public function getTotalEntitiesAttribute()
    {
        return $this->entities()->count();
    }

    public function getTotalWaypointsAttribute()
    {
        return $this->waypoints()->count();
    }

    /**
     * The order the payload belongs to.
     */
    public function order()
    {
        return $this->hasOne(Order::class)->without(['payload']);
    }

    /**
     * The address the shipment will be delivered to.
     */
    public function dropoff()
    {
        return $this->belongsTo(Place::class, 'dropoff_uuid')->whereNull('deleted_at')->withoutGlobalScopes();
    }

    /**
     * The address the shipment will be delivered from.
     */
    public function pickup()
    {
        return $this->belongsTo(Place::class, 'pickup_uuid')->whereNull('deleted_at')->withoutGlobalScopes();
    }

    /**
     * The address the shipment will be sent to upon failed delivery.
     */
    public function return()
    {
        return $this->belongsTo(Place::class)->withoutGlobalScopes();
    }

    /**
     * The current waypoint of the payload in progress.
     */
    public function currentWaypoint()
    {
        return $this->belongsTo(Place::class, 'current_waypoint_uuid')->withoutGlobalScopes();
    }

    /**
     * The current waypoint of the payload in progress.
     */
    public function currentWaypointMarker()
    {
        return $this->belongsTo(Waypoint::class, 'current_waypoint_uuid', 'place_uuid')->withoutGlobalScopes();
    }

    /**
     * Waypoints between start and end.
     *
     * @return \Illuminate\Database\Eloquent\Concerns\HasManyThrough
     */
    public function waypoints()
    {
        return $this->hasManyThrough(Place::class, Waypoint::class, 'payload_uuid', 'uuid', 'uuid', 'place_uuid')->whereNull('waypoints.deleted_at')->withoutGlobalScopes();
    }

    /**
     * Always convert fee and rate to integer before insert.
     */
    public function setCodAmountAttribute($value)
    {
        $this->attributes['cod_amount'] = Utils::numbersOnly($value);
    }

    public function setEntities($entities = [])
    {
        if (empty($entities) || !is_array($entities)) {
            return $this;
        }

        foreach ($entities as $attributes) {
            if (isset($attributes['_import_id'])) {
                $waypoint = $this->waypoints->firstWhere('_import_id', $attributes['_import_id']);

                if ($waypoint) {
                    $attributes['destination_uuid'] = $waypoint->uuid;
                }
            }

            // if a destination or waypoint is explicitly set
            if (empty($attributes['destination_uuid'])) {
                $destinationKey = Utils::or($attributes, ['waypoint', 'destination']);
                $destination    = $this->findDestinationFromKey($destinationKey);
                if ($destination instanceof Place) {
                    $attributes['destination_uuid'] = $destination->uuid;
                }
            }

            // Validate destination actually exists
            if (isset($attributes['destination_uuid']) && Place::where('uuid', $attributes['destination_uuid'])->doesntExist()) {
                $validDestination = $this->_findCorrectDestinationForEntity($attributes);
                if ($validDestination) {
                    $attributes['destination_uuid'] = $validDestination->uuid;
                }
            }

            // Handle entity photo upload
            if (isset($attributes['photo'])) {
                $path = 'uploads/' . session('company') . '/entities';
                $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($attributes['photo'], $path);

                if ($file) {
                    $attributes['photo_uuid'] = $file->uuid;
                }

                // Clean up raw photo data
                unset($attributes['photo']);
            }

            $entity = new Entity($attributes);
            $this->entities()->save($entity);
        }

        return $this;
    }

    public function insertEntities($entities = [])
    {
        if (empty($entities) || !is_array($entities)) {
            return $this;
        }

        $this->load(['waypoints']);

        foreach ($entities as $attributes) {
            if (isset($attributes['_import_id']) && !isset($attributes['destination_uuid'])) {
                $waypoint = $this->waypoints->firstWhere('_import_id', $attributes['_import_id']);

                if ($waypoint) {
                    $attributes['destination_uuid'] = $waypoint->uuid;
                }
            }

            // if a destination or waypoint is explicitly set
            if (empty($attributes['destination_uuid'])) {
                $destinationKey = Utils::or($attributes, ['waypoint', 'destination']);
                $destination    = $this->findDestinationFromKey($destinationKey);
                if ($destination instanceof Place) {
                    $attributes['destination_uuid'] = $destination->uuid;
                }
            }

            // Validate destination actually exists
            if (isset($attributes['destination_uuid']) && Place::where('uuid', $attributes['destination_uuid'])->doesntExist()) {
                $validDestination = $this->_findCorrectDestinationForEntity($attributes);
                if ($validDestination) {
                    $attributes['destination_uuid'] = $validDestination->uuid;
                }
            }

            // Handle entity photo upload
            if (isset($attributes['photo'])) {
                $path = 'uploads/' . session('company') . '/entities';
                $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($attributes['photo'], $path);

                if ($file) {
                    $attributes['photo_uuid'] = $file->uuid;
                }

                // Clean up raw photo data
                unset($attributes['photo']);
            }

            Entity::insertGetUuid($attributes, $this);
        }

        $this->load(['entities']);

        return $this;
    }

    public function setCurrentWaypoint(Place|Waypoint $destination, bool $save = true): Payload
    {
        if ($destination instanceof Waypoint) {
            $destination = $destination->getPlace();
        }

        $this->current_waypoint_uuid = $destination->uuid;
        if ($save) {
            DB::table($this->getTable())->where('uuid', $this->uuid)->update(['current_waypoint_uuid' => $destination->uuid]);
        }

        return $this;
    }

    /**
     * Set waypoints for the current entity by resolving or creating Places and
     * upserting Waypoint records in order.
     *
     * Input supports multiple shapes per item:
     * - Top-level fields (preferred): 'type', 'place_uuid', 'id' (public_id), 'uuid' (temp search UUID),
     *   'customer_uuid' (preferred), 'customer_id' (customer public_id), 'customer_type'
     * - Or wrapped place payload: { type, place: {...} }
     *
     * Place resolution order:
     *   1) attributes.place_uuid (must exist in DB)
     *   2) attributes.id (public_id) -> resolve to uuid
     *   3) Place::createFromMixed($attributes)
     *      - If attributes.uuid is present and differs from created uuid, stores it as meta: search_uuid
     *
     * Customer resolution (optional):
     *   - Uses 'customer_type' (default: 'fleetops:contact') to determine the model via Utils::getMutationType()
     *   - Tries 'customer_uuid' first (must exist)
     *   - If not found, tries 'customer_id' (public_id) and resolves to uuid
     *   - Only sets customer_uuid/customer_type if a valid record is found
     *
     * Uniqueness for each waypoint is defined by: (payload_uuid, place_uuid, order).
     * Updatable fields include: type, customer_uuid, customer_type.
     *
     * @param array<int, array<string,mixed>> $waypoints list of waypoint attribute maps
     *
     * @return static
     */
    public function setWaypoints($waypoints = [])
    {
        if (!is_array($waypoints)) {
            return $this;
        }

        foreach ($waypoints as $index => $attributes) {
            // Keep a copy to safely read top-level fields regardless of { place: {...} } normalization.
            $raw = $attributes;

            // Read top-level fields BEFORE normalizing the place shape.
            $type            = data_get($raw, 'type', 'dropoff');
            $customerUuidIn  = data_get($raw, 'customer_uuid'); // preferred
            $customerPubIdIn = data_get($raw, 'customer_id');   // public_id fallback
            $customerType    = data_get($raw, 'customer_type', 'fleetops:contact');

            // Normalize { place: {...} } shape if present.
            if (Utils::isset($attributes, 'place') && is_array(Utils::get($attributes, 'place'))) {
                $attributes = Utils::get($attributes, 'place');
            }

            // -------- Resolve Place (uuid) --------
            $placeUuid = null;

            // Path 1: explicit place_uuid, ensure it exists
            if (
                is_array($attributes)
                && isset($attributes['place_uuid'])
                && Place::where('uuid', $attributes['place_uuid'])->exists()
            ) {
                $placeUuid = $attributes['place_uuid'];

            // Path 2: public_id under "uuid" -> resolve to uuid
            } elseif (
                is_array($attributes)
                && isset($attributes['uuid'])
                && ($resolvedUuid = Place::where('uuid', $attributes['uuid'])->value('uuid'))
            ) {
                $placeUuid = $resolvedUuid;

            // Path 3: public_id under "id" -> resolve to uuid
            } elseif (
                is_array($attributes)
                && isset($attributes['id'])
                && ($resolvedUuid = Place::where('public_id', $attributes['id'])->value('uuid'))
            ) {
                $placeUuid = $resolvedUuid;

            // Path 4: create from mixed payload
            } else {
                $place = Place::createFromMixed($attributes);

                // Store temp search UUID for traceability if present and different
                if ($place instanceof Place && isset($attributes['uuid']) && $place->uuid !== $attributes['uuid']) {
                    $place->updateMeta('search_uuid', $attributes['uuid']);
                }

                $placeUuid = $place->uuid;
            }

            // -------- Resolve Customer (uuid) --------
            $customerUuid          = null;
            $customerTypeNamespace = null;

            if ($customerType) {
                $customerTypeNamespace = Utils::getMutationType($customerType);
                $customerModel         = app($customerTypeNamespace);

                // Try by UUID first (preferred)
                if ($customerUuidIn && $customerModel->where('uuid', $customerUuidIn)->exists()) {
                    $customerUuid = $customerUuidIn;
                }
                // If not found by UUID, try by public_id
                if (!$customerUuid && $customerPubIdIn) {
                    $maybeUuid = $customerModel->where('public_id', $customerPubIdIn)->value('uuid');
                    if ($maybeUuid) {
                        $customerUuid = $maybeUuid;
                    } else {
                        // If neither lookup succeeds, drop the type namespace to avoid FK/type mismatch
                        $customerTypeNamespace = null;
                    }
                }

                // If no valid uuid after both attempts, clear the type
                if (!$customerUuid) {
                    $customerTypeNamespace = null;
                }
            }

            // -------- Upsert Waypoint --------
            // Uniqueness: payload + place + order for deterministic row per position.
            $unique = [
                'payload_uuid' => $this->uuid,
                'place_uuid'   => $placeUuid,
                'order'        => $index,
            ];

            // Only mutable/updatable fields go here.
            $values = array_filter([
                'type'           => $type,
                'customer_uuid'  => $customerUuid,
                'customer_type'  => $customerTypeNamespace,
            ], fn ($v) => !is_null($v));

            $waypointRecord = Waypoint::updateOrCreate($unique, $values);

            // Track it (assumes this is a Collection)
            $this->waypointMarkers->push($waypointRecord);
        }

        return $this;
    }

    public function insertWaypoints($waypoints = [])
    {
        if (!is_array($waypoints)) {
            return $this;
        }

        foreach ($waypoints as $index => $attributes) {
            $waypoint = ['payload_uuid' => $this->uuid, 'order' => $index, 'type' => data_get($attributes, 'type', 'dropoff')];

            if (Utils::isset($attributes, 'place') && is_array(Utils::get($attributes, 'place'))) {
                $placeAttributes = Utils::get($attributes, 'place');
                if (is_array($placeAttributes)) {
                    $attributes = array_merge($attributes, $placeAttributes);
                }
            }

            if (is_array($attributes) && array_key_exists('place_uuid', $attributes) && Place::where('uuid', $attributes['place_uuid'])->exists()) {
                $waypoint['place_uuid'] = $attributes['place_uuid'];
            } else {
                $placeUuid = Place::insertFromMixed($attributes);

                // if has a temporary uuid from search create meta attr for search_uuid
                if (Str::isUuid($placeUuid) && isset($attributes['uuid']) && $placeUuid !== $attributes['uuid']) {
                    $place = Place::where('uuid', $placeUuid)->first();

                    // set the original destination uuid in meta
                    if ($place instanceof Place) {
                        $place->updateMeta('_console_destination_uuid', $attributes['uuid']);
                        $place->updateMeta('search_uuid', $attributes['uuid']);
                    }
                }

                $waypoint['place_uuid'] = $placeUuid;
            }

            // Handle customer assosciation for waypoint
            $customerId   = data_get($attributes, 'customer_uuid');
            $customerType = data_get($attributes, 'customer_type', 'fleetops:contact');
            if ($customerId && $customerType) {
                $customerTypeNamespace = Utils::getMutationType($customerType);
                $customerExists        = app($customerTypeNamespace)->where('uuid', $customerId)->exists();
                if ($customerExists) {
                    $waypoint['customer_uuid'] = $customerId;
                    $waypoint['customer_type'] = $customerTypeNamespace;
                }
            }

            Waypoint::insertGetUuid($waypoint, $this);
        }

        return $this;
    }

    public function updateWaypoints($waypoints = [])
    {
        if (!is_array($waypoints)) {
            return $this;
        }

        $this->loadMissing('waypointMarkers');
        $keptWaypointIds          = [];
        $availableWaypointMarkers = $this->waypointMarkers()->get();

        foreach ($waypoints as $index => $attributes) {
            $raw             = $attributes;
            $type            = data_get($raw, 'type', 'dropoff');
            $customerUuidIn  = data_get($raw, 'customer_uuid');
            $customerPubIdIn = data_get($raw, 'customer_id');
            $customerType    = data_get($raw, 'customer_type', 'fleetops:contact');

            if (Utils::isset($attributes, 'place') && is_array(Utils::get($attributes, 'place'))) {
                $attributes = Utils::get($attributes, 'place');
            }

            $placeUuid = null;

            if (
                is_array($attributes)
                && isset($attributes['place_uuid'])
                && Place::where('uuid', $attributes['place_uuid'])->exists()
            ) {
                $placeUuid = $attributes['place_uuid'];
            } elseif (
                is_array($attributes)
                && isset($attributes['uuid'])
                && ($resolvedUuid = Place::where('uuid', $attributes['uuid'])->value('uuid'))
            ) {
                $placeUuid = $resolvedUuid;
            } elseif (
                is_array($attributes)
                && isset($attributes['id'])
                && ($resolvedUuid = Place::where('public_id', $attributes['id'])->value('uuid'))
            ) {
                $placeUuid = $resolvedUuid;
            } else {
                $place = Place::createFromMixed($attributes);

                if ($place instanceof Place && isset($attributes['uuid']) && $place->uuid !== $attributes['uuid']) {
                    $place->updateMeta('search_uuid', $attributes['uuid']);
                }

                $placeUuid = $place->uuid;
            }

            $customerUuid          = null;
            $customerTypeNamespace = null;

            if ($customerType) {
                $customerTypeNamespace = Utils::getMutationType($customerType);
                $customerModel         = app($customerTypeNamespace);

                if ($customerUuidIn && $customerModel->where('uuid', $customerUuidIn)->exists()) {
                    $customerUuid = $customerUuidIn;
                }

                if (!$customerUuid && $customerPubIdIn) {
                    $maybeUuid = $customerModel->where('public_id', $customerPubIdIn)->value('uuid');
                    if ($maybeUuid) {
                        $customerUuid = $maybeUuid;
                    } else {
                        $customerTypeNamespace = null;
                    }
                }

                if (!$customerUuid) {
                    $customerTypeNamespace = null;
                }
            }

            $values = [
                'payload_uuid'   => $this->uuid,
                'place_uuid'     => $placeUuid,
                'order'          => $index,
                'type'           => $type,
                'customer_uuid'  => $customerUuid,
                'customer_type'  => $customerTypeNamespace,
            ];

            $existingWaypointMarker = $availableWaypointMarkers
                ->first(fn ($waypointMarker) => $waypointMarker->place_uuid === $placeUuid && !in_array($waypointMarker->uuid, $keptWaypointIds));

            if ($existingWaypointMarker) {
                $existingWaypointMarker->update($values);
                $keptWaypointIds[] = $existingWaypointMarker->uuid;
                continue;
            }

            $waypointRecord    = Waypoint::create($values);
            $keptWaypointIds[] = $waypointRecord->uuid;
        }

        $this->waypointMarkers()
            ->whereNotIn('uuid', $keptWaypointIds)
            ->get()
            ->each
            ->delete();

        return $this->refresh()->load(['waypoints']);
    }

    public function _findCorrectDestinationForEntity($entityAttributes = []): ?Place
    {
        $destinationId = Utils::get($entityAttributes, 'destination_uuid');

        $destination = Place::where('meta->search_uuid', $destinationId)->first();
        if (!$destination) {
            $destination = Place::where('meta->_console_destination_uuid', $destinationId)->first();
        }

        return $destination;
    }

    /**
     * Get the payload pickup point or the first waypoint.
     *
     * @return \Fleetbase\Models\Place|null
     */
    public function getDropoffOrLastWaypoint(): ?Place
    {
        $this->loadMissing(['dropoff', 'waypoints']);

        if ($this->dropoff instanceof Place) {
            return $this->dropoff;
        }

        if ($this->waypoints()->count()) {
            return $this->waypoints->last();
        }

        return null;
    }

    /**
     * Get the payload pickup point or the first waypoint.
     *
     * @return \Fleetbase\Models\Place|null
     */
    public function getPickupOrFirstWaypoint(): ?Place
    {
        $this->loadMissing(['pickup', 'waypoints']);

        if ($this->pickup instanceof Place) {
            return $this->pickup;
        }

        if ($this->waypoints()->count()) {
            return $this->waypoints->first();
        }

        return null;
    }

    /**
     * Get the payload pickup point or the current waypoint.
     *
     * @return \Fleetbase\Models\Place|null
     */
    public function getPickupOrCurrentWaypoint(): ?Place
    {
        $this->loadMissing(['pickup', 'dropoff', 'waypoints']);

        if ($this->pickup instanceof Place) {
            return $this->pickup;
        }

        // special case where starting point is drivers current location
        // this special case can be set in order meta `pickup_is_driver_location`
        // this will start the order at the current location of the driver
        if ($this->hasMeta('pickup_is_driver_location')) {
            // if should use the driver location attempt to use dropoff
            if ($this->dropoff instanceof Place) {
                return $this->dropoff;
            }
        }

        // use the current waypoint
        // if the current waypoint isn't found fallback to first waypoint
        if ($this->waypoints()->count()) {
            $destination = null;

            if (Str::isUuid($this->current_waypoint_uuid)) {
                $destination = $this->waypoints->firstWhere('uuid', $this->current_waypoint_uuid);
            }

            if (!$destination) {
                $destination = $this->waypoints->first();
            }

            return $destination;
        }

        return null;
    }

    /**
     * Get the lightweight pickup fallback for index resources.
     */
    public function getIndexPickupPlaceAttribute(): ?Place
    {
        if ($this->pickup instanceof Place) {
            return $this->pickup;
        }

        $this->loadMissing('firstWaypointMarker.place');

        return data_get($this, 'firstWaypointMarker.place');
    }

    /**
     * Get the lightweight dropoff fallback for index resources.
     */
    public function getIndexDropoffPlaceAttribute(): ?Place
    {
        if ($this->dropoff instanceof Place) {
            return $this->dropoff;
        }

        $this->loadMissing('lastWaypointMarker.place');

        return data_get($this, 'lastWaypointMarker.place');
    }

    public function getPickupRegion(): string
    {
        $pickup = $this->getPickupOrCurrentWaypoint();

        return $pickup->country ?? $pickup->province ?? $pickup->district ?? 'SG';
    }

    public function getCountryCode(): string
    {
        $start = $this->getPickupOrCurrentWaypoint();

        return $start->country;
    }

    public function getAllStops()
    {
        $this->loadMissing(['pickup', 'dropoff', 'waypoints']);
        $stops = collect();

        if ($this->pickup) {
            $stops->push($this->pickup);
        }

        if ($this->dropoff) {
            $stops->push($this->dropoff);
        }

        if ($this->waypoints) {
            foreach ($this->waypoints as $waypoint) {
                $stops->push($waypoint);
            }
        }

        // ensure all stops/waypoints are instances of Place
        $stops = $stops->map(function ($place) {
            if (is_array($place)) {
                return new Place($place);
            }

            if ($place instanceof Place) {
                return $place;
            }

            return null;
        });

        return $stops->filter();
    }

    /**
     * Get the pickup location for the payload.
     *
     * @return Point
     */
    public function getPickupLocation()
    {
        $pickup = $this->getPickupOrCurrentWaypoint();

        return $pickup->location ?? new Point(0, 0);
    }

    public function getOrder()
    {
        if ($this->order) {
            return $this->order;
        }

        $this->load('order');

        return $this->order;
    }

    public function removeWaypoints()
    {
        Waypoint::where('payload_uuid', $this->uuid)->delete();
        $this->setRelation('waypoints', collect());

        return $this;
    }

    public function removePlace($property, array $options = [])
    {
        // remove multiple places
        if (is_array($property)) {
            foreach ($property as $prop) {
                if (is_string($prop)) {
                    $this->removePlace($prop, $options);
                }
            }

            return $this;
        }

        $attr     = $property . '_uuid';
        $save     = data_get($options, 'save', false);
        $callback = data_get($options, 'callback', false);

        $this->setAttribute($attr, null);
        $this->setRelation($property, null);

        if ($save) {
            $this->updateQuietly([$attr => null]);
        }

        if (is_callable($callback)) {
            $callback($this);
        }

        return $this;
    }

    public function setPlace($property, Place $place, array $options = [])
    {
        $attr     = $property . '_uuid';
        $instance = Place::createFromMixed($place);
        $save     = data_get($options, 'save', false);
        $callback = data_get($options, 'callback', false);

        if ($instance) {
            if (Str::isUuid($instance)) {
                $this->setAttribute($attr, $instance);
            } elseif ($instance instanceof Model) {
                $this->setAttribute($attr, $instance->uuid);
            } else {
                $this->setAttribute($attr, $instance);
            }
        }

        // Get the ID property
        $id = $this->{$attr};

        // set relationship to model instance to
        if ($instance instanceof Model) {
            $this->setRelation($property, $instance);
        }

        // If optioned to save
        if ($save) {
            $this->updateQuietly([$attr => $id]);
        }

        if (is_callable($callback)) {
            $callback($instance, $this);
        }

        return $this;
    }

    public function setPickup($place, array $options = [])
    {
        // if using the special [driver] value, set the meta `pickup_is_driver_location`
        if ($place === '[driver]') {
            $this->setMeta('pickup_is_driver_location', true);

            return;
        }

        if (!$place instanceof Place) {
            $place = Place::createFromMixed($place);
        }

        return $this->setPlace('pickup', $place, $options);
    }

    public function setDropoff($place, array $options = [])
    {
        if (!$place instanceof Place) {
            $place = Place::createFromMixed($place);
        }

        return $this->setPlace('dropoff', $place, $options);
    }

    public function setReturn($place, array $options = [])
    {
        if (!$place instanceof Place) {
            $place = Place::createFromMixed($place);
        }

        return $this->setPlace('return', $place, $options);
    }

    // when an order only has waypoints -- no pickup/dropoff
    public function getIsMultipleDropOrderAttribute()
    {
        return !$this->pickup && $this->waypoints && $this->waypoints->count() > 0;
    }

    /**
     * Set the first waypoint and update activity.
     *
     * @param Point $location
     *
     * @return void
     */
    public function setFirstWaypoint(?Activity $activity = null, $location = null)
    {
        $destination = null;

        if ($this->isMultipleDropOrder) {
            $destination = $this->waypoints->first();
        } else {
            $destination = $this->pickup ? $this->pickup : $this->waypoints->first();
        }

        if (!$destination) {
            return $this;
        }

        $this->current_waypoint_uuid = $destination->uuid;
        $this->saveQuietly();
        $this->updateWaypointActivity($activity, $location);

        return $this->load('currentWaypoint');
    }

    /**
     * Update the current waypoint activity and it's entities.
     *
     * @param Point                               $location
     * @param \Fleetbase\Models\Proof|string|null $proof    resolvable proof of delivery/activity
     *
     * @return $this
     */
    public function updateWaypointActivity(?Activity $activity = null, $location = null, $proof = null)
    {
        $this->loadMissing('order');

        if ($this->isMultipleDropOrder && Utils::isActivity($activity) && $location) {
            // update activity for the current waypoint
            $currentWaypoint = $this->waypointMarkers->firstWhere('place_uuid', $this->current_waypoint_uuid);
            if ($currentWaypoint) {
                $currentWaypoint->insertActivity($activity, $location, $proof);
                $activity->fireEvents($this->order, $currentWaypoint);
            }

            // update activity for all entities for this destination/waypoint
            $entities = $this->entities->where('destination_uuid', $this->current_waypoint_uuid);
            foreach ($entities as $entity) {
                $entity->insertActivity($activity, $location, $proof);
                if ($activity && $activity->complete()) {
                    event(new EntityCompleted($entity, $activity));
                } else {
                    event(new EntityActivityChanged($entity, $activity));
                }
            }

            // if this activity completes the waypoint notify waypoint customer
            if ($activity && $activity->complete()) {
                event(new WaypointCompleted($currentWaypoint, $activity));
            } else {
                event(new WaypointActivityChanged($currentWaypoint, $activity));
            }
        }

        return $this;
    }

    /**
     * Set the next waypoint in sequence.
     *
     * @return void
     */
    public function setNextWaypointDestination()
    {
        $nextWaypoint = $this->waypointMarkers->first(function ($waypoint) {
            return !$waypoint->complete && $waypoint->place_uuid !== $this->current_waypoint_uuid;
        });

        if (!$nextWaypoint || $this->current_waypoint_uuid === $nextWaypoint->place_uuid) {
            return $this;
        }

        // if next waypoint is waypoint get the place record
        $destination = $nextWaypoint;
        if ($nextWaypoint instanceof Waypoint) {
            $destination = $nextWaypoint->getPlace();
        }

        // Always set the destination using a place model
        $this->setCurrentWaypoint($destination);
        $this->setRelation('currentWaypoint', $destination);

        return $this;
    }

    public function updateOrderDistanceAndTime(): ?Order
    {
        // load the order
        $this->load(['order']);

        // get the order
        $order = $this->order;

        // set google matrix based distance and time
        if ($order instanceof Order) {
            return $order->setDistanceAndTime();
        }

        return null;
    }

    public function findDestinationFromKey(?string $destinationKey = null): ?Place
    {
        if ($destinationKey === null) {
            return null;
        }

        // if waypoint index provided
        if (is_numeric($destinationKey)) {
            $waypoint = $this->waypoints->values()->get($destinationKey);

            if ($waypoint) {
                return $waypoint;
            }
        }

        // if explicitly set to pickup
        if ($destinationKey === 'pickup' && $this->pickup) {
            return $this->pickup;
        }

        // if explicitly set to dropoff
        if ($destinationKey === 'dropoff' && $this->dropoff) {
            return $this->dropoff;
        }

        // if waypoint public_id
        if (Utils::isPublicId($destinationKey)) {
            $waypoint = $this->waypoints->firstWhere('public_id', $destinationKey);

            // if no waypoint found from public_id check pickup/dropoff
            if (!$waypoint) {
                $waypoint = collect([$this->pickup, $this->dropoff])->firstWhere('public_id', $destinationKey);
            }

            if ($waypoint) {
                return $waypoint;
            }
        }

        // if waypoint uuid
        if (Str::isUuid($destinationKey)) {
            $waypoint = $this->waypoints->firstWhere('uuid', $destinationKey);

            // if no waypoint found from uuid check pickup/dropoff
            if (!$waypoint) {
                $waypoint = collect([$this->pickup, $this->dropoff])->firstWhere('uuid', $destinationKey);
            }

            if ($waypoint) {
                return $waypoint;
            }
        }

        // confirm destination_uuid is indeed a place record
        if (isset($attributes['destination_uuid']) && Place::where('uuid', $attributes['destination_uuid'])->doesntExist()) {
            // search waypoints for search_uuid if any
            $destination = Place::where('meta->search_uuid', $attributes['destination_uuid'])->first();

            if ($destination instanceof Place) {
                return $destination;
            }
        }

        // Validate destination actually exists
        if (isset($attributes['destination_uuid']) && Place::where('uuid', $attributes['destination_uuid'])->doesntExist()) {
            $destination = $this->_findCorrectDestinationForEntity($attributes);

            if ($destination instanceof Place) {
                return $destination;
            }
        }

        return null;
    }
}
