<?php

namespace modules\selfserve\classes;

require_once WD . '/classes/selfserve.php';
require_once WD . '/modules/edgegateway/classes/edge_gateway_manager.php';
require_once WD . '/modules/selfserve/classes/selfserve_wash_flow.php';
require_once WD . '/objects/department_lanes_o.php';
require_once WD . '/objects/edge_gateway_relay_bindings_o.php';

use classes\edge_gateway_manager;
use classes\selfserve;
use objects\department_lanes_o;
use objects\edge_gateway_relay_bindings_o;

class selfserve_machine_signal
{
    /**
     * Normalize Shelly Cloud webhook payloads and local edge gateway poll payloads.
     *
     * Shelly Gen2 emits switch.on/input.toggle_on webhooks and exposes Switch.GetStatus.output
     * / Input.GetStatus.state for state reads. Legacy or custom payloads are accepted as long
     * as they carry equivalent boolean fields.
     *
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    public function normalizeShellyPayload(array $payload): array
    {
        if (isset($payload['events']) && is_array($payload['events'])) {
            foreach ((array)$payload['events'] as $eventPayload) {
                if (is_array($eventPayload)) {
                    $payload = array_replace($payload, (array)$eventPayload);
                    break;
                }
            }
        }

        $event = $this->firstString($payload, ['event', 'event_type', 'eventType', 'name', 'type']);
        $component = $this->normalizeComponent($this->firstString($payload, ['component', 'component_id', 'componentId']));
        $relayId = $this->firstString($payload, ['relay_id', 'logical_relay_id', 'logicalRelayId', 'relayId']);
        $deviceId = $this->firstString($payload, ['device_id', 'deviceId', 'device']);
        $channel = $this->firstInt($payload, ['channel', 'id', 'input_id', 'switch_id']);
        $on = $this->extractOnState($payload);
        $eventName = strtolower(trim((string)$event));

        if ($component === null && str_starts_with($eventName, 'input.')) {
            $component = 'input';
        }
        if ($component === null && str_starts_with($eventName, 'switch.')) {
            $component = 'switch';
        }

        $positiveEvents = [
            'on',
            'toggle_on',
            'btn_down',
            'single_push',
            'machine.on',
            'switch.on',
            'switch.toggle_on',
            'input.on',
            'input.toggle_on',
            'input.btn_down',
            'input.single_push',
        ];
        $negativeEvents = [
            'off',
            'toggle_off',
            'btn_up',
            'machine.off',
            'switch.off',
            'switch.toggle_off',
            'input.off',
            'input.toggle_off',
            'input.btn_up',
        ];

        $eventIsOn = in_array($eventName, $positiveEvents, true);
        $eventIsOff = in_array($eventName, $negativeEvents, true);
        $recognized = $eventIsOn || $eventIsOff || $on !== null;
        $onState = $eventIsOn || ($on === true && !$eventIsOff);

        return [
            'recognized' => $recognized,
            'on' => $onState,
            'event' => $event !== null ? (string)$event : null,
            'component' => $component,
            'relay_id' => $relayId,
            'device_id' => $deviceId,
            'channel' => $channel,
            'source' => (string)($payload['source'] ?? 'shelly'),
            'raw_status' => $this->extractStatusPayload($payload),
        ];
    }

    /**
     * @param array<string,mixed> $payload
     * @param array<string,mixed> $context
     * @return array<string,mixed>
     */
    public function recordCloudShellySignal(int $departmentId, ?int $laneId, array $payload, array $context = []): array
    {
        $signal = $this->normalizeShellyPayload($payload + ['source' => 'shelly_cloud']);
        if (!$signal['recognized']) {
            return [
                'recorded' => false,
                'ignored' => true,
                'reason' => 'Payload does not contain a recognized Shelly ON/OFF signal.',
                'signal' => $signal,
            ];
        }
        if (!$signal['on']) {
            return [
                'recorded' => false,
                'ignored' => true,
                'reason' => 'Shelly signal was recognized but it was not ON.',
                'signal' => $signal,
            ];
        }

        $resolvedLaneId = $this->resolveLaneId($departmentId, $laneId, $signal['relay_id'] ?? null);
        $summary = (new selfserve_wash_flow())->recordMachineStartWebhook(
            $resolvedLaneId,
            $this->extractRegistration($payload),
            $payload + [
                'source' => 'shelly_cloud',
                'shelly_signal' => $signal,
                'context' => $context,
            ]
        );

        return [
            'recorded' => true,
            'ignored' => false,
            'lane_id' => $resolvedLaneId,
            'signal' => $signal,
            'selfserve' => $summary,
        ];
    }

