<?php

namespace classes;

use Exception;
use objects\department_gates_o;
use objects\department_lanes_o;
use objects\department_relays_o;
use objects\departments_o;
use objects\plate_scanners_o;
use objects\plate_scans_o;

class edge_gateway_department_workspace_service
{
    public function __construct(private readonly ?edge_gateway_manager $manager = null)
    {
        edge_gateway_schema_bootstrap::ensureTables();
    }

    /**
     * @return array<int,array<string,mixed>>
     * @throws Exception
     */
    public function listDepartmentSummaries(): array
    {
        $departments = (new departments_o())->list(true);
        usort($departments, static function (array $left, array $right): int {
            $leftPriority = (int)($left['order_priority'] ?? PHP_INT_MAX);
            $rightPriority = (int)($right['order_priority'] ?? PHP_INT_MAX);
            if ($leftPriority !== $rightPriority) {
                return $leftPriority <=> $rightPriority;
            }

            return (int)($left['id'] ?? 0) <=> (int)($right['id'] ?? 0);
        });

        $summaries = [];
        foreach ($departments as $departmentRow) {
            $departmentId = (int)($departmentRow['id'] ?? 0);
            if ($departmentId <= 0) {
                continue;
            }

            try {
                $workspace = $this->buildDepartmentWorkspace($departmentId, false, $departmentRow);
            } catch (Exception $exception) {
                if ($exception->getMessage() === 'Department not found') {
                    $this->manager()->deleteOrphanedGatewaysForDepartment($departmentId);
                    $this->clearDepartmentCache();
                    continue;
                }

                throw $exception;
            }

            $summaries[] = $workspace['summary'];
        }

        return $summaries;
    }

    /**
     * @return array<string,mixed>
     * @throws Exception
     */
    public function getDepartmentWorkspace(int $departmentId): array
    {
        return $this->buildDepartmentWorkspace($departmentId, true);
    }

    /**
     * @param array<string,mixed>|null $departmentRow
     * @return array<string,mixed>
     * @throws Exception
     */
    private function buildDepartmentWorkspace(int $departmentId, bool $includeGateways, ?array $departmentRow = null): array
    {
        $department = (new departments_o())->select($departmentId);
        if (!$department->exists()) {
            throw new Exception('Department not found');
        }

        $departmentPayload = [
            'id' => $departmentId,
            'name' => (string)$department->name->value(),
            'description' => (string)$department->description->value(),
            'order_priority' => (int)$department->order_priority->value(),
        ];

        $transportMode = $this->manager()->getDepartmentTransportMode($departmentId);
        $gateways = $this->manager()->listGateways($departmentId, true);
        $bindingsByRelayId = $this->indexBindingsByRelayId($gateways);
        $relayCatalog = $this->indexRelayCatalog($departmentId);
        $consumersByRelayId = [];

        $lanes = $this->buildLanePayloads($departmentId, $bindingsByRelayId, $relayCatalog, $consumersByRelayId);
        $gates = $this->buildGatePayloads($departmentId, $bindingsByRelayId, $relayCatalog, $consumersByRelayId);
        $relays = $this->buildRelayPayloads($relayCatalog, $bindingsByRelayId, $consumersByRelayId);
        $gateways = $this->applyBindingConsumerContexts($gateways, $consumersByRelayId);
        $selfServe = $this->buildSelfServePayload($department, $lanes);
        $scanners = $this->buildScannerPayloads($departmentId, $lanes);
        $issues = $this->buildIssues($transportMode, $gateways, $lanes, $gates, $scanners, $selfServe);
        $actions = $this->buildActions($departmentId, $gateways, $lanes, $gates, $scanners, $selfServe);
        $summary = $this->buildSummary(
            $departmentPayload,
            $departmentRow,
            $transportMode,
            $gateways,
            $lanes,
            $gates,
            $scanners,
            $selfServe,
            $issues
        );

        return [
            'department' => $departmentPayload,
            'summary' => $summary,
            'gateways' => $includeGateways ? $gateways : [],
            'lanes' => $lanes,
            'self_serve' => $selfServe,
            'gates' => $gates,
            'relays' => $relays,
            'scanners' => $scanners,
            'issues' => $issues,
            'actions' => $actions,
        ];
    }

