<?php
namespace Models;

class Clientsscales extends \DB\SQL\Mapper{
	public $db, $f3, $sets, $scale, $synchronization, $logs, $api_data;

	public function __construct($db, $f3, $api_data) {
		parent::__construct($db, 'clients_scales'); // подключаемся к таблице с настройками
		$this->synchronization=new \DB\SQL\Mapper($db, 'device_synchronization');
		$this->f3=$f3;
		$this->db=$db;
		$this->api_data=$api_data;
		$this->logs=$this->f3->get('logs_model');
		/*$this->sets=$this->get_sets($method_id);
		if($this->sets['active'] && $this->sets['driver']!='' && file_exists($this->sets['driver'].'/server.php')) {
			$class=explode('/', $this->sets['driver']);
			$class=array_reverse($class);
			$model='\Models\\'.$class[0];
			if(!class_exists($model, false)) {
				require_once($this->sets['driver'].'/server.php');
			}
			$s=new \Models\Settings($this->f3, $this->db);
			$config=$s->get_driver_config($this->sets['driver'], 'clientsscales', $this->sets['driver_config']);
			$config=$config['config'];
			$this->scale=new $model($f3, $this->sets, $config);
		}
		else {
			$this->sets['active']=0;
		}*/
	}

	function select_scales($query=null, $options=null) {
		$scales=array();
		$this->load($query, $options);
		if(!$this->dry()) {
			do {
				$scales[]=$this->cast();
			}
			while($this->skip());
		}
		return $scales;
	}

	function delete_scale($id) {
		$scale=$this->select_scales(array('id=?', $id), array('limit'=>1));
		if(empty($scale)) {
			return array(
				'success'=>false,
				'txt'=>'scale_not_found'
			);
		}
		if($this->db->exec("DELETE FROM clients_scales WHERE id=?", $id)===false) {
			return array(
				'success'=>false,
				'txt'=>'db_error'
			);
		}
		$this->db->exec("DELETE FROM driver_settings WHERE path=? AND print_group_id=? AND device='clientsscales'", array($scale[0]['driver'], $scale[0]['driver_config']));
		return array(
			'success'=>true,
			'txt'=>'delete_successful',
			'reload'=>array('.scales_list'),
		);
	}

	function get_sets($method_id) {
		$sets=array(
			'id'=>0,
			'active'=>0,
			'driver'=>'',
			'title'=>'',
			'driver_config'=>'',
		);
		$this->reset();
		$this->load(array('id=?', $method_id), array('limit'=>1));
		if($this->dry()) {
			return $sets;
		}
		return $this->cast();
	}

	// Список драйверов
	function get_drivers() {
		$drivers=array();
		$scales_dir=$this->f3->get('clientsscales_dir');
		$dirs=scandir($scales_dir); // Ищем все драйвера
		if($dirs) {
			foreach ($dirs as $d) {
				$folder=$scales_dir.$d;
				$file=$folder.'/driver.config';
				if(is_dir($folder) && file_exists($file)) {
					$config=parse_ini_file($file);
					if($config) {
						$drivers[$folder]=array(
							'title'=>$config['ServerName'],
							'method_title'=>$config['ScaleName'],
							'path'=>$folder
						);
					}
				}
			}

			foreach ($dirs as $d) {
				$folder=$scales_dir.$d;
				$file=$folder.'/server.config';
				if(is_dir($folder) && file_exists($file)) {
					$config=parse_ini_file($file);
					if($config) {
						$drivers[$folder]=array(
							'title'=>$config['ServerName'],
							'method_title'=>$config['ScaleName'],
							'path'=>$folder
						);
					}
				}
			}
		}
		return $drivers;
	}

	// Сохраняем настройки
	function save_scales($data) {
		if($data['id']) {
			foreach($data['id'] as $n=>$id) {
				$this->reset();
				$this->load(array('title=?', $data[$n]['title']), array('limit'=>1));
				if(!$this->dry() && $this->id!=$id) {
					return array(
						'success'=>false,
						'txt'=>'scale_is_exists'
					);
				}
				$this->reset();
				if($id>0) {
					$this->load(array('id=?', $id), array('limit'=>1));
				}
				$scale=array(
					'driver'=>$data['driver'][$n],
					'title'=>$data['title'][$n],
					'active'=>$data['active'][$n],
					'driver_config'=>$data['driver_config'][$n],
				);
				$this->copyFrom($scale);
				if($this->save()===false) {
					return array(
						'success'=>true,
						'txt'=>'db_error'
					);
				}
			}
		}
		return array(
			'success'=>true,
			'txt'=>'save_successful',
			'reload'=>array('.scales_list'),
		);
	}

