<?php

namespace Modules\Users\Services;

use App\Models\ActivityLogModel;
use App\Models\PermissionModel;
use App\Models\RoleModel;
use App\Models\RolePermissionModel;
use CodeIgniter\HTTP\Files\UploadedFile;
use Config\Uploads as UploadsConfig;
use Modules\Auth\Models\UserModel;
use Modules\Auth\Models\UserSessionModel;
use Modules\Auth\Services\PasswordPolicyService;
use Modules\Tpin\Services\TpinService;

class UserService
{
    protected UserModel $users;
    protected UserSessionModel $sessions;
    protected ActivityLogModel $activity;
    protected UploadsConfig $uploadsConfig;
    protected RoleModel $roles;
    protected RolePermissionModel $rolePermissions;

    public function __construct()
    {
        $this->users            = new UserModel();
        $this->sessions         = new UserSessionModel();
        $this->activity         = new ActivityLogModel();
        $this->uploadsConfig    = config('Uploads');
        $this->roles            = new RoleModel();
        $this->rolePermissions  = new RolePermissionModel();
    }

    /**
     * @return array{success: bool, id?: int, errors?: array, tempPassword?: string}
     */
    public function create(array $input, ?int $actingUserId): array
    {
        if ($this->users->isUsernameOrEmailTaken($input['username'], $input['email'])) {
            return ['success' => false, 'errors' => ['username' => 'That username or email is already registered.']];
        }

        $access = $this->resolveAccess($input, null, $actingUserId);
        if (! $access['success']) {
            return $access;
        }

        $tempPassword = $this->generateTempPassword();

        $data = [
            'uuid'                 => $this->uuid(),
            'first_name'           => $input['first_name'],
            'last_name'            => $input['last_name'],
            'username'             => $input['username'],
            'email'                => $input['email'],
            'phone'                => $input['phone'] ?? null,
            'password_hash'        => password_hash($tempPassword, PASSWORD_BCRYPT, ['cost' => 12]),
            'password_changed_at'  => date('Y-m-d H:i:s'),
            'must_change_password' => 1,
            'role_id'              => $access['role_id'],
            'branch_id'            => $input['branch_id'] ?: null,
            'department_id'        => $input['department_id'] ?: null,
            'status'               => $input['status'] ?? 'active',
            'email_verified_at'    => date('Y-m-d H:i:s'),
            'tpin_required'        => $this->resolveTpinRequired($input),
            'created_by'           => $actingUserId,
            'updated_by'           => $actingUserId,
        ];

        if (! $this->users->insert($data, true)) {
            return ['success' => false, 'errors' => $this->users->errors()];
        }

        $id = $this->users->getInsertID();
        $this->activity->log('users', 'created', $actingUserId, "Created user #{$id} ({$input['username']})", (string) $id);

        return ['success' => true, 'id' => $id, 'tempPassword' => $tempPassword];
    }

    /**
     * @return array{success: bool, errors?: array}
     */
    public function update(int $id, array $input, ?int $actingUserId): array
    {
        if ($this->users->isUsernameOrEmailTaken($input['username'], $input['email'], $id)) {
            return ['success' => false, 'errors' => ['username' => 'That username or email is already registered to another user.']];
        }

        $user = $this->users->find($id);
        if (! $user) {
            return ['success' => false, 'errors' => ['id' => 'User not found.']];
        }

        $access = $this->resolveAccess($input, $id, $actingUserId);
        if (! $access['success']) {
            return $access;
        }

        $roleChanged = (int) $user->role_id !== (int) $access['role_id'];

        $data = [
            'first_name'    => $input['first_name'],
            'last_name'     => $input['last_name'],
            'username'      => $input['username'],
            'email'         => $input['email'],
            'phone'         => $input['phone'] ?? null,
            'role_id'       => $access['role_id'],
            'branch_id'     => $input['branch_id'] ?: null,
            'department_id' => $input['department_id'] ?: null,
            'updated_by'    => $actingUserId,
        ];

        if (array_key_exists('tpin_required', $input)) {
            $data['tpin_required'] = ! empty($input['tpin_required']) ? 1 : 0;
        }

        if (! $this->users->update($id, $data)) {
            return ['success' => false, 'errors' => $this->users->errors()];
        }

        if ($roleChanged) {
            $this->sessions->revokeAllForUser($id);
            $this->cleanupOrphanCustomRole((int) $user->role_id, $id);
        }

        $this->activity->log('users', 'updated', $actingUserId, "Updated user #{$id}", (string) $id);

        return ['success' => true];
    }