    /**
     * @param array<int,array<string,mixed>> $gateways
     * @return array<string,array<int,array<string,mixed>>>
     */
    private function indexBindingsByRelayId(array $gateways): array
    {
        $bindingsByRelayId = [];

        foreach ($gateways as $gateway) {
            $bindings = isset($gateway['bindings']) && is_array($gateway['bindings'])
                ? (array)$gateway['bindings']
                : [];

            foreach ($bindings as $binding) {
                if (!is_array($binding)) {
                    continue;
                }

                $relayId = trim((string)($binding['relay_id'] ?? ''));
                if ($relayId === '') {
                    continue;
                }

                $binding['gateway_label'] = (string)($gateway['label'] ?? ('Gateway ' . ($gateway['id'] ?? '')));
                $binding['gateway_status'] = (string)($gateway['status'] ?? edge_gateway_manager::STATUS_OFFLINE);
                $binding['is_primary_gateway'] = (bool)($gateway['is_primary'] ?? false);
                $bindingsByRelayId[$relayId][] = $binding;
            }
        }

        foreach ($bindingsByRelayId as $relayId => $bindings) {
            usort($bindings, static function (array $left, array $right): int {
                return ((int)($right['is_primary_gateway'] ?? 0) <=> (int)($left['is_primary_gateway'] ?? 0))
                    ?: ((int)($left['gateway_id'] ?? 0) <=> (int)($right['gateway_id'] ?? 0));
            });
            $bindingsByRelayId[$relayId] = $bindings;
        }

        return $bindingsByRelayId;
    }

    /**
     * @return array<string,array<string,mixed>>
     * @throws Exception
     */
    private function indexRelayCatalog(int $departmentId): array
    {
        $catalog = [];
        foreach ((new department_relays_o())->getDepartmentRelays($departmentId) as $relay) {
            if (!$relay->exists()) {
                continue;
            }

            $relayId = trim((string)$relay->relay_id->value());
            if ($relayId === '') {
                continue;
            }

            $catalog[$relayId] = $relay->asArray();
        }

        return $catalog;
    }

    /**
     * @param array<string,array<int,array<string,mixed>>> $bindingsByRelayId
     * @param array<string,array<string,mixed>> $relayCatalog
     * @param array<string,array<int,array<string,mixed>>> $consumersByRelayId
     * @return array<int,array<string,mixed>>
     * @throws Exception
     */
    private function buildLanePayloads(
        int $departmentId,
        array $bindingsByRelayId,
        array $relayCatalog,
        array &$consumersByRelayId
    ): array {
        $lanes = [];
        $slotMap = [
            'relay_in_id' => 'ENTRY',
            'relay_out_id' => 'EXIT',
            'relay_machine_id' => 'MACHINE',
            'relay_machine_program_picker_id' => 'PROGRAM_PICKER',
            'relay_machine_cleaner_id' => 'CLEANER',
        ];

        foreach ((new department_lanes_o())->getDepartmentLanes($departmentId) as $lane) {
            if (!$lane->exists()) {
                continue;
            }

            $relaySlots = [];
            $boundRelayCount = 0;
            foreach ($slotMap as $property => $slotName) {
                $relayId = trim((string)$lane->{$property}->value());
                if ($relayId === '') {
                    continue;
                }

                $consumersByRelayId[$relayId][] = [
                    'type' => 'lane',
                    'id' => (int)$lane->id,
                    'slot' => $slotName,
                    'label' => (string)$lane->name->value(),
                ];

                $coverage = $this->buildRelayCoverage($relayId, $bindingsByRelayId);
                if ((bool)($coverage['covered'] ?? false)) {
                    $boundRelayCount += 1;
                }

                $relaySlots[] = [
                    'slot' => $slotName,
                    'relay_id' => $relayId,
                    'catalog' => $relayCatalog[$relayId] ?? null,
                    'coverage' => $coverage,
                ];
            }

            $requiredRelayCount = count($relaySlots);
            $laneStatus = 'UNKNOWN';
            try {
                $laneStatus = (string)$lane->getLaneStatus()->name;
            } catch (\Throwable) {
            }

            $lanes[] = [
                'id' => (int)$lane->id,
                'department' => (int)$lane->department->value(),
                'name' => (string)$lane->name->value(),
                'relay_in_id' => $lane->relay_in_id->value() === null ? null : (string)$lane->relay_in_id->value(),
                'relay_out_id' => $lane->relay_out_id->value() === null ? null : (string)$lane->relay_out_id->value(),
                'relay_machine_id' => $lane->relay_machine_id->value() === null ? null : (string)$lane->relay_machine_id->value(),
                'relay_machine_program_picker_id' => $lane->relay_machine_program_picker_id->value() === null ? null : (string)$lane->relay_machine_program_picker_id->value(),
                'relay_machine_cleaner_id' => $lane->relay_machine_cleaner_id->value() === null ? null : (string)$lane->relay_machine_cleaner_id->value(),
                'dynamic_image_id' => $lane->dynamic_image_id->value() === null ? null : (int)$lane->dynamic_image_id->value(),
                'machine_type_id' => $lane->machine_type_id->value() === null ? null : (int)$lane->machine_type_id->value(),
                'selfserve_enabled' => $lane->isSelfServeEnabled(),
                'status' => $laneStatus,
                'self_serve_products' => $lane->getSelfServeLaneProducts(),
                'relay_slots' => $relaySlots,
                'binding_coverage' => [
                    'required' => $requiredRelayCount,
                    'bound' => $boundRelayCount,
                    'missing' => max(0, $requiredRelayCount - $boundRelayCount),
                    'state' => $requiredRelayCount === 0
                        ? 'NOT_REQUIRED'
                        : ($boundRelayCount === $requiredRelayCount ? 'READY' : 'MISSING'),
                ],
                'links' => [
                    'legacy' => '/superuser/department/lanes/' . (int)$lane->id,
                    'self_serve_studio' => '/admin/' . $departmentId . '/modules/self-serve/studio',
                ],
            ];
        }

        return $lanes;
    }

