<?php

declare(strict_types=1);

const WORKER_MAX_RELAY_TOGGLE_AFTER_SECONDS = 5;

function worker_json_response(int $status, array $payload): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($payload, JSON_UNESCAPED_SLASHES);
}

function worker_read_json_body(): array
{
    $raw = file_get_contents('php://input');
    if (!is_string($raw) || trim($raw) === '') {
        return [];
    }

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

function worker_config_path(): string
{
    $configuredPath = trim((string)getenv('TRUCKWASH_WORKER_CONFIG_PATH'));
    return $configuredPath !== '' ? $configuredPath : '/config/config.json';
}

function worker_expected_token(): string
{
    $environmentToken = trim((string)getenv('TRUCKWASH_WORKER_TOKEN'));
    if ($environmentToken !== '') {
        return $environmentToken;
    }

    $configPath = worker_config_path();
    if (!is_file($configPath)) {
        return '';
    }

    $decoded = json_decode((string)file_get_contents($configPath), true);
    return is_array($decoded) ? trim((string)($decoded['agentToken'] ?? '')) : '';
}

function worker_request_token(): string
{
    $headerToken = trim((string)($_SERVER['HTTP_X_TRUCKWASH_WORKER_TOKEN'] ?? ''));
    if ($headerToken !== '') {
        return $headerToken;
    }

    $authorization = trim((string)($_SERVER['HTTP_AUTHORIZATION'] ?? ''));
    if (str_starts_with(strtolower($authorization), 'bearer ')) {
        return trim(substr($authorization, 7));
    }

    return '';
}

function worker_require_authorization(): bool
{
    $expectedToken = worker_expected_token();
    if ($expectedToken === '') {
        worker_json_response(503, [
            'message' => 'LAN worker authorization is not configured',
            'error_code' => 'EDGE_GATEWAY_WORKER_AUTH_UNCONFIGURED',
        ]);
        return false;
    }

    if (!hash_equals($expectedToken, worker_request_token())) {
        worker_json_response(401, [
            'message' => 'Unauthorized',
            'error_code' => 'EDGE_GATEWAY_WORKER_UNAUTHORIZED',
        ]);
        return false;
    }

    return true;
}

function worker_http_get_json(string $url, int $timeoutSeconds = 8): array
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => $timeoutSeconds,
        CURLOPT_CONNECTTIMEOUT => min(5, $timeoutSeconds),
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTPHEADER => ['Accept: application/json'],
    ]);

    $raw = curl_exec($ch);
    $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($raw === false || $status >= 400) {
        throw new RuntimeException($error !== '' ? $error : 'HTTP ' . $status);
    }

    $decoded = json_decode((string)$raw, true);
    if (!is_array($decoded)) {
        throw new RuntimeException('Invalid JSON response from device');
    }

    return $decoded;
}

function worker_fetch_shelly_state(string $localIp, int $channel, bool $includeInput = false): array
{
    if ($localIp === '') {
        throw new RuntimeException('Missing Shelly IP address');
    }

    $input = $includeInput ? worker_fetch_shelly_input_state($localIp, $channel) : null;

    try {
        $payload = worker_http_get_json(sprintf('http://%s/rpc/Switch.GetStatus?id=%d', $localIp, $channel));
        return [
            'online' => true,
            'on' => (bool)($payload['output'] ?? false),
            'output' => (bool)($payload['output'] ?? false),
            'input_state' => $input['state'] ?? null,
            'input' => $input,
            'raw' => $payload,
        ];
    } catch (Throwable) {
        $payload = worker_http_get_json(sprintf('http://%s/relay/%d', $localIp, $channel));
        return [
            'online' => true,
            'on' => (bool)($payload['ison'] ?? false),
            'output' => (bool)($payload['ison'] ?? false),
            'input_state' => $input['state'] ?? null,
            'input' => $input,
            'raw' => $payload,
        ];
    }
}

function worker_fetch_shelly_input_state(string $localIp, int $channel): ?array
{
    if ($localIp === '') {
        throw new RuntimeException('Missing Shelly IP address');
    }

    try {
        $payload = worker_http_get_json(sprintf('http://%s/rpc/Input.GetStatus?id=%d', $localIp, $channel), 2);
        return [
            'online' => true,
            'state' => (bool)($payload['state'] ?? false),
            'raw' => $payload,
        ];
    } catch (Throwable) {
        return null;
    }
}

function worker_normalize_shelly_device_generation(mixed $value): ?int
{
    if (is_int($value) || is_float($value) || (is_string($value) && is_numeric(trim($value)))) {
        $generation = (int)$value;
        return $generation > 0 ? $generation : null;
    }

    return null;
}

