<?php
/*
 * package examples/crash_reporter/backend/src/Auth.php
 * Auth.php
 * Created at: Wed 20 May 2026 14:31:55 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 56 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	a6e49e8d07788ac2b4ea48f886f9a46ec6f5c1eab5b9723e1554e1fe53765efd
 */
declare(strict_types=1);

final class Auth {
    public static function user(): ?array {
        $u = $_SESSION['user'] ?? null;
        if (!$u || !empty($u['companies'])) {
            return $u;
        }
        $uid = (int) ($u['id'] ?? 0);
        if ($uid <= 0) {
            return $u;
        }
        $stmt = Database::pdo()->prepare('SELECT * FROM users WHERE id = ? LIMIT 1');
        $stmt->execute([$uid]);
        $row = $stmt->fetch();
        if (!$row) {
            return $u;
        }
        Database::ensureRbacSchema();
        Rbac::seedMembershipsForUser($uid, (string) ($row['role'] ?? 'viewer'));
        $_SESSION['user'] = Rbac::buildSessionUser($row);
        return $_SESSION['user'];
    }

    public static function check(): void {
        if (!self::user()) {
            header('Location: ' . self::authUrl('/login'));
            exit;
        }
    }

    public static function can(string $action): bool {
        return Rbac::can($action);
    }

    public static function canEditTags(?int $companyId = null): bool {
        return Rbac::can('edit_tags', $companyId);
    }

    /** Ticket owner or company admin+ (same gate as tag edit for alpha). */
    public static function canEditTicket(array $ticket, ?int $companyId = null): bool {
        if (self::canEditTags($companyId)) {
            return true;
        }
        $user = self::user();
        if (!$user) {
            return false;
        }
        $owner = trim((string) ($ticket['owner_username'] ?? ''));
        return $owner !== '' && strcasecmp($owner, (string) ($user['username'] ?? '')) === 0;
    }

    /**
     * @return true|'pending_2fa'|false
     */
    public static function login(string $username, string $password): bool|string {
        if (AuthAttempts::isRateLimited($username)) {
            return false;
        }
        $pdo = Database::pdo();
        AuthEmailSchema::ensure($pdo);
        $stmt = $pdo->prepare('SELECT * FROM users WHERE username = ? LIMIT 1');
        $stmt->execute([$username]);
        $row = $stmt->fetch();
        if (!$row || !password_verify($password, $row['password_hash'])) {
            AuthAttempts::record('login_fail', $username);
            return false;
        }
        $status = (string) ($row['status'] ?? 'active');
        if ($status === 'pending') {
            return false;
        }
        if ($status === 'locked' || $status === 'disabled') {
            AuthAttempts::record('login_fail', $username);
            return false;
        }
        $uid = (int) ($row['id'] ?? 0);
        if ($uid > 0 && AuthFactors::hasTotp($uid)) {
            $_SESSION['pending_2fa_user_id'] = $uid;
            $_SESSION['pending_2fa_exp'] = time() + 300;
            AuthAttempts::record('login_ok', $username);
            return 'pending_2fa';
        }
        $_SESSION['user'] = Rbac::buildSessionUser($row);
        AuthAttempts::record('login_ok', $username);
        return true;
    }

    public static function completeTotpLogin(string $code): bool {
        $uid = (int) ($_SESSION['pending_2fa_user_id'] ?? 0);
        $exp = (int) ($_SESSION['pending_2fa_exp'] ?? 0);
        if ($uid <= 0 || $exp < time()) {
            self::clearPending2fa();
            return false;
        }
        $secret = AuthFactors::getTotpSecret($uid);
        if ($secret === null || !AuthTotp::verify($secret, $code)) {
            return false;
        }
        self::clearPending2fa();
        $stmt = Database::pdo()->prepare('SELECT * FROM users WHERE id = ? LIMIT 1');
        $stmt->execute([$uid]);
        $row = $stmt->fetch();
        if (!$row) {
            return false;
        }
        $_SESSION['user'] = Rbac::buildSessionUser($row);
        return true;
    }

    public static function pending2faUserId(): int {
        $exp = (int) ($_SESSION['pending_2fa_exp'] ?? 0);
        if ($exp < time()) {
            return 0;
        }
        return (int) ($_SESSION['pending_2fa_user_id'] ?? 0);
    }

    public static function clearPending2fa(): void {
        unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_exp']);
    }

    public static function logout(): void {
        self::clearPending2fa();
        unset($_SESSION['user']);
    }

    public static function basePath(): string {
        $bp = rtrim((string) cfg('base_path', ''), '/');
        if ($bp === '') {
            return $bp;
        }
        // If nginx serves /issues/ but config.php still has legacy /crashes, asset URLs must match nginx.
        $uri = (string) ($_SERVER['REQUEST_URI'] ?? '');
        if (str_contains($uri, '/issues') && str_ends_with($bp, '/crashes')) {
            return substr($bp, 0, -strlen('/crashes')) . '/issues';
        }
        return $bp;
    }

    /** Shared project prefix, e.g. /app/androidcast_project */
    public static function projectBasePath(): string {
        $p = trim((string) cfg('project_base_path', ''));
        if ($p !== '') {
            return rtrim($p, '/');
        }
        $bp = self::basePath();
        if (preg_match('#^(.+)/(?:crashes|issues|graphs|build)$#', $bp, $m)) {
            return $m[1];
        }
        return '/app/androidcast_project';
    }

    /** Login/logout/register/2FA live at project root, not under /issues/. */
    public static function authUrl(string $path = ''): string {
        $root = self::projectBasePath();
        $path = '/' . ltrim($path, '/');
        if ($path === '/' || $path === '') {
            return $root;
        }
        return $root . $path;
    }
}
