<?php
declare(strict_types=1);

/** RBAC admin: global roles, company memberships, privilege sets. */
final class RbacAdminRepository {
    private function __construct() {}

    public static function buildPanel(?array $actor = null): array {
        $actor ??= Auth::user();
        if (!$actor || !Rbac::canManageRbac($actor)) {
            return ['ok' => false, 'error' => 'forbidden'];
        }
        $actorId = (int) ($actor['id'] ?? 0);
        $globalAdmin = Rbac::isGlobalAdmin($actor);
        $companies = self::listCompaniesForActor($actor);
        $companyIds = array_map(static fn($c) => (int) $c['id'], $companies);

        return [
            'ok' => true,
            'actor' => [
                'id' => $actorId,
                'username' => (string) ($actor['username'] ?? ''),
                'role' => (string) ($actor['role'] ?? ''),
                'is_root' => Rbac::isRoot($actor),
                'is_global_admin' => $globalAdmin,
            ],
            'companies' => $companies,
            'users' => self::listUsersWithMemberships($companyIds, $globalAdmin),
            'global_roles' => [
                Rbac::GLOBAL_ROOT,
                Rbac::GLOBAL_PLATFORM_ADMIN,
                Rbac::GLOBAL_VIEWER,
            ],
            'company_roles' => [
                Rbac::COMPANY_OWNER,
                Rbac::COMPANY_ADMIN,
                Rbac::COMPANY_MEMBER,
            ],
            'privilege_sets' => array_map(
                static fn($key) => [
                    'key' => $key,
                    'label' => str_replace('_', ' ', $key),
                    'grants' => Rbac::grantsForPrivilegeSet($key),
                ],
                Rbac::privilegeSetKeys()
            ),
            'permissions' => [
                'can_edit_global_roles' => Rbac::isRoot($actor),
                'can_manage_platform' => Rbac::can('manage_platform', null, $actor),
                'can_manage_company_users' => $globalAdmin,
            ],
        ];
    }

    public static function setGlobalRole(int $targetUserId, string $role, ?array $actor = null): array {
        $actor ??= Auth::user();
        if (!$actor || !Rbac::isRoot($actor)) {
            return ['ok' => false, 'error' => 'forbidden'];
        }
        $role = Rbac::normalizeGlobalRole($role);
        $allowed = [Rbac::GLOBAL_ROOT, Rbac::GLOBAL_PLATFORM_ADMIN, Rbac::GLOBAL_VIEWER];
        if (!in_array($role, $allowed, true)) {
            return ['ok' => false, 'error' => 'invalid_role'];
        }
        if ($targetUserId <= 0) {
            return ['ok' => false, 'error' => 'invalid_user'];
        }
        $pdo = Database::pdo();
        $stmt = $pdo->prepare('UPDATE users SET role = ? WHERE id = ?');
        $stmt->execute([$role, $targetUserId]);
        if ($stmt->rowCount() < 1) {
            return ['ok' => false, 'error' => 'user_not_found'];
        }
        if (in_array($role, [Rbac::GLOBAL_ROOT, Rbac::GLOBAL_PLATFORM_ADMIN], true)) {
            Rbac::seedMembershipsForUser($targetUserId, $role);
        }
        return ['ok' => true];
    }

    public static function setCompanyRole(int $targetUserId, int $companyId, string $role, ?array $actor = null): array {
        $actor ??= Auth::user();
        if (!$actor || !self::canEditMembership($actor, $companyId)) {
            return ['ok' => false, 'error' => 'forbidden'];
        }
        $allowed = [Rbac::COMPANY_OWNER, Rbac::COMPANY_ADMIN, Rbac::COMPANY_MEMBER];
        if (!in_array($role, $allowed, true)) {
            return ['ok' => false, 'error' => 'invalid_role'];
        }
        if ($targetUserId <= 0 || $companyId <= 0) {
            return ['ok' => false, 'error' => 'invalid_input'];
        }
        $pdo = Database::pdo();
        if (Database::isMysql()) {
            $sql = 'INSERT INTO company_memberships (user_id, company_id, role) VALUES (?, ?, ?)
                    ON DUPLICATE KEY UPDATE role = VALUES(role)';
        } else {
            $sql = 'INSERT INTO company_memberships (user_id, company_id, role) VALUES (?, ?, ?)
                    ON CONFLICT(user_id, company_id) DO UPDATE SET role = excluded.role';
        }
        $stmt = $pdo->prepare($sql);
        $stmt->execute([$targetUserId, $companyId, $role]);
        return ['ok' => true];
    }

    public static function applyPrivilegeSet(int $targetUserId, int $companyId, string $setKey, ?array $actor = null): array {
        $actor ??= Auth::user();
        if (!$actor || !self::canEditMembership($actor, $companyId)) {
            return ['ok' => false, 'error' => 'forbidden'];
        }
        if ($targetUserId <= 0 || $companyId <= 0) {
            return ['ok' => false, 'error' => 'invalid_input'];
        }
        $setKey = trim($setKey);
        if ($setKey !== '' && $setKey !== Rbac::PRIVILEGE_SET_NONE && !isset(Rbac::PRIVILEGE_SETS[$setKey])) {
            return ['ok' => false, 'error' => 'invalid_privilege_set'];
        }
        $json = Rbac::encodePermissionsJson($setKey === Rbac::PRIVILEGE_SET_NONE ? '' : $setKey);
        $pdo = Database::pdo();
        $stmt = $pdo->prepare(
            'UPDATE company_memberships SET permissions_json = ? WHERE user_id = ? AND company_id = ?'
        );
        $stmt->execute([$json, $targetUserId, $companyId]);
        if ($stmt->rowCount() < 1) {
            return ['ok' => false, 'error' => 'membership_not_found'];
        }
        return ['ok' => true];
    }

