<?php
// ============================================================
//  MJPOS – Auth  (Session · Login · RBAC · CSRF · Brute-force)
// ============================================================

class Auth
{
    // ── Session bootstrap ────────────────────────────────────
    public static function startSession(): void
    {
        if (session_status() !== PHP_SESSION_NONE) return;

        // Harden session cookie
        session_name(SESSION_NAME);
        session_set_cookie_params([
            'lifetime' => SESSION_LIFETIME,
            'path'     => '/',
            'secure'   => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
            'httponly' => true,
            'samesite' => 'Strict',
        ]);
        session_start();

        // Regenerate ID every 30 min to reduce session fixation risk
        $now = time();
        if (!isset($_SESSION['_created'])) {
            $_SESSION['_created'] = $now;
        } elseif ($now - $_SESSION['_created'] > 1800) {
            session_regenerate_id(true);
            $_SESSION['_created'] = $now;
        }

        // Validate user-agent fingerprint to detect session hijack
        $ua = md5($_SERVER['HTTP_USER_AGENT'] ?? '');
        if (!isset($_SESSION['_ua'])) {
            $_SESSION['_ua'] = $ua;
        } elseif ($_SESSION['_ua'] !== $ua) {
            self::logout();
            header('Location: ' . APP_URL . '/auth/login');
            exit;
        }

        // Track last_active every 30s for online users feature
        if (!empty($_SESSION['user_id'])) {
            $lastPing = $_SESSION['_last_ping'] ?? 0;
            if ($now - $lastPing > 30) {
                $_SESSION['_last_ping'] = $now;
                try {
                    Database::execute(
                        'UPDATE users SET last_active = NOW() WHERE id = ?',
                        [$_SESSION['user_id']]
                    );
                } catch (Exception $e) {}
            }

            // ── Real-time permissions refresh ─────────────────
            // Check if role permissions were updated since last session load
            $roleId = (int)($_SESSION['role_id'] ?? 0);
            if ($roleId) {
                $lastPermCheck = $_SESSION['_perm_check'] ?? 0;
                // Check DB every 10 seconds
                if ($now - $lastPermCheck > 10) {
                    $_SESSION['_perm_check'] = $now;
                    try {
                        $dbVersion = (int)Database::fetchColumn(
                            'SELECT perms_version FROM roles WHERE id = ?', [$roleId]
                        );
                        $sesVersion = (int)($_SESSION['perms_version'] ?? 0);
                        // If DB version is newer, reload permissions into session
                        if ($dbVersion > $sesVersion) {
                            $_SESSION['permissions'] = array_column(
                                Database::fetchAll(
                                    'SELECT p.permission_key FROM role_permissions rp
                                     JOIN permissions p ON p.id = rp.permission_id
                                     WHERE rp.role_id = ?', [$roleId]
                                ),
                                'permission_key'
                            );
                            $_SESSION['perms_version'] = $dbVersion;
                        }
                    } catch (Exception $e) {}
                }
            }
        }
    }

    // ── Brute-force protection ───────────────────────────────
    public static function loginAttempts(string $ip): int
    {
        $key  = 'login_attempts_' . md5($ip);
        $data = $_SESSION[$key] ?? ['count' => 0, 'first' => time()];

        // Reset window if expired
        if (time() - $data['first'] > LOGIN_WINDOW) {
            $data = ['count' => 0, 'first' => time()];
        }
        return (int)$data['count'];
    }

    public static function recordFailedAttempt(string $ip): void
    {
        $key  = 'login_attempts_' . md5($ip);
        $data = $_SESSION[$key] ?? ['count' => 0, 'first' => time()];

        if (time() - $data['first'] > LOGIN_WINDOW) {
            $data = ['count' => 0, 'first' => time()];
        }
        $data['count']++;
        $data['last'] = time();
        $_SESSION[$key] = $data;

        // After max attempts, set lockout
        if ($data['count'] >= MAX_LOGIN_ATTEMPTS) {
            $_SESSION['login_locked_until_' . md5($ip)] = time() + LOGIN_LOCKOUT_TIME;
        }
    }

    public static function isLockedOut(string $ip): bool
    {
        $key    = 'login_locked_until_' . md5($ip);
        $locked = $_SESSION[$key] ?? 0;
        if ($locked && time() < $locked) return true;
        if ($locked && time() >= $locked) {
            unset($_SESSION[$key], $_SESSION['login_attempts_' . md5($ip)]);
        }
        return false;
    }

    public static function lockoutRemaining(string $ip): int
    {
        $key = 'login_locked_until_' . md5($ip);
        return max(0, (int)($_SESSION[$key] ?? 0) - time());
    }

    public static function clearAttempts(string $ip): void
    {
        unset(
            $_SESSION['login_attempts_' . md5($ip)],
            $_SESSION['login_locked_until_' . md5($ip)]
        );
    }

