<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Models\Concerns\HasCompressedImages;
use App\Notifications\ResetPasswordNotification;
use App\Notifications\VerifyEmailNotification;
use Database\Factories\UserFactory;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Filament\Panel;
use Illuminate\Contracts\Auth\MustVerifyEmail;
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\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Notifications\DatabaseNotificationCollection;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;

/**
 * @property int $id
 * @property string $name
 * @property string|null $first_name
 * @property string|null $last_name
 * @property string $email
 * @property string|null $phone
 * @property string|null $address
 * @property string|null $address_street
 * @property string|null $address_city
 * @property string|null $address_postal
 * @property string|null $profile_picture
 * @property Carbon|null $email_verified_at
 * @property string $password
 * @property string|null $remember_token
 * @property Carbon|null $created_at
 * @property Carbon|null $updated_at
 * @property string|null $two_factor_secret
 * @property string|null $two_factor_recovery_codes
 * @property string|null $two_factor_confirmed_at
 * @property bool $receive_new_user_alerts
 * @property bool $receive_new_adoption_alerts
 * @property bool $receive_new_fostering_alerts
 * @property bool $receive_new_assistance_alerts
 * @property bool $receive_draw_result_alerts
 * @property bool $receive_ticket_purchase_alerts
 * @property bool $receive_note_alerts
 * @property string $note_alert_preference
 * @property bool $receive_approved_adoption_alerts
 * @property int|null $current_membership_id
 * @property-read Collection<int, AdoptionApplication> $adoptionApplications
 * @property-read int|null $adoption_applications_count
 * @property-read Collection<int, BlogPost> $blogPosts
 * @property-read int|null $blog_posts_count
 * @property-read Collection<int, Membership> $memberships
 * @property-read int|null $memberships_count
 * @property-read Collection<int, DrawTicket> $drawTickets
 * @property-read int|null $draw_tickets_count
 * @property-read Membership|null $currentMembership
 * @property-read DatabaseNotificationCollection<int, DatabaseNotification> $notifications
 * @property-read int|null $notifications_count
 *
 * @method static \Database\Factories\UserFactory factory($count = null, $state = [])
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCreatedAt($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmail($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerifiedAt($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereId($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereIsAdmin($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereName($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePassword($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRememberToken($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereTwoFactorConfirmedAt($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereTwoFactorRecoveryCodes($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereTwoFactorSecret($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
 *
 * @property string $timezone
 * @property bool $is_admin
 * @property bool $banned
 * @property-read Collection<int, AssistanceApplication> $assistanceApplications
 * @property-read int|null $assistance_applications_count
 * @property-read Collection<int, FosteringApplication> $fosteringApplications
 * @property-read int|null $fostering_applications_count
 * @property-read string $full_address
 *
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereAddress($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereAddressCity($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereAddressPostal($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereAddressStreet($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereBanned($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCurrentMembershipId($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereFirstName($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereLastName($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereNoteAlertPreference($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePhone($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereProfilePicture($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereReceiveApprovedAdoptionAlerts($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereReceiveDrawResultAlerts($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereReceiveNewAdoptionAlerts($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereReceiveNewAssistanceAlerts($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereReceiveNewFosteringAlerts($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereReceiveNewUserAlerts($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereReceiveNoteAlerts($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereReceiveTicketPurchaseAlerts($value)
 * @method static \Illuminate\Database\Eloquent\Builder<static>|User whereTimezone($value)
 *
 * @mixin \Eloquent
 */
class User extends Authenticatable implements FilamentUser, HasAvatar, MustVerifyEmail
{
    /** @use HasFactory<UserFactory> */
    use HasCompressedImages, HasFactory, Notifiable, TwoFactorAuthenticatable;

    /**
     * The attributes that are mass assignable.
     *
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'first_name',
        'last_name',
        'email',
        'password',
        'phone',
        'address',
        'address_street',
        'address_city',
        'address_postal',
        'profile_picture',
        'timezone',
        'receive_new_user_alerts',
        'receive_new_adoption_alerts',
        'receive_new_fostering_alerts',
        'receive_new_assistance_alerts',
        'receive_draw_result_alerts',
        'receive_ticket_purchase_alerts',
        'receive_note_alerts',
        'note_alert_preference',
        'receive_approved_adoption_alerts',
    ];

    /**
     * The "booted" method of the model.
     */
    protected static function booted(): void
    {
        static::saving(function (User $user): void {
            if ($user->isDirty(['first_name', 'last_name'])) {
                $user->name = trim(($user->first_name ?? '').' '.($user->last_name ?? ''));
            }
        });
    }