    /** @return list<array{id:int,slug:string,name:string}> */
    private static function listCompaniesForActor(array $actor): array {
        $pdo = Database::pdo();
        $allowed = Rbac::allowedCompanyIds($actor);
        if ($allowed === null) {
            $stmt = $pdo->query('SELECT id, slug, name FROM companies ORDER BY name ASC');
            return self::mapCompanies($stmt->fetchAll(PDO::FETCH_ASSOC));
        }
        if ($allowed === []) {
            return [];
        }
        $ph = implode(',', array_fill(0, count($allowed), '?'));
        $stmt = $pdo->prepare("SELECT id, slug, name FROM companies WHERE id IN ($ph) ORDER BY name ASC");
        $stmt->execute($allowed);
        return self::mapCompanies($stmt->fetchAll(PDO::FETCH_ASSOC));
    }

    /** @param list<array<string,mixed>> $rows @return list<array{id:int,slug:string,name:string}> */
    private static function mapCompanies(array $rows): array {
        $out = [];
        foreach ($rows as $row) {
            $out[] = [
                'id' => (int) $row['id'],
                'slug' => (string) $row['slug'],
                'name' => (string) $row['name'],
            ];
        }
        return $out;
    }

    /**
     * @param list<int> $companyIds
     * @return list<array<string, mixed>>
     */
    private static function listUsersWithMemberships(array $companyIds, bool $globalAdmin): array {
        $pdo = Database::pdo();
        if ($globalAdmin) {
            $users = $pdo->query('SELECT id, username, role FROM users ORDER BY username ASC')->fetchAll(PDO::FETCH_ASSOC);
        } else {
            if ($companyIds === []) {
                return [];
            }
            $ph = implode(',', array_fill(0, count($companyIds), '?'));
            $stmt = $pdo->prepare(
                "SELECT DISTINCT u.id, u.username, u.role
                 FROM users u
                 INNER JOIN company_memberships m ON m.user_id = u.id
                 WHERE m.company_id IN ($ph)
                 ORDER BY u.username ASC"
            );
            $stmt->execute($companyIds);
            $users = $stmt->fetchAll(PDO::FETCH_ASSOC);
        }
        $out = [];
        foreach ($users as $u) {
            $uid = (int) $u['id'];
            $memberships = self::membershipsForUser($uid, $companyIds, $globalAdmin);
            $out[] = [
                'id' => $uid,
                'username' => (string) $u['username'],
                'global_role' => Rbac::normalizeGlobalRole((string) ($u['role'] ?? '')),
                'memberships' => $memberships,
                'auth_failures' => AuthAttempts::recentFailureCount((string) $u['username']),
            ];
        }
        return $out;
    }

    /**
     * @param list<int> $companyIds
     * @return list<array<string, mixed>>
     */
    private static function membershipsForUser(int $userId, array $companyIds, bool $globalAdmin): array {
        $pdo = Database::pdo();
        if ($globalAdmin) {
            $stmt = $pdo->prepare(
                'SELECT m.company_id, c.slug, c.name, m.role, m.permissions_json
                 FROM company_memberships m
                 INNER JOIN companies c ON c.id = m.company_id
                 WHERE m.user_id = ?
                 ORDER BY c.name ASC'
            );
            $stmt->execute([$userId]);
        } else {
            if ($companyIds === []) {
                return [];
            }
            $ph = implode(',', array_fill(0, count($companyIds), '?'));
            $params = array_merge([$userId], $companyIds);
            $stmt = $pdo->prepare(
                "SELECT m.company_id, c.slug, c.name, m.role, m.permissions_json
                 FROM company_memberships m
                 INNER JOIN companies c ON c.id = m.company_id
                 WHERE m.user_id = ? AND m.company_id IN ($ph)
                 ORDER BY c.name ASC"
            );
            $stmt->execute($params);
        }
        $out = [];
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $raw = isset($row['permissions_json']) ? (string) $row['permissions_json'] : '';
            $out[] = [
                'company_id' => (int) $row['company_id'],
                'slug' => (string) $row['slug'],
                'name' => (string) $row['name'],
                'role' => (string) $row['role'],
                'privilege_set' => Rbac::privilegeSetFromPermissionsJson($raw),
            ];
        }
        return $out;
    }

    private static function canEditMembership(array $actor, int $companyId): bool {
        if (Rbac::isGlobalAdmin($actor)) {
            return true;
        }
        if (!Rbac::canAccessCompany($companyId, $actor)) {
            return false;
        }
        $uid = (int) ($actor['id'] ?? 0);
        $role = $uid > 0 ? Rbac::companyRole($uid, $companyId) : null;
        return $role === Rbac::COMPANY_OWNER || $role === Rbac::COMPANY_ADMIN;
    }
}
