<?php

namespace classes;

use Exception;

class shelly_relay_inventory
{
    private ?shelly $client;
    /** @var callable|null */
    private $inventory_fetcher = null;
    /** @var callable|null */
    private $device_list_fetcher = null;

    public function __construct(?shelly $client = null)
    {
        $this->client = $client;
    }

    public function setInventoryFetcher(callable $fetcher): self
    {
        $this->inventory_fetcher = $fetcher;
        return $this;
    }

    public function setDeviceListFetcher(callable $fetcher): self
    {
        $this->device_list_fetcher = $fetcher;
        return $this;
    }

    /**
     * @return array<int,array<string,mixed>>
     * @throws Exception
     */
    public function listRelayOptions(bool $include_sensitive_network_details = false): array
    {
        $devices_status = $this->fetchOwnedDevicesStatus();
        $device_catalog = $this->fetchOwnedDeviceCatalog();
        $options_by_id = [];

        foreach ($devices_status as $device) {
            $normalized_device = $this->normalizeToArray($device);
            $device_id = $this->extractFirstString([
                $normalized_device['_dev_info']['id'] ?? null,
                $normalized_device['id'] ?? null,
            ]);
            $catalog_entry = $device_id !== '' ? ($device_catalog[$device_id] ?? null) : null;

            $option = $this->buildRelayOption(
                $normalized_device,
                is_array($catalog_entry) ? $catalog_entry : null,
                $include_sensitive_network_details
            );
            if ($option === null) {
                continue;
            }

            $options_by_id[$option['id']] = $option;
        }

        $options = array_values($options_by_id);
        usort($options, static function (array $left, array $right): int {
            $status_compare = self::statusSortWeight((string)($left['status_color'] ?? ''))
                <=> self::statusSortWeight((string)($right['status_color'] ?? ''));
            if ($status_compare !== 0) {
                return $status_compare;
            }

            $name_compare = strcasecmp((string)($left['name'] ?? ''), (string)($right['name'] ?? ''));
            if ($name_compare !== 0) {
                return $name_compare;
            }

            return strcmp((string)($left['id'] ?? ''), (string)($right['id'] ?? ''));
        });

        return $options;
    }

    /**
     * @return array<string,array<string,mixed>>
     */
    private function fetchOwnedDeviceCatalog(): array
    {
        try {
            $payload = is_callable($this->device_list_fetcher)
                ? ($this->device_list_fetcher)()
                : $this->getClient()->sendGetRequest('/interface/device/list', [
                    'no_shared' => 'true',
                ]);
        } catch (\Throwable) {
            return [];
        }

        $normalized = $this->normalizeToArray($payload);
        if (($normalized['isok'] ?? true) === false) {
            return [];
        }

        $devices = $normalized['data']['devices'] ?? null;
        if (!is_array($devices)) {
            return [];
        }

        $catalog_by_id = [];
        foreach ($devices as $device_key => $device) {
            $normalized_device = $this->normalizeToArray($device);
            $device_id = $this->extractFirstString([
                $normalized_device['id'] ?? null,
                is_string($device_key) ? $device_key : null,
            ]);
            if ($device_id === '') {
                continue;
            }

            $catalog_by_id[$device_id] = $normalized_device;
        }

        return $catalog_by_id;
    }

    /**
     * @return array<string,mixed>
     * @throws Exception
     */
    private function fetchOwnedDevicesStatus(): array
    {
        $payload = is_callable($this->inventory_fetcher)
            ? ($this->inventory_fetcher)()
            : $this->getClient()->sendGetRequest('/device/all_status', [
                'show_info' => 'true',
                'no_shared' => 'true',
            ]);

        $normalized = $this->normalizeToArray($payload);
        $is_ok = $normalized['isok'] ?? null;
        if ($is_ok === false) {
            throw new Exception('Shelly relay inventory request failed');
        }

        $devices_status = $normalized['data']['devices_status'] ?? null;
        if (!is_array($devices_status)) {
            throw new Exception('Shelly relay inventory response was missing devices_status');
        }

        return $devices_status;
    }

    private function getClient(): shelly
    {
        if ($this->client instanceof shelly) {
            return $this->client;
        }

        $this->client = new shelly();
        return $this->client;
    }

