<?php

namespace App\Models;

use App\Models\Concerns\BelongsToTenant;
use App\Models\Concerns\HasPermissions;
use App\Models\Sales\Till;
use App\Services\AuditLog;
use App\Services\CurrentContext;
use App\Traits\HasUserstamps;
use Database\Factories\UserFactory;
use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthentication;
use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthenticationRecovery;
use Filament\Auth\MultiFactor\Email\Contracts\HasEmailAuthentication;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use SensitiveParameter;

/**
 * TOTP 2FA uses Filament's native AppAuthentication provider against the
 * existing `two_factor_secret`/`two_factor_recovery_codes` columns (no
 * Fortify dependency — see docs/migration-plan.md §5). Email-OTP second
 * factor is the pre-existing bespoke flow (LoginOtpService), wired through a
 * custom MultiFactorAuthenticationProvider; `two_factor_email_enabled` is
 * unchanged from the source app.
 */
class User extends Authenticatable implements FilamentUser, HasAppAuthentication, HasAppAuthenticationRecovery, HasEmailAuthentication
{
    /** @use HasFactory<UserFactory> */
    use BelongsToTenant, HasFactory, HasPermissions, HasUserstamps, Notifiable, SoftDeletes;

    public const STATUS_ACTIVE = 'active';

    public const STATUS_SUSPENDED = 'suspended';

    public const STATUS_INACTIVE = 'inactive';

    public const STATUSES = [
        self::STATUS_ACTIVE,
        self::STATUS_SUSPENDED,
        self::STATUS_INACTIVE,
    ];

    public const LOCKOUT_THRESHOLD = 20;