    /**
     * Split a free-text full name into first/last name parts.
     *
     * Used only to derive structured names from legacy free-text `name` values
     * (the historical migration commands and the one-time backfill command) —
     * every other write path collects first_name/last_name directly.
     *
     * @return array{first_name: string|null, last_name: string|null}
     */
    public static function splitFullName(string $fullName): array
    {
        $normalized = trim(preg_replace('/\s+/', ' ', $fullName));

        if ($normalized === '') {
            return ['first_name' => null, 'last_name' => null];
        }

        $parts = explode(' ', $normalized, 2);

        return [
            'first_name' => $parts[0],
            'last_name' => $parts[1] ?? null,
        ];
    }

    /**
     * Shared validation rules for first_name/last_name, used by every write path
     * (registration, profile updates, admin panel, application forms) so the
     * whitespace-only guard can't silently drift between entry points.
     *
     * @return array<string, array<int, string>>
     */
    public static function nameValidationRules(): array
    {
        return [
            'first_name' => ['required', 'string', 'max:255', 'regex:/\S/'],
            'last_name' => ['required', 'string', 'max:255', 'regex:/\S/'],
        ];
    }

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var list<string>
     */
    protected $hidden = [
        'password',
        'two_factor_secret',
        'two_factor_recovery_codes',
        'remember_token',
    ];

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'two_factor_confirmed_at' => 'datetime',
            'password' => 'hashed',
            'is_admin' => 'boolean',
            'banned' => 'boolean',
            'receive_new_user_alerts' => 'boolean',
            'receive_new_adoption_alerts' => 'boolean',
            'receive_new_fostering_alerts' => 'boolean',
            'receive_new_assistance_alerts' => 'boolean',
            'receive_draw_result_alerts' => 'boolean',
            'receive_ticket_purchase_alerts' => 'boolean',
            'receive_note_alerts' => 'boolean',
            'receive_approved_adoption_alerts' => 'boolean',
        ];
    }

    /**
     * Get the image attribute configuration for this model.
     *
     * @return array<string, array{type: string, width?: int, height?: int, quality?: int}>
     */
    protected function getImageAttributes(): array
    {
        return [
            'profile_picture' => [
                'type' => 'single',
                'width' => 150,
                'height' => 150,
                'quality' => 90,
            ],
        ];
    }

    /**
     * Get the profile_picture attribute.
     *
     * @return Attribute<string|null, string|null>
     */
    protected function profilePicture(): Attribute
    {
        $config = $this->getImageConfig('profile_picture');
        $width = $config['width'] ?? 150;
        $height = $config['height'] ?? 150;
        $quality = $config['quality'] ?? 90;

        return Attribute::make(
            set: fn ($value) => $this->processSingleImage($value, $width, $height, $quality)
        );
    }

    /**
     * Get the user's initials
     */
    public function initials(): string
    {
        return Str::of($this->first_name ?? '')->substr(0, 1)
            ->append(Str::of($this->last_name ?? '')->substr(0, 1))
            ->toString();
    }

    /**
     * Get the user's first name
     */
    public function firstName(): string
    {
        return $this->first_name ?? '';
    }

    /**
     * @return HasMany<AdoptionApplication, $this>
     */
    public function adoptionApplications(): HasMany
    {
        return $this->hasMany(AdoptionApplication::class);
    }

    /**
     * @return HasMany<FosteringApplication, $this>
     */
    public function fosteringApplications(): HasMany
    {
        return $this->hasMany(FosteringApplication::class);
    }

    /**
     * @return HasMany<AssistanceApplication, $this>
     */
    public function assistanceApplications(): HasMany
    {
        return $this->hasMany(AssistanceApplication::class);
    }

    /**
     * @return HasMany<BlogPost, $this>
     */
    public function blogPosts(): HasMany
    {
        return $this->hasMany(BlogPost::class);
    }

    /**
     * @return HasMany<Membership, $this>
     */
    public function memberships(): HasMany
    {
        return $this->hasMany(Membership::class);
    }

    /**
     * @return BelongsTo<Membership, $this>
     */
    public function currentMembership(): BelongsTo
    {
        return $this->belongsTo(Membership::class, 'current_membership_id');
    }

    /**
     * @return HasMany<DrawTicket, $this>
     */
    public function drawTickets(): HasMany
    {
        return $this->hasMany(DrawTicket::class);
    }

    /**
     * Check if the user has an active membership.
     */
    public function hasActiveMembership(): bool
    {
        return $this->currentMembership?->isActive() ?? false;
    }

    public function canAccessPanel(Panel $panel): bool
    {
        return $this->is_admin;
    }

    /**
     * Get the URL for Filament avatar
     */
    public function getFilamentAvatarUrl(): ?string
    {
        if ($this->profile_picture) {
            return Storage::disk('public')->url($this->profile_picture);
        }

        return url('/images/default-avatar.svg');
    }

    /**
     * Get the URL for the user's profile picture or a placeholder
     */
    public function profilePictureUrl(): string
    {
        if ($this->profile_picture) {
            return Storage::disk('public')->url($this->profile_picture);
        }

        return '';
    }

    /**
     * Returns a single formatted address string for geocoding.
     * Uses structured fields when available, falls back to the legacy address column.
     */
    public function getFullAddressAttribute(): string
    {
        $structured = implode(', ', array_filter([
            $this->address_street,
            $this->address_city,
            $this->address_postal,
        ]));

        return $structured ?: ($this->address ?? '');
    }

    /**
     * Check if the user has completed their profile for adoption requests
     */
    public function hasCompletedProfileForAdoption(): bool
    {
        return ! empty($this->phone)
            && ! empty($this->address_street)
            && ! empty($this->address_city)
            && ! empty($this->address_postal);
    }

    /**
     * Get the count of active adoption applications (excluding archived)
     */
    public function activeAdoptionApplicationsCount(bool $lockForUpdate = false): int
    {
        $query = $this->adoptionApplications()
            ->whereNotIn('status', ['archived']);

        // Postgres rejects "SELECT count(*) ... FOR UPDATE" outright (0A000,
        // "FOR UPDATE is not allowed with aggregate functions"), so when the
        // rows must be locked select their ids and count them in PHP. This
        // takes the same row locks the guard relies on without asking the
        // database to aggregate under a lock.
        if ($lockForUpdate) {
            /** @phpstan-ignore larastan.noUnnecessaryCollectionCall (counting in the query would emit the aggregate Postgres rejects under FOR UPDATE) */
            return $query->lockForUpdate()->pluck('id')->count();
        }

        return $query->count();
    }

    /**
     * Check if the user has an active fostering application (excluding archived)
     */
    public function hasActiveFosteringApplication(bool $lockForUpdate = false): bool
    {
        $query = $this->fosteringApplications()
            ->whereNotIn('status', ['archived']);

        // See activeAdoptionApplicationsCount(): exists() compiles to
        // "select exists(select * ... for update)", which Postgres rejects for
        // the same reason it rejects a locked count() (0A000). Select the ids
        // under the lock and test the result in PHP instead.
        if ($lockForUpdate) {
            /** @phpstan-ignore larastan.noUnnecessaryCollectionCall (testing in the query would emit the "select exists(...)" Postgres rejects under FOR UPDATE) */
            return $query->lockForUpdate()->pluck('id')->isNotEmpty();
        }

        return $query->exists();
    }

    /**
     * Check if the user can submit a new adoption application
     */
    public function canSubmitAdoptionApplication(bool $lockForUpdate = false): bool
    {
        $maxApplications = (int) Setting::get('max_active_adoption_applications', 2);

        return $this->activeAdoptionApplicationsCount($lockForUpdate) < $maxApplications;
    }

    /**
     * Check if the user can submit a new fostering application
     */
    public function canSubmitFosteringApplication(bool $lockForUpdate = false): bool
    {
        return ! $this->hasActiveFosteringApplication($lockForUpdate);
    }

    /**
     * Get the count of active assistance applications (excluding archived)
     */
    public function activeAssistanceApplicationsCount(bool $lockForUpdate = false): int
    {
        $query = $this->assistanceApplications()
            ->whereNotIn('status', ['archived', 'hold']);

        // See activeAdoptionApplicationsCount(): Postgres will not aggregate
        // under FOR UPDATE, so lock the rows and count them in PHP.
        if ($lockForUpdate) {
            /** @phpstan-ignore larastan.noUnnecessaryCollectionCall (counting in the query would emit the aggregate Postgres rejects under FOR UPDATE) */
            return $query->lockForUpdate()->pluck('id')->count();
        }

        return $query->count();
    }

    /**
     * Check if the user can submit a new assistance application
     */
    public function canSubmitAssistanceApplication(bool $lockForUpdate = false): bool
    {
        $maxApplications = (int) Setting::get('max_active_assistance_applications', 2);

        return $this->activeAssistanceApplicationsCount($lockForUpdate) < $maxApplications;
    }

    /**
     * Check if this user is the demo user
     */
    public function isDemoUser(): bool
    {
        if (! config('demo.enabled')) {
            return false;
        }

        return $this->email === config('demo.user.email');
    }

    /**
     * Send the password reset notification.
     */
    public function sendPasswordResetNotification($token): void
    {
        $this->notify(new ResetPasswordNotification($token));
    }

    /**
     * Send the email verification notification.
     */
    public function sendEmailVerificationNotification(): void
    {
        $this->notify(new VerifyEmailNotification);
    }
}