    /**
     * @param array<string,mixed> $device
     * @return array<string,mixed>|null
     */
    private function buildRelayOption(
        array $device,
        ?array $catalog_entry = null,
        bool $include_sensitive_network_details = false
    ): ?array
    {
        if ($device === [] || !$this->isRelayCapableDevice($device)) {
            return null;
        }

        $device_id = $this->extractFirstString([
            $device['_dev_info']['id'] ?? null,
            $device['id'] ?? null,
        ]);
        if ($device_id === '') {
            return null;
        }

        $cloud_name = $this->extractFirstString([
            $catalog_entry['name'] ?? null,
        ]);
        $local_device_name = $this->extractFirstString([
            $device['name'] ?? null,
            $device['_dev_info']['name'] ?? null,
            $device['settings']['name'] ?? null,
            $device['settings']['device']['name'] ?? null,
            $device['status']['name'] ?? null,
            $device['status']['sys']['device']['name'] ?? null,
        ]);
        $device_name = $cloud_name !== '' ? $cloud_name : $local_device_name;
        $device_code = $this->extractFirstString([
            $device['_dev_info']['code'] ?? null,
            $device['code'] ?? null,
        ]);
        $device_model = $this->extractDeviceModel($device, $device_code, $catalog_entry);
        if ($device_code === '' && $device_model !== null) {
            $device_code = $device_model;
        }
        $device_type = $this->extractDeviceType($device, $device_code, $catalog_entry);
        $device_generation = $this->extractDeviceGeneration($device, $device_code, $device_type, $catalog_entry);
        $control_type = $this->extractControlType($device);
        $control_name = $this->extractControlName($device);
        $online = $this->extractOnlineState($device);
        if ($online === null) {
            $online = $this->normalizeBoolean($catalog_entry['cloud_online'] ?? null);
        }
        $status_color = $this->extractStatusColor($online);

        $option = [
            'id' => $device_id,
            'name' => $this->buildRelayLabel(
                $device_type,
                $cloud_name,
                $local_device_name,
                $control_name,
                $device_id
            ),
            'device_id' => $device_id,
            'device_name' => $device_name !== '' ? $device_name : null,
            'cloud_name' => $cloud_name !== '' ? $cloud_name : null,
            'device_type' => $device_type,
            'code' => $device_code !== '' ? $device_code : null,
            'device_model' => $device_model,
            'device_generation' => $device_generation,
            'control_type' => $control_type,
            'control_name' => $control_name !== '' ? $control_name : null,
            'status_color' => $status_color,
            'online' => $online,
        ];

        if ($include_sensitive_network_details) {
            $option['local_ip'] = $this->extractLocalIp($device, $catalog_entry);
        }

        return $option;
    }

    /**
     * @param array<string,mixed> $device
     */
    private function isRelayCapableDevice(array $device): bool
    {
        if ($this->payloadContainsRelayState($device)) {
            return true;
        }

        if ($this->payloadContainsRelayState($device['status'] ?? null)) {
            return true;
        }

        return $this->payloadContainsRelayState($device['settings'] ?? null);
    }

