<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Facades\DB;

class Location extends Model
{

    /**
     * The attributes that are mass assignable.
     *
     * @var array<string>
     */
    protected $fillable = [
        'name',
        'location_type',
        'address',
        'city',
        'state',
        'country',
        'postal_code',
        'coordinates',
        'latitude',  // Legacy field
        'longitude', // Legacy field
        'elevation',
        'timezone',
        'metadata',
        'is_approximate',
        'source',
        'geocoding_data',
        'geocoded_at',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        // 'coordinates' => 'point', // This cast is not available in Laravel by default
        'latitude' => 'decimal:6',
        'longitude' => 'decimal:6',
        'elevation' => 'decimal:2',
        'metadata' => 'array',
        'geocoding_data' => 'array',
        'geocoded_at' => 'datetime',
        'is_approximate' => 'boolean',
    ];


    /**
     * Boot the model.
     */
    protected static function boot()
    {
        parent::boot();

        static::saving(function ($model) {
            // Sync point field with lat/lon fields for backward compatibility
            if ($model->latitude && $model->longitude && !$model->coordinates) {
                $model->coordinates = DB::raw(
                    "ST_SetSRID(ST_MakePoint($model->longitude, $model->latitude), 4326)"
                );
            } elseif ($model->coordinates && (!$model->latitude || !$model->longitude)) {
                $point = json_decode($model->coordinates);
                $model->longitude = $point->lng;
                $model->latitude = $point->lat;
            }
        });
    }

    /**
     * Get the location's string representation.
     */
    public function __toString(): string
    {
        $locationParts = [];
        if ($this->city) {
            $locationParts[] = $this->city;
        }
        if ($this->country) {
            $locationParts[] = $this->country;
        }
        $locationStr = $locationParts ? implode(', ', $locationParts) : 'Unknown location';
        
        return "{$this->name} ({$locationStr})";
    }

    /**
     * Get the formatted address.
     */
    public function getFormattedAddressAttribute(): string
    {
        $components = array_filter([
            $this->address,
            $this->city,
            $this->state,
            $this->postal_code,
            $this->country
        ]);

        return implode(', ', $components);
    }

    /**
     * Normalize a coordinate value.
     */
    protected function normalizeCoordinate($value, int $decimalPlaces = 6): ?float
    {
        if ($value === null) {
            return null;
        }

        try {
            // Convert to string first to handle both float and string inputs
            $value = (string) $value;
            // Use BC Math for precise decimal handling
            return round((float) $value, $decimalPlaces);
        } catch (\Exception $e) {
            return null;
        }
    }

    /**
     * Get the parent locatable model.
     */
    public function locatable(): MorphTo
    {
        return $this->morphTo();
    }

    /**
     * Get Google Maps URL for the location.
     *
     * @return string|null
     */
    public function getMapUrlAttribute(): ?string
    {
        if (!$this->latitude || !$this->longitude) {
            return null;
        }

        return sprintf(
            'https://www.google.com/maps?q=%f,%f',
            $this->latitude,
            $this->longitude
        );
    }

    /**
     * Update the location's coordinates.
     *
     * @param float $latitude
     * @param float $longitude
     * @param float|null $elevation
     * @return bool
     */
    public function updateCoordinates(float $latitude, float $longitude, ?float $elevation = null): bool
    {
        return $this->update([
            'coordinates' => DB::raw("ST_SetSRID(ST_MakePoint($longitude, $latitude), 4326)"),
            'elevation' => $elevation,
        ]);
    }

    /**
     * Set the location's address components.
     *
     * @param array<string, string> $components
     * @return bool
     */
    public function setAddress(array $components): bool
    {
        return $this->update([
            'address' => $components['address'] ?? $this->address,
            'city' => $components['city'] ?? $this->city,
            'state' => $components['state'] ?? $this->state,
            'country' => $components['country'] ?? $this->country,
            'postal_code' => $components['postal_code'] ?? $this->postal_code,
        ]);
    }

    /**
     * Scope a query to find locations within a radius.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param float $latitude
     * @param float $longitude
     * @param float $radius
     * @param string $unit
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeNearby($query, float $latitude, float $longitude, float $radius, string $unit = 'km')
    {
        $point = DB::raw("ST_MakePoint($longitude, $latitude)");
        $distance = $unit === 'mi' ? $radius * 1609.34 : $radius * 1000;

        return $query->whereRaw(
            "ST_DWithin(coordinates::geography, ?::geography, ?)",
            [$point, $distance]
        );
    }

    /**
     * Scope a query to find locations within bounds.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param array<string, float> $ne Northeast corner [lat, lng]
     * @param array<string, float> $sw Southwest corner [lat, lng]
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeInBounds($query, array $ne, array $sw)
    {
        $bounds = DB::raw(
            "ST_MakeEnvelope(
                {$sw['lng']}, {$sw['lat']},
                {$ne['lng']}, {$ne['lat']},
                4326
            )"
        );

        return $query->whereRaw("ST_Within(coordinates::geometry, ?::geometry)", [$bounds]);
    }

    /**
     * Calculate the distance to a point.
     *
     * @param float $latitude
     * @param float $longitude
     * @param string $unit
     * @return float|null
     */
    public function distanceTo(float $latitude, float $longitude, string $unit = 'km'): ?float
    {
        if (!$this->coordinates) {
            return null;
        }

        $point = DB::raw("ST_MakePoint($longitude, $latitude)");
        $distance = DB::selectOne(
            "SELECT ST_Distance(
                ST_GeomFromText(?),
                ST_GeomFromText(?),
                true
            ) as distance",
            [
                "POINT({$this->longitude} {$this->latitude})",
                "POINT($longitude $latitude)"
            ]
        )->distance;

        return $unit === 'mi' ? $distance * 0.000621371 : $distance / 1000;
    }
}