<?php

namespace App\Models;

use App\Casts\CarbonCast;
use App\Casts\DistanceCast;
use App\Casts\FuelCast;
use App\Contracts\Model;
use App\Enums\AcarsType;
use App\Enums\FlightType;
use App\Enums\PirepFieldSource;
use App\Enums\PirepSource;
use App\Enums\PirepState;
use App\Enums\PirepStatus;
use App\Events\PirepStateChange;
use App\Events\PirepStatusChange;
use App\Traits\HashIdTrait;
use Illuminate\Database\Eloquent\Attributes\WithoutIncrementing;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Notifications\DatabaseNotificationCollection;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Kleemans\AttributeEvents;
use Kyslik\ColumnSortable\Sortable;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Models\Activity;
use Spatie\Activitylog\Traits\LogsActivity;

/**
 * @property string           $id
 * @property int              $user_id
 * @property int              $airline_id
 * @property int|null         $aircraft_id
 * @property int|null         $event_id
 * @property string|null      $flight_id
 * @property string|null      $flight_number
 * @property string|null      $route_code
 * @property string|null      $route_leg
 * @property FlightType       $flight_type
 * @property string           $dpt_airport_id
 * @property string           $arr_airport_id
 * @property string|null      $alt_airport_id
 * @property int|null         $level
 * @property mixed|null       $distance
 * @property mixed|null       $planned_distance
 * @property int|null         $flight_time
 * @property int|null         $planned_flight_time
 * @property float|null       $zfw
 * @property mixed|null       $block_fuel
 * @property mixed|null       $fuel_used
 * @property float|null       $landing_rate
 * @property int|null         $score
 * @property string|null      $route
 * @property string|null      $notes
 * @property PirepSource|null $source
 * @property string|null      $source_name
 * @property PirepState       $state
 * @property PirepStatus      $status
 * @property mixed|null       $submitted_at
 * @property mixed|null       $block_off_time
 * @property mixed|null       $block_on_time
 * @property Carbon|null      $created_at
 * @property Carbon|null      $updated_at
 * @property Carbon|null      $deleted_at
 * @property-read Collection<int, Acars> $acars
 * @property-read int|null $acars_count
 * @property-read Collection<int, Acars> $acars_logs
 * @property-read int|null $acars_logs_count
 * @property-read Collection<int, Acars> $acars_route
 * @property-read int|null $acars_route_count
 * @property-read Collection<int, Activity> $activities
 * @property-read int|null $activities_count
 * @property-read Aircraft|null $aircraft
 * @property-read Airline|null $airline
 * @property-read Airport|null $alt_airport
 * @property-read Airport|null $arr_airport
 * @property-read bool $cancelled
 * @property-read Collection<int, PirepComment> $comments
 * @property-read int|null $comments_count
 * @property-read Airport|null $dpt_airport
 * @property-read Collection<int, PirepFare> $fares
 * @property-read int|null $fares_count
 * @property-read Collection<int, PirepFieldValue> $field_values
 * @property-read int|null $field_values_count
 * @property-read mixed $fields
 * @property-read Flight|null $flight
 * @property-read string $ident
 * @property-read DatabaseNotificationCollection<int, DatabaseNotification> $notifications
 * @property-read int|null $notifications_count
 * @property-read User|null $pilot
 * @property-read Acars|null $position
 * @property-read float $progress_percent
 * @property-read bool $read_only
 * @property-read SimBrief|null $simbrief
 * @property-read Collection<int, JournalTransaction> $transactions
 * @property-read int|null $transactions_count
 * @property-read User|null $user
 *
 * @method static Builder<static>|Pirep            activeFlights(int $liveTime = 0)
 * @method static \Database\Factories\PirepFactory factory($count = null, $state = [])
 * @method static Builder<static>|Pirep            newModelQuery()
 * @method static Builder<static>|Pirep            newQuery()
 * @method static Builder<static>|Pirep            onlyTrashed()
 * @method static Builder<static>|Pirep            query()
 * @method static Builder<static>|Pirep            sortable($defaultParameters = null)
 * @method static Builder<static>|Pirep            whereAircraftId($value)
 * @method static Builder<static>|Pirep            whereAirlineId($value)
 * @method static Builder<static>|Pirep            whereAltAirportId($value)
 * @method static Builder<static>|Pirep            whereArrAirportId($value)
 * @method static Builder<static>|Pirep            whereBlockFuel($value)
 * @method static Builder<static>|Pirep            whereBlockOffTime($value)
 * @method static Builder<static>|Pirep            whereBlockOnTime($value)
 * @method static Builder<static>|Pirep            whereCreatedAt($value)
 * @method static Builder<static>|Pirep            whereDeletedAt($value)
 * @method static Builder<static>|Pirep            whereDistance($value)
 * @method static Builder<static>|Pirep            whereDptAirportId($value)
 * @method static Builder<static>|Pirep            whereEventId($value)
 * @method static Builder<static>|Pirep            whereFlightId($value)
 * @method static Builder<static>|Pirep            whereFlightNumber($value)
 * @method static Builder<static>|Pirep            whereFlightTime($value)
 * @method static Builder<static>|Pirep            whereFlightType($value)
 * @method static Builder<static>|Pirep            whereFuelUsed($value)
 * @method static Builder<static>|Pirep            whereId($value)
 * @method static Builder<static>|Pirep            whereLandingRate($value)
 * @method static Builder<static>|Pirep            whereLevel($value)
 * @method static Builder<static>|Pirep            whereNotes($value)
 * @method static Builder<static>|Pirep            wherePlannedDistance($value)
 * @method static Builder<static>|Pirep            wherePlannedFlightTime($value)
 * @method static Builder<static>|Pirep            whereRoute($value)
 * @method static Builder<static>|Pirep            whereRouteCode($value)
 * @method static Builder<static>|Pirep            whereRouteLeg($value)
 * @method static Builder<static>|Pirep            whereScore($value)
 * @method static Builder<static>|Pirep            whereSource($value)
 * @method static Builder<static>|Pirep            whereSourceName($value)
 * @method static Builder<static>|Pirep            whereState($value)
 * @method static Builder<static>|Pirep            whereStatus($value)
 * @method static Builder<static>|Pirep            whereSubmittedAt($value)
 * @method static Builder<static>|Pirep            whereUpdatedAt($value)
 * @method static Builder<static>|Pirep            whereUserId($value)
 * @method static Builder<static>|Pirep            whereZfw($value)
 * @method static Builder<static>|Pirep            withTrashed(bool $withTrashed = true)
 * @method static Builder<static>|Pirep            withoutTrashed()
 *
 * @mixin \Eloquent
 */