    private function payloadContainsRelayState(array|object|null $payload): bool
    {
        $normalized = $this->normalizeToArray($payload);
        if ($normalized === []) {
            return false;
        }

        foreach (['switch:0', 'switch_0', 'switch0'] as $switch_key) {
            if (array_key_exists($switch_key, $normalized)) {
                return true;
            }
        }

        foreach (['relays', 'switches'] as $collection_key) {
            if (isset($normalized[$collection_key]) && is_array($normalized[$collection_key]) && $normalized[$collection_key] !== []) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param array<string,mixed> $device
     */
    private function extractOnlineState(array $device): ?bool
    {
        $online = $device['_dev_info']['online'] ?? $device['online'] ?? null;
        if (is_bool($online)) {
            return $online;
        }

        if (is_numeric($online)) {
            return (int)$online === 1;
        }

        return null;
    }

    private function extractStatusColor(?bool $online): string
    {
        if ($online === true) {
            return 'Green';
        }

        if ($online === false) {
            return 'Red';
        }

        return 'Yellow';
    }

    /**
     * @param array<string,mixed> $device
     */
    private function extractDeviceType(array $device, string $device_code, ?array $catalog_entry = null): ?string
    {
        $candidates = [
            $device['_dev_info']['type'] ?? null,
            $device['type'] ?? null,
            $device['settings']['device']['type'] ?? null,
            $device['_dev_info']['model'] ?? null,
            $device['model'] ?? null,
            $device['_dev_info']['app'] ?? null,
            $device['app'] ?? null,
            $catalog_entry['type'] ?? null,
            $device_code,
        ];

        foreach ($candidates as $candidate) {
            $normalized = trim((string)($candidate ?? ''));
            if ($normalized !== '' && !$this->looksLikeShellyModelCode($normalized)) {
                return $normalized;
            }
        }

        $device_type = $this->extractFirstString($candidates);

        return $device_type !== '' ? $device_type : null;
    }

    /**
     * @param array<string,mixed> $device
     */
    private function extractDeviceModel(array $device, string $device_code, ?array $catalog_entry = null): ?string
    {
        $candidates = [
            $device_code,
            $device['_dev_info']['model'] ?? null,
            $device['model'] ?? null,
            $catalog_entry['type'] ?? null,
            $catalog_entry['model'] ?? null,
            $device['_dev_info']['type'] ?? null,
            $device['type'] ?? null,
        ];

        foreach ($candidates as $candidate) {
            $normalized = trim((string)($candidate ?? ''));
            if ($normalized !== '' && $this->looksLikeShellyModelCode($normalized)) {
                return $normalized;
            }
        }

        return null;
    }

    /**
     * @param array<string,mixed> $device
     */
    private function extractDeviceGeneration(
        array $device,
        string $device_code,
        ?string $device_type,
        ?array $catalog_entry = null
    ): ?int {
        foreach ([
            $device['_dev_info']['gen'] ?? null,
            $device['_dev_info']['generation'] ?? null,
            $device['gen'] ?? null,
            $device['generation'] ?? null,
            $device['settings']['device']['gen'] ?? null,
            $device['settings']['device']['generation'] ?? null,
            $device['status']['sys']['gen'] ?? null,
            $device['status']['sys']['generation'] ?? null,
            $catalog_entry['gen'] ?? null,
            $catalog_entry['generation'] ?? null,
        ] as $candidate) {
            $generation = $this->normalizeDeviceGeneration($candidate);
            if ($generation !== null) {
                return $generation;
            }
        }

        foreach ([
            $device_code,
            $device_type,
            $device['_dev_info']['model'] ?? null,
            $device['_dev_info']['type'] ?? null,
            $device['_dev_info']['app'] ?? null,
            $device['model'] ?? null,
            $device['type'] ?? null,
            $device['app'] ?? null,
            $catalog_entry['type'] ?? null,
            $catalog_entry['model'] ?? null,
        ] as $candidate) {
            $generation = $this->inferDeviceGenerationFromString((string)($candidate ?? ''));
            if ($generation !== null) {
                return $generation;
            }
        }

        return null;
    }

    private function normalizeDeviceGeneration(mixed $value): ?int
    {
        if (is_int($value)) {
            return $value > 0 ? $value : null;
        }

        if (is_numeric($value)) {
            $generation = (int)$value;
            return $generation > 0 ? $generation : null;
        }

        return $this->inferDeviceGenerationFromString((string)($value ?? ''));
    }

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

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

        $upper = strtoupper($normalized);
        if (preg_match('/\bS([3-9])(?:[A-Z0-9]+)?-[A-Z0-9-]+\b/', $upper, $matches) === 1) {
            return (int)$matches[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;
    }

    private function looksLikeShellyModelCode(string $value): bool
    {
        $normalized = strtoupper(trim($value));
        if ($normalized === '') {
            return false;
        }

        return preg_match('/^(?:S[1-9]|SP|SN|SH)[A-Z0-9]+-[A-Z0-9-]+$/', $normalized) === 1;
    }

    /**
     * @param array<string,mixed> $device
     */
    private function extractControlType(array $device): string
    {
        $status = $this->normalizeToArray($device['status'] ?? null);
        $settings = $this->normalizeToArray($device['settings'] ?? null);

        foreach (['switch:0', 'switch_0', 'switch0'] as $switch_key) {
            if (array_key_exists($switch_key, $device) || array_key_exists($switch_key, $status)) {
                return 'Switch';
            }
        }

        if ((isset($device['switches']) && is_array($device['switches']) && $device['switches'] !== [])
            || (isset($settings['switches']) && is_array($settings['switches']) && $settings['switches'] !== [])) {
            return 'Switch';
        }

        if ((isset($device['relays']) && is_array($device['relays']) && $device['relays'] !== [])
            || (isset($settings['relays']) && is_array($settings['relays']) && $settings['relays'] !== [])) {
            return 'Relay';
        }

        return 'Device';
    }

    /**
     * @param array<string,mixed> $device
     */
    private function extractControlName(array $device): string
    {
        $status = $this->normalizeToArray($device['status'] ?? null);
        $settings = $this->normalizeToArray($device['settings'] ?? null);

        return $this->extractFirstString([
            $status['switch:0']['name'] ?? null,
            $status['switch_0']['name'] ?? null,
            $status['switch0']['name'] ?? null,
            $device['switches'][0]['name'] ?? null,
            $settings['switches'][0]['name'] ?? null,
            $device['relays'][0]['name'] ?? null,
            $settings['relays'][0]['name'] ?? null,
        ]);
    }

    /**
     * @param array<string,mixed> $device
     * @param array<string,mixed>|null $catalog_entry
     */
    private function extractLocalIp(array $device, ?array $catalog_entry = null): ?string
    {
        $candidates = [
            $device['local_ip'] ?? null,
            $device['localIp'] ?? null,
            $device['ip'] ?? null,
            $device['_dev_info']['local_ip'] ?? null,
            $device['_dev_info']['localIp'] ?? null,
            $device['_dev_info']['ip'] ?? null,
            $device['wifi_sta']['ip'] ?? null,
            $device['wifi']['ip'] ?? null,
            $device['eth']['ip'] ?? null,
            $device['status']['wifi_sta']['ip'] ?? null,
            $device['status']['wifi']['ip'] ?? null,
            $device['status']['eth']['ip'] ?? null,
            $device['status']['sta_ip'] ?? null,
            $device['settings']['wifi_sta']['ip'] ?? null,
            $device['settings']['wifi']['ip'] ?? null,
            $device['settings']['eth']['ip'] ?? null,
            $catalog_entry['local_ip'] ?? null,
            $catalog_entry['localIp'] ?? null,
            $catalog_entry['ip'] ?? null,
        ];

        foreach ($candidates as $candidate) {
            $normalized = trim((string)($candidate ?? ''));
            if ($normalized !== '' && filter_var($normalized, FILTER_VALIDATE_IP) !== false) {
                return $normalized;
            }
        }

        return null;
    }

    private function buildRelayLabel(
        ?string $device_type,
        string $cloud_name,
        string $local_device_name,
        string $control_name,
        string $device_id
    ): string {
        if ($cloud_name !== '') {
            $display_name = $cloud_name;
        } elseif ($local_device_name !== '' && $control_name !== '' && strcasecmp($local_device_name, $control_name) !== 0) {
            $display_name = $local_device_name . ' / ' . $control_name;
        } else {
            $display_name = $control_name !== '' ? $control_name : ($local_device_name !== '' ? $local_device_name : $device_id);
        }

        if ($device_type !== null && $device_type !== '') {
            return $display_name . ' (' . $device_type . ')';
        }

        return $display_name;
    }

    /**
     * @param array<int,mixed> $values
     */
    private function extractFirstString(array $values): string
    {
        foreach ($values as $value) {
            $normalized = trim((string)($value ?? ''));
            if ($normalized !== '') {
                return $normalized;
            }
        }

        return '';
    }

    private function normalizeBoolean(mixed $value): ?bool
    {
        if (is_bool($value)) {
            return $value;
        }

        if (is_numeric($value)) {
            return (int)$value === 1;
        }

        return null;
    }

    private static function statusSortWeight(string $status_color): int
    {
        return match (strtolower($status_color)) {
            'green' => 0,
            'yellow' => 1,
            'red' => 2,
            default => 3,
        };
    }

    /**
     * @return array<string,mixed>
     */
    private function normalizeToArray(array|object|null $payload): array
    {
        if (is_array($payload)) {
            return $payload;
        }

        if (is_object($payload)) {
            $encoded = json_encode($payload, JSON_UNESCAPED_UNICODE);
            if (!is_string($encoded)) {
                return [];
            }

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

        return [];
    }
}