    public function activate(int $id, ?int $actingUserId): void
    {
        $this->users->resetFailedLogins($id);
        $this->users->setStatus($id, 'active');
        $this->activity->log('users', 'activated', $actingUserId, "Activated user #{$id}", (string) $id);
    }

    public function deactivate(int $id, ?int $actingUserId): void
    {
        $this->users->setStatus($id, 'inactive');
        foreach ($this->sessions->activeForUser($id) as $session) {
            $this->sessions->where('id', $session['id'])
                ->set('revoked_at', date('Y-m-d H:i:s'))
                ->update();
        }
        $this->activity->log('users', 'deactivated', $actingUserId, "Deactivated user #{$id}", (string) $id);
    }

    public function softDelete(int $id, ?int $actingUserId): void
    {
        $this->users->delete($id);
        $this->activity->log('users', 'deleted', $actingUserId, "Soft-deleted user #{$id}", (string) $id);
    }

    public function restore(int $id, ?int $actingUserId): void
    {
        $this->users->restore($id);
        $this->activity->log('users', 'restored', $actingUserId, "Restored user #{$id}", (string) $id);
    }

    public function adminResetPassword(int $id, ?int $actingUserId): string
    {
        $tempPassword = $this->generateTempPassword();
        (new PasswordPolicyService())->setPassword($id, $tempPassword);
        $this->users->update($id, ['must_change_password' => 1]);

        $this->activity->log('users', 'password_reset_by_admin', $actingUserId, "Password reset for user #{$id} by administrator", (string) $id);

        return $tempPassword;
    }

    public function uploadAvatar(int $id, UploadedFile $file): array
    {
        if (! $file->isValid() || $file->hasMoved()) {
            return ['success' => false, 'error' => 'Upload failed. Please try again.'];
        }

        if (! in_array($file->getMimeType(), $this->uploadsConfig->avatarAllowedMimes, true)) {
            return ['success' => false, 'error' => 'Only JPEG, PNG or WEBP images are allowed.'];
        }

        if ($file->getSizeByUnit('kb') > $this->uploadsConfig->avatarMaxSizeKB) {
            return ['success' => false, 'error' => 'Image must be smaller than ' . $this->uploadsConfig->avatarMaxSizeKB . 'KB.'];
        }

        $newName = $file->getRandomName();

        if (! is_dir($this->uploadsConfig->avatarUploadPath)) {
            mkdir($this->uploadsConfig->avatarUploadPath, 0755, true);
        }

        $file->move($this->uploadsConfig->avatarUploadPath, $newName);

        $relativePath = rtrim($this->uploadsConfig->avatarPublicPath, '/') . '/' . $newName;
        $this->users->update($id, ['avatar_path' => $relativePath]);

        return ['success' => true, 'path' => $relativePath];
    }

