<?php

namespace App\Models;

use App\Enums\ApplicationStatus;
use App\Enums\ApplicationStep;
use App\Enums\PaymentStatus;
use App\Enums\VerificationStatus;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class Application extends Model
{
    use HasFactory;

    protected $fillable = [
        'application_number', 'applicant_id', 'intake_id', 'status', 'current_step',
        'completed_steps', 'applicant_type', 'study_mode', 'campus',
        'declaration_accepted', 'declaration_accepted_at', 'declaration_ip', 'declaration_version',
        'submitted_at', 'reviewed_at', 'reviewed_by', 'assigned_to',
        'decision_at', 'decision_by', 'decision_reason',
    ];

    protected function casts(): array
    {
        return [
            'completed_steps' => 'array',
            'declaration_accepted' => 'boolean',
            'declaration_accepted_at' => 'datetime',
            'submitted_at' => 'datetime',
            'reviewed_at' => 'datetime',
            'decision_at' => 'datetime',
            'status' => ApplicationStatus::class,
            'current_step' => ApplicationStep::class,
        ];
    }

    public function getRouteKeyName(): string
    {
        return 'application_number';
    }

    // ---------------------------------------------------------------- Relations

    public function applicant(): BelongsTo
    {
        return $this->belongsTo(Applicant::class);
    }

    public function intake(): BelongsTo
    {
        return $this->belongsTo(Intake::class);
    }

    public function choices(): HasMany
    {
        return $this->hasMany(ApplicationChoice::class)->orderBy('choice_number');
    }

    public function documents(): HasMany
    {
        return $this->hasMany(Document::class);
    }

    public function payments(): HasMany
    {
        return $this->hasMany(Payment::class);
    }

    public function notes(): HasMany
    {
        return $this->hasMany(ApplicationNote::class)->latest();
    }

    public function statusHistory(): HasMany
    {
        return $this->hasMany(ApplicationStatusHistory::class)->latest();
    }

    public function admissionRecord(): HasOne
    {
        return $this->hasOne(AdmissionRecord::class);
    }

    public function generatedDocuments(): HasMany
    {
        return $this->hasMany(GeneratedDocument::class);
    }

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

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

    // ------------------------------------------------------------------ Scopes

    public function scopeStatus(Builder $query, ApplicationStatus|string|null $status): Builder
    {
        if (blank($status)) {
            return $query;
        }

        return $query->where('status', $status instanceof ApplicationStatus ? $status->value : $status);
    }

    public function scopeSubmitted(Builder $query): Builder
    {
        return $query->whereNotNull('submitted_at');
    }

    public function scopeForIntake(Builder $query, int|string|null $intakeId): Builder
    {
        return $intakeId ? $query->where('intake_id', $intakeId) : $query;
    }

    public function scopeForProgramme(Builder $query, int|string|null $programmeId): Builder
    {
        if (! $programmeId) {
            return $query;
        }

        return $query->whereHas('choices', fn (Builder $q) => $q->where('programme_id', $programmeId));
    }

    public function scopeForSchool(Builder $query, int|string|null $schoolId): Builder
    {
        if (! $schoolId) {
            return $query;
        }

        return $query->whereHas(
            'choices.programme',
            fn (Builder $q) => $q->where('school_id', $schoolId)
        );
    }

    /** Free-text search over the identifiers staff actually type (PRD 66). */
    public function scopeSearch(Builder $query, ?string $term): Builder
    {
        if (blank($term)) {
            return $query;
        }

        $term = trim($term);

        return $query->where(function (Builder $q) use ($term) {
            $q->where('application_number', 'like', "%{$term}%")
                ->orWhereHas('applicant', function (Builder $a) use ($term) {
                    $a->where('first_name', 'like', "%{$term}%")
                        ->orWhere('last_name', 'like', "%{$term}%")
                        ->orWhere('phone', 'like', "%{$term}%")
                        ->orWhereHas('user', fn (Builder $u) => $u->where('email', 'like', "%{$term}%"));
                })
                ->orWhereHas('admissionRecord', fn (Builder $r) => $r->where('admission_number', 'like', "%{$term}%"));
        });
    }

    // ----------------------------------------------------------- Derived state

    public function firstChoice(): ?ApplicationChoice
    {
        return $this->choices->firstWhere('choice_number', 1) ?? $this->choices->first();
    }

    public function programme(): ?Programme
    {
        return $this->firstChoice()?->programme;
    }

    /** The payment that settles this application's fee, if any. */
    public function settledPayment(): ?Payment
    {
        return $this->payments->firstWhere('status', PaymentStatus::Paid);
    }

    public function isPaid(): bool
    {
        return $this->settledPayment() !== null;
    }

    public function isEditable(): bool
    {
        return $this->status->isEditable();
    }

    public function isSubmitted(): bool
    {
        return $this->submitted_at !== null;
    }

    public function hasCompletedStep(ApplicationStep $step): bool
    {
        return in_array($step->value, $this->completed_steps ?? [], true);
    }

    public function markStepCompleted(ApplicationStep $step): void
    {
        $this->completed_steps = collect($this->completed_steps ?? [])
            ->push($step->value)
            ->unique()
            ->values()
            ->all();
    }

    public function progressPercentage(): int
    {
        return (int) round(count($this->completed_steps ?? []) / ApplicationStep::total() * 100);
    }

    public function requiredDocumentsUploaded(): bool
    {
        $progress = $this->documentProgress();

        return $progress['uploaded'] >= $progress['required'];
    }

    /**
     * How many of the required document types have a usable upload.
     * Rejected uploads do not count towards the total.
     */
    public function documentProgress(): array
    {
        $required = DocumentType::query()->required()->pluck('slug');

        $uploaded = $this->documents
            ->where('verification_status', '!==', VerificationStatus::Rejected)
            ->pluck('document_type')
            ->unique();

        return [
            'uploaded' => $required->intersect($uploaded)->count(),
            'required' => $required->count(),
        ];
    }

    public function applicationFee(): float
    {
        $programme = $this->programme();

        $override = $programme
            ? $this->intake?->intakeProgrammes->firstWhere('programme_id', $programme->id)?->application_fee_override
            : null;

        return (float) ($override ?? $programme?->applicationFee() ?? setting('application_fee', 50000));
    }

    public function displayStatus(): string
    {
        return $this->status->applicantLabel();
    }
}