    /**
     * @param array<string,array<int,array<string,mixed>>> $bindingsByRelayId
     * @param array<string,array<string,mixed>> $relayCatalog
     * @param array<string,array<int,array<string,mixed>>> $consumersByRelayId
     * @return array<int,array<string,mixed>>
     * @throws Exception
     */
    private function buildGatePayloads(
        int $departmentId,
        array $bindingsByRelayId,
        array $relayCatalog,
        array &$consumersByRelayId
    ): array {
        $gates = [];

        foreach ((new department_gates_o())->getDepartmentGates($departmentId) as $gate) {
            if (!$gate->exists()) {
                continue;
            }

            $config = (array)$gate->config->value();
            $gateType = strtoupper(trim((string)($config['type'] ?? 'UNKNOWN')));
            $relayId = trim((string)($config['relay_id'] ?? ''));

            if ($gateType === 'RELAY' && $relayId !== '') {
                $consumersByRelayId[$relayId][] = [
                    'type' => 'gate',
                    'id' => (int)$gate->id,
                    'slot' => ((bool)$gate->is_entrance->value() ? 'ENTRANCE' : ((bool)$gate->is_exit->value() ? 'EXIT' : 'GENERAL')),
                    'label' => (string)$gate->name->value(),
                ];
            }

            $coverage = $gateType === 'RELAY' && $relayId !== ''
                ? $this->buildRelayCoverage($relayId, $bindingsByRelayId)
                : null;

            $gates[] = [
                'id' => (int)$gate->id,
                'department' => (int)$gate->department->value(),
                'name' => (string)$gate->name->value(),
                'is_entrance' => (bool)$gate->is_entrance->value(),
                'is_exit' => (bool)$gate->is_exit->value(),
                'config' => $config,
                'transport_type' => $gateType,
                'config_complete' => $this->isGateConfigComplete($config),
                'relay' => $relayId !== '' ? ($relayCatalog[$relayId] ?? ['relay_id' => $relayId]) : null,
                'coverage' => $coverage,
            ];
        }

        return $gates;
    }