function worker_infer_shelly_device_generation_from_string(mixed $value): ?int
{
    $normalized = trim((string)$value);
    if ($normalized === '') {
        return null;
    }

    if (preg_match('/\bgen(?:eration)?\s*([1-9]\d*)\b/i', $normalized, $explicit) === 1) {
        return (int)$explicit[1];
    }

    $upper = strtoupper($normalized);
    if (preg_match('/\bS([3-9])(?:[A-Z0-9]+)?-[A-Z0-9-]+\b/', $upper, $series) === 1) {
        return (int)$series[1];
    }

    if (preg_match('/\b(?:SHELLY\s+)?(?:PLUS|PRO)\b/i', $normalized) === 1 || preg_match('/\bSP[A-Z0-9]+-[A-Z0-9-]+\b/', $upper) === 1) {
        return 2;
    }

    if (preg_match('/\bSH[A-Z0-9]+-?[A-Z0-9-]*\b/', $upper) === 1) {
        return 1;
    }

    return null;
}

function worker_resolve_shelly_command_generation(array $command): ?int
{
    foreach ([
        $command['gen'] ?? null,
        $command['generation'] ?? null,
        $command['deviceGeneration'] ?? null,
        $command['device_generation'] ?? null,
        is_array($command['capabilities'] ?? null) ? ($command['capabilities']['generation'] ?? null) : null,
    ] as $candidate) {
        $generation = worker_normalize_shelly_device_generation($candidate);
        if ($generation !== null) {
            return $generation;
        }
    }

    foreach ([
        $command['model'] ?? null,
        $command['deviceModel'] ?? null,
        $command['device_model'] ?? null,
        $command['type'] ?? null,
        $command['deviceType'] ?? null,
        $command['device_type'] ?? null,
        $command['app'] ?? null,
        $command['name'] ?? null,
        $command['deviceName'] ?? null,
        $command['device_name'] ?? null,
        $command['deviceId'] ?? null,
        $command['device_id'] ?? null,
        $command['relayId'] ?? null,
        $command['relay_id'] ?? null,
        $command['mac'] ?? null,
    ] as $candidate) {
        $generation = worker_infer_shelly_device_generation_from_string($candidate);
        if ($generation !== null) {
            return $generation;
        }
    }

    return null;
}

function worker_resolve_relay_toggle_after_seconds(array $command): ?int
{
    $configured = $command['toggleAfter'] ?? $command['toggle_after'] ?? $command['timer'] ?? null;
    if (!is_int($configured) && !is_float($configured) && !(is_string($configured) && is_numeric(trim($configured)))) {
        return null;
    }

    $seconds = (int)floor((float)$configured);
    if ($seconds <= 0) {
        return null;
    }

    return min($seconds, WORKER_MAX_RELAY_TOGGLE_AFTER_SECONDS);
}

function worker_switch_shelly_state(string $localIp, int $channel, bool $on, array $command = []): array
{
    if ($localIp === '') {
        throw new RuntimeException('Missing Shelly IP address');
    }

    $toggleAfter = worker_resolve_relay_toggle_after_seconds($command);
    $timerQuery = $toggleAfter === null ? '' : '&toggle_after=' . rawurlencode((string)$toggleAfter);
    $legacyTimerQuery = $toggleAfter === null ? '' : '&timer=' . rawurlencode((string)$toggleAfter);
    $runRpcSwitch = static fn(): array => worker_http_get_json(
        sprintf('http://%s/rpc/Switch.Set?id=%d&on=%s%s', $localIp, $channel, $on ? 'true' : 'false', $timerQuery)
    );
    $runLegacySwitch = static fn(): array => worker_http_get_json(
        sprintf('http://%s/relay/%d?turn=%s%s', $localIp, $channel, $on ? 'on' : 'off', $legacyTimerQuery)
    );

    if ($toggleAfter !== null) {
        $attempts = worker_resolve_shelly_command_generation($command) === 1
            ? [$runLegacySwitch, $runRpcSwitch]
            : [$runRpcSwitch, $runLegacySwitch];
        $lastError = null;
        foreach ($attempts as $attempt) {
            try {
                $attempt();
                return worker_fetch_shelly_state($localIp, $channel);
            } catch (Throwable $throwable) {
                $lastError = $throwable;
            }
        }

        throw $lastError ?? new RuntimeException('Unable to switch relay');
    }

    try {
        $runRpcSwitch();
    } catch (Throwable) {
        $runLegacySwitch();
    }

    return worker_fetch_shelly_state($localIp, $channel);
}

function worker_normalize_relay_commands(array $body): array
{
    $commands = $body['commands'] ?? [];
    return is_array($commands) ? array_values($commands) : [];
}

