<?php

namespace App\Models;

use App\Http\Middleware\CheckTenantPermission;
use App\Services\Finance\FinanceNavService;
use App\Services\TenantRoleService;
use App\Support\AfterResponse;
use Carbon\CarbonInterface;
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 Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\DB;
use Laravel\Sanctum\HasApiTokens;

/**
 * Tenant authorization (quick reference):
 * - {@see hasPermission()}: required by {@see CheckTenantPermission} using {@see 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.');
            }
        });
    }

    /**
     * Privilege / auth-control attributes — assign only via forceFill() in trusted code.
     *
     * @var list<string>
     */
    public const PRIVILEGED_ATTRIBUTES = [
        'company_id',
        'role_id',
        'status',
        'user_type',
        'allow_login',
        'is_superadmin',
        'two_factor_enabled',
        'two_factor_method',
        'two_factor_secret',
    ];

    protected $fillable = [
        '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',
        'attendance_mode',
        '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',
        'notification_preferences',
    ];

    protected $hidden = [
        'password',
        'remember_token',
        'email_verification_code',
        'reset_code',
        'two_factor_secret',
        'account_number',
        'pan_number',
        'ifsc_code',
        'uan_number',
        'bank_name',
        'branch_name',
        'account_holder_name',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
            'is_superadmin' => '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',
            'notification_preferences' => 'array',
            'two_factor_enabled' => 'boolean',
        ];
    }

    /**
     * Send notifications after the HTTP response (no queue). Console runs immediately.
     */
    public function notify($instance): void
    {
        AfterResponse::run(function () use ($instance): void {
            $this->notifyNow($instance);
        });
    }

    public function company(): BelongsTo
    {
        return $this->belongsTo(Company::class);
    }

    /**
     * Token storage key for password resets — scopes by company so the same
     * email in two tenants does not share a reset row.
     */
    public function getEmailForPasswordReset(): string
    {
        return $this->email.'|'.($this->company_id ?? 'platform');
    }

    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);
    }

    /**
     * True when last working day is recorded and that date is already over (not including today).
     */
    public function hasPassedLastWorkingDate(): bool
    {
        if (! $this->last_working_date) {
            return false;
        }

        return $this->last_working_date->toDateString() < now()->toDateString();
    }

    /**
     * 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';
    }

    /** Profile email-notification toggles (admin / HR / company account admin only). */
    public function canManageEmailNotificationPreferences(): bool
    {
        if ($this->isSuperAdmin()) {
            return true;
        }

        return $this->isCompanyAccountAdmin() || $this->isTenantAdmin() || $this->isTenantHr();
    }

    /**
     * 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 deviceTokens(): HasMany
    {
        return $this->hasMany(DeviceToken::class);
    }

    /**
     * @return list<string>
     */
    public function routeNotificationForFcm(): array
    {
        return $this->deviceTokens()->pluck('token')->filter()->values()->all();
    }

    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 the employee's direct reporting manager (report_to).
     */
    public function isDirectReportingManagerOf(User $employee): bool
    {
        if ((int) $employee->id === (int) $this->id) {
            return false;
        }

        return (int) ($employee->report_to ?? 0) === (int) $this->id;
    }

    /**
     * Company-wide access to other employees' personal documents (Admin/HR only; not line managers).
     */
    public function canManageEmployeeDocuments(): bool
    {
        if ($this->isSuperAdmin() || $this->isCompanyAccountAdmin()) {
            return true;
        }

        if ($this->hasPermission('users_edit')) {
            return true;
        }

        return $this->isTenantHr() || $this->isTenantAdmin();
    }

    /**
     * Full employee profile (bank, PAN, documents, etc.) vs team-scoped view for line managers.
     */
    public function canViewFullEmployeeProfile(User $employee): bool
    {
        if ($this->canManageEmployeeDocuments()) {
            return true;
        }

        return (int) $this->id === (int) $employee->id;
    }

    /**
     * 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 canReviewAttendanceRegularizations(): bool
    {
        return $this->hasPermission('attendances_approve_regularization')
            || $this->hasPermission('attendances_edit');
    }

    /**
     * Admin, HR, and company account admins may review any regularization in the company.
     */
    public function canReviewAttendanceRegularizationsCompanyWide(): bool
    {
        if (! $this->canReviewAttendanceRegularizations()) {
            return false;
        }

        return $this->isTenantAdmin()
            || $this->isTenantHr()
            || $this->isCompanyAccountAdmin();
    }

    public function hasActiveReportingManager(User $employee): bool
    {
        $directManagerId = (int) ($employee->report_to ?? 0);
        if ($directManagerId <= 0) {
            return false;
        }

        return self::withoutGlobalScopes()
            ->where('id', $directManagerId)
            ->where('company_id', $employee->company_id)
            ->exists();
    }

    public function canReviewRemoteAttendance(Attendance $attendance): bool
    {
        if ($attendance->remote_approval_status !== 'pending') {
            return false;
        }
        if ((int) $attendance->company_id !== (int) $this->company_id) {
            return false;
        }
        if (! $this->canReviewAttendanceRegularizations()) {
            return false;
        }

        $requester = $attendance->relationLoaded('user')
            ? $attendance->user
            : $attendance->user()->first();
        if (! $requester || (int) $requester->id === (int) $this->id) {
            return false;
        }

        if ($this->canReviewAttendanceRegularizationsCompanyWide()) {
            return true;
        }

        if ($this->isReportingManagerOf($requester)) {
            return true;
        }

        return ! $this->hasActiveReportingManager($requester);
    }

    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->canReviewAttendanceRegularizations()) {
            return false;
        }

        $requester = $regularization->relationLoaded('user')
            ? $regularization->user
            : $regularization->user()->first();
        if (! $requester || (int) $requester->id === (int) $this->id) {
            return false;
        }

        if ($this->canReviewAttendanceRegularizationsCompanyWide()) {
            return true;
        }

        if ($this->isReportingManagerOf($requester)) {
            return true;
        }

        return ! $this->hasActiveReportingManager($requester);
    }

    /** Own payslips, salary summary, and income-tax self-service. */
    public function canViewOwnPayroll(): bool
    {
        if ($this->isSuperAdmin()) {
            return true;
        }

        return $this->hasPermission('finance_self_view')
            || $this->hasPermission('payrolls_view');
    }

    /** Company payroll run, settings, and statutory configuration. */
    public function canManagePayroll(): bool
    {
        if ($this->isSuperAdmin()) {
            return true;
        }

        return $this->hasPermission('payrolls_view');
    }

    public function isTenantFinance(): bool
    {
        if ($this->isSuperAdmin()) {
            return true;
        }

        $role = $this->role;

        return $role && ($role->name ?? '') === 'finance';
    }

    public function canApproveLeave(Leave $leave): bool
    {
        if ($leave->status !== 'pending') {
            return false;
        }
        if ((int) $leave->company_id !== (int) $this->company_id) {
            return false;
        }
        if (! $this->hasPermission('leaves_approve_reject')) {
            return false;
        }

        $requester = $leave->relationLoaded('user')
            ? $leave->user
            : $leave->user()->first();
        if (! $requester) {
            return false;
        }

        if ($this->isReportingManagerOf($requester)) {
            return true;
        }

        return $this->isTenantAdmin() || $this->isTenantHr() || $this->isCompanyAccountAdmin();
    }

    public function canApproveExpense(Expense $expense): bool
    {
        if (! in_array($expense->status, ['pending', 'approved'], true)) {
            return false;
        }
        if ((int) $expense->company_id !== (int) $this->company_id) {
            return false;
        }
        if (! $this->hasPermission('finance_edit') && ! $this->hasPermission('expenses_approve')) {
            return false;
        }

        $nav = app(FinanceNavService::class);

        if ($nav->canViewCompanyExpenses($this)) {
            return true;
        }

        if ($this->hasPermission('finance_team_view') || $this->hasPermission('expenses_approve')) {
            $ids = $this->visibleStaffUserIds();

            return in_array((int) $expense->user_id, $ids, true)
                || in_array((int) $expense->submitted_by, $ids, true);
        }

        return false;
    }

    /**
     * Whether email notifications are enabled for a category (in-app bell is always on).
     */
    public function wantsEmailNotification(string $category): bool
    {
        $prefs = $this->notification_preferences ?? [];
        if (isset($prefs['email'][$category])) {
            return (bool) $prefs['email'][$category];
        }

        return (bool) config("hrms_notifications.categories.{$category}.default_email", true);
    }

    /**
     * @param  array<string, mixed>  $preferences
     */
    public function mergeNotificationPreferences(array $preferences): void
    {
        $current = $this->notification_preferences ?? [];
        $merged = array_replace_recursive($current, $preferences);
        $this->forceFill(['notification_preferences' => $merged])->save();
    }
}
