<?php
declare(strict_types=1);

class SettingsController
{
    public static function profile(): void
    {
        Auth::requireLogin();
        $db      = Database::getInstance();
        $user    = Auth::user();
        $errors  = [];
        $success = Helper::flash('success');

        if (Helper::isPost()) {
            Helper::verifyCsrf();
            $action = Helper::post('action');

            if ($action === 'update_profile') {
                $username = trim(Helper::post('username'));
                $email    = strtolower(trim(Helper::post('email')));
                $lang     = Helper::post('language', Lang::FALLBACK);
                if (!Lang::isSupported($lang)) $lang = Lang::FALLBACK;
                $tz       = trim((string)Helper::post('timezone', ''));
                if ($tz !== '' && !Helper::isValidTimezone($tz)) $tz = '';
                $theme    = Helper::post('theme', 'light');
                if (!in_array($theme, ['light', 'dark', 'sepia', 'midnight', 'ocean', 'matrix', 'bluescreen', 'forest', 'crimson', 'slate'], true)) $theme = 'light';

                if (strlen($username) < 3) $errors[] = Lang::get('username_min');
                if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = Lang::get('invalid_email');
                // Detect whether the user is requesting an email change. If so we
                // park the new address in pending_email and wait for verification
                // (security-audit #2) — preventing the post-hijack takeover path
                // where an attacker swaps the email and then triggers a reset.
                $emailChanged = !hash_equals(strtolower($user['email']), $email);
                if ($emailChanged && empty($errors)) {
                    $taken = $db->fetch(
                        'SELECT id FROM users WHERE (email = ? OR pending_email = ?) AND id != ?',
                        [$email, $email, Auth::id()]
                    );
                    if ($taken) $errors[] = Lang::get('email_taken');
                }

                if (empty($errors)) {
                    // Always update the non-credential profile fields immediately.
                    try {
                        $db->execute(
                            'UPDATE users SET username=?, language=?, timezone=?, theme=? WHERE id=?',
                            [$username, $lang, ($tz === '' ? null : $tz), $theme, Auth::id()]
                        );
                    } catch (\PDOException $e) {
                        // Username column has no UNIQUE in current schema, so this is the only realistic failure path.
                        $errors[] = $e->getMessage();
                    }
                }

                if (empty($errors) && $emailChanged) {
                    // Stage the new email as pending and send the verification flow.
                    $tokenRaw  = bin2hex(random_bytes(32));
                    $tokenHash = hash('sha256', $tokenRaw);
                    $expires   = time() + 86400; // 24 h
                    try {
                        $db->execute(
                            'UPDATE users SET pending_email=?, pending_email_token=?, pending_email_expires=? WHERE id=?',
                            [$email, $tokenHash, $expires, Auth::id()]
                        );
                    } catch (\PDOException $e) {
                        if (str_contains($e->getMessage(), 'UNIQUE')) {
                            $errors[] = Lang::get('email_taken');
                        } else {
                            throw $e;
                        }
                    }
                    if (empty($errors)) {
                        $verifyUrl = Helper::baseUrl() . APP_BASE . '/verify-email-change?token=' . $tokenRaw;
                        try { Mailer::sendEmailChangeVerify($email,         $username, $verifyUrl); } catch (\Throwable) {}
                        try { Mailer::sendEmailChangeNotice($user['email'], $username, $email); }    catch (\Throwable) {}
                        SecurityLog::log('email_change_requested', SecurityLog::SEV_INFO, [
                            'old_hint' => Helper::maskEmail($user['email']),
                            'new_hint' => Helper::maskEmail($email),
                        ]);
                    }
                }

                if (empty($errors)) {
                    $_SESSION['lang']  = $lang;
                    $_SESSION['theme'] = $theme;
                    Lang::load($lang);
                    $flashKey = $emailChanged ? 'email_change_pending' : 'profile_updated';
                    Helper::redirect('/settings', ['success' => Lang::get($flashKey)]);
                }
            }

            if ($action === 'change_password') {
                $current = Helper::post('current_password');
                $new     = Helper::post('new_password');
                $confirm = Helper::post('password_confirm');

                if (!password_verify($current, $user['password'])) {
                    $errors[] = Lang::get('password_wrong');
                } elseif ($pwErr = PasswordPolicy::validate($new)) {
                    $errors[] = t($pwErr);
                } elseif ($new !== $confirm) {
                    $errors[] = Lang::get('password_mismatch');
                } else {
                    $changedAt = time();
                    $db->execute(
                        'UPDATE users SET password=?, password_changed_at=? WHERE id=?',
                        [password_hash($new, PASSWORD_BCRYPT, ['cost' => Auth::BCRYPT_COST]), $changedAt, Auth::id()]
                    );
                    // Re-stamp the current session so the user who is doing the
                    // change stays logged in; other sessions for this user will
                    // be killed on their next request via the password_changed_at
                    // check in index.php (security-audit #14).
                    Auth::stampSessionLogin();
                    session_regenerate_id(true);
                    SecurityLog::log('password_changed', SecurityLog::SEV_INFO);
                    // Notify the account email so the user notices a change they did not make.
                    try { Mailer::notifyPasswordChanged($user); } catch (\Throwable) {}
                    Helper::redirect('/settings', ['success' => Lang::get('password_changed')]);
                }
            }

            if ($action === 'delete_account') {
                $userId = Auth::id();
                if ($user['role'] === 'admin') {
                    $errors[] = Lang::get('admin_cannot_self_delete');
                } else {
                    $password = Helper::post('confirm_password', '');
                    if (!password_verify($password, $user['password'])) {
                        $errors[] = Lang::get('password_wrong');
                    } else {
                        self::deleteUser($db, $userId);
                        Auth::logout();
                        Helper::redirect('/login', ['success' => Lang::get('account_deleted')]);
                    }
                }
            }

            if ($action === 'upload_avatar') {
                if (!empty($_FILES['avatar']['name'])) {
                    $allowedExts  = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
                    $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
                    $ext      = strtolower(pathinfo($_FILES['avatar']['name'], PATHINFO_EXTENSION));
                    $maxSize  = 2 * 1024 * 1024; // 2MB
                    $finfo    = new \finfo(FILEINFO_MIME_TYPE);
                    $mime     = $finfo->file($_FILES['avatar']['tmp_name']);

                    if (!in_array($ext, $allowedExts) || !in_array($mime, $allowedMimes)) {
                        $errors[] = t('image_only');
                    } elseif ($_FILES['avatar']['size'] > $maxSize) {
                        $errors[] = t('image_too_large');
                    } else {
                        $dir = DATA_PATH . '/avatars';
                        if (!is_dir($dir)) mkdir($dir, 0750, true);

                        $filename = 'avatar_' . Auth::id() . '.' . $ext;
                        $dest     = $dir . '/' . $filename;

                        foreach (glob($dir . '/avatar_' . Auth::id() . '.*') as $old) {
                            if ($old !== $dest) unlink($old);
                        }

                        if (move_uploaded_file($_FILES['avatar']['tmp_name'], $dest)) {
                            $db->execute('UPDATE users SET avatar=? WHERE id=?', [$filename, Auth::id()]);
                            Helper::redirect('/settings', ['success' => t('avatar_updated')]);
                        } else {
                            $errors[] = t('upload_failed');
                        }
                    }
                }
            }
        }

        // Reload user data
        $user = $db->fetch('SELECT * FROM users WHERE id = ?', [Auth::id()]);

        // Notification count
        $unread = $db->fetch('SELECT COUNT(*) as c FROM notifications WHERE user_id = ? AND is_read = 0', [Auth::id()])['c'];

        view('settings/profile', compact('user', 'errors', 'success', 'unread'));
    }