function worker_relay_command_result(array $command, callable $handler): array
{
    $target = trim((string)($command['target'] ?? $command['relay'] ?? ''));
    $relayId = trim((string)($command['relayId'] ?? $command['relay_id'] ?? ''));

    try {
        $localIp = trim((string)($command['local_ip'] ?? $command['localIp'] ?? ''));
        $channel = (int)($command['channel'] ?? 0);
        return [
            'target' => $target,
            'relayId' => $relayId,
            'relay_id' => $relayId,
            'ok' => true,
            'payload' => $handler($localIp, $channel, $command),
        ];
    } catch (Throwable $throwable) {
        return [
            'target' => $target,
            'relayId' => $relayId,
            'relay_id' => $relayId,
            'ok' => false,
            'error' => $throwable->getMessage(),
        ];
    }
}

$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
$path = (string)(parse_url((string)($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH) ?? '/');
$body = worker_read_json_body();
$hostname = gethostname() ?: 'truckwash-edge';

try {
    if ($method === 'GET' && $path === '/health') {
        worker_json_response(200, [
            'status' => 'healthy',
            'service' => 'lan-worker',
            'timestamp' => date(DateTimeInterface::ATOM),
        ]);
        return;
    }

    if ($method === 'POST' && $path === '/discover') {
        if (!worker_require_authorization()) {
            return;
        }

        worker_json_response(200, [
            'inventory' => [[
                'device_id' => 'gateway-runtime-' . substr(sha1($hostname), 0, 10),
                'local_ip' => gethostbyname($hostname),
                'model' => 'TruckWash Edge Gateway',
                'channel_count' => 1,
                'online' => true,
                'capabilities' => [
                    'local_discovery' => true,
                    'relay_commands' => true,
                    'gateway_management_v2' => true,
                ],
                'metadata' => [
                    'hostname' => $hostname,
                    'runtime' => 'compose-lan-worker',
                    'php_version' => PHP_VERSION,
                ],
            ]],
        ]);
        return;
    }

    if ($method === 'POST' && $path === '/relay/status') {
        if (!worker_require_authorization()) {
            return;
        }

        $localIp = trim((string)($body['local_ip'] ?? $body['localIp'] ?? ''));
        $channel = (int)($body['channel'] ?? 0);
        $includeInput = (bool)($body['include_input'] ?? $body['includeInput'] ?? false);
        worker_json_response(200, worker_fetch_shelly_state($localIp, $channel, $includeInput));
        return;
    }

    if ($method === 'POST' && $path === '/relay/input-status') {
        if (!worker_require_authorization()) {
            return;
        }

        $localIp = trim((string)($body['local_ip'] ?? $body['localIp'] ?? ''));
        $channel = (int)($body['channel'] ?? 0);
        $input = worker_fetch_shelly_input_state($localIp, $channel);
        if ($input === null) {
            throw new RuntimeException('Shelly input status is not available');
        }
        worker_json_response(200, $input);
        return;
    }

    if ($method === 'POST' && $path === '/relay/switch') {
        if (!worker_require_authorization()) {
            return;
        }

        $localIp = trim((string)($body['local_ip'] ?? $body['localIp'] ?? ''));
        $channel = (int)($body['channel'] ?? 0);
        $on = (bool)($body['on'] ?? false);
        worker_json_response(200, worker_switch_shelly_state($localIp, $channel, $on, $body));
        return;
    }

    if ($method === 'POST' && $path === '/relay/batch-status') {
        if (!worker_require_authorization()) {
            return;
        }

        $results = array_map(
            fn(array $command): array => worker_relay_command_result(
                $command,
                fn(string $localIp, int $channel): array => worker_fetch_shelly_state($localIp, $channel)
            ),
            worker_normalize_relay_commands($body)
        );
        worker_json_response(200, [
            'batch_id' => $body['batch_id'] ?? $body['batchId'] ?? null,
            'results' => $results,
        ]);
        return;
    }

    if ($method === 'POST' && $path === '/relay/batch-switch') {
        if (!worker_require_authorization()) {
            return;
        }

        $results = array_map(
            fn(array $command): array => worker_relay_command_result(
                $command,
                fn(string $localIp, int $channel, array $entry): array => worker_switch_shelly_state(
                    $localIp,
                    $channel,
                    (bool)($entry['on'] ?? false),
                    $entry
                )
            ),
            worker_normalize_relay_commands($body)
        );
        worker_json_response(200, [
            'batch_id' => $body['batch_id'] ?? $body['batchId'] ?? null,
            'results' => $results,
        ]);
        return;
    }

    worker_json_response(404, ['message' => 'Not found']);
} catch (Throwable $throwable) {
    worker_json_response(422, [
        'message' => $throwable->getMessage(),
        'error_code' => 'EDGE_GATEWAY_WORKER_FAILED',
    ]);
}
