<?php

declare(strict_types=1);

namespace App\Models;

use App\Enums\UserRole;
use App\Enums\UserStatus;
use App\Enums\UserVerificationType;
use App\Enums\VerificationStatus;
use App\Models\Concerns\HasPublicId;
use App\Notifications\ResetPasswordNotification;
use App\Notifications\VerifyEmailNotification;
use Database\Factories\UserFactory;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
use Laravel\Sanctum\HasApiTokens;

/**
 * A marketplace account.
 *
 * There is deliberately no separate buyer or seller account type: the same user
 * buys and sells, and seller-specific state hangs off the optional
 * {@see SellerProfile} relation.
 *
 * @property-read Collection<int, UserVerification> $verifications
 */
class User extends Authenticatable implements MustVerifyEmail
{
    /** @use HasFactory<UserFactory> */
    use HasApiTokens, HasFactory, HasPublicId, Notifiable, SoftDeletes;

    /**
     * Role and status are intentionally absent: privilege escalation and
     * suspension must never ride in on a mass assignment from a request body.
     *
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'phone',
        'locale',
        'timezone',
    ];

    /** @var list<string> */
    protected $hidden = [
        'password',
        'remember_token',
        'two_factor_secret',
        'two_factor_recovery_codes',
    ];

    /** @return array<string, string> */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'phone_verified_at' => 'datetime',
            'password' => 'hashed',
            'role' => UserRole::class,
            'status' => UserStatus::class,
            'suspended_at' => 'datetime',
            // Encrypted so a database dump does not hand over 2FA seeds.
            'two_factor_secret' => 'encrypted',
            'two_factor_recovery_codes' => 'encrypted:array',
            'two_factor_confirmed_at' => 'datetime',
            'last_login_at' => 'datetime',
            'password_changed_at' => 'datetime',
        ];
    }

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

    /** @return HasOne<Profile, $this> */
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }

    /** @return HasOne<SellerProfile, $this> */
    public function sellerProfile(): HasOne
    {
        return $this->hasOne(SellerProfile::class);
    }

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

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

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

    /**
     * Domains this account holds, listed or not.
     *
     * Hangs off the user rather than the seller profile because a domain is
     * added before someone necessarily has one -- adding inventory is often the
     * act that creates the seller profile.
     *
     * @return HasMany<Domain, $this>
     */
    public function domains(): HasMany
    {
        return $this->hasMany(Domain::class);
    }

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

    // -----------------------------------------------------------------
    // Roles and access
    // -----------------------------------------------------------------

    public function hasRole(UserRole $role): bool
    {
        return $this->role === $role;
    }

    public function hasAtLeastRole(UserRole $role): bool
    {
        return $this->role->atLeast($role);
    }

    public function isStaff(): bool
    {
        return $this->role->isStaff();
    }

    public function isAdmin(): bool
    {
        return $this->role->atLeast(UserRole::Admin);
    }

    /** Whether the account may sign in and read its own data. */
    public function canAuthenticate(): bool
    {
        return $this->status->canAuthenticate() && ! $this->trashed();
    }

    /**
     * Whether the account may take marketplace actions that move money or
     * create obligations. Checked server-side on every such action -- an
     * authenticated session is not by itself permission to transact.
     */
    public function canTransact(): bool
    {
        return $this->status->canTransact()
            && ! $this->trashed()
            && $this->hasVerifiedEmail();
    }

    public function isSuspended(): bool
    {
        return in_array($this->status, [UserStatus::Suspended, UserStatus::Banned], true);
    }

    // -----------------------------------------------------------------
    // Seller identity
    // -----------------------------------------------------------------

    /**
     * Whether this account has a seller side.
     *
     * Falls back to an existence query only for a single unloaded model, which
     * is cheap and correct. Collection serialisation must eager-load the
     * relation -- see {@see scopeWithTrustSignals()}.
     */
    public function isSeller(): bool
    {
        return $this->relationLoaded('sellerProfile')
            ? $this->sellerProfile !== null
            : $this->sellerProfile()->exists();
    }

    /**
     * Eager-loads everything the user resources read.
     *
     * Named as a scope so the requirement travels with the query rather than
     * living in a comment. Any endpoint serialising more than one user must use
     * it, and the N+1 guard catches the ones that forget.
     *
     * @param  Builder<static>  $query
     */
    public function scopeWithTrustSignals($query): void
    {
        $query->with(['profile', 'sellerProfile', 'verifications']);
    }

    public function hasTwoFactorEnabled(): bool
    {
        return $this->two_factor_secret !== null && $this->two_factor_confirmed_at !== null;
    }

    // -----------------------------------------------------------------
    // Trust signals
    // -----------------------------------------------------------------

    /**
     * The verification badges this account has actually earned.
     *
     * Email and phone are read from the user record itself since they are
     * proven by the auth flow; everything else comes from the verification
     * ledger. Nothing here exposes the evidence behind a badge.
     *
     * @return list<UserVerificationType>
     */
    public function earnedVerifications(): array
    {
        $earned = [];

        if ($this->hasVerifiedEmail()) {
            $earned[] = UserVerificationType::Email;
        }

        if ($this->phone_verified_at !== null) {
            $earned[] = UserVerificationType::Phone;
        }

        /*
         * The relation must already be loaded.
         *
         * Deliberately no loadMissing() here. That call is a per-model eager
         * load, so serialising a collection issued one query per row -- and
         * because it is technically an *explicit* load, Model::preventLazyLoading
         * never fired on it. The guardrail was silently bypassed by the very
         * pattern used to satisfy it. Requiring the caller to eager-load hands
         * that protection back: forget it, and the framework throws in
         * development instead of quietly costing a query per seller in
         * production. Callers use `User::withTrustSignals()` or the relation
         * list on the resource.
         */
        foreach ($this->verifications as $verification) {
            if ($verification->status === VerificationStatus::Verified
                && ! in_array($verification->type, $earned, true)) {
                $earned[] = $verification->type;
            }
        }

        return $earned;
    }

    public function hasVerification(UserVerificationType $type): bool
    {
        return in_array($type, $this->earnedVerifications(), true);
    }

    // -----------------------------------------------------------------
    // Notifications
    // -----------------------------------------------------------------

    /**
     * Verification links must point at the SPA, not at the API, so the user
     * lands in the application rather than on a bare JSON response.
     */
    public function sendEmailVerificationNotification(): void
    {
        $this->notify(new VerifyEmailNotification);
    }

    /** Reset links must also land in the SPA, not on the API. */
    public function sendPasswordResetNotification($token): void
    {
        $this->notify(new ResetPasswordNotification($token));
    }

    public function routeNotificationForMail(): string
    {
        return $this->email;
    }
}