    public static function preferences(): void
    {
        Auth::requireLogin();
        $db      = Database::getInstance();
        $user    = Auth::user();
        $errors  = [];
        $success = Helper::flash('success');

        if (Helper::isPost()) {
            Helper::verifyCsrf();
            $action = Helper::post('action');

            if ($action === 'update_preferences') {
                $preferredView = in_array(Helper::post('preferred_view'), ['kanban', 'list', 'gantt'])
                                 ? Helper::post('preferred_view') : 'kanban';
                $db->execute('UPDATE users SET preferred_view=? WHERE id=?', [$preferredView, Auth::id()]);
                Helper::redirect('/settings/preferences', ['success' => Lang::get('preferences_saved_msg')]);
            }

            // Allow plugins to handle additional POST actions (e.g. group request)
            PluginManager::fire('settings_preferences_post', ['action' => $action, 'user' => $user]);
        }

        $user   = $db->fetch('SELECT * FROM users WHERE id = ?', [Auth::id()]);
        $unread = $db->fetch('SELECT COUNT(*) as c FROM notifications WHERE user_id = ? AND is_read = 0', [Auth::id()])['c'];

        view('settings/preferences', compact('user', 'errors', 'success', 'unread'));
    }

    public static function totp(): void
    {
        Auth::requireLogin();
        $db   = Database::getInstance();
        $user = $db->fetch('SELECT * FROM users WHERE id = ?', [Auth::id()]);

        $error   = null;
        $success = Helper::flash('success');

        // Step-up gate: enrolling a new TOTP authenticator from a logged-in
        // session is dangerous because a session-hijacked attacker could
        // register their own seed and use it as a permanent foothold even
        // after the legitimate user rotates the password. We therefore
        // require a fresh proof of email control before any TOTP setup
        // action is accepted.
        $stepUpOk = (int)($_SESSION['totp_setup_verified_until'] ?? 0) > time();
        $codeSent = !empty($_SESSION['totp_setup_step_up'])
                    && (int)($_SESSION['totp_setup_step_up']['expires'] ?? 0) > time();

        if (Helper::isPost()) {
            Helper::verifyCsrf();
            $action = Helper::post('action');

            // ── Step-up: send email code ────────────────────────────────────
            if ($action === 'totp_step_up_send' && empty($user['totp_enabled'])) {
                $last = (int)($_SESSION['totp_setup_step_up']['sent_at'] ?? 0);
                if (time() - $last < 60) {
                    $error = Lang::get('mfa_resend_too_fast');
                } else {
                    $code = str_pad((string)random_int(0, 999999), 6, '0', STR_PAD_LEFT);
                    $_SESSION['totp_setup_step_up'] = [
                        'hash'     => password_hash($code, PASSWORD_BCRYPT, ['cost' => Auth::BCRYPT_COST]),
                        'expires'  => time() + 600,
                        'attempts' => 0,
                        'sent_at'  => time(),
                    ];
                    try { Mailer::sendEmailMfaCode($user['email'], $code); } catch (\Throwable) {}
                    SecurityLog::log('totp_setup_step_up_sent', SecurityLog::SEV_INFO);
                    Helper::redirect('/settings/totp', ['success' => Lang::get('totp_setup_step_up_sent')]);
                }
            }

            // ── Step-up: verify email code ──────────────────────────────────
            if ($action === 'totp_step_up_verify' && empty($user['totp_enabled'])) {
                $code  = trim(Helper::post('step_up_code', ''));
                $entry = $_SESSION['totp_setup_step_up'] ?? null;
                if (!$entry || (int)$entry['expires'] < time()) {
                    unset($_SESSION['totp_setup_step_up']);
                    $error = Lang::get('totp_setup_step_up_expired');
                } elseif ((int)$entry['attempts'] >= 5) {
                    unset($_SESSION['totp_setup_step_up']);
                    SecurityLog::log('totp_setup_step_up_locked', SecurityLog::SEV_WARN);
                    $error = Lang::get('mfa_attempts_exceeded');
                } elseif (!password_verify($code, $entry['hash'])) {
                    $_SESSION['totp_setup_step_up']['attempts'] = (int)$entry['attempts'] + 1;
                    SecurityLog::log('totp_setup_step_up_failed', SecurityLog::SEV_WARN);
                    $error = Lang::get('totp_setup_step_up_invalid');
                } else {
                    unset($_SESSION['totp_setup_step_up']);
                    $_SESSION['totp_setup_verified_until'] = time() + 600;
                    $stepUpOk = true;
                    SecurityLog::log('totp_setup_step_up_ok', SecurityLog::SEV_INFO);
                    Helper::redirect('/settings/totp', ['success' => Lang::get('totp_setup_step_up_ok')]);
                }
            }

            // ── Enable TOTP ──────────────────────────────────────────────────
            if ($action === 'totp_enable') {
                if (!$stepUpOk) {
                    $error = Lang::get('totp_setup_step_up_required');
                } else {
                    $secret = $user['totp_secret'] ?? ''; // always use the server-stored secret, never POST
                    $code   = trim(Helper::post('totp_code', ''));

                    if (!$secret || !Auth::verifyTotpCode((int)Auth::id(), $secret, $code)) {
                        $error = Lang::get('totp_invalid');
                    } else {
                        $db->execute(
                            'UPDATE users SET totp_enabled=1 WHERE id=?',
                            [Auth::id()]
                        );
                        unset($_SESSION['totp_setup_verified_until']);
                        SecurityLog::log('totp_enabled', SecurityLog::SEV_INFO);
                        Helper::redirect('/settings/totp', ['success' => Lang::get('totp_enabled_ok')]);
                    }
                    // Re-fetch user to show updated state in view
                    $user = $db->fetch('SELECT * FROM users WHERE id = ?', [Auth::id()]);
                }
            }

            // ── Disable TOTP ─────────────────────────────────────────────────
            // Requires the account password in addition to the TOTP code so a
            // hijacked session alone (without the password) cannot strip MFA.
            if ($action === 'totp_disable') {
                $code     = trim(Helper::post('totp_code', ''));
                $password = Helper::post('confirm_password', '');

                if (!password_verify($password, $user['password'])) {
                    $error = Lang::get('password_wrong');
                } elseif (!Auth::verifyTotpCode((int)Auth::id(), $user['totp_secret'], $code)) {
                    $error = Lang::get('totp_invalid');
                } else {
                    $db->execute(
                        'UPDATE users SET totp_secret=NULL, totp_enabled=0 WHERE id=?',
                        [Auth::id()]
                    );
                    SecurityLog::log('mfa_disable', SecurityLog::SEV_WARN, ['method' => 'totp']);
                    Helper::redirect('/settings/totp', ['success' => Lang::get('totp_disabled_ok')]);
                }
            }

            // ── Generate new secret (shown to user before enabling) ───────────
            if ($action === 'totp_generate') {
                if (!$stepUpOk) {
                    $error = Lang::get('totp_setup_step_up_required');
                } else {
                    $newSecret = TOTP::generateSecret();
                    $db->execute(
                        'UPDATE users SET totp_secret=?, totp_enabled=0 WHERE id=?',
                        [Crypto::encrypt($newSecret), Auth::id()]
                    );
                    $user = $db->fetch('SELECT * FROM users WHERE id = ?', [Auth::id()]);
                }
            }

            // ── Enable Email MFA ─────────────────────────────────────────────
            if ($action === 'email_mfa_enable') {
                $db->execute('UPDATE users SET email_mfa_enabled=1 WHERE id=?', [Auth::id()]);
                Helper::redirect('/settings/totp', ['success' => Lang::get('email_mfa_enabled_ok')]);
            }

            // ── Disable Email MFA ────────────────────────────────────────────
            // Requires the account password — same reasoning as totp_disable.
            if ($action === 'email_mfa_disable') {
                $password = Helper::post('confirm_password', '');
                if (!password_verify($password, $user['password'])) {
                    $error = Lang::get('password_wrong');
                } else {
                    $db->execute('UPDATE users SET email_mfa_enabled=0 WHERE id=?', [Auth::id()]);
                    SecurityLog::log('mfa_disable', SecurityLog::SEV_WARN, ['method' => 'email']);
                    Helper::redirect('/settings/totp', ['success' => Lang::get('email_mfa_disabled_ok')]);
                }
            }
        }

        $appName   = Helper::getSetting('app_name', 'Plenvo');
        $otpUrl    = null;
        if ($user['totp_secret']) {
            $otpUrl = TOTP::getOtpAuthUrl($user['totp_secret'], $user['email'], $appName);
        }

        $webauthnCredentials = $db->fetchAll(
            'SELECT id, name, created_at, last_used_at FROM webauthn_credentials WHERE user_id=? ORDER BY created_at DESC',
            [Auth::id()]
        );

        // Recompute after possible mutations during POST handling above.
        $stepUpOk = (int)($_SESSION['totp_setup_verified_until'] ?? 0) > time();
        $codeSent = !empty($_SESSION['totp_setup_step_up'])
                    && (int)($_SESSION['totp_setup_step_up']['expires'] ?? 0) > time();
        $emailHint = Helper::maskEmail($user['email']);

        view('settings/totp', compact('user', 'error', 'success', 'otpUrl', 'webauthnCredentials', 'stepUpOk', 'codeSent', 'emailHint'));
    }

