<?php
/**
 * NIS2 Agile - Auth Controller
 *
 * Gestisce registrazione, login, JWT tokens, profilo utente.
 */

require_once __DIR__ . '/BaseController.php';
require_once APP_PATH . '/services/RateLimitService.php';
require_once APP_PATH . '/services/SsoHelper.php';

class AuthController extends BaseController
{
    /**
     * Restituisce l'IP reale del client, gestendo proxy/nginx.
     */
    private function getClientIP(): string
    {
        $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        // Fidati di X-Forwarded-For solo se la richiesta arriva da localhost (nginx proxy)
        if (in_array($remoteAddr, ['127.0.0.1', '::1', 'unknown'])
            && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            $xForwardedFor = $_SERVER['HTTP_X_FORWARDED_FOR'];
            $ips = array_map('trim', explode(',', $xForwardedFor));
            $firstIp = filter_var($ips[0], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
            if ($firstIp !== false) {
                return $firstIp;
            }
        }
        return $remoteAddr;
    }

    /**
     * POST /api/auth/register
     */
    public function register(): void
    {
        // Rate limiting
        $ip = $this->getClientIP();
        RateLimitService::check("register:{$ip}", RATE_LIMIT_AUTH_REGISTER);
        RateLimitService::increment("register:{$ip}"); // $ip defined above via getClientIP()

        $this->validateRequired(['email', 'password', 'full_name']);

        $email = strtolower(trim($this->getParam('email')));
        $password = $this->getParam('password');
        $fullName = trim($this->getParam('full_name'));
        $phone = $this->getParam('phone');

        // Supporta sia `role` diretto (nuovo register.html) che `user_type` legacy
        $validRoles = ['super_admin', 'org_admin', 'compliance_manager', 'board_member', 'auditor', 'employee', 'consultant'];
        $roleParam  = trim($this->getParam('role', ''));
        $userType   = $this->getParam('user_type', 'azienda');

        if ($roleParam && in_array($roleParam, $validRoles, true) && $roleParam !== 'super_admin') {
            $role = $roleParam;
        } elseif ($userType === 'consultant') {
            $role = 'consultant';
        } else {
            $role = 'employee';
        }

        // Validazione email
        if (!$this->validateEmail($email)) {
            $this->jsonError('Formato email non valido', 400, 'INVALID_EMAIL');
        }

        // Validazione password
        $passwordErrors = $this->validatePassword($password);
        if (!empty($passwordErrors)) {
            $this->jsonError(
                implode('. ', $passwordErrors),
                400,
                'WEAK_PASSWORD'
            );
        }

        // Verifica email duplicata
        $existing = Database::fetchOne('SELECT id FROM users WHERE email = ?', [$email]);
        if ($existing) {
            $this->jsonError('Email già registrata', 409, 'EMAIL_EXISTS');
        }

        // Crea utente
        $userId = Database::insert('users', [
            'email'         => $email,
            'password_hash' => password_hash($password, PASSWORD_DEFAULT),
            'full_name'     => $fullName,
            'phone'         => $phone,
            'role'          => $role,
            'is_active'     => 1,
        ]);

        // Genera tokens
        $accessToken = $this->generateJWT($userId);
        $refreshToken = $this->generateRefreshToken($userId);

        // Audit log
        $this->currentUser = ['id' => $userId];
        $this->logAudit('user_registered', 'user', $userId, ['user_type' => $userType]);

        $this->jsonSuccess([
            'user' => [
                'id'        => $userId,
                'email'     => $email,
                'full_name' => $fullName,
                'role'      => $role,
            ],
            'access_token'  => $accessToken,
            'refresh_token' => $refreshToken,
            'expires_in'    => JWT_EXPIRES_IN,
        ], 'Registrazione completata', 201);
    }

    /**
     * POST /api/auth/login
     */
    public function login(): void
    {
        // Rate limiting
        $ip = $this->getClientIP();
        RateLimitService::check("login:{$ip}", RATE_LIMIT_AUTH_LOGIN);
        RateLimitService::increment("login:{$ip}");

        $this->validateRequired(['email', 'password']);

        $email = strtolower(trim($this->getParam('email')));
        $password = $this->getParam('password');

        // Trova utente
        $user = Database::fetchOne(
            'SELECT * FROM users WHERE email = ? AND is_active = 1',
            [$email]
        );

        // --- SSO Federation (Fase 1 / G02) ---
        // In SSO_MODE=local (default) tutto questo blocco è no-op: SsoHelper::login()
        // ritorna null e $ssoAuthorized resta false → cade nel fallback locale invariato.
        $ssoAuthorized = false;
        $sso = new SsoHelper();
        if (!$sso->isLocalOnly()) {
            $ssoResp = $sso->login($email, $password, 'nis2');
            if ($ssoResp === null) {
                // SSO unreachable
                if ($sso->isSsoOnly()) {
                    $this->jsonError('SSO non disponibile, riprovare', 503, 'SSO_UNAVAILABLE');
                }
                // dual: fallback locale (procedi al password_verify sotto)
            } else {
                $httpStatus = (int) ($ssoResp['_httpStatus'] ?? 0);
                if ($httpStatus === 200) {
                    $ssoIdentityId      = (int) ($ssoResp['identity_id'] ?? $ssoResp['user']['sso_identity_id'] ?? 0);
                    $ssoPasswordVersion = (int) ($ssoResp['password_version'] ?? 1);
                    if (!$user) {
                        // Utente SSO non ancora presente localmente: crealo
                        $ssoUser = $ssoResp['user'] ?? [];
                        $newId = Database::insert('users', [
                            'email'             => $email,
                            'password_hash'     => password_hash($password, PASSWORD_DEFAULT),
                            'full_name'         => $ssoUser['full_name'] ?? $email,
                            'role'              => 'employee',
                            'sso_identity_id'   => $ssoIdentityId,
                            'password_version'  => $ssoPasswordVersion,
                            'is_active'         => 1,
                        ]);
                        $user = Database::fetchOne('SELECT * FROM users WHERE id = ?', [$newId]);
                    } else {
                        // Utente esistente: link/sync se necessario (lazy backfill — decisione 2)
                        $updates = [];
                        if ((int) ($user['sso_identity_id'] ?? 0) !== $ssoIdentityId) {
                            $updates['sso_identity_id'] = $ssoIdentityId;
                        }
                        if ((int) ($user['password_version'] ?? 0) !== $ssoPasswordVersion) {
                            $updates['password_hash']    = password_hash($password, PASSWORD_DEFAULT);
                            $updates['password_version'] = $ssoPasswordVersion;
                        }
                        if ($updates) {
                            Database::update('users', $updates, 'id = ?', [$user['id']]);
                            $user = Database::fetchOne('SELECT * FROM users WHERE id = ?', [$user['id']]);
                        }
                    }
                    $ssoAuthorized = true;
                } elseif ($httpStatus === 401 && $sso->isSsoOnly()) {
                    $this->jsonError('Credenziali non valide', 401, 'INVALID_CREDENTIALS');
                } elseif ($sso->isSsoOnly()) {
                    $this->jsonError('Errore SSO', 502, 'SSO_ERROR');
                }
                // dual + non-200: cade nel fallback locale
            }
        }

        if (!$ssoAuthorized) {
            if (!$user || !password_verify($password, $user['password_hash'])) {
                $this->jsonError('Credenziali non valide', 401, 'INVALID_CREDENTIALS');
            }
        }

        // Aggiorna ultimo login
        Database::update('users', [
            'last_login_at' => date('Y-m-d H:i:s'),
        ], 'id = ?', [$user['id']]);

        // --- Multi-device session tracking (Fase 2 / G05-G06) ---
        $jti = bin2hex(random_bytes(16));
        $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
        Database::insert('active_sessions', [
            'id'             => $jti,
            'user_id'        => (int) $user['id'],
            'organization_id'=> null,
            'ip_address'     => $ip,
            'user_agent'     => substr($ua, 0, 512),
            'device_label'   => $this->parseDeviceLabel($ua),
            'expires_at'     => date('Y-m-d H:i:s', time() + JWT_REFRESH_EXPIRES_IN),
        ]);

        // Genera tokens (jti come claim JWT + FK su refresh_tokens.session_jti)
        $accessToken = $this->generateJWT((int) $user['id'], ['jti' => $jti]);
        $refreshToken = $this->generateRefreshToken((int) $user['id'], $jti);

        // Carica organizzazioni
        $organizations = Database::fetchAll(
            'SELECT uo.organization_id, uo.role, uo.is_primary, o.name, o.sector, o.entity_type
             FROM user_organizations uo
             JOIN organizations o ON o.id = uo.organization_id
             WHERE uo.user_id = ? AND o.is_active = 1',
            [$user['id']]
        );

        $this->currentUser = $user;
        $this->logAudit('user_login', 'user', (int) $user['id']);

        $this->jsonSuccess([
            'user' => [
                'id'                 => (int) $user['id'],
                'email'              => $user['email'],
                'full_name'          => $user['full_name'],
                'role'               => $user['role'],
                'preferred_language' => $user['preferred_language'],
            ],
            'organizations' => $organizations,
            'access_token'  => $accessToken,
            'refresh_token' => $refreshToken,
            'expires_in'    => JWT_EXPIRES_IN,
        ], 'Login effettuato');
    }

    /**
     * POST /api/auth/logout
     */
    public function logout(): void
    {
        $this->requireAuth();

        // --- Revoca selettiva della sessione corrente (Fase 2 / G06) ---
        $jti = $this->currentUser['session_jti'] ?? null;
        if ($jti) {
            Database::query(
                "UPDATE active_sessions SET revoked_at = NOW(), revoked_reason = 'logout'
                 WHERE id = ? AND revoked_at IS NULL",
                [$jti]
            );
            Database::delete('refresh_tokens', 'session_jti = ?', [$jti]);
        } else {
            // Fallback (non dovrebbe accadere — requireAuth ora pretende jti)
            Database::delete('refresh_tokens', 'user_id = ?', [$this->getCurrentUserId()]);
        }

        $this->logAudit('user_logout', 'user', $this->getCurrentUserId());

        $this->jsonSuccess(null, 'Logout effettuato');
    }

    /**
     * POST /api/auth/refresh
     */
    public function refresh(): void
    {
        $this->validateRequired(['refresh_token']);

        $refreshToken = $this->getParam('refresh_token');
        $hashedToken = hash('sha256', $refreshToken);

        // Transazione atomica per evitare race condition (double-spend del refresh token)
        Database::beginTransaction();
        try {
            // SELECT FOR UPDATE: blocca il record per tutta la transazione
            $tokenRecord = Database::fetchOne(
                'SELECT * FROM refresh_tokens WHERE token = ? AND expires_at > NOW() FOR UPDATE',
                [$hashedToken]
            );

            if (!$tokenRecord) {
                Database::rollback();
                $this->jsonError('Refresh token non valido o scaduto', 401, 'INVALID_REFRESH_TOKEN');
            }

            $userId = (int) $tokenRecord['user_id'];
            $jti    = $tokenRecord['session_jti'] ?? null;

            // Verifica che la sessione associata sia ancora valida (Fase 2 / G06)
            if ($jti) {
                $session = Database::fetchOne(
                    'SELECT id FROM active_sessions
                     WHERE id = ? AND revoked_at IS NULL AND expires_at > NOW()',
                    [$jti]
                );
                if (!$session) {
                    Database::rollback();
                    $this->jsonError('Sessione revocata, login richiesto', 401, 'SESSION_REVOKED');
                }
                // Touch last_activity
                Database::update('active_sessions',
                    ['last_activity_at' => date('Y-m-d H:i:s')],
                    'id = ?',
                    [$jti]
                );
            }

            // Elimina vecchio token atomicamente
            Database::delete('refresh_tokens', 'id = ?', [$tokenRecord['id']]);

            // Genera nuovi tokens preservando il jti della sessione
            $accessToken     = $this->generateJWT($userId, $jti ? ['jti' => $jti] : []);
            $newRefreshToken = $this->generateRefreshToken($userId, $jti);

            Database::commit();
        } catch (Throwable $e) {
            Database::rollback();
            throw $e;
        }

        $this->jsonSuccess([
            'access_token'  => $accessToken,
            'refresh_token' => $newRefreshToken,
            'expires_in'    => JWT_EXPIRES_IN,
        ], 'Token rinnovato');
    }

    /**
     * GET /api/auth/me
     */
    public function me(): void
    {
        $this->requireAuth();

        $user = $this->getCurrentUser();

        // Carica organizzazioni
        $organizations = Database::fetchAll(
            'SELECT uo.organization_id, uo.role as org_role, uo.is_primary,
                    o.name, o.sector, o.entity_type, o.subscription_plan
             FROM user_organizations uo
             JOIN organizations o ON o.id = uo.organization_id
             WHERE uo.user_id = ? AND o.is_active = 1',
            [$user['id']]
        );

        $this->jsonSuccess([
            'id'                 => (int) $user['id'],
            'email'              => $user['email'],
            'full_name'          => $user['full_name'],
            'phone'              => $user['phone'],
            'role'               => $user['role'],
            'preferred_language' => $user['preferred_language'],
            'last_login_at'      => $user['last_login_at'],
            'created_at'         => $user['created_at'],
            'organizations'      => $organizations,
        ]);
    }

    /**
     * PUT /api/auth/profile
     */
    public function updateProfile(): void
    {
        $this->requireAuth();

        $updates = [];

        if ($this->hasParam('full_name')) {
            $updates['full_name'] = trim($this->getParam('full_name'));
        }
        if ($this->hasParam('phone')) {
            $updates['phone'] = $this->getParam('phone');
        }
        if ($this->hasParam('preferred_language')) {
            $lang = $this->getParam('preferred_language');
            if (in_array($lang, ['it', 'en', 'fr', 'de'])) {
                $updates['preferred_language'] = $lang;
            }
        }

        if (empty($updates)) {
            $this->jsonError('Nessun campo da aggiornare', 400, 'NO_UPDATES');
        }

        Database::update('users', $updates, 'id = ?', [$this->getCurrentUserId()]);

        $this->logAudit('profile_updated', 'user', $this->getCurrentUserId(), $updates);

        $this->jsonSuccess($updates, 'Profilo aggiornato');
    }

    /**
     * POST /api/auth/change-password
     */
    public function changePassword(): void
    {
        $this->requireAuth();
        $this->validateRequired(['current_password', 'new_password']);

        $currentPassword = $this->getParam('current_password');
        $newPassword = $this->getParam('new_password');

        // Verifica password attuale
        if (!password_verify($currentPassword, $this->currentUser['password_hash'])) {
            $this->jsonError('Password attuale non corretta', 400, 'WRONG_PASSWORD');
        }

        // Validazione nuova password
        $errors = $this->validatePassword($newPassword);
        if (!empty($errors)) {
            $this->jsonError(implode('. ', $errors), 400, 'WEAK_PASSWORD');
        }

        // --- SSO change-password propagation (Fase 1 / G03) ---
        // Solo per utenti federati (sso_identity_id != NULL) e SSO_MODE != local.
        // Se SSO 200 → procediamo con cambio locale.
        // Se SSO error → blocchiamo: niente cambio locale, manteniamo consistency.
        // Se SSO unreachable in dual → log e procediamo localmente (sync verrà via cron).
        $sso = new SsoHelper();
        $isFederated = !empty($this->currentUser['sso_identity_id']);
        $newPasswordVersion = (int) ($this->currentUser['password_version'] ?? 1) + 1;

        if ($isFederated && !$sso->isLocalOnly()) {
            $jwt = $this->getBearerToken() ?? '';
            $ssoResp = $sso->changePassword($jwt, $currentPassword, $newPassword);
            if ($ssoResp === null) {
                // unreachable
                if ($sso->isSsoOnly()) {
                    $this->jsonError('SSO non disponibile, riprovare', 503, 'SSO_UNAVAILABLE');
                }
                // dual: log e procedi locale (cron riallineerà appena SSO torna)
                error_log("[SSO] changePassword unreachable for user {$this->getCurrentUserId()} (dual mode → local only)");
            } else {
                $httpStatus = (int) ($ssoResp['_httpStatus'] ?? 0);
                if ($httpStatus !== 200) {
                    $msg = $ssoResp['error'] ?? $ssoResp['message'] ?? 'Errore SSO change-password';
                    $this->jsonError($msg, $httpStatus ?: 502, 'SSO_CHANGE_PWD_FAILED');
                }
                // 200: usa la versione restituita da SSO se disponibile
                if (isset($ssoResp['password_version'])) {
                    $newPasswordVersion = (int) $ssoResp['password_version'];
                }
            }
        }

        Database::update('users', [
            'password_hash'    => password_hash($newPassword, PASSWORD_DEFAULT),
            'password_version' => $newPasswordVersion,
        ], 'id = ?', [$this->getCurrentUserId()]);

        // --- Revoca altre sessioni, mantieni quella corrente (Fase 2 / G06) ---
        $currentJti = $this->currentUser['session_jti'] ?? null;
        $userId = $this->getCurrentUserId();
        if ($currentJti) {
            Database::query(
                "UPDATE active_sessions
                 SET revoked_at = NOW(), revoked_reason = 'password_change'
                 WHERE user_id = ? AND id != ? AND revoked_at IS NULL",
                [$userId, $currentJti]
            );
            Database::query(
                "DELETE FROM refresh_tokens WHERE user_id = ? AND (session_jti IS NULL OR session_jti != ?)",
                [$userId, $currentJti]
            );
        } else {
            // Nessun jti corrente → revoca tutto (comportamento pre-Fase2)
            Database::query(
                "UPDATE active_sessions
                 SET revoked_at = NOW(), revoked_reason = 'password_change'
                 WHERE user_id = ? AND revoked_at IS NULL",
                [$userId]
            );
            Database::delete('refresh_tokens', 'user_id = ?', [$userId]);
        }

        $this->logAudit('password_changed', 'user', $this->getCurrentUserId(), [
            'federated' => $isFederated,
            'password_version' => $newPasswordVersion,
        ]);

        $this->jsonSuccess(null, 'Password modificata. Le altre sessioni sono state disconnesse.');
    }

    /**
     * POST /api/auth/validate-invite
     *
     * Valida un codice invito B2B e restituisce piano e metadati.
     * Nessuna autenticazione richiesta — usato dalla pagina register.html
     * prima della registrazione per mostrare l'anteprima del piano.
     *
     * Body: { "invite_token": "inv_xxxx..." }
     * Response: { valid: true, plan: "professional", duration_months: 12, ... }
     */
    public function validateInvite(): void
    {
        $token = trim($this->getParam('invite_token', ''));

        if (!$token) {
            $this->jsonError('invite_token mancante', 400, 'MISSING_TOKEN');
        }

        require_once APP_PATH . '/controllers/InviteController.php';
        $result = InviteController::resolveInvite($token);

        if (!$result['valid']) {
            $this->jsonError($result['error'], 422, $result['code'] ?? 'INVALID_INVITE');
        }

        $inv = $result['invite'];

        $metadata  = !empty($inv['metadata']) ? json_decode($inv['metadata'], true) : [];
        $recipient = $metadata['recipient'] ?? null;

        $this->jsonSuccess([
            'valid'           => true,
            'plan'            => $inv['plan'],
            'duration_months' => (int) $inv['duration_months'],
            'expires_at'      => $inv['expires_at'],
            'remaining_uses'  => (int)$inv['max_uses'] - (int)$inv['used_count'],
            'channel'         => $inv['channel'],
            'label'           => $inv['label'],
            'restrict_vat'    => $inv['restrict_vat']    ? true : false,
            'restrict_email'  => $inv['restrict_email']  ? true : false,
            'recipient'       => $recipient,  // dati pre-compilazione form (null se non presenti)
        ], 'Invito valido');
    }

    // ═══════════════════════════════════════════════════════════════════════
    // PREFERENCES — Tema / timezone / notifiche (Fase 4 / G12)
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * GET /api/auth/preferences
     */
    public function getPreferences(): void
    {
        $this->requireAuth();
        $u = $this->currentUser;
        $this->jsonSuccess([
            'preferred_language' => $u['preferred_language'] ?? 'it',
            'theme'              => $u['theme'] ?? 'auto',
            'timezone'           => $u['timezone'] ?? 'Europe/Rome',
            'notif_email'        => (bool) ($u['notif_email'] ?? 1),
            'notif_inapp'        => (bool) ($u['notif_inapp'] ?? 1),
        ]);
    }

    /**
     * PUT /api/auth/preferences
     * Body: { theme?, timezone?, preferred_language?, notif_email?, notif_inapp? }
     */
    public function updatePreferences(): void
    {
        $this->requireAuth();
        $updates = [];

        if ($this->hasParam('preferred_language')) {
            $lang = $this->getParam('preferred_language');
            if (!in_array($lang, ['it','en','fr','de'], true)) {
                $this->jsonError('Lingua non supportata', 400, 'INVALID_LANGUAGE');
            }
            $updates['preferred_language'] = $lang;
        }
        if ($this->hasParam('theme')) {
            $theme = $this->getParam('theme');
            if (!in_array($theme, ['light','dark','auto'], true)) {
                $this->jsonError('Tema non valido', 400, 'INVALID_THEME');
            }
            $updates['theme'] = $theme;
        }
        if ($this->hasParam('timezone')) {
            $tz = $this->getParam('timezone');
            // Validation minimale: max 64 chars, formato IANA-like (Region/City)
            if (!preg_match('#^[A-Za-z_]+(?:/[A-Za-z_+\-0-9]+)*$#', $tz) || strlen($tz) > 64) {
                $this->jsonError('Timezone non valida', 400, 'INVALID_TIMEZONE');
            }
            $updates['timezone'] = $tz;
        }
        if ($this->hasParam('notif_email')) {
            $updates['notif_email'] = $this->getParam('notif_email') ? 1 : 0;
        }
        if ($this->hasParam('notif_inapp')) {
            $updates['notif_inapp'] = $this->getParam('notif_inapp') ? 1 : 0;
        }
        if (empty($updates)) {
            $this->jsonError('Nessun campo da aggiornare', 400, 'NO_UPDATES');
        }

        Database::update('users', $updates, 'id = ?', [$this->getCurrentUserId()]);
        $this->logAudit('preferences_updated', 'user', $this->getCurrentUserId(), $updates);

        $this->jsonSuccess($updates, 'Preferenze aggiornate');
    }

    // ═══════════════════════════════════════════════════════════════════════
    // IMPERSONATE — Super admin / Consulente entra come altro utente (Fase 4 / G11)
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * POST /api/auth/impersonate
     * Body: { user_id }
     * Emette un JWT speciale per "loggarsi come" l'utente target.
     * - Permesso solo a super_admin (qualunque target) o consulente verso utenti delle org del suo firm.
     * - JWT TTL ridotto (~1h, no refresh) + claim `impersonated_by` per audit.
     * - Sessione corrente del super_admin/consulente NON viene revocata.
     */
    public function impersonate(): void
    {
        $this->requireAuth();
        $this->validateRequired(['user_id']);

        $targetId = (int) $this->getParam('user_id');
        $actorId  = $this->getCurrentUserId();
        $actorRole = $this->currentUser['role'];

        if ($targetId === $actorId) {
            $this->jsonError('Non puoi impersonare te stesso', 400, 'SELF_IMPERSONATE');
        }

        $target = Database::fetchOne(
            'SELECT id, email, full_name, role, is_active, consulting_firm_id
             FROM users WHERE id = ?',
            [$targetId]
        );
        if (!$target || (int) $target['is_active'] !== 1) {
            $this->jsonError('Utente target non trovato o disabilitato', 404, 'TARGET_NOT_FOUND');
        }
        if ($target['role'] === 'super_admin' && $actorRole !== 'super_admin') {
            $this->jsonError('Non puoi impersonare un super admin', 403, 'IMPERSONATE_FORBIDDEN');
        }

        // Permessi: super_admin OR consulente sullo stesso firm
        $allowed = false;
        if ($actorRole === 'super_admin') {
            $allowed = true;
        } elseif ($actorRole === 'consultant') {
            $actorFirmId = (int) ($this->currentUser['consulting_firm_id'] ?? 0);
            $targetFirmId = (int) ($target['consulting_firm_id'] ?? 0);
            if ($actorFirmId > 0 && $actorFirmId === $targetFirmId) {
                $allowed = true;
            }
            // In alternativa: target è org_admin/employee di una org del consulente.
            // Per ora copertura minimale: stesso firm.
        }
        if (!$allowed) {
            $this->jsonError('Permessi insufficienti per impersonare questo utente', 403, 'IMPERSONATE_FORBIDDEN');
        }

        // Nuova sessione tracciata (durata ridotta: 1h, niente refresh persistente)
        $jti = bin2hex(random_bytes(16));
        $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
        Database::insert('active_sessions', [
            'id'             => $jti,
            'user_id'        => $targetId,
            'organization_id'=> null,
            'ip_address'     => $this->getClientIP(),
            'user_agent'     => substr($ua, 0, 512),
            'device_label'   => 'Impersonato da #' . $actorId,
            'expires_at'     => date('Y-m-d H:i:s', time() + 3600),
        ]);

        $accessToken = $this->generateJWT($targetId, [
            'jti'             => $jti,
            'impersonated_by' => $actorId,
            'exp'             => time() + 3600,
        ]);

        $this->logAudit('user_impersonated', 'user', $targetId, [
            'actor_id' => $actorId,
            'actor_role' => $actorRole,
            'duration_seconds' => 3600,
        ]);

        $this->jsonSuccess([
            'user' => [
                'id'        => (int) $target['id'],
                'email'     => $target['email'],
                'full_name' => $target['full_name'],
                'role'      => $target['role'],
            ],
            'access_token' => $accessToken,
            'expires_in'   => 3600,
            'impersonated' => true,
        ], 'Impersonate attivo per 1 ora');
    }

    // ═══════════════════════════════════════════════════════════════════════
    // CONTEXT SWITCH — Cambio organization attiva (Fase 3 / G09)
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * POST /api/auth/switch-context
     * Body: { organization_id }
     * Verifica membership, revoca la sessione corrente con reason='context_switch',
     * crea una nuova sessione e ritorna nuovo JWT + refresh con organization_id come claim.
     */
    public function switchContext(): void
    {
        $this->requireAuth();
        $this->validateRequired(['organization_id']);

        $targetOrgId = (int) $this->getParam('organization_id');
        $userId = $this->getCurrentUserId();
        $oldJti = $this->currentUser['session_jti'] ?? null;

        // Super admin bypass membership check
        if ($this->currentUser['role'] !== 'super_admin') {
            $membership = Database::fetchOne(
                'SELECT role FROM user_organizations WHERE user_id = ? AND organization_id = ?',
                [$userId, $targetOrgId]
            );
            if (!$membership) {
                $this->jsonError('Non sei membro di questa organizzazione', 403, 'NOT_MEMBER');
            }
        }

        $org = Database::fetchOne(
            'SELECT id, name FROM organizations WHERE id = ? AND is_active = 1',
            [$targetOrgId]
        );
        if (!$org) {
            $this->jsonError('Organizzazione non trovata o disabilitata', 404, 'ORG_NOT_FOUND');
        }

        Database::beginTransaction();
        try {
            // Revoca sessione corrente
            if ($oldJti) {
                Database::query(
                    "UPDATE active_sessions
                     SET revoked_at = NOW(), revoked_reason = 'context_switch'
                     WHERE id = ? AND revoked_at IS NULL",
                    [$oldJti]
                );
                Database::delete('refresh_tokens', 'session_jti = ?', [$oldJti]);
            }

            // Nuova sessione con organization_id valorizzato
            $newJti = bin2hex(random_bytes(16));
            $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
            Database::insert('active_sessions', [
                'id'              => $newJti,
                'user_id'         => $userId,
                'organization_id' => $targetOrgId,
                'ip_address'      => $this->getClientIP(),
                'user_agent'      => substr($ua, 0, 512),
                'device_label'    => $this->parseDeviceLabel($ua),
                'expires_at'      => date('Y-m-d H:i:s', time() + JWT_REFRESH_EXPIRES_IN),
            ]);

            $accessToken = $this->generateJWT($userId, [
                'jti'             => $newJti,
                'organization_id' => $targetOrgId,
            ]);
            $refreshToken = $this->generateRefreshToken($userId, $newJti);

            Database::commit();
        } catch (Throwable $e) {
            Database::rollback();
            throw $e;
        }

        $this->logAudit('context_switched', 'organization', $targetOrgId);

        $this->jsonSuccess([
            'organization' => [
                'id'   => (int) $org['id'],
                'name' => $org['name'],
            ],
            'access_token'  => $accessToken,
            'refresh_token' => $refreshToken,
            'expires_in'    => JWT_EXPIRES_IN,
        ], 'Contesto cambiato');
    }

    // ═══════════════════════════════════════════════════════════════════════
    // PASSWORD RESET — Forgot/Reset (Fase 3 / G08)
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * POST /api/auth/forgot-password
     * Body: { email }
     * Risposta SEMPRE 200 + messaggio generico (anti enumeration).
     */
    public function forgotPassword(): void
    {
        $this->validateRequired(['email']);
        $email = strtolower(trim($this->getParam('email')));
        $ip    = $this->getClientIP();

        // Rate limit per IP+email combinati (anti spam)
        $rlKey = 'forgot:' . $ip . ':' . md5($email);
        RateLimitService::check($rlKey, RATE_LIMIT_AUTH_FORGOT);
        RateLimitService::increment($rlKey);

        $user = Database::fetchOne(
            'SELECT id, email, full_name FROM users WHERE email = ? AND is_active = 1',
            [$email]
        );

        // Risposta opaca: anche se l'email non esiste rispondiamo come se ok
        $okMessage = 'Se l\'indirizzo email è registrato, riceverai a breve un link per reimpostare la password.';

        if ($user) {
            // Invalida eventuali token precedenti non usati
            Database::query(
                'UPDATE password_reset_tokens SET used_at = NOW()
                 WHERE user_id = ? AND used_at IS NULL',
                [$user['id']]
            );

            // Genera token (in chiaro inviato via mail, hash sul DB)
            $token = bin2hex(random_bytes(32));
            Database::insert('password_reset_tokens', [
                'user_id'    => (int) $user['id'],
                'token_hash' => hash('sha256', $token),
                'expires_at' => date('Y-m-d H:i:s', time() + PASSWORD_RESET_TTL_SECONDS),
                'ip_address' => $ip,
            ]);

            // Invia email
            try {
                require_once APP_PATH . '/services/EmailService.php';
                $email_service = new EmailService();
                $email_service->sendPasswordReset($user, $token, PASSWORD_RESET_TTL_SECONDS);
            } catch (Throwable $e) {
                error_log("[forgot-password] sendPasswordReset failed for user {$user['id']}: " . $e->getMessage());
            }

            $this->logAudit('password_reset_requested', 'user', (int) $user['id']);
        }

        $this->jsonSuccess(null, $okMessage);
    }

    /**
     * POST /api/auth/reset-password
     * Body: { token, new_password }
     */
    public function resetPassword(): void
    {
        $this->validateRequired(['token', 'new_password']);
        $token = $this->getParam('token');
        $newPassword = $this->getParam('new_password');

        $errors = $this->validatePassword($newPassword);
        if (!empty($errors)) {
            $this->jsonError(implode('. ', $errors), 400, 'WEAK_PASSWORD');
        }

        $hash = hash('sha256', $token);
        $row = Database::fetchOne(
            'SELECT prt.id, prt.user_id, prt.expires_at, prt.used_at, u.email, u.sso_identity_id
             FROM password_reset_tokens prt
             JOIN users u ON u.id = prt.user_id
             WHERE prt.token_hash = ?',
            [$hash]
        );

        if (!$row) {
            $this->jsonError('Token non valido', 400, 'INVALID_TOKEN');
        }
        if ($row['used_at']) {
            $this->jsonError('Token già utilizzato', 400, 'TOKEN_USED');
        }
        if (strtotime($row['expires_at']) < time()) {
            $this->jsonError('Token scaduto. Richiedere un nuovo link.', 400, 'TOKEN_EXPIRED');
        }

        $userId = (int) $row['user_id'];
        $isFederated = !empty($row['sso_identity_id']);
        $newPasswordVersion = 2; // bump da 1 base; ci aggiorneremo dal cron sync

        Database::beginTransaction();
        try {
            // Consume token
            Database::update('password_reset_tokens',
                ['used_at' => date('Y-m-d H:i:s')],
                'id = ?',
                [$row['id']]
            );

            // Update password
            Database::update('users', [
                'password_hash'    => password_hash($newPassword, PASSWORD_DEFAULT),
                'password_version' => $newPasswordVersion,
            ], 'id = ?', [$userId]);

            // Revoca TUTTE le sessioni (no sessione corrente = reset da non-loggato)
            Database::query(
                "UPDATE active_sessions
                 SET revoked_at = NOW(), revoked_reason = 'password_change'
                 WHERE user_id = ? AND revoked_at IS NULL",
                [$userId]
            );
            Database::delete('refresh_tokens', 'user_id = ?', [$userId]);

            Database::commit();
        } catch (Throwable $e) {
            Database::rollback();
            throw $e;
        }

        $this->logAudit('password_reset_completed', 'user', $userId, [
            'federated_user' => $isFederated,
        ]);

        // Nota: per utenti federati (sso_identity_id != NULL) il cambio è solo locale.
        // Il cron `sso-password-sync` (richiesto via AgileHub Ticket #220) potrebbe
        // riallineare la password lato SSO al prossimo run.

        $this->jsonSuccess(null, 'Password reimpostata. Puoi accedere con la nuova password.');
    }

    // ═══════════════════════════════════════════════════════════════════════
    // SESSIONS — Multi-device management (Fase 2 / G06)
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * GET /api/auth/sessions
     * Lista le sessioni attive dell'utente corrente.
     */
    public function listSessions(): void
    {
        $this->requireAuth();
        $userId = $this->getCurrentUserId();
        $currentJti = $this->currentUser['session_jti'] ?? null;

        $rows = Database::fetchAll(
            'SELECT id, ip_address, device_label, created_at, last_activity_at, expires_at
             FROM active_sessions
             WHERE user_id = ? AND revoked_at IS NULL AND expires_at > NOW()
             ORDER BY last_activity_at DESC',
            [$userId]
        );

        $sessions = array_map(function ($r) use ($currentJti) {
            return [
                'id'               => $r['id'],
                'ip_address'       => $r['ip_address'],
                'device_label'     => $r['device_label'] ?? 'Browser',
                'created_at'       => $r['created_at'],
                'last_activity_at' => $r['last_activity_at'],
                'expires_at'       => $r['expires_at'],
                'is_current'       => $r['id'] === $currentJti,
            ];
        }, $rows);

        $this->jsonSuccess(['sessions' => $sessions]);
    }

    /**
     * DELETE /api/auth/sessions/{id}
     * Revoca una sessione specifica. Se id == sessione corrente equivale a logout.
     */
    public function revokeSession(string $id): void
    {
        $this->requireAuth();
        $userId = $this->getCurrentUserId();
        $currentJti = $this->currentUser['session_jti'] ?? null;

        $row = Database::fetchOne(
            'SELECT id FROM active_sessions
             WHERE id = ? AND user_id = ? AND revoked_at IS NULL',
            [$id, $userId]
        );
        if (!$row) {
            $this->jsonError('Sessione non trovata', 404, 'SESSION_NOT_FOUND');
        }

        $reason = ($id === $currentJti) ? 'logout' : 'admin';
        Database::query(
            "UPDATE active_sessions SET revoked_at = NOW(), revoked_reason = ?
             WHERE id = ?",
            [$reason, $id]
        );
        Database::delete('refresh_tokens', 'session_jti = ?', [$id]);

        $this->logAudit('session_revoked', 'session', null, [
            'session_id' => $id,
            'self'       => ($id === $currentJti),
        ]);

        $this->jsonSuccess(null, 'Sessione revocata');
    }

    /**
     * DELETE /api/auth/sessions
     * Revoca tutte le sessioni dell'utente tranne quella corrente.
     */
    public function revokeAllSessions(): void
    {
        $this->requireAuth();
        $userId = $this->getCurrentUserId();
        $currentJti = $this->currentUser['session_jti'] ?? null;

        if ($currentJti) {
            Database::query(
                "UPDATE active_sessions SET revoked_at = NOW(), revoked_reason = 'admin'
                 WHERE user_id = ? AND id != ? AND revoked_at IS NULL",
                [$userId, $currentJti]
            );
            Database::query(
                "DELETE FROM refresh_tokens
                 WHERE user_id = ? AND (session_jti IS NULL OR session_jti != ?)",
                [$userId, $currentJti]
            );
        } else {
            Database::query(
                "UPDATE active_sessions SET revoked_at = NOW(), revoked_reason = 'admin'
                 WHERE user_id = ? AND revoked_at IS NULL",
                [$userId]
            );
            Database::delete('refresh_tokens', 'user_id = ?', [$userId]);
        }

        $this->logAudit('sessions_revoked_all', 'user', $userId);

        $this->jsonSuccess(null, 'Sessioni revocate (tranne questa)');
    }
}