    /**
     * @param array<string,array<string,mixed>> $relayCatalog
     * @param array<string,array<int,array<string,mixed>>> $bindingsByRelayId
     * @param array<string,array<int,array<string,mixed>>> $consumersByRelayId
     * @return array<int,array<string,mixed>>
     */
    private function buildRelayPayloads(
        array $relayCatalog,
        array $bindingsByRelayId,
        array $consumersByRelayId
    ): array {
        $relays = [];

        foreach ($relayCatalog as $relayId => $relay) {
            $config = isset($relay['config']) && is_array($relay['config'])
                ? (array)$relay['config']
                : [];

            $relays[] = [
                'id' => isset($relay['id']) ? (int)$relay['id'] : 0,
                'department' => isset($relay['department']) ? (int)$relay['department'] : 0,
                'relay_id' => (string)($relay['relay_id'] ?? $relayId),
                'name' => (string)($relay['name'] ?? $relayId),
                'type' => (string)($relay['type'] ?? ''),
                'config' => $config,
                'coverage' => $this->buildRelayCoverage((string)$relayId, $bindingsByRelayId),
                'consumer_contexts' => $consumersByRelayId[(string)$relayId] ?? [],
            ];
        }

        return $relays;
    }

    /**
     * @param array<int,array<string,mixed>> $lanes
     * @return array<string,mixed>
     * @throws Exception
     */
    private function buildSelfServePayload(departments_o $department, array $lanes): array
    {
        $enabled = false;
        try {
            $enabled = $department->getSelfServeEnabled();
        } catch (\Throwable) {
        }

        $enabledLanes = array_values(array_filter($lanes, static function (array $lane): bool {
            return ($lane['selfserve_enabled'] ?? true) !== false;
        }));
        $readyLanes = array_values(array_filter($enabledLanes, static function (array $lane): bool {
            return (string)($lane['binding_coverage']['state'] ?? 'UNKNOWN') === 'READY';
        }));

        $taskRows = (new \objects\department_selfserve_tasks_o())->getFieldsWhere([
            'department' => (int)$department->id,
            'deleted_at' => null,
        ], ['id', 'lane', 'product']);

        $productIds = [];
        foreach ($taskRows as $taskRow) {
            if (isset($taskRow['product'])) {
                $productIds[(int)$taskRow['product']] = true;
            }
        }

        return [
            'enabled' => $enabled,
            'lane_count' => count($lanes),
            'enabled_lanes' => count($enabledLanes),
            'ready_lanes' => count($readyLanes),
            'configured_task_count' => count($taskRows),
            'configured_product_count' => count($productIds),
            'readiness_state' => !$enabled
                ? 'DISABLED'
                : (count($enabledLanes) === 0 ? 'UNCONFIGURED' : (count($readyLanes) === count($enabledLanes) ? 'READY' : 'PARTIAL')),
            'links' => [
                'studio' => '/admin/' . (int)$department->id . '/modules/self-serve/studio',
                'legacy' => '/superuser/selfserve',
            ],
        ];
    }

    /**
     * @param array<int,array<string,mixed>> $lanes
     * @return array<int,array<string,mixed>>
     * @throws Exception
     */
    private function buildScannerPayloads(int $departmentId, array $lanes): array
    {
        $laneIndex = [];
        foreach ($lanes as $lane) {
            $laneIndex[(int)$lane['id']] = $lane;
        }

        $recentScansByScannerId = $this->groupRecentScansByScannerId($departmentId);
        $scanners = [];
        foreach ((new plate_scanners_o())->getDepartmentScanners($departmentId) as $scanner) {
            if (!$scanner->exists()) {
                continue;
            }

            $scannerPayload = $scanner->asArray();
            $laneId = isset($scannerPayload['lane_id']) ? (int)($scannerPayload['lane_id'] ?? 0) : 0;
            $assignedLane = $laneId > 0 ? ($laneIndex[$laneId] ?? null) : null;
            $recentScans = $recentScansByScannerId[(int)$scanner->id] ?? [];
            $recentScanAt = $recentScans !== [] ? ($recentScans[0]['created_at'] ?? null) : null;

            $assignmentState = $laneId <= 0
                ? 'UNASSIGNED'
                : ($assignedLane === null
                    ? 'INVALID'
                    : (((int)($assignedLane['binding_coverage']['missing'] ?? 0) === 0) ? 'READY' : 'PARTIAL'));

            $scanners[] = [
                ...$scannerPayload,
                'assigned_lane' => $assignedLane,
                'assignment_state' => $assignmentState,
                'recent_scan_at' => $recentScanAt,
                'recent_scans' => $recentScans,
                'recent_scan_count' => count($recentScans),
            ];
        }

        return $scanners;
    }