#[WithoutIncrementing]
class Pirep extends Model
{
    use AttributeEvents;
    use HasFactory;
    use HashIdTrait;
    use LogsActivity;
    use Notifiable;
    use SoftDeletes;
    use Sortable;

    public $table = 'pireps';

    protected $keyType = 'string';

    /** The form wants this */
    public $hours;

    public $minutes;

    protected $fillable = [
        'id',
        'user_id',
        'airline_id',
        'aircraft_id',
        'event_id',
        'flight_number',
        'route_code',
        'route_leg',
        'flight_id',
        'dpt_airport_id',
        'arr_airport_id',
        'alt_airport_id',
        'level',
        'distance',
        'planned_distance',
        'block_time',
        'flight_time',
        'planned_flight_time',
        'zfw',
        'block_fuel',
        'fuel_used',
        'landing_rate',
        'route',
        'notes',
        'score',
        'source',
        'source_name',
        'flight_type',
        'state',
        'status',
        'block_off_time',
        'block_on_time',
        'submitted_at',
        'created_at',
        'updated_at',
    ];

    public static array $rules = [
        'airline_id'     => 'required|exists:airlines,id',
        'aircraft_id'    => 'required|exists:aircraft,id',
        'event_id'       => 'nullable|numeric',
        'flight_number'  => 'required|integer|max_digits:4',
        'dpt_airport_id' => 'required',
        'arr_airport_id' => 'required',
        'block_fuel'     => 'nullable|numeric',
        'fuel_used'      => 'nullable|numeric',
        'level'          => 'nullable|numeric',
        'notes'          => 'nullable',
        'route'          => 'nullable',
    ];