    public const LOCKOUT_DURATION_HOURS = 24;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'phone',
        'profile_image',
        'status',
        'last_login_at',
        'last_login_ip',
        'failed_login_count',
        'locked_until',
        'must_change_password',
        'password_changed_at',
        'two_factor_email_enabled',
        'pin_hash',
        'otp_hash',
        'otp_expires_at',
        'otp_attempts',
        'last_otp_sent_at',
        'password_reset_otp_hash',
        'password_reset_otp_expires_at',
        'role_id',
        'is_all_branches',
        'created_by',
        'updated_by',
        'email_verified_at',
        'dismissed_cheque_alert_ids',
    ];

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

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
            'last_login_at' => 'datetime',
            'locked_until' => 'datetime',
            'must_change_password' => 'boolean',
            'password_changed_at' => 'datetime',
            'two_factor_email_enabled' => 'boolean',
            'otp_expires_at' => 'datetime',
            'otp_attempts' => 'integer',
            'last_otp_sent_at' => 'datetime',
            'password_reset_otp_expires_at' => 'datetime',
            'two_factor_confirmed_at' => 'datetime',
            'failed_login_count' => 'integer',
            'is_all_branches' => 'boolean',
            'dismissed_cheque_alert_ids' => 'array',
            'two_factor_secret' => 'encrypted',
            'two_factor_recovery_codes' => 'encrypted:array',
        ];
    }

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

    public function getAppAuthenticationSecret(): ?string
    {
        return $this->two_factor_secret;
    }

    public function saveAppAuthenticationSecret(#[SensitiveParameter] ?string $secret): void
    {
        $this->two_factor_secret = $secret;
        $this->two_factor_confirmed_at = $secret !== null ? now() : null;
        $this->save();

        // Ported from FortifyServiceProvider's TwoFactorAuthenticationEnabled/
        // Disabled listeners — Filament's native provider fires no events, and
        // only ever calls this once the setup code has already been verified
        // (see docs/migration-plan.md §5 / Decisions Log), so "secret saved"
        // and "confirmed" are the same moment here; folded into one audit
        // action rather than two.
        AuditLog::record(
            $secret !== null ? 'user.two_factor_enabled' : 'user.two_factor_disabled',
            $this,
        );
    }

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

    /**
     * @return ?array<string>
     */
    public function getAppAuthenticationRecoveryCodes(): ?array
    {
        return $this->two_factor_recovery_codes;
    }

    /**
     * @param  ?array<string>  $codes
     */
    public function saveAppAuthenticationRecoveryCodes(#[SensitiveParameter] ?array $codes): void
    {
        $this->two_factor_recovery_codes = $codes;
        $this->save();
    }

    public function hasEmailAuthentication(): bool
    {
        return (bool) $this->two_factor_email_enabled;
    }

    /**
     * Whether Quick PIN login is actually usable for this user: they have set a
     * PIN *and* have not opted into a real second factor (PIN must never be a
     * downgrade path around TOTP/email-OTP — see PinLoginController::store()).
     * Used to decide where a trusted-device token is minted and whether the
     * PIN pad is offered on the login page at all, so the pad never appears
     * for users it cannot serve.
     */
    public function canUseQuickLogin(): bool
    {
        return $this->pin_hash !== null
            && $this->two_factor_confirmed_at === null
            && ! $this->hasEmailAuthentication();
    }

    public function toggleEmailAuthentication(bool $condition): void
    {
        $this->two_factor_email_enabled = $condition;
        $this->save();
    }

    protected static function booted(): void
    {
        // Track password rotation. must_change_password is no longer inferred
        // here — every writer (the admin form's toggle, the self-service
        // profile page, the admin reset-password action) sets it explicitly.
        static::saving(function (User $user): void {
            if ($user->isDirty('password')) {
                $user->password_changed_at = now();
            }
        });
    }

    protected function email(): Attribute
    {
        return Attribute::make(
            set: fn ($value) => strtolower(trim((string) $value)),
        );
    }

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

    /**
     * Every role assigned to this user (the real basis for permissions).
     */
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, 'user_roles')->withTimestamps();
    }

    public function warehouses(): BelongsToMany
    {
        return $this->belongsToMany(Branch::class, 'user_warehouse_access', 'user_id', 'warehouse_id');
    }

    public function tills(): BelongsToMany
    {
        return $this->belongsToMany(Till::class, 'user_till_access');
    }

    public function createdBy(): BelongsTo
    {
        return $this->belongsTo(self::class, 'created_by');
    }

    public function updatedBy(): BelongsTo
    {
        return $this->belongsTo(self::class, 'updated_by');
    }

    public function isFullAdmin(): bool
    {
        return Cache::remember(
            "user:{$this->id}:full_admin",
            now()->addHour(),
            fn () => $this->roles()->where('is_full_admin', true)->where('is_active', true)->exists(),
        );
    }

    /**
     * An explicit, independently-grantable "sees every branch" override —
     * distinct from {@see isFullAdmin()} (full RBAC bypass) so e.g. a
     * regional manager can be given cross-branch visibility without also
     * being made a full admin. Not to be confused with
     * {@see CurrentContext::isAllBranches()}, which is an
     * unrelated session-level "aggregate view" toggle.
     */
    public function hasAllBranchAccess(): bool
    {
        return Cache::remember(
            "user:{$this->id}:all_branches",
            now()->addHour(),
            fn () => $this->is_all_branches
                || $this->roles()->where('is_all_branches', true)->where('is_active', true)->exists(),
        );
    }

    public function isActive(): bool
    {
        return $this->status === self::STATUS_ACTIVE;
    }

    public function isSuspended(): bool
    {
        return $this->status === self::STATUS_SUSPENDED;
    }

    public function isLocked(): bool
    {
        return $this->locked_until !== null && $this->locked_until->isFuture();
    }

    public function initials(): string
    {
        $parts = preg_split('/\s+/', trim((string) $this->name)) ?: [];
        $initials = collect($parts)
            ->filter()
            ->take(2)
            ->map(fn (string $part): string => Str::upper(Str::substr($part, 0, 1)))
            ->implode('');

        return $initials !== '' ? $initials : '?';
    }
}
