<?php

namespace modules\selfserve\classes;

require_once WD . '/classes/db.php';
require_once WD . '/classes/selfserve_schema_bootstrap.php';

use classes\db;
use classes\selfserve_schema_bootstrap;

class selfserve_virtual_hardware
{
    public const SCHEMA_VERSION = 1;
    public const DEFAULT_GATEWAY_KEY = 'virtual-main';

    public function __construct()
    {
        selfserve_schema_bootstrap::ensureTables();
    }

    /**
     * @return array<string,mixed>
     */
    public function getConfig(int $departmentId): array
    {
        $pdo = db::getPDO();
        $statement = $pdo->prepare(
            'SELECT config_json
             FROM department_selfserve_studio_virtual_hardware
             WHERE department_id = :department_id AND deleted_at IS NULL
             LIMIT 1'
        );
        $statement->execute([':department_id' => $departmentId]);
        $row = $statement->fetch(\PDO::FETCH_ASSOC);
        if (!is_array($row)) {
            return $this->emptyConfig();
        }

        $decoded = json_decode((string)($row['config_json'] ?? '{}'), true);
        return $this->normalizeConfig(is_array($decoded) ? $decoded : []);
    }

    /**
     * @param array<string,mixed> $config
     * @return array<string,mixed>
     */
    public function saveConfig(int $departmentId, array $config, ?int $userId = null): array
    {
        $normalized = $this->normalizeConfig($config);
        $json = json_encode($normalized, JSON_UNESCAPED_SLASHES);
        if ($json === false) {
            throw new \RuntimeException('Unable to encode virtual hardware config.');
        }

        $pdo = db::getPDO();
        $statement = $pdo->prepare(
            'INSERT INTO department_selfserve_studio_virtual_hardware
                (department_id, config_json, created_by, updated_by, deleted_at)
             VALUES
                (:department_id, :config_json, :created_by, :updated_by, NULL)
             ON DUPLICATE KEY UPDATE
                config_json = VALUES(config_json),
                updated_by = VALUES(updated_by),
                deleted_at = NULL'
        );
        $statement->execute([
            ':department_id' => $departmentId,
            ':config_json' => $json,
            ':created_by' => $userId,
            ':updated_by' => $userId,
        ]);

        return $normalized;
    }

    /**
     * @param array<string,mixed> $payload
     * @param array<string,mixed> $realWorkspace
     * @return array<string,mixed>
     */
    public function applyOperation(int $departmentId, string $operation, array $payload, ?int $userId, array $realWorkspace): array
    {
        $config = $this->getConfig($departmentId);
        $operation = strtolower(trim($operation));

        if ($operation === 'generate_from_lanes') {
            $config = $this->generateFromLanes($realWorkspace, $config);
        } elseif ($operation === 'upsert_gateway') {
            $config = $this->upsertGateway($config, $payload);
        } elseif ($operation === 'upsert_binding') {
            $config = $this->upsertBinding($config, $payload, $realWorkspace);
        } elseif ($operation === 'delete_binding') {
            $config = $this->deleteBinding($config, $payload);
        } elseif ($operation === 'reset') {
            $config = $this->emptyConfig();
        } else {
            throw new \RuntimeException('Unsupported virtual hardware operation: ' . $operation);
        }

        return $this->saveConfig($departmentId, $config, $userId);
    }

    /**
     * @param array<string,mixed> $workspace
     * @return array<string,mixed>
     */
    public function mergeWorkspace(array $workspace, int $departmentId): array
    {
        return $this->mergeWorkspaceWithConfig($workspace, $this->getConfig($departmentId));
    }

