<?php

namespace classes;

use Throwable;

class edge_gateway_view_cache
{
    public const PREFIX = 'edge_gateway:view:v1:';

    /**
     * Optional runtime adapter for tests.
     */
    private static ?object $adapter = null;

    public static function setAdapterForTests(?object $adapter): void
    {
        self::$adapter = $adapter;
    }

    public static function getTtl(): int
    {
        $raw = getenv('EDGE_GATEWAY_VIEW_CACHE_TTL');
        if ($raw === false || trim((string)$raw) === '') {
            return 15;
        }

        return max(0, (int)$raw);
    }

    public static function listKey(?int $departmentId = null, bool $includeDetail = true): string
    {
        return self::PREFIX
            . 'list:department:'
            . ($departmentId === null ? 'all' : (string)$departmentId)
            . ':detail:'
            . ($includeDetail ? '1' : '0');
    }

    public static function detailKey(int $gatewayId): string
    {
        return self::PREFIX . 'detail:' . $gatewayId;
    }

    /**
     * @return array{gateways: array<int,array<string,mixed>>, fleet_usage: array<string,mixed>}|null
     */
    public static function getListPayload(?int $departmentId = null, bool $includeDetail = true): ?array
    {
        return self::decodeListPayload(self::redisGet(self::listKey($departmentId, $includeDetail)));
    }

    /**
     * @param array{gateways: array<int,array<string,mixed>>, fleet_usage: array<string,mixed>} $payload
     */
    public static function storeListPayload(?int $departmentId, bool $includeDetail, array $payload, ?int $ttl = null): void
    {
        self::storePayload(self::listKey($departmentId, $includeDetail), $payload, $ttl);
    }

    /**
     * @return array<string,mixed>|null
     */
    public static function getDetailPayload(int $gatewayId): ?array
    {
        $decoded = self::decodePayload(self::redisGet(self::detailKey($gatewayId)));
        if (!is_array($decoded) || !isset($decoded['gateway']) || !is_array($decoded['gateway'])) {
            return null;
        }

        return $decoded['gateway'];
    }

    /**
     * @param array<string,mixed> $gateway
     */
    public static function storeDetailPayload(int $gatewayId, array $gateway, ?int $ttl = null): void
    {
        self::storePayload(self::detailKey($gatewayId), ['gateway' => $gateway], $ttl);
    }

    public static function clearAll(): void
    {
        self::clearPattern(self::PREFIX . '*');
    }

    public static function clearGateway(int $gatewayId, ?int $departmentId = null): void
    {
        self::clearPattern(self::detailKey($gatewayId));
        self::clearPattern(self::listKey(null, true));
        self::clearPattern(self::listKey(null, false));

        if ($departmentId !== null) {
            self::clearPattern(self::listKey($departmentId, true));
            self::clearPattern(self::listKey($departmentId, false));
        }
    }

    /**
     * @param array<string,mixed> $gateway
     */
    public static function syncGateway(array $gateway): void
    {
        $gatewayId = (int)($gateway['id'] ?? 0);
        $departmentId = isset($gateway['department_id']) ? (int)$gateway['department_id'] : null;

        if ($gatewayId <= 0) {
            return;
        }

        $detailGateway = edge_gateway_manager::prepareGatewayForListCache($gateway, true);

        self::storeDetailPayload($gatewayId, $detailGateway);
        self::syncListPayload(null, true, $detailGateway);
        self::syncListPayload(null, false, $detailGateway);

        if ($departmentId !== null && $departmentId > 0) {
            self::syncListPayload($departmentId, true, $detailGateway);
            self::syncListPayload($departmentId, false, $detailGateway);
        }
    }

    public static function removeGateway(int $gatewayId, ?int $departmentId = null): void
    {
        self::clearPattern(self::detailKey($gatewayId));
        self::removeGatewayFromListPayload(null, true, $gatewayId);
        self::removeGatewayFromListPayload(null, false, $gatewayId);

        if ($departmentId !== null) {
            self::removeGatewayFromListPayload($departmentId, true, $gatewayId);
            self::removeGatewayFromListPayload($departmentId, false, $gatewayId);
        }
    }