    /**
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    public function recordEdgeGatewaySignal(int $gatewayId, string $agentToken, array $payload): array
    {
        $gateway = (new edge_gateway_manager())->authenticateGateway($gatewayId, $agentToken);
        $departmentId = (int)$gateway->department_id->value();

        return $this->recordEdgeGatewaySignalForDepartment($gatewayId, $departmentId, $payload);
    }

    /**
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    public function recordBrokerEdgeGatewaySignal(int $gatewayId, array $payload): array
    {
        $gateway = (new edge_gateway_manager())->getGateway($gatewayId);
        return $this->recordEdgeGatewaySignalForDepartment($gatewayId, (int)$gateway['department_id'], $payload);
    }

    /**
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    private function recordEdgeGatewaySignalForDepartment(int $gatewayId, int $departmentId, array $payload): array
    {
        return $this->recordCloudShellySignal(
            $departmentId,
            isset($payload['lane_id']) ? (int)$payload['lane_id'] : null,
            $payload + [
                'source' => 'edge_gateway',
                'gateway_id' => $gatewayId,
            ],
            [
                'source' => 'edge_gateway',
                'gateway_id' => $gatewayId,
                'agent_instance_id' => $payload['agent_instance_id'] ?? null,
            ]
        );
    }

    /**
     * @return array<string,mixed>
     */
    public function listEdgeGatewayMachineSignalMonitors(int $gatewayId, string $agentToken): array
    {
        $manager = new edge_gateway_manager();
        $gateway = $manager->authenticateGateway($gatewayId, $agentToken);
        $departmentId = (int)$gateway->department_id->value();

        $bindings = [];
        foreach ($this->bindingRows($gatewayId, $departmentId) as $binding) {
            $relayId = trim((string)($binding['relay_id'] ?? ''));
            if ($relayId !== '') {
                $bindings[$relayId] = $binding;
            }
        }

        $monitors = [];
        foreach ((new department_lanes_o())->getDepartmentLanes($departmentId) as $lane) {
            $relayId = trim((string)$lane->relay_machine_id->value());
            if ($relayId === '' || !isset($bindings[$relayId])) {
                continue;
            }

            $binding = $bindings[$relayId];
            $metadata = isset($binding['metadata']) && is_array($binding['metadata']) ? (array)$binding['metadata'] : [];
            $component = $this->normalizeMonitorComponent((string)($metadata['machine_signal_component'] ?? $metadata['signal_component'] ?? 'input'));
            $channel = (int)($metadata['machine_signal_channel'] ?? $metadata['input_channel'] ?? $binding['channel'] ?? 0);

            $monitors[] = [
                'gateway_id' => $gatewayId,
                'department_id' => $departmentId,
                'lane_id' => (int)$lane->id,
                'lane_label' => (string)$lane->name->value(),
                'relay_id' => $relayId,
                'device_id' => (string)($metadata['machine_signal_device_id'] ?? $binding['device_id'] ?? ''),
                'local_ip' => $metadata['machine_signal_local_ip'] ?? $binding['local_ip'] ?? null,
                'channel' => $channel,
                'component' => $component,
                'expected_event' => $component === 'switch' ? 'switch.on' : 'input.toggle_on',
            ];
        }

        return [
            'gateway_id' => $gatewayId,
            'department_id' => $departmentId,
            'monitors' => $monitors,
        ];
    }

    public function resolveLaneId(int $departmentId, ?int $laneId, ?string $relayId = null): int
    {
        if ($laneId !== null && $laneId > 0) {
            $lane = (new department_lanes_o())->select($laneId);
            if (!$lane->exists()) {
                throw new \RuntimeException('Department lane not found.');
            }
            if ((int)$lane->department->value() !== $departmentId) {
                throw new \RuntimeException('The lane does not belong to the Shelly signal department.');
            }

            return (int)$lane->id;
        }

        $relayId = trim((string)$relayId);
        if ($relayId !== '') {
            $matches = (new department_lanes_o())->getFieldsWhere(
                [
                    'department' => $departmentId,
                    'relay_machine_id' => $relayId,
                    'deleted_at' => null,
                ],
                ['id']
            );
            if ($matches !== []) {
                return (int)$matches[0]['id'];
            }
        }

        $lanes = (new department_lanes_o())->getDepartmentLanes($departmentId);
        if (count($lanes) === 1) {
            return (int)$lanes[0]->id;
        }

        throw new \RuntimeException('lane_id or relay_id is required when the department has multiple self-serve lanes.');
    }