	function multi_curl_range($start, $end, $path) {
		$start=ip2long($start);
		$end=ip2long($end);
		$range=array();
		for($ip=$start; $ip<=$end; $ip++) {
			$range[]=long2ip($ip);
		}
		return $this->multi_curl($range, $path);
	}

	function multi_curl($range, $path) {
		$data=array();
		$curl_arr=array();
		$curl=curl_multi_init();
		
		$range_port=array();
		foreach($range as $ip){
			if(mb_strpos($ip, ":")>0) {
				$range_port[]=$ip;
			}
			else {
				$range_port[]=$ip;
			}
		}

		foreach($range_port as $ip_port) {
			$url=$path.$ip_port;
			$curl_arr[$ip_port]=curl_init($url);
			curl_setopt($curl_arr[$ip_port], CURLOPT_RETURNTRANSFER, true);
			curl_setopt($curl_arr[$ip_port], CURLOPT_HEADER, false);
			curl_setopt($curl_arr[$ip_port], CURLOPT_TIMEOUT, 5);
			curl_setopt($curl_arr[$ip_port], CURLOPT_CONNECTTIMEOUT, 2);
    		$r=curl_multi_add_handle($curl, $curl_arr[$ip_port]);
		}
		do {
			curl_multi_exec($curl,$running);
			if($running>0) {
				curl_multi_select($curl,1);
			}
		}
		while($running>0);
		$result=array();
		foreach($range_port as $ip_port) {
			$res=curl_multi_getcontent($curl_arr[$ip_port]);
   			if($res) {
   				$result[$ip_port]=$res;
   			}
		}
		curl_multi_close($curl);
		return $result;
	}

	function updatescale($data) {
		$sets=$this->get_sets($data['id']);
		if(!$sets['active'] || $sets['driver']=='' || !file_exists($sets['driver'].'/server.php')) {
			return array(
				'success'=>true,
				'txt'=>'save_successful'
			);
		}
		// Запускаем драйверов весов
		$class=explode('/', $sets['driver']);
		$class=array_reverse($class);
		$model='\Models\\'.$class[0];
		if(!class_exists($model, false)) {
			require_once($sets['driver'].'/server.php');
		}
		$s=new \Models\Settings($this->f3, $this->db);
		$config=$s->get_driver_config($sets['driver'], 'clientsscales', $sets['driver_config']);
		$config['config']['ip']=$data['ip'];
		$config['config']['api_data']=$this->api_data;
		$config=$config['config'];
		$scale=new $model($this->f3, $this->sets, $config);
		// Узнаем информацию о весах, есть ли они на этом ip
		$info=$scale->getScaleInfo();

		// Если весов нет, то просто сообщаем об успехе, что бы не вываливаться в ошибку
		if(!isset($info['serialNumber'])) {
			return array(
				'success'=>true,
				//'txt'=>' ip '.$config['ip'].'. Результат: весов тут нет'
			);
		}

		// Загружаем данные для обнаруженных весов, когда было последнее обновление
		$this->synchronization->reset();
		$this->synchronization->load(array('serial_number=? AND type="clientsscales"', array($info['serialNumber'])), array('limit'=>1));
		if($this->synchronization->dry()) {
			// Если весы обнаружены впервые, то время обновления 0, то есть выгрузить все
			$this->synchronization->type='clientsscales';
			$this->synchronization->serial_number=$info['serialNumber'];
			$this->synchronization->time=0;
		}

		// Если время обновления 0, то нужно сначала удалить все товары на весах
		$this->exportType = 1;
		if($this->synchronization->time==0) {
			$this->exportType=0;
		}

		// Получим товары обновленные после даты обновления
		$products=$this->db->exec("SELECT p.*, c.title AS category_title, c.img AS category_img FROM products AS p LEFT JOIN productcategories AS c ON c.id=p.category_id WHERE p.plu!='' AND p.plu IS NOT NULL AND p.date_update>? ORDER BY p.date_update", $this->synchronization->time);
		if(!$products && $this->synchronization->time>0) {
			return array(
				'success'=>true,
				//'txt'=>' ip '.$config['ip'].'. Результат: нет товаров для синхронизации'
			);
		}

		if(!$products && $this->synchronization->time==0) {
			// Если нужно выгрузить все товары, но их нет, то нужно на весах удалить все загруженные ранее товары
			$res=$scale->delete_products();
		}
		else {
			foreach($products as $n=>$p) {
				if($p['date_delete']==0) {
		            $barcodes=$this->db->exec("SELECT * FROM barcodes WHERE product_id=?", $p['id']);
		            if(!$barcodes) {
		            	unset($products[$n]);
		            	continue;
		            }
		            $barcode='';
		            foreach($barcodes as $b) {
		                if(mb_strlen($b['barcode'])==7) {
		                    $barcode=$b['barcode'];
		                    break;
		                }
		            }
		            if($barcode=='') {
		            	unset($products[$n]);
		            	continue;
		            }
		            $products[$n]['barcode']=$barcode;
		        }
	        }
	        $res=$scale->add_products($products, $this->synchronization->cast(), $this->exportType);
	    }

        if($res['success']==false) {
        	return $res;
        }

		$this->synchronization->time=$res['time'];
		$this->synchronization->save();
		return array(
			'success'=>true
		);
	}