    /**
     * @param array<string,mixed> $gateway
     */
    private static function syncListPayload(?int $departmentId, bool $includeDetail, array $gateway): void
    {
        $payload = self::getListPayload($departmentId, $includeDetail);
        if ($payload === null) {
            return;
        }

        $rows = isset($payload['gateways']) && is_array($payload['gateways']) ? array_values($payload['gateways']) : [];
        $preparedGateway = edge_gateway_manager::prepareGatewayForListCache($gateway, $includeDetail);
        $gatewayId = (int)($preparedGateway['id'] ?? 0);
        $matchesDepartment = $departmentId === null
            || (int)($preparedGateway['department_id'] ?? 0) === (int)$departmentId;

        if ($gatewayId <= 0 || !$matchesDepartment) {
            return;
        }

        $updated = false;
        foreach ($rows as $index => $row) {
            if ((int)($row['id'] ?? 0) !== $gatewayId) {
                continue;
            }

            $rows[$index] = self::mergeGatewayPayload($row, $preparedGateway, $includeDetail);
            $updated = true;
            break;
        }

        if (!$updated) {
            $rows[] = $preparedGateway;
        }

        usort($rows, static fn(array $left, array $right): int => ((int)($left['department_id'] ?? 0) <=> (int)($right['department_id'] ?? 0))
            ?: ((int)($left['id'] ?? 0) <=> (int)($right['id'] ?? 0)));

        self::storeListPayload($departmentId, $includeDetail, [
            'gateways' => $rows,
            'fleet_usage' => edge_gateway_manager::summarizeFleetUsageFromGatewayRows($rows),
        ]);
    }

    private static function removeGatewayFromListPayload(?int $departmentId, bool $includeDetail, int $gatewayId): void
    {
        $payload = self::getListPayload($departmentId, $includeDetail);
        if ($payload === null) {
            return;
        }

        $rows = array_values(array_filter(
            isset($payload['gateways']) && is_array($payload['gateways']) ? $payload['gateways'] : [],
            static fn(mixed $row): bool => (int)(is_array($row) ? ($row['id'] ?? 0) : 0) !== $gatewayId
        ));

        self::storeListPayload($departmentId, $includeDetail, [
            'gateways' => $rows,
            'fleet_usage' => edge_gateway_manager::summarizeFleetUsageFromGatewayRows($rows),
        ]);
    }

    /**
     * @param array<string,mixed> $currentGateway
     * @param array<string,mixed> $nextGateway
     * @return array<string,mixed>
     */
    private static function mergeGatewayPayload(array $currentGateway, array $nextGateway, bool $includeDetail): array
    {
        $merged = array_merge($currentGateway, $nextGateway);
        $merged['metadata'] = array_merge(
            isset($currentGateway['metadata']) && is_array($currentGateway['metadata']) ? $currentGateway['metadata'] : [],
            isset($nextGateway['metadata']) && is_array($nextGateway['metadata']) ? $nextGateway['metadata'] : []
        );

        foreach (['inventory', 'bindings', 'recent_commands', 'audit_logs', 'operations', 'relay_health', 'diagnostics'] as $listKey) {
            if (array_key_exists($listKey, $nextGateway)) {
                $merged[$listKey] = $nextGateway[$listKey];
                continue;
            }

            if (array_key_exists($listKey, $currentGateway)) {
                $merged[$listKey] = $currentGateway[$listKey];
            }
        }

        return edge_gateway_manager::prepareGatewayForListCache($merged, $includeDetail);
    }

    /**
     * @return array{gateways: array<int,array<string,mixed>>, fleet_usage: array<string,mixed>}|null
     */
    private static function decodeListPayload(?string $raw): ?array
    {
        $decoded = self::decodePayload($raw);
        if (!is_array($decoded)) {
            return null;
        }

        if (!isset($decoded['gateways']) || !is_array($decoded['gateways'])) {
            return null;
        }

        if (!isset($decoded['fleet_usage']) || !is_array($decoded['fleet_usage'])) {
            return null;
        }

        return [
            'gateways' => array_values($decoded['gateways']),
            'fleet_usage' => $decoded['fleet_usage'],
        ];
    }

    /**
     * @return array<string,mixed>|null
     */
    private static function decodePayload(?string $raw): ?array
    {
        if ($raw === null || trim($raw) === '') {
            return null;
        }

        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : null;
    }

    /**
     * @param array<string,mixed> $payload
     */
    private static function storePayload(string $key, array $payload, ?int $ttl = null): void
    {
        $cacheTtl = $ttl ?? self::getTtl();
        if ($cacheTtl <= 0) {
            return;
        }

        $encoded = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($encoded)) {
            return;
        }

        self::redisSetEx($key, $encoded, $cacheTtl);
    }

    private static function clearPattern(string $pattern): void
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return;
            }
            $client->clear_keys($pattern);
        } catch (Throwable) {
            // Cache invalidation must never break request flow.
        }
    }

    private static function redisSetEx(string $key, string $value, int $ttl): void
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return;
            }
            $client->setEx($key, $value, $ttl);
        } catch (Throwable) {
            // Best-effort cache write.
        }
    }

    private static function redisGet(string $key): ?string
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return null;
            }

            $value = $client->get($key);
            return is_string($value) ? $value : null;
        } catch (Throwable) {
            return null;
        }
    }

    private static function redisClient(): ?object
    {
        if (self::$adapter !== null) {
            return self::$adapter;
        }

        try {
            if (defined('redis')) {
                $instance = constant('redis');
                if (is_object($instance)) {
                    return $instance;
                }
            }

            return (new redis())->connect();
        } catch (Throwable) {
            return null;
        }
    }
}