    /**
     * Pure merge helper used by graph serialization and tests.
     *
     * @param array<string,mixed> $workspace
     * @param array<string,mixed> $config
     * @return array<string,mixed>
     */
    public function mergeWorkspaceWithConfig(array $workspace, array $config): array
    {
        $config = $this->normalizeConfig($config);
        $workspace += [
            'gateways' => [],
            'relays' => [],
            'lanes' => [],
            'issues' => [],
            'actions' => [],
        ];

        $virtualBindings = (array)($config['bindings'] ?? []);
        $enabled = (bool)($config['enabled'] ?? true);
        if (!$enabled || $virtualBindings === []) {
            $workspace['virtual'] = $this->workspaceVirtualSummary($config, 0, 0);
            return $workspace;
        }

        $realBindingRelayIds = $this->realBindingRelayIds((array)($workspace['gateways'] ?? []));
        $virtualGateways = $this->virtualGatewaysForWorkspace($config);
        $virtualRelays = $this->virtualRelaysForWorkspace($config);
        $bindingsByRelayId = $this->indexVirtualBindingsByRelayId($virtualGateways);

        $workspace['gateways'] = array_values(array_merge((array)($workspace['gateways'] ?? []), $virtualGateways));
        $workspace['relays'] = $this->mergeRelays((array)($workspace['relays'] ?? []), $virtualRelays);
        $workspace['lanes'] = $this->mergeLaneCoverage((array)($workspace['lanes'] ?? []), $bindingsByRelayId);

        $coveredRelayIds = array_fill_keys(array_keys($bindingsByRelayId), true);
        $workspace['issues'] = $this->mergeIssues((array)($workspace['issues'] ?? []), $coveredRelayIds, $virtualGateways);
        $workspace['virtual'] = $this->workspaceVirtualSummary($config, count($virtualGateways), count($virtualBindings), $realBindingRelayIds);
        $workspace['summary'] = $this->mergeSummary((array)($workspace['summary'] ?? []), $workspace);

        return $workspace;
    }

    /**
     * @param array<string,mixed> $workspace
     * @return array<int,string>
     */
    public function validationWarnings(array $workspace): array
    {
        $virtual = is_array($workspace['virtual'] ?? null) ? (array)$workspace['virtual'] : [];
        if (($virtual['has_virtual_hardware'] ?? false) !== true) {
            return [];
        }

        $warnings = [
            'Studio uses virtual hardware coverage. Publishing is allowed, but live relay dispatch still requires a real edge gateway and real relay bindings.',
        ];
        $virtualOnlyRelays = (array)($virtual['virtual_only_relay_ids'] ?? []);
        if ($virtualOnlyRelays !== []) {
            $warnings[] = 'Virtual coverage only for relay IDs: ' . implode(', ', $virtualOnlyRelays) . '.';
        }

        return $warnings;
    }

    /**
     * @param array<string,mixed> $workspace
     * @param array<string,mixed>|null $baseConfig
     * @return array<string,mixed>
     */
    public function generateFromLanes(array $workspace, ?array $baseConfig = null): array
    {
        $config = $this->normalizeConfig($baseConfig ?? $this->emptyConfig());
        $gatewayKey = self::DEFAULT_GATEWAY_KEY;
        $config = $this->upsertGateway($config, [
            'key' => $gatewayKey,
            'label' => 'Virtual Studio Gateway',
            'status' => 'VIRTUAL',
        ]);

        foreach ((array)($workspace['lanes'] ?? []) as $lane) {
            if (!is_array($lane)) {
                continue;
            }
            foreach ((array)($lane['relay_slots'] ?? []) as $slot) {
                if (!is_array($slot)) {
                    continue;
                }
                $relayId = trim((string)($slot['relay_id'] ?? ''));
                $role = $this->normalizeRole($slot['slot'] ?? $slot['role'] ?? '');
                if ($relayId === '' || $role === '') {
                    continue;
                }
                $config = $this->upsertBinding($config, [
                    'gateway_key' => $gatewayKey,
                    'relay_id' => $relayId,
                    'role' => $role,
                    'services' => [$role],
                    'label' => trim((string)($lane['name'] ?? ('Lane ' . ($lane['id'] ?? '')))) . ' ' . $role,
                    'lane_id' => (int)($lane['id'] ?? 0),
                    'slot' => $role,
                    'generated' => true,
                ], $workspace);
            }
        }

        return $config;
    }

    /**
     * @return array<string,mixed>
     */
    public function emptyConfig(): array
    {
        return [
            'schema_version' => self::SCHEMA_VERSION,
            'enabled' => true,
            'gateways' => [],
            'relays' => [],
            'bindings' => [],
        ];
    }

