<?php

namespace App\Models;

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\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Carbon\CarbonInterface;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\DB;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

/**
 * Tenant authorization (quick reference):
 * - {@see hasPermission()}: required by {@see \App\Http\Middleware\CheckTenantPermission} using {@see \App\Services\TenantRoleService} permission names.
 * - {@see isTenantManager()}: full dashboard, leave-approval UIs, payroll admin area — keyed off role name admin|hr|manager.
 * - {@see visibleStaffUserIds()}: team-scoped IDs; tenant role admin/HR **or** {@see isCompanyAccountAdmin()} = all directory staff; else reporting tree + self.
 * - {@see self::USER_TYPE_SUPERADMIN} / {@see self::USER_TYPE_ADMIN} / {@see self::USER_TYPE_STAFF}: `user_type` encodes account class (platform, company admin, or employee directory).
 */
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /** Platform (global) super admin — not a tenant employee. */
    public const USER_TYPE_SUPERADMIN = 'superadmin';

    /** Company-level admin (settings, user management) — not in the HR employee directory. */
    public const USER_TYPE_ADMIN = 'admin';

    /** People operations / payroll / attendance directory (employees). */
    public const USER_TYPE_STAFF = 'staff_members';

    public const STATUS_ACTIVE = 'active';

    public const STATUS_INACTIVE = 'inactive';

    protected $table = 'users';

    protected static function booted(): void
    {
        static::updating(function (User $user): void {
            if (! $user->isDirty('user_type')) {
                return;
            }
            $from = (string) $user->getOriginal('user_type', '');
            $to = (string) $user->user_type;
            if ($from !== self::USER_TYPE_STAFF || $to !== self::USER_TYPE_ADMIN) {
                return;
            }
            if ($user->attendances()->exists() || $user->leaves()->exists() || $user->employeePayrolls()->exists()) {
                throw new \InvalidArgumentException('Cannot change user_type to admin while this user has attendance, leave, or payroll records.');
            }
            if (DB::table('payrolls')->where('user_id', $user->id)->exists()) {
                throw new \InvalidArgumentException('Cannot change user_type to admin while this user has payroll records.');
            }
        });
    }

    protected $fillable = [
        'company_id',
        'role_id',
        'name',
        'email',
        'profile_photo',
        'password',
        'employee_number',
        'phone',
        'gender',
        'dob',
        'joining_date',
        'registration_date',
        'confirmation_date',
        'probation_end_date',
        'last_working_date',
        'report_to',
        'department_id',
        'designation_id',
        'location_id',
        'work_state',
        'shift_id',
        'salary_group_id',
        'account_holder_name',
        'bank_name',
        'account_number',
        'branch_name',
        'city',
        'ifsc_code',
        'pan_number',
        'father_name',
        'uan_number',
        'pf_join_date',
        'pt_location',
        'status',
        'user_type',
        'login_enabled',
        'allow_login',
        'is_superadmin',
    ];

    protected $hidden = [
        'password',
        'remember_token',
        'email_verification_code',
        'reset_code',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
            'is_superadmin' => 'boolean',
            'login_enabled' => 'boolean',
            'allow_login' => 'boolean',
            'dob' => 'date',
            'joining_date' => 'date',
            'registration_date' => 'date',
            'confirmation_date' => 'date',
            'probation_end_date' => 'date',
            'last_working_date' => 'date',
            'pf_join_date' => 'date',
        ];
    }

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

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

    public function profileIntro(): HasOne
    {
        return $this->hasOne(UserProfileIntro::class, 'user_id');
    }

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

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

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

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

    public function salaryGroup(): BelongsTo
    {
        return $this->belongsTo(SalaryGroup::class, 'salary_group_id');
    }

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

    /**
     * People in the HR / payroll / attendance “directory” (`user_type` = staff).
     */
    public function scopeEmployees(Builder $query): Builder
    {
        return $query->where('user_type', self::USER_TYPE_STAFF);
    }

    /**
     * Same as {@see scopeEmployees()}: tenant workforce rows (not platform or company-admin accounts).
     */
    public function scopeForTenantDirectory(Builder $query): Builder
    {
        return $query->where('user_type', self::USER_TYPE_STAFF);
    }

    /**
     * Active workforce only — use for dropdowns, team pickers, payroll runs, and scoped team lists.
     */
    public function scopeForActiveDirectory(Builder $query): Builder
    {
        return $query->forTenantDirectory()->where('status', self::STATUS_ACTIVE);
    }

    /**
     * Employees who should be considered for a payroll run (active, joined, or exited within the period).
     */
    public function scopeForPayrollPeriod(Builder $query, CarbonInterface $start, CarbonInterface $end): Builder
    {
        $from = $start->format('Y-m-d');
        $to = $end->format('Y-m-d');

        return $query->forTenantDirectory()->where(function (Builder $q) use ($from, $to) {
            $q->where('status', self::STATUS_ACTIVE)
                ->orWhereBetween('last_working_date', [$from, $to])
                ->orWhereBetween('joining_date', [$from, $to]);
        });
    }

    public function isSuperAdmin(): bool
    {
        return (bool) ($this->is_superadmin ?? false);
    }

    /**
     * Company admin account (`user_type` = admin). Not the same as tenant role name "admin" — this is the
     * org-level login that is excluded from the employee directory but must see all employees in the company.
     */
    public function isCompanyAccountAdmin(): bool
    {
        if ($this->isSuperAdmin()) {
            return false;
        }

        return ($this->user_type ?? '') === self::USER_TYPE_ADMIN && $this->company_id !== null;
    }

    /**
     * Tenant users who count as employees for self-service attendance, face clocking, and similar workforce features.
     */
    public function isWorkforceMember(): bool
    {
        if ($this->isSuperAdmin()) {
            return false;
        }

        return ($this->user_type ?? '') === self::USER_TYPE_STAFF;
    }

    /**
     * Check if the user has the given permission (via tenant role).
     * Super admins have all permissions. Users without a role have none.
     */
    public function hasPermission(string $name): bool
    {
        if ($this->isSuperAdmin()) {
            return true;
        }
        $role = $this->role;
        if (! $role) {
            return false;
        }
        return $role->permissions()->where('name', $name)->exists();
    }

    /**
     * Whether the user gets "management" UI: full dashboard, leave approval entry points, etc.
     * Uses system role *name* (not only permission table). Prefer {@see self::hasPermission()} for
     * route access; use {@see self::visibleStaffUserIds()} for which employees appear in team lists.
     * Includes HR + admin + line manager roles.
     */
    public function isTenantManager(): bool
    {
        if ($this->isSuperAdmin()) {
            return true;
        }
        if ($this->isCompanyAccountAdmin()) {
            return true;
        }
        $role = $this->role;
        if (! $role) {
            return false;
        }

        return in_array($role->name ?? '', ['admin', 'hr', 'manager'], true);
    }

    public function isTenantHr(): bool
    {
        if ($this->isSuperAdmin()) {
            return true;
        }
        $role = $this->role;

        return $role && ($role->name ?? '') === 'hr';
    }

    public function isTenantAdmin(): bool
    {
        if ($this->isSuperAdmin()) {
            return true;
        }

        $role = $this->role;
        if (! $role) {
            return false;
        }

        return ($role->name ?? '') === 'admin';
    }

    /**
     * Resolve staff user IDs for scoped queries (attendance, leaves, etc.).
     * - admin + HR: all staff in company
     * - manager / other roles: self plus descendants in the reporting tree (report_to)
     * - permissions still gate which routes a user can open; this only limits *which user IDs* are listed.
     */
    public function visibleStaffUserIds(): array
    {
        $companyId = $this->company_id;
        if (! $companyId) {
            return [(int) $this->id];
        }

        if ($this->isTenantAdmin() || $this->isTenantHr() || $this->isCompanyAccountAdmin()) {
            return self::query()
                ->where('company_id', $companyId)
                ->forActiveDirectory()
                ->pluck('id')
                ->map(fn ($id) => (int) $id)
                ->all();
        }

        // Build reporting edges once for the company using report_to.
        $users = self::query()
            ->where('company_id', $companyId)
            ->forActiveDirectory()
            ->get(['id', 'report_to']);

        $childrenByManager = [];
        foreach ($users as $u) {
            $managerId = $u->report_to;
            if (! $managerId) {
                continue;
            }
            $childrenByManager[(int) $managerId] ??= [];
            $childrenByManager[(int) $managerId][] = (int) $u->id;
        }

        $seen = [];
        $queue = $childrenByManager[(int) $this->id] ?? [];
        while (! empty($queue)) {
            $id = array_shift($queue);
            if (isset($seen[$id])) {
                continue;
            }
            $seen[$id] = true;
            foreach ($childrenByManager[$id] ?? [] as $childId) {
                if (! isset($seen[$childId])) {
                    $queue[] = $childId;
                }
            }
        }
        
        $ids = array_values(array_unique(array_merge(array_keys($seen), [(int) $this->id])));
        sort($ids);
        
        return $ids;
    }

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

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

    public function employeePayrolls(): HasMany
    {
        return $this->hasMany(EmployeePayroll::class, 'user_id');
    }

    public function face(): HasOne
    {
        return $this->hasOne(UserFace::class, 'user_id');
    }

    public function employeeComponentValues(): HasMany
    {
        return $this->hasMany(EmployeeComponentValue::class, 'user_id');
    }

    public function salaryRevisions(): HasMany
    {
        return $this->hasMany(SalaryRevision::class, 'user_id');
    }

    public function latestEffectiveSalaryRevision(?CarbonInterface $asOf = null): ?SalaryRevision
    {
        $date = ($asOf ?? now())->toDateString();

        return $this->salaryRevisions()
            ->whereDate('effective_date', '<=', $date)
            ->orderByDesc('effective_date')
            ->orderByDesc('id')
            ->first();
    }

    public function documents(): MorphMany
    {
        return $this->morphMany(Document::class, 'documentable');
    }

    public function assignedLeads(): HasMany
    {
        return $this->hasMany(Lead::class, 'assigned_to');
    }

    public function createdLeadFollowups(): HasMany
    {
        return $this->hasMany(LeadFollowup::class, 'created_by');
    }

    /**
     * Profile photo URL for display (e.g. in avatars). Returns null if no photo.
     */
    public function getAvatarUrl(): ?string
    {
        if (empty($this->profile_photo)) {
            return null;
        }
        if (str_starts_with($this->profile_photo, 'http://') || str_starts_with($this->profile_photo, 'https://')) {
            return $this->profile_photo;
        }
        return asset('storage/' . ltrim($this->profile_photo, '/'));
    }

    /**
     * Two-letter initials from name (e.g. "John Doe" -> "JD"). Use when no profile photo.
     */
    public function getInitials(): string
    {
        $name = trim((string) ($this->name ?? ''));
        if ($name === '') {
            return '??';
        }
        $parts = preg_split('/\s+/', $name, 3);
        if (count($parts) === 1) {
            return strtoupper(mb_substr($parts[0], 0, 2));
        }
        return strtoupper(mb_substr($parts[0], 0, 1) . mb_substr($parts[1], 0, 1));
    }

    /**
     * Consistent avatar color for initials (bg + text). Same user => same color.
     * Returns full Tailwind classes so the compiler can detect them.
     */
    public function getAvatarColorClass(): string
    {
        $seed = (string) ($this->id ?? 0) . trim((string) ($this->name ?? ''));
        $classes = [
            'bg-emerald-100 text-emerald-800',
            'bg-blue-100 text-blue-800',
            'bg-violet-100 text-violet-800',
            'bg-amber-100 text-amber-800',
            'bg-rose-100 text-rose-800',
            'bg-teal-100 text-teal-800',
            'bg-indigo-100 text-indigo-800',
            'bg-cyan-100 text-cyan-800',
            'bg-orange-100 text-orange-800',
            'bg-pink-100 text-pink-800',
            'bg-lime-100 text-lime-800',
            'bg-fuchsia-100 text-fuchsia-800',
        ];
        $index = abs(crc32($seed)) % count($classes);
        return $classes[$index];
    }

    /**
     * Whether this user is in the reporting chain above the employee (direct or higher manager).
     */
    public function isReportingManagerOf(User $employee): bool
    {
        if ((int) $employee->id === (int) $this->id) {
            return false;
        }

        $cursorId = $employee->report_to;
        while ($cursorId) {
            if ((int) $cursorId === (int) $this->id) {
                return true;
            }
            $manager = self::withoutGlobalScopes()->find($cursorId);
            if (! $manager) {
                break;
            }
            $cursorId = $manager->report_to;
        }

        return false;
    }

    public function canApproveRegularization(AttendanceRegularization $regularization): bool
    {
        if ($regularization->status !== AttendanceRegularization::STATUS_PENDING) {
            return false;
        }
        if ((int) $regularization->company_id !== (int) $this->company_id) {
            return false;
        }
        if (! $this->hasPermission('attendances_edit')) {
            return false;
        }

        $requester = $regularization->relationLoaded('user')
            ? $regularization->user
            : $regularization->user()->first();
        if (! $requester) {
            return false;
        }

        if ($this->isReportingManagerOf($requester)) {
            return true;
        }

        return $this->isTenantAdmin() || $this->isTenantHr() || $this->isCompanyAccountAdmin();
    }
}