    /**
     * @param array<string,mixed> $payload
     */
    private function extractRegistration(array $payload): ?string
    {
        $reg = $this->firstString($payload, ['reg', 'registration', 'license_plate', 'licensePlate', 'plate']);
        return $reg === null || trim($reg) === '' ? null : selfserve::standardize_registration($reg);
    }

    /**
     * @param array<string,mixed> $payload
     * @param array<int,string> $keys
     */
    private function firstString(array $payload, array $keys): ?string
    {
        foreach ($keys as $key) {
            if (!array_key_exists($key, $payload)) {
                continue;
            }
            $value = $payload[$key];
            if (is_scalar($value) && trim((string)$value) !== '') {
                return trim((string)$value);
            }
        }

        foreach (['params', 'data', 'status'] as $container) {
            if (!isset($payload[$container]) || !is_array($payload[$container])) {
                continue;
            }
            $match = $this->firstString((array)$payload[$container], $keys);
            if ($match !== null) {
                return $match;
            }
        }

        return null;
    }

    /**
     * @param array<string,mixed> $payload
     * @param array<int,string> $keys
     */
    private function firstInt(array $payload, array $keys): ?int
    {
        foreach ($keys as $key) {
            if (array_key_exists($key, $payload) && is_numeric($payload[$key])) {
                return (int)$payload[$key];
            }
        }

        foreach (['params', 'data', 'status'] as $container) {
            if (!isset($payload[$container]) || !is_array($payload[$container])) {
                continue;
            }
            $match = $this->firstInt((array)$payload[$container], $keys);
            if ($match !== null) {
                return $match;
            }
        }

        return null;
    }

    /**
     * @param array<string,mixed> $payload
     */
    private function extractOnState(array $payload): ?bool
    {
        foreach (['on', 'output', 'state', 'ison'] as $key) {
            if (array_key_exists($key, $payload)) {
                return $this->boolValue($payload[$key]);
            }
        }

        foreach (['input', 'switch', 'params', 'data', 'status'] as $key) {
            if (!isset($payload[$key]) || !is_array($payload[$key])) {
                continue;
            }
            $value = $this->extractOnState((array)$payload[$key]);
            if ($value !== null) {
                return $value;
            }
        }

        foreach (['input:0', 'switch:0'] as $componentKey) {
            if (!isset($payload[$componentKey]) || !is_array($payload[$componentKey])) {
                continue;
            }
            $value = $this->extractOnState((array)$payload[$componentKey]);
            if ($value !== null) {
                return $value;
            }
        }

        return null;
    }

    private function boolValue(mixed $value): ?bool
    {
        if (is_bool($value)) {
            return $value;
        }
        if (is_int($value) || is_float($value)) {
            return (int)$value === 1;
        }
        if (is_string($value)) {
            $normalized = strtolower(trim($value));
            if (in_array($normalized, ['1', 'true', 'on', 'yes'], true)) {
                return true;
            }
            if (in_array($normalized, ['0', 'false', 'off', 'no'], true)) {
                return false;
            }
        }

        return null;
    }

    private function normalizeComponent(?string $component): ?string
    {
        $component = strtolower(trim((string)$component));
        if ($component === '') {
            return null;
        }
        if (str_starts_with($component, 'input')) {
            return 'input';
        }
        if (str_starts_with($component, 'switch') || str_starts_with($component, 'relay')) {
            return 'switch';
        }

        return null;
    }

    private function normalizeMonitorComponent(string $component): string
    {
        return $this->normalizeComponent($component) === 'switch' ? 'switch' : 'input';
    }

    /**
     * @param array<string,mixed> $payload
     * @return array<string,mixed>|null
     */
    private function extractStatusPayload(array $payload): ?array
    {
        if (isset($payload['status']) && is_array($payload['status'])) {
            return (array)$payload['status'];
        }
        if (isset($payload['raw']) && is_array($payload['raw'])) {
            return (array)$payload['raw'];
        }

        return null;
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function bindingRows(int $gatewayId, int $departmentId): array
    {
        $rows = (new edge_gateway_relay_bindings_o())->getFieldsWhere(
            [
                'gateway_id' => $gatewayId,
                'department_id' => $departmentId,
                'deleted_at' => null,
            ],
            ['id']
        );

        return array_map(
            static fn(array $row): array => (new edge_gateway_relay_bindings_o())->select((int)$row['id'])->asArray(),
            $rows
        );
    }
}