    /**
     * @param array<string,mixed> $config
     * @return array<string,mixed>
     */
    public function normalizeConfig(array $config): array
    {
        $normalized = $this->emptyConfig();
        $normalized['schema_version'] = (int)($config['schema_version'] ?? self::SCHEMA_VERSION);
        $normalized['enabled'] = filter_var($config['enabled'] ?? true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== false;

        foreach ((array)($config['gateways'] ?? []) as $gateway) {
            if (!is_array($gateway)) {
                continue;
            }
            $key = $this->normalizeGatewayKey($gateway['key'] ?? $gateway['gateway_key'] ?? $gateway['id'] ?? '');
            if ($key === '') {
                continue;
            }
            $normalized['gateways'][$key] = [
                'key' => $key,
                'label' => trim((string)($gateway['label'] ?? ('Virtual Gateway ' . $key))),
                'status' => strtoupper(trim((string)($gateway['status'] ?? 'VIRTUAL'))) ?: 'VIRTUAL',
                'metadata' => is_array($gateway['metadata'] ?? null) ? (array)$gateway['metadata'] : [],
            ];
        }

        foreach ((array)($config['relays'] ?? []) as $relay) {
            if (!is_array($relay)) {
                continue;
            }
            $relayId = trim((string)($relay['relay_id'] ?? $relay['id'] ?? ''));
            if ($relayId === '') {
                continue;
            }
            $normalized['relays'][$relayId] = [
                'relay_id' => $relayId,
                'name' => trim((string)($relay['name'] ?? $relay['label'] ?? ('Virtual relay ' . $relayId))),
                'status' => strtoupper(trim((string)($relay['status'] ?? 'VIRTUAL'))) ?: 'VIRTUAL',
                'virtual' => true,
            ];
        }

        $bindings = [];
        foreach ((array)($config['bindings'] ?? []) as $binding) {
            if (!is_array($binding)) {
                continue;
            }
            $gatewayKey = $this->normalizeGatewayKey($binding['gateway_key'] ?? $binding['gateway_id'] ?? self::DEFAULT_GATEWAY_KEY);
            $relayId = trim((string)($binding['relay_id'] ?? ''));
            $role = $this->normalizeRole($binding['role'] ?? $binding['slot'] ?? $binding['service'] ?? '');
            if ($gatewayKey === '' || $relayId === '') {
                continue;
            }
            if (!isset($normalized['gateways'][$gatewayKey])) {
                $normalized['gateways'][$gatewayKey] = [
                    'key' => $gatewayKey,
                    'label' => 'Virtual Gateway ' . $gatewayKey,
                    'status' => 'VIRTUAL',
                    'metadata' => [],
                ];
            }
            if (!isset($normalized['relays'][$relayId])) {
                $normalized['relays'][$relayId] = [
                    'relay_id' => $relayId,
                    'name' => trim((string)($binding['label'] ?? ('Virtual relay ' . $relayId))),
                    'status' => 'VIRTUAL',
                    'virtual' => true,
                ];
            }
            $services = $this->normalizeServiceList($binding['services'] ?? ($role !== '' ? [$role] : []));
            if ($services === [] && $role !== '') {
                $services = [$role];
            }
            $id = $this->bindingId($gatewayKey, $relayId, $role);
            $bindings[$id] = [
                'id' => $id,
                'gateway_key' => $gatewayKey,
                'relay_id' => $relayId,
                'role' => $role,
                'slot' => $role,
                'services' => $services,
                'label' => trim((string)($binding['label'] ?? ('Virtual ' . ($role ?: 'relay') . ' ' . $relayId))),
                'channel' => array_key_exists('channel', $binding) ? (int)$binding['channel'] : 0,
                'lane_id' => isset($binding['lane_id']) ? (int)$binding['lane_id'] : null,
                'generated' => (bool)($binding['generated'] ?? false),
                'virtual' => true,
            ];
        }

        $normalized['gateways'] = array_values($normalized['gateways']);
        $normalized['relays'] = array_values($normalized['relays']);
        $normalized['bindings'] = array_values($bindings);

        return $normalized;
    }

    /**
     * @param array<string,mixed> $config
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    private function upsertGateway(array $config, array $payload): array
    {
        $config = $this->normalizeConfig($config);
        $key = $this->normalizeGatewayKey($payload['key'] ?? $payload['gateway_key'] ?? $payload['id'] ?? self::DEFAULT_GATEWAY_KEY);
        if ($key === '') {
            throw new \RuntimeException('Virtual gateway key is required.');
        }

        $gateways = [];
        foreach ((array)$config['gateways'] as $gateway) {
            $gateways[$this->normalizeGatewayKey($gateway['key'] ?? $gateway['id'] ?? '')] = $gateway;
        }
        $gateways[$key] = [
            'key' => $key,
            'label' => trim((string)($payload['label'] ?? $gateways[$key]['label'] ?? ('Virtual Gateway ' . $key))),
            'status' => strtoupper(trim((string)($payload['status'] ?? $gateways[$key]['status'] ?? 'VIRTUAL'))) ?: 'VIRTUAL',
            'metadata' => is_array($payload['metadata'] ?? null) ? (array)$payload['metadata'] : (array)($gateways[$key]['metadata'] ?? []),
        ];
        $config['gateways'] = array_values($gateways);

        return $this->normalizeConfig($config);
    }

    /**
     * @param array<string,mixed> $config
     * @param array<string,mixed> $payload
     * @param array<string,mixed> $workspace
     * @return array<string,mixed>
     */
    private function upsertBinding(array $config, array $payload, array $workspace): array
    {
        $config = $this->normalizeConfig($config);
        $gatewayKey = $this->normalizeGatewayKey($payload['gateway_key'] ?? $payload['gateway_id'] ?? self::DEFAULT_GATEWAY_KEY);
        $relayId = trim((string)($payload['relay_id'] ?? ''));
        $role = $this->normalizeRole($payload['role'] ?? $payload['slot'] ?? $payload['service'] ?? '');
        if ($gatewayKey === '' || $relayId === '') {
            throw new \RuntimeException('Virtual gateway key and relay id are required.');
        }

        $config = $this->upsertGateway($config, ['key' => $gatewayKey]);
        $services = $this->normalizeServiceList($payload['services'] ?? ($role !== '' ? [$role] : []));
        if ($services === [] && $role !== '') {
            $services = [$role];
        }
        $binding = [
            'id' => $this->bindingId($gatewayKey, $relayId, $role),
            'gateway_key' => $gatewayKey,
            'relay_id' => $relayId,
            'role' => $role,
            'slot' => $role,
            'services' => $services,
            'label' => trim((string)($payload['label'] ?? ('Virtual ' . ($role ?: 'relay') . ' ' . $relayId))),
            'channel' => array_key_exists('channel', $payload) ? (int)$payload['channel'] : 0,
            'lane_id' => isset($payload['lane_id']) ? (int)$payload['lane_id'] : $this->laneIdForRelay($workspace, $relayId),
            'generated' => (bool)($payload['generated'] ?? false),
            'virtual' => true,
        ];

        $bindings = [];
        foreach ((array)$config['bindings'] as $existing) {
            if (!is_array($existing)) {
                continue;
            }
            $bindings[(string)($existing['id'] ?? $this->bindingId((string)($existing['gateway_key'] ?? ''), (string)($existing['relay_id'] ?? ''), (string)($existing['role'] ?? '')))] = $existing;
        }
        $bindings[$binding['id']] = $binding;
        $config['bindings'] = array_values($bindings);
        $config['relays'][] = [
            'relay_id' => $relayId,
            'name' => trim((string)($payload['relay_label'] ?? $payload['label'] ?? ('Virtual relay ' . $relayId))),
            'status' => 'VIRTUAL',
            'virtual' => true,
        ];

        return $this->normalizeConfig($config);
    }

    /**
     * @param array<string,mixed> $config
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    private function deleteBinding(array $config, array $payload): array
    {
        $config = $this->normalizeConfig($config);
        $id = trim((string)($payload['id'] ?? ''));
        if ($id === '') {
            $id = $this->bindingId(
                $this->normalizeGatewayKey($payload['gateway_key'] ?? $payload['gateway_id'] ?? self::DEFAULT_GATEWAY_KEY),
                trim((string)($payload['relay_id'] ?? '')),
                $this->normalizeRole($payload['role'] ?? $payload['slot'] ?? '')
            );
        }

        $config['bindings'] = array_values(array_filter((array)$config['bindings'], static function (array $binding) use ($id): bool {
            return (string)($binding['id'] ?? '') !== $id;
        }));

        return $this->normalizeConfig($config);
    }

    /**
     * @param array<string,mixed> $config
     * @return array<int,array<string,mixed>>
     */
    private function virtualGatewaysForWorkspace(array $config): array
    {
        $bindingsByGateway = [];
        foreach ((array)($config['bindings'] ?? []) as $binding) {
            if (!is_array($binding)) {
                continue;
            }
            $gatewayKey = $this->normalizeGatewayKey($binding['gateway_key'] ?? self::DEFAULT_GATEWAY_KEY);
            $bindingsByGateway[$gatewayKey][] = [
                ...$binding,
                'gateway_id' => $gatewayKey,
                'gateway_key' => $gatewayKey,
                'node_id' => 'binding:' . $gatewayKey . ':' . (string)($binding['relay_id'] ?? '') . ':' . count($bindingsByGateway[$gatewayKey] ?? []),
                'virtual' => true,
            ];
        }

        $gateways = [];
        foreach ((array)($config['gateways'] ?? []) as $gateway) {
            if (!is_array($gateway)) {
                continue;
            }
            $key = $this->normalizeGatewayKey($gateway['key'] ?? self::DEFAULT_GATEWAY_KEY);
            $bindings = array_values($bindingsByGateway[$key] ?? []);
            if ($bindings === []) {
                continue;
            }
            $gateways[] = [
                'id' => $key,
                'key' => $key,
                'label' => (string)($gateway['label'] ?? ('Virtual Gateway ' . $key)),
                'status' => (string)($gateway['status'] ?? 'VIRTUAL'),
                'virtual' => true,
                'studio_only' => true,
                'bindings' => $bindings,
                'metadata' => (array)($gateway['metadata'] ?? []),
            ];
        }

        return $gateways;
    }

    /**
     * @param array<string,mixed> $config
     * @return array<int,array<string,mixed>>
     */
    private function virtualRelaysForWorkspace(array $config): array
    {
        return array_values(array_map(static function (array $relay): array {
            return [
                ...$relay,
                'virtual' => true,
                'studio_only' => true,
            ];
        }, (array)($config['relays'] ?? [])));
    }

    /**
     * @param array<int,array<string,mixed>> $gateways
     * @return array<string,array<int,array<string,mixed>>>
     */
    private function indexVirtualBindingsByRelayId(array $gateways): array
    {
        $bindings = [];
        foreach ($gateways as $gateway) {
            foreach ((array)($gateway['bindings'] ?? []) as $binding) {
                if (!is_array($binding)) {
                    continue;
                }
                $relayId = trim((string)($binding['relay_id'] ?? ''));
                if ($relayId === '') {
                    continue;
                }
                $bindings[$relayId][] = [
                    ...$binding,
                    'gateway_id' => (string)($gateway['id'] ?? ''),
                    'gateway_label' => (string)($gateway['label'] ?? 'Virtual Gateway'),
                    'gateway_status' => (string)($gateway['status'] ?? 'VIRTUAL'),
                    'virtual' => true,
                    'studio_only' => true,
                ];
            }
        }
        return $bindings;
    }

    /**
     * @param array<int,array<string,mixed>> $realRelays
     * @param array<int,array<string,mixed>> $virtualRelays
     * @return array<int,array<string,mixed>>
     */
    private function mergeRelays(array $realRelays, array $virtualRelays): array
    {
        $rows = [];
        foreach (array_merge($realRelays, $virtualRelays) as $relay) {
            if (!is_array($relay)) {
                continue;
            }
            $relayId = trim((string)($relay['relay_id'] ?? $relay['id'] ?? ''));
            if ($relayId === '') {
                continue;
            }
            $rows[$relayId . ':' . (!empty($relay['virtual']) ? 'virtual' : 'real')] = $relay;
        }
        return array_values($rows);
    }

    /**
     * @param array<int,array<string,mixed>> $lanes
     * @param array<string,array<int,array<string,mixed>>> $bindingsByRelayId
     * @return array<int,array<string,mixed>>
     */
    private function mergeLaneCoverage(array $lanes, array $bindingsByRelayId): array
    {
        foreach ($lanes as $laneIndex => $lane) {
            if (!is_array($lane)) {
                continue;
            }
            $required = 0;
            $bound = 0;
            foreach ((array)($lane['relay_slots'] ?? []) as $slotIndex => $slot) {
                if (!is_array($slot)) {
                    continue;
                }
                $required += 1;
                $relayId = trim((string)($slot['relay_id'] ?? ''));
                $covered = (bool)($slot['coverage']['covered'] ?? false);
                if (!$covered && $relayId !== '' && isset($bindingsByRelayId[$relayId])) {
                    $slot['coverage'] = [
                        'relay_id' => $relayId,
                        'covered' => true,
                        'status' => 'VIRTUAL',
                        'binding_count' => count($bindingsByRelayId[$relayId]),
                        'primary_binding' => $bindingsByRelayId[$relayId][0] ?? null,
                        'bindings' => $bindingsByRelayId[$relayId],
                        'virtual' => true,
                        'studio_only' => true,
                    ];
                    $slot['virtual'] = true;
                    $covered = true;
                }
                if ($covered) {
                    $bound += 1;
                }
                $lane['relay_slots'][$slotIndex] = $slot;
            }
            $lane['binding_coverage'] = [
                'required' => $required,
                'bound' => $bound,
                'missing' => max(0, $required - $bound),
                'state' => $required === 0 ? 'NOT_REQUIRED' : ($bound === $required ? 'READY' : 'MISSING'),
                'virtual' => $this->laneHasVirtualCoverage($lane),
            ];
            $lanes[$laneIndex] = $lane;
        }

        return $lanes;
    }

    /**
     * @param array<string,mixed> $lane
     */
    private function laneHasVirtualCoverage(array $lane): bool
    {
        foreach ((array)($lane['relay_slots'] ?? []) as $slot) {
            if (is_array($slot) && !empty($slot['coverage']['virtual'])) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param array<int,array<string,mixed>> $issues
     * @param array<string,bool> $coveredRelayIds
     * @param array<int,array<string,mixed>> $virtualGateways
     * @return array<int,array<string,mixed>>
     */
    private function mergeIssues(array $issues, array $coveredRelayIds, array $virtualGateways): array
    {
        $filtered = [];
        foreach ($issues as $issue) {
            if (!is_array($issue)) {
                continue;
            }
            $code = strtoupper((string)($issue['code'] ?? ''));
            if (in_array($code, ['LANE_BINDING_GAP', 'SCANNER_LANE_PARTIAL', 'SELFSERVE_PARTIAL_READY'], true)) {
                $filtered[] = [
                    ...$issue,
                    'severity' => 'info',
                    'virtual' => true,
                    'message' => (string)($issue['message'] ?? 'Relay coverage is incomplete.') . ' Studio virtual hardware covers this for dry runs only.',
                ];
                continue;
            }
            if (in_array($code, ['NO_GATEWAY', 'NO_ONLINE_GATEWAY'], true) && $virtualGateways !== []) {
                $filtered[] = [
                    ...$issue,
                    'severity' => 'warning',
                    'virtual' => true,
                    'message' => (string)($issue['message'] ?? 'Real gateway is not ready.') . ' A virtual studio gateway is available for dry runs only.',
                ];
                continue;
            }
            $filtered[] = $issue;
        }

        $filtered[] = [
            'severity' => 'warning',
            'code' => 'VIRTUAL_HARDWARE_ACTIVE',
            'message' => 'Virtual studio hardware is active. It counts for studio validation and simulation only; live dispatch still requires real gateway bindings.',
            'virtual' => true,
            'relay_ids' => array_keys($coveredRelayIds),
        ];

        return $filtered;
    }

    /**
     * @param array<string,mixed> $summary
     * @param array<string,mixed> $workspace
     * @return array<string,mixed>
     */
    private function mergeSummary(array $summary, array $workspace): array
    {
        $virtual = (array)($workspace['virtual'] ?? []);
        $summary['virtual_gateway_count'] = (int)($virtual['gateway_count'] ?? 0);
        $summary['virtual_binding_count'] = (int)($virtual['binding_count'] ?? 0);
        $summary['has_virtual_hardware'] = (bool)($virtual['has_virtual_hardware'] ?? false);
        return $summary;
    }

    /**
     * @param array<string,mixed> $config
     * @return array<string,mixed>
     */
    /**
     * @param array<string,bool> $realBindingRelayIds
     */
    private function workspaceVirtualSummary(array $config, int $gatewayCount, int $bindingCount, array $realBindingRelayIds = []): array
    {
        $relayIds = [];
        foreach ((array)($config['bindings'] ?? []) as $binding) {
            if (is_array($binding) && trim((string)($binding['relay_id'] ?? '')) !== '') {
                $relayId = trim((string)$binding['relay_id']);
                if (!isset($realBindingRelayIds[$relayId])) {
                    $relayIds[] = $relayId;
                }
            }
        }

        return [
            'schema_version' => (int)($config['schema_version'] ?? self::SCHEMA_VERSION),
            'enabled' => (bool)($config['enabled'] ?? true),
            'has_virtual_hardware' => $bindingCount > 0,
            'gateway_count' => $gatewayCount,
            'binding_count' => $bindingCount,
            'virtual_only_relay_ids' => array_values(array_unique($relayIds)),
            'config' => $config,
        ];
    }

    /**
     * @param array<int,array<string,mixed>> $gateways
     * @return array<string,bool>
     */
    private function realBindingRelayIds(array $gateways): array
    {
        $relayIds = [];
        foreach ($gateways as $gateway) {
            if (!is_array($gateway) || !empty($gateway['virtual'])) {
                continue;
            }
            foreach ((array)($gateway['bindings'] ?? []) as $binding) {
                if (!is_array($binding)) {
                    continue;
                }
                $relayId = trim((string)($binding['relay_id'] ?? ''));
                if ($relayId !== '') {
                    $relayIds[$relayId] = true;
                }
            }
        }
        return $relayIds;
    }

    /**
     * @param array<string,mixed> $workspace
     */
    private function laneIdForRelay(array $workspace, string $relayId): ?int
    {
        foreach ((array)($workspace['lanes'] ?? []) as $lane) {
            if (!is_array($lane)) {
                continue;
            }
            foreach ((array)($lane['relay_slots'] ?? []) as $slot) {
                if (is_array($slot) && trim((string)($slot['relay_id'] ?? '')) === $relayId) {
                    return (int)($lane['id'] ?? 0) ?: null;
                }
            }
        }
        return null;
    }

    private function normalizeGatewayKey(mixed $value): string
    {
        return trim((string)$value);
    }

    private function normalizeRole(mixed $value): string
    {
        return strtoupper(trim((string)$value));
    }

    /**
     * @return array<int,string>
     */
    private function normalizeServiceList(mixed $value): array
    {
        if (is_string($value)) {
            $decoded = json_decode($value, true);
            $value = json_last_error() === JSON_ERROR_NONE && is_array($decoded) ? $decoded : explode(',', $value);
        }
        if (!is_array($value)) {
            $value = [$value];
        }

        $services = [];
        foreach ($value as $entry) {
            if (is_array($entry)) {
                continue;
            }
            $service = $this->normalizeRole($entry);
            if ($service !== '') {
                $services[$service] = true;
            }
        }
        return array_keys($services);
    }

    private function bindingId(string $gatewayKey, string $relayId, string $role): string
    {
        return $gatewayKey . ':' . $relayId . ':' . ($role !== '' ? $role : 'relay');
    }
}