    public $sortable = [
        'user_id',
        'airline_id',
        'aircraft_id',
        'event_id',
        'flight_number',
        'route_code',
        'route_leg',
        'flight_id',
        'dpt_airport_id',
        'arr_airport_id',
        'alt_airport_id',
        'distance',
        'flight_time',
        'fuel_used',
        'landing_rate',
        'score',
        'flight_type',
        'source',
        'state',
        'status',
        'submitted_at',
        'created_at',
    ];

    /**
     * Auto-dispatch events for lifecycle state changes
     */
    protected $dispatchesEvents = [
        'status:*' => PirepStatusChange::class,
        'state:*'  => PirepStateChange::class,
    ];

    /*
     * If a PIREP is in these states, then it can't be changed.
     */
    public static $read_only_states = [
        PirepState::ACCEPTED,
        PirepState::REJECTED,
        PirepState::CANCELLED,
    ];

    /*
     * If a PIREP is in one of these states, it can't be cancelled
     */
    public static $cancel_states = [
        PirepState::ACCEPTED,
        PirepState::REJECTED,
        PirepState::CANCELLED,
        PirepState::DELETED,
    ];

    /**
     * Create a new PIREP model from a given flight. Pre-populates the fields
     */
    public static function fromFlight(Flight $flight): self
    {
        return new self([
            'flight_id'      => $flight->id,
            'airline_id'     => $flight->airline_id,
            'event_id'       => $flight->event_id,
            'flight_number'  => $flight->flight_number,
            'route_code'     => $flight->route_code,
            'route_leg'      => $flight->route_leg,
            'dpt_airport_id' => $flight->dpt_airport_id,
            'arr_airport_id' => $flight->arr_airport_id,
            'route'          => $flight->route,
            'level'          => $flight->level,
        ]);
    }

    /**
     * Create a new PIREP from a SimBrief instance
     */
    public static function fromSimBrief(SimBrief $simbrief): self
    {
        return new self([
            'flight_id'      => $simbrief->flight->id,
            'airline_id'     => $simbrief->flight->airline_id,
            'event_id'       => $simbrief->flight->event_id,
            'flight_number'  => $simbrief->flight->flight_number,
            'route_code'     => $simbrief->flight->route_code,
            'route_leg'      => $simbrief->flight->route_leg,
            'dpt_airport_id' => $simbrief->flight->dpt_airport_id,
            'arr_airport_id' => $simbrief->flight->arr_airport_id,
            'route'          => $simbrief->ofp?->general->route,
            'level'          => $simbrief->ofp?->general->initial_altitude,
        ]);
    }

    /**
     * Get the flight ident, e.,g JBU1900/C.nn/L.yy
     */
    public function ident(): Attribute
    {
        return Attribute::make(get: function ($value, $attrs): string {
            $flight_id = optional($this->airline)->code;
            $flight_id .= $this->flight_number;

            if (filled($this->route_code)) {
                $flight_id .= '/C.'.$this->route_code;
            }

            if (filled($this->route_leg)) {
                $flight_id .= '/L.'.$this->route_leg;
            }

            return $flight_id;
        });
    }

    /**
     * Return if this PIREP can be edited or not
     */
    public function readOnly(): Attribute
    {
        return Attribute::make(get: fn ($_, $attrs): bool => \in_array(
            $this->state,
            static::$read_only_states,
            true
        ));
    }