	// Обновляем товары на всех весах
	function products_update() {
		// Ищем все активные протоколы весов самообслуживания
		$scales=$this->select_scales(array('active>0 AND driver!=""'));
		if(!$scales) {
			return array(
				'success'=>true,
				'txt'=>'save_successful'
			);
		}
		
		// Перебираем все протоколы
		$errors=0;
		$base_settings=$this->f3->get('base_settings');
		foreach($scales as $scale) {
			if(!$scale['active'] || $scale['driver']=='' || !file_exists($scale['driver'].'/server.php')) {
				$this->logs->save_log('Весы самообслуживания "'. $scale['title'].'": Драйвер не активен или не обнаружен');
				$errors++;
			}
			
			$s=new \Models\Settings($this->f3, $this->db);
			$config=$s->get_driver_config($scale['driver'], 'clientsscales', $scale['driver_config']);
			if(isset($config['notMultiCurl']) && (int)$config['notMultiCurl']==1) {
				$res = $this->updatescale(array('id'=>$scale['id'], 'ip'=>''));
				if(!$res['success'] && isset($res['txt'])) {
					$this->logs->save_log('Весы самообслуживания "'. $scale['title'].'": '.$res['txt']);
					$errors++;
				}
				continue;
			}
			// Теперь мультикурлом делаем запросы по всем локальным ip чтобы обнаружить открытый порт, порт принимает бинарные данные, а курл отправляет туда http запросы, поэтому ожидаем на открытом порту код ошибки 56, т.е. сброс соединения с той стороны curl: (56) Recv failure: Connection reset by peer
			$ips = $this->scanIPRange($base_settings['ip_clientsscales_range_start'], $base_settings['ip_clientsscales_range_end'], $config['config']['port'], [56], $config['config']['timeout']);
			
			// Для каждого найденного ip с открытым портом делаем обновление данных
			foreach($ips as $ip=>$ipv) {
				$res = $this->updatescale(array('id'=>$scale['id'], 'ip'=>$ip));
				
				if(!$res['success'] && isset($res['txt'])) {
					$this->logs->save_log('Весы самообслуживания "'. $scale['title'].'": '.$res['txt']);
					$errors++;
				}
			}
			
		}

		// Если были ошибки
		if($errors>0) {
			return array(
				'success'=>false,
				'txt'=>'Выполнено с ошибками. Кол-во ошибок: '.$errors.'. Ознакомиться с ошибками можно в логах.'
			);
		}

		return array(
			'success'=>true,
			'txt'=>'save_successful'
		);
	}
	
	function scanIPRange($start, $end, $port = 1111, $errorCodesToReturn=array(0,56), $timeout=4, $connectTimeout=2) {
    $results = [];
    
    $mh = curl_multi_init();
    $handles = [];
    
    // Создаем handles для каждого IP
    for ($iip=ip2long($start); $iip<=ip2long($end); $iip++) {
        $ip = long2ip($iip);
        $url = "http://{$ip}:{$port}";
        
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_CONNECTTIMEOUT => $connectTimeout,
            CURLOPT_NOBODY => true
        ]);
        
        curl_multi_add_handle($mh, $ch);
        $handles[$ip] = $ch;
    }
    
    // Выполняем запросы
    $active = null;
    do {
        curl_multi_exec($mh, $active);
        curl_multi_select($mh);
        
        // КРИТИЧЕСКИ ВАЖНО: читаем информацию о завершенных запросах
        while ($info = curl_multi_info_read($mh)) {
            $ch = $info['handle'];
            $result = $info['result']; // Реальный код ошибки
            
            // Находим IP для этого handle
            $ip = array_search($ch, $handles, true);
            
            if ($ip !== false) {
                if(in_array($result, $errorCodesToReturn)) {
                    $results[$ip] = [
                        'result' => $result,
                        'error_text' => ($result === CURLE_OK) ? 'Success' : curl_strerror($result),
                        'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE)
                    ];
                }
                
                // Убираем handle
                curl_multi_remove_handle($mh, $ch);
                curl_close($ch);
                unset($handles[$ip]);
            }
        }
    } while ($active > 0);
    
    curl_multi_close($mh);
    return $results;
}
}