    /**
     * @return array<string,array<int,array<string,mixed>>>
     */
    private function groupRecentScansByScannerId(int $departmentId): array
    {
        $rows = (new plate_scans_o())->getFieldsWhere([
            'department_id' => $departmentId,
        ], ['id', 'plate_scanner_id', 'plate', 'bay_id', 'created_at']);

        usort($rows, static function (array $left, array $right): int {
            $rightTimestamp = strtotime((string)($right['created_at'] ?? '')) ?: 0;
            $leftTimestamp = strtotime((string)($left['created_at'] ?? '')) ?: 0;
            return $rightTimestamp <=> $leftTimestamp ?: ((int)($right['id'] ?? 0) <=> (int)($left['id'] ?? 0));
        });

        $grouped = [];
        foreach ($rows as $row) {
            $scannerId = (int)($row['plate_scanner_id'] ?? 0);
            if ($scannerId <= 0) {
                continue;
            }

            if (!isset($grouped[$scannerId])) {
                $grouped[$scannerId] = [];
            }

            if (count($grouped[$scannerId]) >= 5) {
                continue;
            }

            $grouped[$scannerId][] = [
                'id' => (int)($row['id'] ?? 0),
                'plate' => (string)($row['plate'] ?? ''),
                'bay_id' => isset($row['bay_id']) ? (string)$row['bay_id'] : null,
                'created_at' => isset($row['created_at']) ? (string)$row['created_at'] : null,
            ];
        }

        return $grouped;
    }

    /**
     * @param array<int,array<string,mixed>> $gateways
     * @param array<int,array<string,mixed>> $lanes
     * @param array<int,array<string,mixed>> $gates
     * @param array<int,array<string,mixed>> $scanners
     * @return array<int,array<string,mixed>>
     */
    private function buildIssues(
        string $transportMode,
        array $gateways,
        array $lanes,
        array $gates,
        array $scanners,
        array $selfServe
    ): array {
        $issues = [];

        if ($gateways === []) {
            $issues[] = [
                'severity' => 'danger',
                'code' => 'NO_GATEWAY',
                'message' => 'No edge gateway has been claimed for this department.',
            ];
        }

        $onlineGateways = array_values(array_filter($gateways, static function (array $gateway): bool {
            return strtoupper((string)($gateway['status'] ?? '')) === edge_gateway_manager::STATUS_ONLINE;
        }));

        if ($transportMode === edge_gateway_manager::TRANSPORT_MODE_GATEWAY && $onlineGateways === []) {
            $issues[] = [
                'severity' => 'danger',
                'code' => 'NO_ONLINE_GATEWAY',
                'message' => 'Gateway transport mode is enabled, but no department gateway is currently online.',
            ];
        }

        foreach ($lanes as $lane) {
            if ((int)($lane['binding_coverage']['missing'] ?? 0) > 0) {
                $issues[] = [
                    'severity' => 'warning',
                    'code' => 'LANE_BINDING_GAP',
                    'message' => 'Lane ' . (string)$lane['name'] . ' is missing relay bindings.',
                    'target_type' => 'lane',
                    'target_id' => (int)$lane['id'],
                ];
            }
        }

        foreach ($gates as $gate) {
            if (!($gate['config_complete'] ?? false)) {
                $issues[] = [
                    'severity' => 'warning',
                    'code' => 'GATE_CONFIG_INCOMPLETE',
                    'message' => 'Gate ' . (string)$gate['name'] . ' has incomplete transport configuration.',
                    'target_type' => 'gate',
                    'target_id' => (int)$gate['id'],
                ];
                continue;
            }

            if (($gate['transport_type'] ?? '') === 'RELAY' && !($gate['coverage']['covered'] ?? false)) {
                $issues[] = [
                    'severity' => 'warning',
                    'code' => 'GATE_BINDING_MISSING',
                    'message' => 'Gate ' . (string)$gate['name'] . ' is assigned to an unbound relay.',
                    'target_type' => 'gate',
                    'target_id' => (int)$gate['id'],
                ];
            }
        }

        foreach ($scanners as $scanner) {
            if (($scanner['assignment_state'] ?? 'UNASSIGNED') === 'UNASSIGNED') {
                $issues[] = [
                    'severity' => 'warning',
                    'code' => 'SCANNER_UNASSIGNED',
                    'message' => 'Scanner ' . (string)$scanner['name'] . ' is not assigned to a default lane.',
                    'target_type' => 'scanner',
                    'target_id' => (int)$scanner['id'],
                ];
            } elseif (($scanner['assignment_state'] ?? '') === 'PARTIAL') {
                $issues[] = [
                    'severity' => 'info',
                    'code' => 'SCANNER_LANE_PARTIAL',
                    'message' => 'Scanner ' . (string)$scanner['name'] . ' is assigned to a lane with missing relay coverage.',
                    'target_type' => 'scanner',
                    'target_id' => (int)$scanner['id'],
                ];
            }
        }

        if (($selfServe['enabled'] ?? false) && (int)($selfServe['ready_lanes'] ?? 0) < (int)($selfServe['enabled_lanes'] ?? 0)) {
            $issues[] = [
                'severity' => 'warning',
                'code' => 'SELFSERVE_PARTIAL_READY',
                'message' => 'Self-serve is enabled, but one or more lanes are missing required relay coverage.',
            ];
        }

        return $issues;
    }