    /**
     * Return the flight progress in a percent.
     */
    public function progressPercent(): Attribute
    {
        return Attribute::make(get: function ($_, array $attrs): float {
            $distance = $attrs['distance'];

            $upper_bound = $distance;
            if (!empty($attrs['planned_distance']) && $attrs['planned_distance'] > 0) {
                $upper_bound = $attrs['planned_distance'];
            }

            $upper_bound = empty($upper_bound) ? 1 : $upper_bound;
            $distance = empty($distance) ? $upper_bound : $distance;

            return round(($distance / $upper_bound) * 100);
        });
    }

    /**
     * Get the pirep_fields and then the pirep_field_values and
     * merge them together. If a field value doesn't exist then add in a fake one
     */
    public function fields(): Attribute
    {
        return Attribute::make(get: function ($_, $attrs) {
            $custom_fields = PirepField::whereIn('pirep_source', [$this->source, PirepFieldSource::BOTH])->get();
            $field_values = PirepFieldValue::where('pirep_id', $this->id)->orderBy(
                'created_at',
                'asc'
            )->get();

            // Merge the field values into $fields
            foreach ($custom_fields as $field) {
                $has_value = $field_values->firstWhere('slug', $field->slug);
                if (!$has_value) {
                    $field_values->push(
                        new PirepFieldValue([
                            'pirep_id' => $this->id,
                            'name'     => $field->name,
                            'slug'     => $field->slug,
                            'value'    => '',
                            'source'   => PirepFieldSource::MANUAL,
                        ])
                    );
                }
            }

            return $field_values;
        });
    }

    /**
     * Do some cleanup on the route
     */
    public function route(): Attribute
    {
        return Attribute::make(set: fn ($route) => strtoupper(trim((string) $route)));
    }

    /**
     * Return if this is cancelled or not
     */
    public function cancelled(): Attribute
    {
        return Attribute::make(get: fn ($_, $attrs): bool => $this->state === PirepState::CANCELLED);
    }

    /**
     * Check if this PIREP is allowed to be updated
     */
    public function allowedUpdates(): bool
    {
        return !$this->read_only;
    }