    public static function notifications(): void
    {
        Auth::requireLogin();
        $db = Database::getInstance();

        if (Helper::isPost()) {
            Helper::verifyCsrf();
            $db->execute('UPDATE notifications SET is_read = 1 WHERE user_id = ?', [Auth::id()]);
        }

        $notifications = $db->fetchAll(
            'SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 50',
            [Auth::id()]
        );

        view('settings/notifications', compact('notifications'));
    }

    public static function deleteUser(Database $db, int $userId): void
    {
        // Find any admin to take over ownership/authorship
        $fallback = $db->fetch(
            "SELECT id FROM users WHERE role='admin' AND id != ? LIMIT 1",
            [$userId]
        );
        $fallbackId = $fallback ? (int)$fallback['id'] : null;

        // Projects: owner_id is NOT NULL — reassign or delete the project if no fallback
        if ($fallbackId) {
            $db->execute('UPDATE projects SET owner_id = ? WHERE owner_id = ?', [$fallbackId, $userId]);
        } else {
            // Edge case: last admin being deleted — delete their projects
            $db->execute('DELETE FROM projects WHERE owner_id = ?', [$userId]);
        }

        // tasks.created_by is NOT NULL — reassign if possible, otherwise delete orphaned tasks
        if ($fallbackId) {
            $db->execute('UPDATE tasks SET created_by = ? WHERE created_by = ?', [$fallbackId, $userId]);
        } else {
            $db->execute('DELETE FROM tasks WHERE created_by = ?', [$userId]);
        }

        // Nullable columns — safe to NULL out
        $db->execute('UPDATE tasks        SET assigned_to = NULL WHERE assigned_to = ?', [$userId]);
        $db->execute('UPDATE comments     SET user_id     = NULL WHERE user_id     = ?', [$userId]);
        $db->execute('UPDATE activity_log SET user_id     = NULL WHERE user_id     = ?', [$userId]);

        // Owned rows — delete
        $db->execute('DELETE FROM notifications    WHERE user_id = ?', [$userId]);
        $db->execute('DELETE FROM project_members  WHERE user_id = ?', [$userId]);
        $db->execute('DELETE FROM task_assignees   WHERE user_id = ?', [$userId]);
        $db->execute('DELETE FROM ai_messages      WHERE user_id = ?', [$userId]);

        $db->execute('DELETE FROM users WHERE id = ?', [$userId]);
    }
}