    /**
     * @param array<int,array<string,mixed>> $gateways
     * @param array<int,array<string,mixed>> $lanes
     * @param array<int,array<string,mixed>> $gates
     * @param array<int,array<string,mixed>> $scanners
     * @return array<int,array<string,mixed>>
     */
    private function buildActions(
        int $departmentId,
        array $gateways,
        array $lanes,
        array $gates,
        array $scanners,
        array $selfServe
    ): array {
        $actions = [
            [
                'code' => 'OPEN_GATEWAY_TAB',
                'label' => 'Open gateway controls',
                'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=gateways',
            ],
        ];

        if ($gateways === []) {
            $actions[] = [
                'code' => 'INSTALL_GATEWAY',
                'label' => 'Install first edge gateway',
                'path' => '/superuser/configuration/edgegateway',
            ];
        }

        foreach ($lanes as $lane) {
            if ((int)($lane['binding_coverage']['missing'] ?? 0) > 0) {
                $actions[] = [
                    'code' => 'REVIEW_LANE_BINDINGS',
                    'label' => 'Resolve lane bindings',
                    'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=lanes',
                ];
                break;
            }
        }

        foreach ($gates as $gate) {
            if (($gate['transport_type'] ?? '') === 'RELAY' && !($gate['coverage']['covered'] ?? false)) {
                $actions[] = [
                    'code' => 'REVIEW_GATE_BINDINGS',
                    'label' => 'Resolve gate relay bindings',
                    'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=gates',
                ];
                break;
            }
        }

        foreach ($scanners as $scanner) {
            if (($scanner['assignment_state'] ?? 'UNASSIGNED') === 'UNASSIGNED') {
                $actions[] = [
                    'code' => 'ASSIGN_SCANNERS',
                    'label' => 'Assign scanners to lanes',
                    'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=scanners',
                ];
                break;
            }
        }

        if (($selfServe['enabled'] ?? false) && (int)($selfServe['lane_count'] ?? 0) > 0) {
            $actions[] = [
                'code' => 'OPEN_SELFSERVE_STUDIO',
                'label' => 'Open self-serve studio',
                'path' => '/admin/' . $departmentId . '/modules/self-serve/studio',
            ];
        }

        return $actions;
    }

