<?php

namespace SHServ\Tools;

/**
 * DeviceScanner — инструмент для сканирования локальной сети
 * и поиска устройств, отвечающих JSON на /about.
 *
 * Требует расширение php-curl.
 */

class DeviceScanner {
  /**
   * Сканирует диапазон IP.
   *
   * @param string $start_ip     Начальный IP (например "192.168.1.1")
   * @param string $end_ip       Конечный IP (например "192.168.1.254")
   * @param int    $port         HTTP порт (по умолчанию 80)
   * @param float  $timeout      Таймаут на IP (сек)
   * @return array               Найденные устройства
   */
  public function scan_range(string $start_ip, string $end_ip, int $port = 80, float $timeout = 1): array {
		$ips = $this -> generate_ip_range($start_ip, $end_ip);
		if (empty($ips)) {
		  return [];
		}

		return $this -> scan_ips($ips, $port, $timeout);
  }

  /**
   * Фильтр по статусу.
   *
   * @param array            $devices
   * @param string|string[]  $status
   * @return array
   */
  public function filter_by_status(array $devices, $status): array {
		$statuses = is_array($status) ? $status : [$status];
		$statuses = array_map('strval', $statuses);

		return array_values(array_filter($devices, function ($device) use ($statuses) {
		  if (!isset($device['status'])) {
			return false;
		  }
		  return in_array((string)$device['status'], $statuses, true);
		}));
	  }

	  /**
	   * Генерация диапазона IP (start..end включительно).
	   */
	  private function generate_ip_range(string $start_ip, string $end_ip): array {
		$start = ip2long($start_ip);
		$end   = ip2long($end_ip);

		if ($start === false || $end === false || $start > $end) {
		  return [];
		}

		$ips = [];
		for ($ip_long = $start; $ip_long <= $end; $ip_long++) {
		  $ips[] = long2ip($ip_long);
		}

		return $ips;
  }

  /**
   * Параллельное сканирование IP с curl_multi.
   */
  private function scan_ips(array $ips, int $port, float $timeout): array {
		$multi = curl_multi_init();
		$handles = [];

		foreach ($ips as $ip) {
			$url = "http://{$ip}:{$port}/about";

			$ch = curl_init();
			curl_setopt_array($ch, [
				CURLOPT_URL            => $url,
				CURLOPT_RETURNTRANSFER => true,
				CURLOPT_CONNECTTIMEOUT => $timeout,
				CURLOPT_TIMEOUT        => $timeout,
				CURLOPT_FOLLOWLOCATION => false,
				CURLOPT_NOBODY         => false,
				CURLOPT_HEADER         => false,
			]);

			curl_multi_add_handle($multi, $ch);

			// тут больше НЕ приводим $ch к строке
			$handles[] = [
				'handle' => $ch,
				'ip'     => $ip,
				'url'    => $url,
			];
		}

		$running = null;
		do {
			$status = curl_multi_exec($multi, $running);
			if ($status > 0) {
				break;
			}

			// на некоторых системах curl_multi_select может возвращать -1
			$select_result = curl_multi_select($multi, 0.1);
			if ($select_result === -1) {
				// маленькая пауза, чтобы не крутить цикл
				usleep(100000);
			}
		} while ($running > 0);

		$devices = [];

		foreach ($handles as $item) {
			$ch  = $item['handle'];
			$ip  = $item['ip'];
			$url = $item['url'];

			$body     = curl_multi_getcontent($ch);
			$curl_err = curl_errno($ch);
			$http     = curl_getinfo($ch, CURLINFO_HTTP_CODE);

			curl_multi_remove_handle($multi, $ch);
			curl_close($ch);

			if ($curl_err !== 0 || $http !== 200 || !$body) {
				continue;
			}

			$json = json_decode($body, true);
			if (!is_array($json)) {
				continue;
			}

			if (!isset($json['ip_address'])) {
				$json['ip_address'] = $ip;
			}
			if (!isset($json['server'])) {
				$json['server'] = "http://{$ip}";
			}

			$json['_meta'] = [
				'ip'        => $ip,
				'url'       => $url,
				'http_code' => $http,
			];

			$devices[] = $json;
		}

		curl_multi_close($multi);

		return $devices;
	}

}