    /**
     * Return a custom field value
     */
    public function field($field_name): string
    {
        $field = $this->fields->where('name', $field_name)->first();
        if ($field) {
            return $field['value'];
        }

        return '';
    }

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly($this->fillable)
            ->logExcept(['created_at', 'updated_at'])
            ->logOnlyDirty()
            ->dontSubmitEmptyLogs()
            // Bypass custom casts to log only internal DB changes (internal unit)
            ->useAttributeRawValues([
                'distance',
                'planned_distance',
                'block_off_time',
                'block_on_time',
                'block_fuel',
                'fuel_used',
                'submitted_at',
            ]);
    }

    /**
     * Relationships
     */
    public function acars(): HasMany
    {
        return $this->hasMany(Acars::class, 'pirep_id')
            ->flightPath()
            ->orderedByCreatedAt()
            ->orderedBySimTime();
    }

    public function acars_logs(): HasMany
    {
        return $this->hasMany(Acars::class, 'pirep_id')
            ->ofType(AcarsType::LOG)
            ->orderedByCreatedAt('desc')
            ->orderedBySimTime();
    }

    public function acars_route(): HasMany
    {
        return $this->hasMany(Acars::class, 'pirep_id')
            ->ofType(AcarsType::ROUTE)
            ->orderedByOrder();
    }

    public function aircraft(): BelongsTo
    {
        return $this->belongsTo(Aircraft::class, 'aircraft_id');
    }

    public function airline(): BelongsTo
    {
        return $this->belongsTo(Airline::class, 'airline_id');
    }

    public function flight(): BelongsTo
    {
        return $this->belongsTo(Flight::class, 'flight_id');
    }

    public function arr_airport(): BelongsTo
    {
        return $this->belongsTo(Airport::class, 'arr_airport_id')->withDefault(function ($model) {
            if (!empty($this->attributes['arr_airport_id'])) {
                $model->id = $this->attributes['arr_airport_id'];
                $model->icao = $this->attributes['arr_airport_id'];
                $model->name = $this->attributes['arr_airport_id'];
            }

            return $model;
        });
    }

    public function alt_airport(): BelongsTo
    {
        return $this->belongsTo(Airport::class, 'alt_airport_id');
    }

    public function dpt_airport(): BelongsTo
    {
        return $this->belongsTo(Airport::class, 'dpt_airport_id')->withDefault(function ($model) {
            if (!empty($this->attributes['dpt_airport_id'])) {
                $model->id = $this->attributes['dpt_airport_id'];
                $model->icao = $this->attributes['dpt_airport_id'];
                $model->name = $this->attributes['dpt_airport_id'];
            }

            return $model;
        });
    }

    public function comments(): HasMany
    {
        return $this->hasMany(PirepComment::class, 'pirep_id')->orderBy('created_at', 'desc');
    }

    public function fares(): HasMany
    {
        return $this->hasMany(PirepFare::class, 'pirep_id');
    }

    public function field_values(): HasMany
    {
        return $this->hasMany(PirepFieldValue::class, 'pirep_id');
    }

    public function pilot(): BelongsTo
    {
        return $this->user();
    }

    /**
     * Relationship that holds the current position, but limits the ACARS
     *  relationship to only one row (the latest), to prevent an N+! problem
     */
    public function position(): HasOne
    {
        return $this->hasOne(Acars::class, 'pirep_id')
            ->flightPath()
            ->latest('created_at')
            ->latest('sim_time');
    }

    public function simbrief(): BelongsTo
    {
        return $this->belongsTo(SimBrief::class, 'id', 'pirep_id');
    }

    public function transactions(): HasMany
    {
        return $this->hasMany(JournalTransaction::class, 'ref_model_id')->where(
            'ref_model_type',
            self::class
        )->orderBy('credit', 'desc')->orderBy('debit', 'desc');
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    #[\Override]
    protected function casts(): array
    {
        return [
            'user_id'             => 'integer',
            'airline_id'          => 'integer',
            'aircraft_id'         => 'integer',
            'event_id'            => 'integer',
            'level'               => 'integer',
            'distance'            => DistanceCast::class,
            'planned_distance'    => DistanceCast::class,
            'block_time'          => 'integer',
            'block_off_time'      => CarbonCast::class,
            'block_on_time'       => CarbonCast::class,
            'flight_time'         => 'integer',
            'flight_type'         => FlightType::class,
            'planned_flight_time' => 'integer',
            'zfw'                 => 'float',
            'block_fuel'          => FuelCast::class,
            'fuel_used'           => FuelCast::class,
            'landing_rate'        => 'float',
            'score'               => 'integer',
            'source'              => PirepSource::class,
            'state'               => PirepState::class,
            'status'              => PirepStatus::class,
            'submitted_at'        => CarbonCast::class,
        ];
    }

    /**
     * Scope: PIREPs with state = IN_PROGRESS, optionally constrained to those
     * updated within the last $liveTime hours, ordered by updated_at desc, with
     * the relations needed by the live-map / live-flights endpoints eager-loaded.
     *
     * Replaces the previously-misnamed AcarsRepository::getPositions() method.
     */
    public function scopeActiveFlights(Builder $query, int $liveTime = 0): Builder
    {
        $query
            ->with(['aircraft', 'airline', 'arr_airport', 'dpt_airport', 'position', 'user'])
            ->where('state', PirepState::IN_PROGRESS);

        if ($liveTime > 0) {
            $query->where('updated_at', '>=', Carbon::now()->subHours($liveTime));
        }

        return $query->orderBy('updated_at', 'desc');
    }
}
