<?php

declare(strict_types=1);

namespace App\Shared\Security;

use App\Core\SchemaInspector;
use PDO;
use Throwable;

final class RbacSupport
{
    private static ?bool $enabledCache = null;

    /** @var array<string,bool> */
    private static array $seededRequestCache = [];

    public static function enabled(PDO $pdo): bool
    {
        if (self::$enabledCache !== null) {
            return self::$enabledCache;
        }

        self::$enabledCache = self::tableExists($pdo, 'roles')
            && self::tableExists($pdo, 'permissions')
            && self::tableExists($pdo, 'user_roles')
            && self::tableExists($pdo, 'role_permissions');

        return self::$enabledCache;
    }

    public static function currentUserRoles(PDO $pdo, int $userId): array
    {
        if ($userId <= 0 || !self::enabled($pdo)) {
            return [];
        }

        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        if (isset($_SESSION['rbac_roles']) && is_array($_SESSION['rbac_roles'])) {
            return $_SESSION['rbac_roles'];
        }

        $st = $pdo->prepare("SELECT r.id, r.name FROM roles r JOIN user_roles ur ON ur.role_id=r.id WHERE ur.user_id=? ORDER BY r.name");
        $st->execute([$userId]);
        $rows = $st->fetchAll();

        $_SESSION['rbac_roles'] = $rows;
        return $rows;
    }

    public static function currentUserPermissions(PDO $pdo, int $userId): array
    {
        if ($userId <= 0 || !self::enabled($pdo)) {
            return [];
        }

        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        if (isset($_SESSION['rbac_perms']) && is_array($_SESSION['rbac_perms'])) {
            return $_SESSION['rbac_perms'];
        }

        $sql = "
            SELECT DISTINCT p.code
            FROM permissions p
            JOIN role_permissions rp ON rp.permission_id=p.id
            JOIN user_roles ur ON ur.role_id=rp.role_id
            WHERE ur.user_id=?
        ";
        $st = $pdo->prepare($sql);
        $st->execute([$userId]);
        $perms = [];
        foreach ($st->fetchAll() as $row) {
            $perms[$row['code']] = true;
        }

        $_SESSION['rbac_perms'] = $perms;
        return $perms;
    }

    public static function ensurePermissionsSeed(PDO $pdo): void
    {
        if (!self::enabled($pdo)) {
            return;
        }

        $codes = PermissionCatalog::seededCodes();
        $hash = sha1(implode('|', $codes));

        if (!empty(self::$seededRequestCache[$hash])) {
            return;
        }

        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }

        if (($_SESSION['rbac_seed_hash'] ?? '') === $hash) {
            self::$seededRequestCache[$hash] = true;
            return;
        }

        $permissions = PermissionCatalog::definitions();

        $values = [];
        foreach ($permissions as [$code, $name, $module]) {
            $values[] = sprintf("('%s','%s','%s')", addslashes($code), addslashes($name), addslashes($module));
        }

        try {
            $pdo->exec("INSERT IGNORE INTO permissions (code, name, module) VALUES
            " . implode(",
            ", $values));
        } catch (Throwable $e) {
        }

        $quoted = array_map(static fn(string $code): string => "'" . addslashes($code) . "'", $codes);
        try {
            $pdo->exec("INSERT IGNORE INTO role_permissions (role_id, permission_id)
            SELECT 1, p.id FROM permissions p
            WHERE p.code IN (" . implode(',', $quoted) . ")");
        } catch (Throwable $e) {
        }

        $_SESSION['rbac_seed_hash'] = $hash;
        self::$seededRequestCache[$hash] = true;
    }

    public static function flushCache(): void
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            return;
        }

        unset($_SESSION['rbac_roles'], $_SESSION['rbac_perms'], $_SESSION['rbac_seed_hash']);
    }

    private static function tableExists(PDO $pdo, string $table): bool
    {
        try {
            return SchemaInspector::tableExists($pdo, $table);
        } catch (Throwable $e) {
            return false;
        }
    }
}