    // ── Login ────────────────────────────────────────────────
    public static function attempt(string $username, string $password): bool
    {
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';

        if (self::isLockedOut($ip)) return false;

        $user = Database::fetchOne(
            'SELECT u.*, r.role_name FROM users u
             JOIN roles r ON r.id = u.role_id
             WHERE (u.username = ? OR u.email = ?) AND u.status = ? LIMIT 1',
            [$username, $username, 'active']
        );

        if (!$user || !password_verify($password, $user['password_hash'])) {
            self::recordFailedAttempt($ip);
            self::logAudit(null, 'login_failed', 'auth',
                "Failed login: {$username} from {$ip}");
            return false;
        }

        // Rehash if cost changed
        if (password_needs_rehash($user['password_hash'], PASSWORD_ALGO, PASSWORD_OPTIONS)) {
            Database::execute(
                'UPDATE users SET password_hash = ? WHERE id = ?',
                [password_hash($password, PASSWORD_ALGO, PASSWORD_OPTIONS), $user['id']]
            );
        }

        // Clear failed attempts on success
        self::clearAttempts($ip);

        session_regenerate_id(true);
        $_SESSION = []; // clear old session data
        $_SESSION['user_id']     = (int)$user['id'];
        $_SESSION['username']    = $user['username'];
        $_SESSION['name']        = $user['name'];
        $_SESSION['role_id']     = (int)$user['role_id'];
        $_SESSION['role_name']   = $user['role_name'];
        $_SESSION['branch_id']   = $user['branch_id'] ? (int)$user['branch_id'] : null;
        $_SESSION['_created']    = time();
        $_SESSION['_ua']         = md5($_SERVER['HTTP_USER_AGENT'] ?? '');
        $_SESSION['permissions'] = self::loadPermissions((int)$user['role_id']);

        Database::execute('UPDATE users SET last_login = NOW() WHERE id = ?', [$user['id']]);
        self::logAudit($user['id'], 'login', 'auth', "Login from {$ip}");
        return true;
    }

    // ── Permissions ──────────────────────────────────────────
    private static function loadPermissions(int $roleId): array
    {
        $rows = Database::fetchAll(
            'SELECT p.permission_key FROM role_permissions rp
             JOIN permissions p ON p.id = rp.permission_id
             WHERE rp.role_id = ?', [$roleId]
        );
        return array_column($rows, 'permission_key');
    }

    public static function check(): bool
    {
        return !empty($_SESSION['user_id']) && is_int($_SESSION['user_id']);
    }

    public static function requireLogin(): void
    {
        if (!self::check()) {
            $_SESSION['intended'] = $_SERVER['REQUEST_URI'] ?? '';
            header('Location: ' . APP_URL . '/auth/login');
            exit;
        }
    }

    public static function can(string $key): bool
    {
        if (!self::check()) return false;
        if (in_array($_SESSION['role_name'] ?? '', ['super_admin', 'admin', 'developer'], true)) return true;
        $perms = $_SESSION['permissions'] ?? [];

        // Direct match
        if (in_array($key, $perms, true)) return true;

        // reports.* — reports.view grants access to all individual report pages
        if (str_starts_with($key, 'reports.') && in_array('reports.view', $perms, true)) return true;

        return false;
    }

    public static function requirePermission(string $key): void
    {
        self::requireLogin();
        if (!self::can($key)) {
            http_response_code(403);
            $pageTitle = '403 – Forbidden';
            require APP_PATH . '/views/errors/403.php';
            exit;
        }
    }

    // ── Logout ───────────────────────────────────────────────
    public static function logout(): void
    {
        $uid = $_SESSION['user_id'] ?? null;
        self::logAudit($uid, 'logout', 'auth', 'User logged out');
        $_SESSION = [];
        if (ini_get('session.use_cookies')) {
            $p = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000,
                $p['path'], $p['domain'], $p['secure'], $p['httponly']);
        }
        session_destroy();
    }

    // ── Helpers ──────────────────────────────────────────────
    public static function id(): ?int      { return $_SESSION['user_id']  ?? null; }
    public static function name(): string  { return $_SESSION['name']      ?? ''; }
    public static function role(): string  { return $_SESSION['role_name'] ?? ''; }
    public static function branchId(): ?int{ return $_SESSION['branch_id'] ?? null; }

    // ── CSRF ─────────────────────────────────────────────────
    public static function csrfToken(): string
    {
        if (empty($_SESSION[CSRF_TOKEN_NAME])) {
            $_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
        }
        return $_SESSION[CSRF_TOKEN_NAME];
    }

    public static function csrfField(): string
    {
        return '<input type="hidden" name="' . CSRF_TOKEN_NAME
             . '" value="' . self::csrfToken() . '">';
    }

    public static function verifyCsrf(): bool
    {
        $token = $_POST[CSRF_TOKEN_NAME]
              ?? $_SERVER['HTTP_X_CSRF_TOKEN']
              ?? '';
        $stored = $_SESSION[CSRF_TOKEN_NAME] ?? '';
        return $stored !== '' && hash_equals($stored, $token);
    }

    public static function requireCsrf(): void
    {
        if (!self::verifyCsrf()) {
            http_response_code(419);
            $isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']);
            if ($isAjax) {
                header('Content-Type: application/json');
                echo json_encode(['success' => false, 'message' => 'Session expired. Please refresh.']);
            } else {
                $_SESSION['flash']['error'][] = 'Session expired. Please try again.';
                header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? APP_URL . '/dashboard'));
            }
            exit;
        }
    }

    // ── Audit log ────────────────────────────────────────────
    public static function logAudit(
        ?int $userId, string $action, string $module,
        string $description = '', array $old = [], array $new = []
    ): void {
        try {
            Database::execute(
                'INSERT INTO audit_logs
                 (user_id,action,module,description,old_value,new_value,ip_address,user_agent)
                 VALUES (?,?,?,?,?,?,?,?)',
                [
                    $userId, $action, $module, $description,
                    $old ? json_encode($old, JSON_UNESCAPED_UNICODE) : null,
                    $new ? json_encode($new, JSON_UNESCAPED_UNICODE) : null,
                    self::getClientIp(),
                    substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255),
                ]
            );
        } catch (Exception) {}
    }

    private static function getClientIp(): string
    {
        $keys = ['HTTP_CF_CONNECTING_IP','HTTP_X_FORWARDED_FOR','HTTP_X_REAL_IP','REMOTE_ADDR'];
        foreach ($keys as $k) {
            if (!empty($_SERVER[$k])) {
                $ip = trim(explode(',', $_SERVER[$k])[0]);
                if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
            }
        }
        return '0.0.0.0';
    }
}