    /**
     * @param array<string,mixed> $departmentPayload
     * @param array<string,mixed>|null $departmentRow
     * @param array<int,array<string,mixed>> $gateways
     * @param array<int,array<string,mixed>> $lanes
     * @param array<int,array<string,mixed>> $gates
     * @param array<int,array<string,mixed>> $scanners
     * @param array<int,array<string,mixed>> $issues
     * @return array<string,mixed>
     */
    private function buildSummary(
        array $departmentPayload,
        ?array $departmentRow,
        string $transportMode,
        array $gateways,
        array $lanes,
        array $gates,
        array $scanners,
        array $selfServe,
        array $issues
    ): array {
        $onlineGatewayCount = count(array_filter($gateways, static function (array $gateway): bool {
            return strtoupper((string)($gateway['status'] ?? '')) === edge_gateway_manager::STATUS_ONLINE;
        }));
        $primaryGateway = null;
        foreach ($gateways as $gateway) {
            if (!empty($gateway['is_primary'])) {
                $primaryGateway = [
                    'id' => (int)$gateway['id'],
                    'label' => (string)($gateway['label'] ?? ('Gateway ' . $gateway['id'])),
                    'status' => (string)($gateway['status'] ?? edge_gateway_manager::STATUS_OFFLINE),
                ];
                break;
            }
        }

        if ($primaryGateway === null && $gateways !== []) {
            $gateway = $gateways[0];
            $primaryGateway = [
                'id' => (int)$gateway['id'],
                'label' => (string)($gateway['label'] ?? ('Gateway ' . $gateway['id'])),
                'status' => (string)($gateway['status'] ?? edge_gateway_manager::STATUS_OFFLINE),
            ];
        }

        $requiredRelayIds = [];
        $coveredRelayIds = [];
        foreach ($lanes as $lane) {
            foreach ((array)($lane['relay_slots'] ?? []) as $slot) {
                $relayId = trim((string)($slot['relay_id'] ?? ''));
                if ($relayId === '') {
                    continue;
                }

                $requiredRelayIds[$relayId] = true;
                if (!empty($slot['coverage']['covered'])) {
                    $coveredRelayIds[$relayId] = true;
                }
            }
        }
        foreach ($gates as $gate) {
            if (($gate['transport_type'] ?? '') !== 'RELAY') {
                continue;
            }

            $relayId = trim((string)($gate['relay']['relay_id'] ?? $gate['config']['relay_id'] ?? ''));
            if ($relayId === '') {
                continue;
            }

            $requiredRelayIds[$relayId] = true;
            if (!empty($gate['coverage']['covered'])) {
                $coveredRelayIds[$relayId] = true;
            }
        }

        $assignedScannerCount = count(array_filter($scanners, static function (array $scanner): bool {
            return (int)($scanner['lane_id'] ?? 0) > 0;
        }));

        $recentScanAt = null;
        foreach ($scanners as $scanner) {
            $candidate = isset($scanner['recent_scan_at']) ? (string)$scanner['recent_scan_at'] : null;
            if ($candidate === null || trim($candidate) === '') {
                continue;
            }
            if ($recentScanAt === null || strtotime($candidate) > strtotime($recentScanAt)) {
                $recentScanAt = $candidate;
            }
        }

        $relayGateCount = count(array_filter($gates, static function (array $gate): bool {
            return ($gate['transport_type'] ?? '') === 'RELAY';
        }));
        $phoneGateCount = count(array_filter($gates, static function (array $gate): bool {
            return ($gate['transport_type'] ?? '') === 'PHONE_CALL';
        }));

        return [
            'department_id' => (int)$departmentPayload['id'],
            'department_name' => (string)$departmentPayload['name'],
            'order_priority' => (int)($departmentRow['order_priority'] ?? $departmentPayload['order_priority'] ?? PHP_INT_MAX),
            'transport_mode' => $transportMode,
            'gateway_count' => count($gateways),
            'online_gateway_count' => $onlineGatewayCount,
            'primary_gateway' => $primaryGateway,
            'lane_count' => count($lanes),
            'self_serve_enabled' => (bool)($selfServe['enabled'] ?? false),
            'self_serve_ready_lanes' => (int)($selfServe['ready_lanes'] ?? 0),
            'required_relay_count' => count($requiredRelayIds),
            'bound_relay_count' => count($coveredRelayIds),
            'missing_binding_count' => max(0, count($requiredRelayIds) - count($coveredRelayIds)),
            'gate_count' => count($gates),
            'gate_transport_mix' => [
                'relay' => $relayGateCount,
                'phone_call' => $phoneGateCount,
            ],
            'scanner_count' => count($scanners),
            'assigned_scanner_count' => $assignedScannerCount,
            'recent_scan_at' => $recentScanAt,
            'issue_count' => count($issues),
            'health' => $this->deriveHealthState($transportMode, $gateways, $issues),
        ];
    }

