<?php

declare(strict_types=1);

namespace App\Models;

use App\Models\Concerns\Auditable;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

/**
 * An application login account.
 *
 * There are exactly two kinds of user, distinguished by one boolean:
 *
 *   is_master = true   may additionally manage users and application settings
 *   is_master = false  everything else in the system, identically
 *
 * That is modelled as a flag rather than a roles table because a roles table
 * would be five tables and a permission cache to represent a single yes/no.
 *
 * A User is not the same thing as an Employee. Most employees never get a
 * login, and the two are linked optionally (from Phase 3 onwards) rather than
 * one being subordinate to the other.
 *
 * @property int $id
 * @property string $name
 * @property string $email
 * @property string $password
 * @property bool $is_master
 * @property bool $is_active
 * @property bool $must_change_password
 * @property Carbon|null $last_login_at
 * @property string|null $last_login_ip
 * @property Carbon|null $password_changed_at
 */
class User extends Authenticatable
{
    /** @use HasFactory<UserFactory> */
    use Auditable, HasFactory, Notifiable, SoftDeletes;

    /**
     * Mass-assignable attributes.
     *
     * An explicit allow-list, never `$guarded = []`: `is_master` is one request
     * parameter away from a privilege escalation if the model is ever filled
     * from unvalidated input.
     *
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'is_master',
        'is_active',
        'must_change_password',
    ];

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

    /**
     * Audit trail entries created *by* this user.
     */
    public function auditLogs(): HasMany
    {
        return $this->hasMany(AuditLog::class);
    }

    /**
     * The HR record linked to this login, if any.
     *
     * Optional and one-directional: most employees never get a login, and
     * `employee_id` is not yet set by any screen — this exists so the column
     * added in Phase 3 has a model-level relation ready for whichever future
     * screen links the two.
     *
     * @return BelongsTo<Employee, $this>
     */
    public function employee(): BelongsTo
    {
        return $this->belongsTo(Employee::class);
    }

    /**
     * Whether this account may manage users and application settings.
     */
    public function isMaster(): bool
    {
        return $this->is_master;
    }

    /**
     * Initials for the topbar avatar, e.g. "Priya Sharma" -> "PS".
     */
    public function initials(): string
    {
        $parts = preg_split('/\s+/', trim($this->name)) ?: [];
        $parts = array_values(array_filter($parts));

        if ($parts === []) {
            return '?';
        }

        $first = mb_substr($parts[0], 0, 1);
        $last = count($parts) > 1 ? mb_substr($parts[count($parts) - 1], 0, 1) : '';

        return mb_strtoupper($first.$last);
    }

    /**
     * Record a successful sign-in against the account.
     *
     * Written without touching `updated_at` or the audit trail: the sign-in
     * itself is logged separately by AuditLogger, and an "updated" entry for
     * every login would bury real changes.
     */
    public function recordSuccessfulLogin(?string $ipAddress): void
    {
        $this->disableAuditing();

        $this->forceFill([
            'last_login_at' => now(),
            'last_login_ip' => $ipAddress,
        ])->saveQuietly();

        $this->enableAuditing();
    }

    /**
     * Destroy this account's live sessions.
     *
     * The database session driver stores `user_id` on every row, which is what
     * makes this possible at all: disabling an account or resetting its
     * password takes effect immediately, rather than whenever the existing
     * session happens to expire.
     *
     * @param  string|null  $exceptSessionId  keep this session (the actor's own)
     * @return int number of sessions destroyed
     */
    public function terminateSessions(?string $exceptSessionId = null): int
    {
        if (config('session.driver') !== 'database') {
            return 0;
        }

        return DB::table((string) config('session.table', 'sessions'))
            ->where('user_id', $this->getKey())
            ->when($exceptSessionId !== null, fn ($query) => $query->where('id', '!=', $exceptSessionId))
            ->delete();
    }

    /**
     * @param  Builder<User>  $query
     */
    public function scopeActive(Builder $query): void
    {
        $query->where('is_active', true);
    }

    /**
     * @param  Builder<User>  $query
     */
    public function scopeMasters(Builder $query): void
    {
        $query->where('is_master', true);
    }

    /**
     * @param  Builder<User>  $query
     */
    public function scopeSearch(Builder $query, ?string $term): void
    {
        $term = trim((string) $term);

        if ($term === '') {
            return;
        }

        $query->where(function (Builder $inner) use ($term): void {
            $inner->where('name', 'like', '%'.$term.'%')
                ->orWhere('email', 'like', '%'.$term.'%');
        });
    }

    /**
     * Email is always stored and looked up in lowercase, so login is case-insensitive.
     */
    protected static function booted(): void
    {
        static::creating(static function (self $model): void {
            $model->email = strtolower($model->email);
        });

        static::updating(static function (self $model): void {
            $model->email = strtolower($model->email);
        });
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'password' => 'hashed',
            'is_master' => 'boolean',
            'is_active' => 'boolean',
            'must_change_password' => 'boolean',
            'last_login_at' => 'datetime',
            'password_changed_at' => 'datetime',
        ];
    }

    protected function auditLabel(): string
    {
        return $this->name;
    }
}