    public function isOnline(int $userId): bool
    {
        $sessions = $this->sessions->activeForUser($userId);
        foreach ($sessions as $s) {
            if (strtotime($s['last_activity_at']) >= (time() - 300)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Whether a role was auto-created for direct per-user module access.
     */
    public function isCustomRole(?array $role): bool
    {
        return $role && str_starts_with($role['slug'] ?? '', 'custom-');
    }

    /**
     * @return array{success: bool, role_id?: int, errors?: array}
     */
    protected function resolveAccess(array $input, ?int $userId, ?int $actingUserId): array
    {
        $mode = $input['access_mode'] ?? 'existing_role';

        if ($mode === 'existing_role') {
            $roleId = (int) ($input['role_id'] ?? 0);
            if ($roleId <= 0) {
                return ['success' => false, 'errors' => ['role_id' => 'Please select a role.']];
            }

            return ['success' => true, 'role_id' => $roleId];
        }

        $permissionIds = array_unique(array_map('intval', (array) ($input['permissions'] ?? [])));
        $permissionIds = array_values(array_filter($permissionIds));

        $dashboardPerm = (new PermissionModel())->where('slug', 'dashboard.view')->first();
        if ($dashboardPerm) {
            $dashboardId = (int) $dashboardPerm['id'];
            if (! in_array($dashboardId, $permissionIds, true)) {
                $permissionIds[] = $dashboardId;
            }
        }

        if ($permissionIds === []) {
            return ['success' => false, 'errors' => ['permissions' => 'Select at least one module permission.']];
        }

        if ($userId) {
            $user = $this->users->find($userId);
            if ($user && $user->role_id) {
                $existingRole = $this->roles->find($user->role_id);
                if ($this->isCustomRole($existingRole)) {
                    $this->rolePermissions->syncForRole((int) $existingRole['id'], $permissionIds);
                    $this->activity->log('roles', 'permissions_updated', $actingUserId, 'Updated custom permissions for user #' . $userId, (string) $existingRole['id']);

                    return ['success' => true, 'role_id' => (int) $existingRole['id']];
                }
            }
        }

        $username = trim((string) ($input['username'] ?? 'user'));
        $roleName = $userId ? "Custom - {$username}" : "Custom - {$username}";
        $slug     = 'custom-' . $this->roles->generateSlug($username);

        $existing = $this->roles->where('slug', $slug)->first();
        if ($existing) {
            $roleId = (int) $existing['id'];
        } else {
            if (! $this->roles->insert([
                'name'        => $roleName,
                'slug'        => $slug,
                'description' => 'Auto-created for direct user module access.',
                'is_system'   => 0,
            ], true)) {
                return ['success' => false, 'errors' => $this->roles->errors()];
            }

            $roleId = $this->roles->getInsertID();
            $this->activity->log('roles', 'created', $actingUserId, "Created custom role #{$roleId} for user access", (string) $roleId);
        }

        $this->rolePermissions->syncForRole($roleId, $permissionIds);

        return ['success' => true, 'role_id' => $roleId];
    }

    /**
     * Remove a per-user custom role when the user is reassigned elsewhere.
     */
    protected function cleanupOrphanCustomRole(?int $oldRoleId, int $userId): void
    {
        if (! $oldRoleId) {
            return;
        }

        $role = $this->roles->find($oldRoleId);
        if (! $this->isCustomRole($role)) {
            return;
        }

        if ($this->roles->isInUse($oldRoleId)) {
            return;
        }

        $this->roles->delete($oldRoleId);
        $this->activity->log('roles', 'deleted', null, "Removed unused custom role #{$oldRoleId} after user #{$userId} reassignment", (string) $oldRoleId);
    }

    protected function generateTempPassword(int $length = 14): string
    {
        $upper   = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
        $lower   = 'abcdefghijkmnopqrstuvwxyz';
        $numbers = '23456789';
        $special = '!@#$%^&*()-_=+';
        $all     = $upper . $lower . $numbers . $special;

        $password  = $upper[random_int(0, strlen($upper) - 1)];
        $password .= $lower[random_int(0, strlen($lower) - 1)];
        $password .= $numbers[random_int(0, strlen($numbers) - 1)];
        $password .= $special[random_int(0, strlen($special) - 1)];

        for ($i = strlen($password); $i < $length; $i++) {
            $password .= $all[random_int(0, strlen($all) - 1)];
        }

        return str_shuffle($password);
    }

    private function resolveTpinRequired(array $input): int
    {
        if (array_key_exists('tpin_required', $input)) {
            return ! empty($input['tpin_required']) ? 1 : 0;
        }

        return (new TpinService())->companyPolicy()['required_by_default'] ? 1 : 0;
    }

    protected function uuid(): string
    {
        $data    = random_bytes(16);
        $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
        $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
    }
}