    /**
     * @param array<int,array<string,mixed>> $gateways
     * @param array<int,array<string,mixed>> $issues
     */
    private function deriveHealthState(string $transportMode, array $gateways, array $issues): string
    {
        foreach ($issues as $issue) {
            if (($issue['severity'] ?? '') === 'danger') {
                return 'AT_RISK';
            }
        }

        if ($transportMode === edge_gateway_manager::TRANSPORT_MODE_GATEWAY) {
            foreach ($gateways as $gateway) {
                if (strtoupper((string)($gateway['status'] ?? '')) === edge_gateway_manager::STATUS_ONLINE) {
                    return $issues === [] ? 'READY' : 'PARTIAL';
                }
            }
            return 'AT_RISK';
        }

        return $issues === [] ? 'READY' : 'PARTIAL';
    }

    /**
     * @param array<string,array<int,array<string,mixed>>> $bindingsByRelayId
     * @return array<string,mixed>
     */
    private function buildRelayCoverage(string $relayId, array $bindingsByRelayId): array
    {
        $bindings = $bindingsByRelayId[$relayId] ?? [];
        $primaryBinding = $bindings[0] ?? null;

        return [
            'relay_id' => $relayId,
            'covered' => $bindings !== [],
            'status' => $bindings !== [] ? 'BOUND' : 'MISSING',
            'binding_count' => count($bindings),
            'primary_binding' => $primaryBinding,
            'bindings' => $bindings,
        ];
    }

    /**
     * @param array<int,array<string,mixed>> $gateways
     * @param array<string,array<int,array<string,mixed>>> $consumersByRelayId
     * @return array<int,array<string,mixed>>
     */
    private function applyBindingConsumerContexts(array $gateways, array $consumersByRelayId): array
    {
        foreach ($gateways as $gatewayIndex => $gateway) {
            $bindings = isset($gateway['bindings']) && is_array($gateway['bindings'])
                ? (array)$gateway['bindings']
                : [];

            foreach ($bindings as $bindingIndex => $binding) {
                if (!is_array($binding)) {
                    continue;
                }

                $relayId = trim((string)($binding['relay_id'] ?? ''));
                if ($relayId === '') {
                    continue;
                }

                $metadata = isset($binding['metadata']) && is_array($binding['metadata'])
                    ? (array)$binding['metadata']
                    : [];
                $consumerContexts = $consumersByRelayId[$relayId] ?? [];
                $metadata['consumer_contexts'] = $consumerContexts;
                $metadata['consumers'] = $consumerContexts;
                $bindings[$bindingIndex]['metadata'] = $metadata;
                $bindings[$bindingIndex]['consumer_contexts'] = $consumerContexts;
            }

            $gateways[$gatewayIndex]['bindings'] = $bindings;
        }

        return $gateways;
    }

    /**
     * @param array<string,mixed> $config
     */
    private function isGateConfigComplete(array $config): bool
    {
        $type = strtoupper(trim((string)($config['type'] ?? '')));
        if ($type === 'PHONE_CALL') {
            return trim((string)($config['phone_number'] ?? '')) !== ''
                && isset($config['call_duration_threshold']);
        }

        if ($type === 'RELAY') {
            return trim((string)($config['relay_id'] ?? '')) !== '';
        }

        return false;
    }

    private function manager(): edge_gateway_manager
    {
        return $this->manager ?? new edge_gateway_manager();
    }

    private function clearDepartmentCache(): void
    {
        try {
            if (!defined('redis')) {
                return;
            }

            $redis = constant('redis');
            if (is_object($redis) && method_exists($redis, 'clear_departments')) {
                $redis->clear_departments();
            }
        } catch (\Throwable) {
        }
    }
}
