<?php
if(!file_exists('app/api_config.php')) {
	header('Location: /');
	exit();
}
if(!file_exists('db/cashbox.sqlite')) {
	header('Location: /');
	exit();
}

$get=$_GET;

if(!isset($get['backup'])) {
	$stage_title=array(
		1=>'Загрузка архива с новой версией',
		2=>'Синхронизация чеков',
		3=>'Создание резервной копии базы данных',
		4=>'Распаковка архива с новой версией',
		5=>'Актуализация базы данных',
		6=>'Синхронизация справочника номенклатуры',
		7=>'Синхронизация справочника пользователей',
		8=>'Синхронизация справочника клиентов',
		9=>'Синхронизация справочника лояльностей',
		10=>'Синхронизация справочника статусов заказов',
		11=>'Синхронизация справочника префиксов',
		12=>'Установка СНО',
		13=>'Завершение сервисного обслуживания',
	);
}
else {
	$stage_title=array(
		1=>'Загрузка файла с резервной копией БД',
		2=>'Установка резервной копии БД',
		3=>'Смена ИД кассы',
		4=>'Очистка данных о чеках',
		5=>'Актуализация базы данных',
		6=>'Синхронизация справочника номенклатуры',
		7=>'Синхронизация справочника пользователей',
		8=>'Синхронизация справочника клиентов',
		9=>'Синхронизация справочника лояльностей',
		10=>'Синхронизация справочника статусов заказов',
		11=>'Синхронизация справочника префиксов',
		12=>'Установка СНО',
		13=>'Завершение сервисного обслуживания',
	);
}

// Получаем текущие настройки
$archive='next_version.zip'; // Так называется архив, который мы сохранеяем, когда принмаем файл для обновления
$backup_archive='db.zip'; // Так называется архив с резервной копией, который мы сохранеяем, когда принмаем файл для восстановления
$host=$_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['SERVER_NAME'];
$root=$_SERVER['DOCUMENT_ROOT'];
include('app/api_config.php');

$ini=parse_ini_file('app/config.ini');
if($_SERVER['SERVER_NAME']=='kassa.loc') {
	$ini['agorta_url']='https://agorta.loc'; // Для разработки
}

$site_version=$ini['site_version'];
$error='';
$db = new SQLite3($ini['db_name'], SQLITE3_OPEN_READWRITE);

$res=$db->query("SELECT * FROM settings");
while ($val=$res->fetchArray(SQLITE3_ASSOC)) {
	$base_settings[$val['name']]=$val['value'];
}

// Если касса автоновная, то шаги с синхронизацией не нужны
if($base_settings['autonomous']==1) {
	unset($stage_title[6], $stage_title[7], $stage_title[8], $stage_title[9], $stage_title[10], $stage_title[11], $stage_title[12]);
}

// Узнаем о новых версиях
function get_update() {
	global $ini;
	$url = $ini['agorta_url'].'/api/update?soft=1';
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
	curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);

	$json = curl_exec($ch);
	curl_close($ch);
	$result=json_decode($json, true);
	return $result;
}

// Сравниваем номера версий
// Если текущая версия меньше версии, которую предлагает Агорта, то веренм true - надо обновляться
function compare_version($version, $po_version) {
	// Номера версия состоят из трех частей, разделенных точкой. Чем больше первая часть версии от Агорты, тем она новее, если равны, то сравниваем вторую часть, если и они равны то третью
	$version_data=explode('.', $version);
	if(count($version_data)<3) { // Если вдруг номер версии состоит не из трех частей, то добавляем части
		for($i=0; $i<3; $i++) {
			if(!isset($version_data[$i])) $version_data[$i]=0;
		}
	}

	$po_data=explode('.', $po_version);
	if(count($po_data)<3) { // Если вдруг номер версии состоит не из трех частей, то добавляем части
		for($i=0; $i<3; $i++) {
			if(!isset($po_data[$i])) $po_data[$i]=0;
		}
	}

	if((int)$po_data[0]>(int)$version_data[0]) { // Если первая часть версии от Агорты больше, то надо обновляться
		return true;
	}
	if((int)$po_data[0]==(int)$version_data[0]) { // Дальше проверять нужно, только если первые части равны. Если первая часть версии от Агорты меньше, значит там версия старее
		if((int)$po_data[1]>(int)$version_data[1]) { // Если вторая часть версии от Агорты больше, то надо обновляться
			return true;
		}
		if((int)$po_data[1]==(int)$version_data[1]) { // Дальше проверять нужно, только если вторые части равны. Если вторая часть версии от Агорты меньше, значит там версия старее
			if((int)$po_data[2]>(int)$version_data[2]) { // Если третья часть версии от Агорты больше, то надо обновляться
				return true;
			}
		}
	}
	return false; // Обновляться не надо
}

// Узнаем о резервных копиях в агорте
function get_backups() {
	global $ini, $host;
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $host.'/api/dbbackups?all='.($_GET['all']??0).'&page='.($_GET['page']??1));
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
	curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
	curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
	$result = curl_exec($ch);
	curl_close($ch);
	$json=json_decode($result, true);
	if(is_array($json)) {
		return $json;
	}
	return array(
		'success'=>false,
		'error'=>'Ошибка запроса резервных копий БД. '.($json['error']??$result)
	);
}

// Узнаем о резервных копиях для автономного режима
function get_local_backups() {
	if(!file_exists('backups')) {
		return array(
			'success'=>false,
			'error'=>'Резервные копии БД отсутствуют'
		);
	}
	$files=scandir('backups', SCANDIR_SORT_DESCENDING);
	$backups=array();
	foreach($files as $f) {
		$file=pathinfo($f);
		if(mb_strtolower($file['extension'])!='sqlite') continue;
		$file_data=explode('_', $file['filename']);
		if($file_data[2]) {
			$date=explode('-', $file_data[2]);
			if(count($date)==6)	$time=strtotime($date[0].'-'.$date[1].'-'.$date[2].' '.$date[3].':'.$date[4].':'.$date[5]);
			else $time=0;
		}
		else {
			$time=0;
		}
		$backups[]=array(
			'file_name'=>$file['basename'],
			'version'=>$file_data[1]??$file['filename'],
			'time'=>$time,
			'hash'=>md5_file('backups/'.$file['basename'])
		);
	}
	return array(
		'success'=>true,
		'data'=>$backups
	);
}

// Распаковка архива
function syncZipToFolder($zipPath, $extractPath, array $protected = []) {
    $extractPath = rtrim($extractPath, '/\\');

    $zip = new ZipArchive;

    if ($zip->open($zipPath)!==false) {

        // Нормализуем protected пути
        $protected = array_map(function ($p) {
            return trim(str_replace('\\', '/', $p), '/');
        }, $protected);

        // 1. Получаем список файлов в архиве
        $zipFiles = [];
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $zipFiles[] = trim($zip->getNameIndex($i), '/');
        }

        // 2. Удаляем лишние файлы
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($extractPath, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($iterator as $file) {
            $fullPath = $file->getPathname();

            $relativePath = str_replace($extractPath . DIRECTORY_SEPARATOR, '', $fullPath);
            $relativePath = trim(str_replace('\\', '/', $relativePath), '/');

            // Проверка: защищён ли файл или папка
            $isProtected = false;
            foreach ($protected as $p) {
                if (
                    $relativePath === $p ||
                    strpos($relativePath, $p . '/') === 0
                ) {
                    $isProtected = true;
                    break;
                }
            }

            if ($isProtected) {
                continue;
            }

            // Если файла нет в архиве — удаляем
            if (!in_array($relativePath, $zipFiles)) {
                if ($file->isDir()) {
                    if (!(new FilesystemIterator($fullPath))->valid()) {
                        rmdir($fullPath);
                    }
                } else {
                    @unlink($fullPath);
                }
            }
        }

        // 3. Распаковка (перезапишет файлы)
        $zip->extractTo($extractPath);

        $zip->close();
        return true;
    }

    return false;
}

if(isset($get['backup']) && (!isset($get['stage']) || $get['stage']>=13)) {
	// Если переходим в раздел восстановления, но еще не указана резервная копия и восстановление еще не началось, то нужно запросить резервные копии
	if(!isset($get['db'])) {
		if($base_settings['autonomous']==0) {
			// Если не автономный режим, то резервные копии ищем в агорте
			$backups=get_backups();
		}
		else {
			$backups=get_local_backups();
		}
	}
	else {
		// Если указана резервная копия
		// Если режим неавтономный, то нужно сначала скачать резервную копию
		if($base_settings['autonomous']==0) {
			$stage=1;
			$next_stage=$stage+1; // Следующий этап - распаковка архива с резервной копией
			$ch = curl_init();
		    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		    curl_setopt($ch, CURLOPT_HEADER, false);
		    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
		    curl_setopt($ch, CURLOPT_URL, $api_domen.'/api/dbdownload?'.http_build_query(['token'=>$api_pass, 'file'=>$get['db']]));
		    curl_setopt($ch, CURLOPT_REFERER, $get['db']);
		    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
		    $output = curl_exec($ch);
		    curl_close($ch);
		    if($output) {
				$fh = fopen($root.'/'.$backup_archive, 'w');
				if($fh) {
					fwrite($fh, $output);
					fclose($fh);
					// Проверяем целостность скачанного архива
					if($get['hash']!='' && $get['hash']!=md5_file($root.'/'.$backup_archive)) {
						$error="Произошла ошибка при скачивании файла резервной копии БД! Неверный хэш файла.";
						@unlink($root.'/'.$backup_archive); // Удаляем, то что скачали, чтобы не думать потом что все получилось
					}
					// Узнаем ИД кассы, вдруг он не совпадает с текущим, тогда надо будет переписать
				}
				else {
					$error="Произошла ошибка при сохранении файла резервной копии БД! Не удалось создать файл.";
				}
			}
			else {
				$error="Произошла ошибка при скачивании файла резервной копии БД! Не получен ответ от сервера.";
			}

			// Если все пройдет хорошо, то нас перекинет на этап восстановления 2 - распаковка архива
		}
		else {
			// Если автономный режим, то сразу заменяем файл бд резервной копией, то есть у нас сразу этап 2
			if(file_exists('backups/'.$get['db'])) {
				$stage=2;
				$next_stage=5; // Следующий этап актуализация БД
				copy('backups/'.$get['db'], $ini['db_name']);
				if($get['hash']!='' && md5_file($ini['db_name'])!=$get['hash']) {
					$error="Произошла ошибка при копировании файла резервной копии БД!";
				}
			}
			else {
				$error="файл резервной копии БД отсутствует";
			}
		}
	}

}

// Если нужно обновлять, то есть передан номер этапа обновления
if(isset($get['stage']) && is_numeric($get['stage']) && $get['stage']>0) {
	$stage=$get['stage'];

	// Если касса автономная, и по каким то причинам указан шаг с синхронизацией, то перепрыгиваем на конец
	if($base_settings['autonomous']==1 && $stage>=6 && $stage<13) {
		$stage=13;
	}

	$ok=false; // Если будет false, то была ошибка, и не нужно переходить к следующему этапу, а если true, то нужно перейти к следующему этапу

	// Этап обновления 1 - запросить новую версию и скачать ее
	if(!isset($get['backup']) && $stage==1) {
		$get_update=get_update();
		if(is_array($get_update) && $get_update['success']===true) {
			// Если есть обновление, его нужно скачать
			$ch = curl_init();
		    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		    curl_setopt($ch, CURLOPT_HEADER, false);
		    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
		    curl_setopt($ch, CURLOPT_URL, $get_update['version']['file']);
		    curl_setopt($ch, CURLOPT_REFERER, $get_update['version']['file']);
		    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
		    $output = curl_exec($ch);
		    curl_close($ch);
		    if($output) {
				$fh = fopen($root.'/'.$archive, 'w');
				if($fh) {
					fwrite($fh, $output);
					fclose($fh);
					// Проверяем целостность скачанного архива
					if($get_update['version']['hash']!='' && $get_update['version']['hash']!=md5_file($root.'/'.$archive)) {
						$error="Произошла ошибка при скачивании файла! Неверный хэш файла.";
						@unlink($root.'/'.$archive); // Удаляем, то что скачали, чтобы не думать потом что все получилось
					}
				}
				else {
					$error="Произошла ошибка при сохранении файла обновления! Не удалось создать файл.";
				}
			}
			else {
				$error="Произошла ошибка при скачивании файла обновления!";
			}
		}

		if(file_exists($root.'/'.$archive)) $ok=true; // Если файл с новой версией все еще есть, значит этап 1 выполнен успешно
	}

	// После этапа обновления 1 и до этапа 13 нужен архив с новой версией, поэтому нужно обязательно проверить его наличие, вдруг перешли к другим этапам, а файл удалили
	// Если файла нет, то нужно прекратить обновление
	if(!isset($get['backup']) && $stage<13 && !file_exists($root.'/'.$archive)) {
		$stage=0;
		$error='Архив с новой версией отсутствует';
	}

	// Этап обновления 2 - проверяем все ли чеки отсинхронизированы, если нет, то синхронизируем, но это не нужно делать при автономном режиме
	if(!isset($get['backup']) && $stage==2) {
		if($base_settings['autonomous']==0) {
			// Метод /api/tickets - сам проверяет есть ли что-нибудь на синхронизацию, и если есть то сам всем отправляет.
			$db->exec('UPDATE synchronization SET time_start=0');
			$ch = curl_init();
			curl_setopt($ch, CURLOPT_URL, $host.'/api/tickets?forcibly=1');
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
			curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
			$result = curl_exec($ch);
			curl_close($ch);
			$json=json_decode($result, true);
			if(is_array($json)) {
				if($json['success']===true) {
					// Если пришел успешный ответ, значит никаких ошибок не было, и все что нужно отсинхронизировано
					$ok=true;
				}
				else {
					$error=$json['txt'];
				}
			}
			else {
				$error='Ошибка запроса синхронизации чеков. '.$result;
			}
		}
		else {
			$ok=true;
		}
	}

	// Этап восстановления 2 - установка БД из скачанного архива
	if(isset($get['backup']) && $stage==2) {
		$res=syncZipToFolder($root.'/'.$backup_archive, $root.'/db', array('.git', '.gitkeep', '.', '..'));
		if($res===true) {
			// Все прошло успешно
			$ok=true;
		}
		else {
			$error='Ошибка при распаковке архива';
		}
	}

	// Этап обновления 3 - создаем резервные копии
	if(!isset($get['backup']) && $stage==3) {
		if($base_settings['autonomous']==0) {
			// Если неавтономный режим, то нам нужно только выгрузить в агорту БД, с чем отлично справится метод из апи
			$ch = curl_init();
			curl_setopt($ch, CURLOPT_URL, $host.'/api/dbupload');
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
			curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
			$result = curl_exec($ch);
			curl_close($ch);
			$json=json_decode($result, true);
			if(is_array($json)) {
				if($json['success']===true) {
					// Если пришел успешный ответ, значит никаких ошибок не было, и все что нужно отправлено
					$ok=true;
				}
				else {
					$error=$json['txt'];
				}
			}
			else {
				$error='Ошибка выгрузки резервной копии БД. '.$result;
			}
		}
		else {
			// Для автономного режима делаем бэкап бд в папку backups
			if(!file_exists($root.'/backups')) {
				// Если папки нет, создаем ее
				@mkdir($root.'/backups', 0777, true);
				chmod($root.'/backups', 0777);
			}
			if(!file_exists($root.'/backups')) {
				// Если папка не создалась, то ошибка
				$error='Не удается создать папку для резервного копирования базы данных';
			}
			else {
				$backup_name='db_'.str_replace('.', '-', $site_version).'_'.date('Y-m-d-H-i-s').'_'.rand(0, 1000).'.sqlite';
				if($db->exec('VACUUM INTO "'.$root.'/backups/'.$backup_name.'"')) {
					$ok=true;
				}
				else {
					$error='Ошибка при резервном копировании базы данных';
					@unlink($root.'/backups/'.$backup_name);
				}
			}
		}
	}

	// Этап восстановления 3 - смена ид кассы
	if(isset($get['backup']) && $stage==3) {
		$api_rm_id=uniqid('rm_');
		$txt="<?php\r\n";
		$txt.='$api_store_id="'.$api_store_id.'";'."\n\r";
		$txt.='$api_rm_id="'.$api_rm_id.'";'."\n\r";
		$txt.='$api_rm_name="'.$api_rm_name.'";'."\n\r";
		$txt.='$api_domen="'.$api_domen.'";'."\n\r";
		$txt.='$api_pass="'.$api_pass.'";'."\n\r";
		$txt.='$api_count="'.$api_count.'";'."\n\r";
		if(file_put_contents('app/api_config.php', $txt)) {
			$ok=true;
			$db->exec("UPDATE cd_settings SET rm='".$api_rm_id."'");
			$db->exec("UPDATE changes SET rm='".$api_rm_id."'");
			$db->exec("UPDATE fr_settings SET rm='".$api_rm_id."'");
			$db->exec("UPDATE scales_settings SET rm='".$api_rm_id."'");
		}
		else {
			$ok=false;
			$error='Ошибка при записи нового ИД кассы';
		}
	}

	// Дошли до важной части обновления, по этому нужно записывать какой этап выполняем
	if(!isset($get['backup']) && $stage>3) {
		$db->exec("INSERT OR REPLACE INTO settings (name, value) VALUES ('update_stage', ".$stage.")");
	}

	// Этап обновления 4 - распаковка архива, переписываение существующих файлов, добавление несуществующих, удаление лишних
	if(!isset($get['backup']) && $stage==4) {
		$res=syncZipToFolder($root.'/'.$archive, $root, array('app/api_config.php', $ini['db_name'], '.osp', '.git', '.', '..', 'backups', $archive));
		if($res===true) {
			// Все прошло успешно
			$ok=true;
		}
		else {
			$error='Ошибка при распаковке архива';
		}
	}

	// Этап восстановления 4 - очищаем данные о чеках
	if(isset($get['backup']) && $stage==4) {
		$db->exec("DELETE from receiptlines");
		$db->exec("UPDATE SQLITE_SEQUENCE SET seq=0 WHERE name='receiptlines'");
		$db->exec("DELETE from receipts");
		$db->exec("UPDATE SQLITE_SEQUENCE SET seq=0 WHERE name='receipts'");
		$ok=true;
	}

	// Дальше этап и для обновления и для восстановления

	// Этап 5 - актуализация базы данных
	if($stage==5) {
		// Проверяем базу, нужно ли вносить изменения
		$actualizer = new DbActualizer($ini['db_name'], $ini['db_instance'], true, false);
    	$res=$actualizer->run();
    	if($res['success']==true) {
    		if($res['update']==true) {
    			// Это значит, что нужно внести изменения
    			$actualizer = new DbActualizer($ini['db_name'], $ini['db_instance'], false, false);
    			$res=$actualizer->run();
    			if($res['success']==true) {
    				// Изменения внесены успешно, но на всякий случай проверим еще
    				$actualizer = new DbActualizer($ini['db_name'], $ini['db_instance'], true, false);
    				$res=$actualizer->run();
    				if($res['success']==true) {
    					if($res['update']==false) { // Это значит, что база теперь актуальная
    						$ok=true;
    					}
    					else {
    						$error='Возникли ошибки при актуализации базы данных. '.$res['txt'];
    					}
    				}
    				else {
    					$error=$res['error'];
    				}
    			}
    			else {
		    		$error=$res['error'];
		    	}
    		}
    		else {
    			$ok=true; // То есть изменений вносить не надо
    		}
    	}
    	else {
    		$error=$res['error'];
    	}
	}

	// Этап 6 - синхронизация товаров
	if($stage==6) {
		if(!$get['next_step']) {
			$db->exec('UPDATE synchronization SET time_start=0');
			$url=$host.'/api/products_all?forcibly=1';
		}
		else {
			$url=$host.urldecode($get['next_step']);
		}

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
		curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
		$result = curl_exec($ch);
		curl_close($ch);
		$json=json_decode($result, true);
		if(is_array($json)) {
			if($json['success']===true) {
				// Если пришел успешный ответ, значит никаких ошибок не было, и все что нужно отсинхронизировано
				if(isset($json['next_step']) && $json['next_step']!='') {
					$next_step=$json['next_step'];
				}
				$ok=true;
			}
			else {
				$error=$json['txt'];
			}
		}
		else {
			$error='Ошибка запроса синхронизации номенклатуры. '.$result;
		}
	}

	// Этап 7 - синхронизация пользователей
	if($stage==7) {
		if(!$get['next_step']) {
			$db->exec('UPDATE synchronization SET time_start=0');
			$url=$host.'/api/users';
		}
		else {
			$url=$host.urldecode($get['next_step']);
		}

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
		curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
		$result = curl_exec($ch);
		curl_close($ch);
		$json=json_decode($result, true);
		if(is_array($json)) {
			if($json['success']===true) {
				// Если пришел успешный ответ, значит никаких ошибок не было, и все что нужно отсинхронизировано
				if(isset($json['next_step']) && $json['next_step']!='') {
					$next_step=$json['next_step'];
				}
				$ok=true;
			}
			else {
				$error=$json['txt'];
			}
		}
		else {
			$error='Ошибка запроса синхронизации пользователей.'.$result;
		}
	}

	// Этап 8 - синхронизация клиентов
	if($stage==8) {
		if(!$get['next_step']) {
			$db->exec('UPDATE synchronization SET time_start=0');
			$url=$host.'/api/partners_all?forcibly=1';
		}
		else {
			$url=$host.urldecode($get['next_step']);
		}

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
		curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
		$result = curl_exec($ch);
		curl_close($ch);
		$json=json_decode($result, true);
		if(is_array($json)) {
			if($json['success']===true) {
				// Если пришел успешный ответ, значит никаких ошибок не было, и все что нужно отсинхронизировано
				if(isset($json['next_step']) && $json['next_step']!='') {
					$next_step=$json['next_step'];
				}
				$ok=true;
			}
			else {
				$error=$json['txt'];
			}
		}
		else {
			$error='Ошибка запроса синхронизации клиентов. '.$result;
		}
	}

	// Этап 9 - синхронизация лояльностей
	if($stage==9) {
		if(!$get['next_step']) {
			$db->exec('UPDATE synchronization SET time_start=0');
			$url=$host.'/api/loyalty';
		}
		else {
			$url=$host.urldecode($get['next_step']);
		}

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
		curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
		$result = curl_exec($ch);
		curl_close($ch);
		$json=json_decode($result, true);
		if(is_array($json)) {
			if($json['success']===true) {
				// Если пришел успешный ответ, значит никаких ошибок не было, и все что нужно отсинхронизировано
				if(isset($json['next_step']) && $json['next_step']!='') {
					$next_step=$json['next_step'];
				}
				$ok=true;
			}
			else {
				$error=$json['txt'];
			}
		}
		else {
			$error='Ошибка запроса синхронизации лояльностей. '.$result;
		}
	}

	// Этап 10 - синхронизация статусов заказов
	if($stage==10) {
		if(!$get['next_step']) {
			$db->exec('UPDATE synchronization SET time_start=0');
			$url=$host.'/api/ordersstatuses';
		}
		else {
			$url=$host.urldecode($get['next_step']);
		}

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
		curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
		$result = curl_exec($ch);
		curl_close($ch);
		$json=json_decode($result, true);
		if(is_array($json)) {
			if($json['success']===true) {
				// Если пришел успешный ответ, значит никаких ошибок не было, и все что нужно отсинхронизировано
				if(isset($json['next_step']) && $json['next_step']!='') {
					$next_step=$json['next_step'];
				}
				$ok=true;
			}
			else {
				$error=$json['txt'];
			}
		}
		else {
			$error='Ошибка запроса синхронизации статусов заказов. '.$result;
		}
	}

	// Этап 11 - синхронизация префиксов
	if($stage==11) {
		if(!$get['next_step']) {
			$db->exec('UPDATE synchronization SET time_start=0');
			$url=$host.'/api/barcodes_pr';
		}
		else {
			$url=$host.urldecode($get['next_step']);
		}

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
		curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
		$result = curl_exec($ch);
		curl_close($ch);
		$json=json_decode($result, true);
		if(is_array($json)) {
			if($json['success']===true) {
				// Если пришел успешный ответ, значит никаких ошибок не было, и все что нужно отсинхронизировано
				if(isset($json['next_step']) && $json['next_step']!='') {
					$next_step=$json['next_step'];
				}
				$ok=true;
			}
			else {
				$error=$json['txt'];
			}
		}
		else {
			$error='Ошибка запроса синхронизации префиксов. '.$result;
		}
	}

	// Этап 12 - установка СНО
	if($stage==12) {
		if(!$get['next_step']) {
			$db->exec('UPDATE synchronization SET time_start=0');
			$url=$host.'/api/sno';
		}
		else {
			$url=$host.urldecode($get['next_step']);
		}

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
		curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
		$result = curl_exec($ch);
		curl_close($ch);
		$json=json_decode($result, true);
		if(is_array($json)) {
			if($json['success']===true) {
				// Если пришел успешный ответ, значит никаких ошибок не было, и все что нужно отсинхронизировано
				if(isset($json['next_step']) && $json['next_step']!='') {
					$next_step=$json['next_step'];
				}
				$ok=true;
			}
			else {
				$error=$json['txt'];
			}
		}
		else {
			$error='Ошибка запроса установки СНО. '.$result;
		}
	}

	// Этап 13 - конец
	if($stage==13) {
		if(!isset($get['backup'])) {
			if(file_exists($root.'/'.$archive)) {
				@unlink($root.'/'.$archive);
				$ok=true;
				$stage=0;
			}
			else {
				unset($get, $ok, $stage);
			}
			$db->exec("INSERT OR REPLACE INTO settings (name, value) VALUES ('update_stage', 0)");
		}
		else {
			if(file_exists($root.'/'.$backup_archive)) {
				@unlink($root.'/'.$backup_archive);
			}
			$ok=true;
			$stage=0;
		}
	}

	if($stage>0 && !$ok && $error=='') {
		$error='Неизвестный этап обновления';
	}
	else {
		// Установим какой этап следующий
		$next_stage=$stage+1;
		// Если касса автономная, и указан шаг с синхронизацией, то перепрыгиваем на конец
		if($base_settings['autonomous']==1 && $next_stage>=6 && $next_stage<13) {
			$next_stage=13;
		}
	}
}

if(!isset($get_update) && !isset($get['backup'])) {
	$get_update=get_update();
	if(is_array($get_update) && $get_update['success']===true && compare_version($site_version, $get_update['version']['version'])) {
		// Если в ответе на запрос последней версии приходит другая версия, значит нужно сделать пометку, что нужно обновиться, поставить этап 1
		$db->exec("INSERT OR REPLACE INTO settings (name, value) VALUES ('update_stage', 1)");
	}
	else {
		$db->exec("INSERT OR REPLACE INTO settings (name, value) VALUES ('update_stage', 0)");
	}
}

// Обновление из файла, можно считать, что это этап 1
if(isset($_FILES['file']['tmp_name']) && $_FILES['file']['tmp_name']!='') {
	$fileinfo=pathinfo($_FILES['file']['name']);
	if(mb_strtolower($fileinfo['extension'])=='zip') {
		$get['stage']=1;
		$stage=1;
		$next_stage=2;
		// Прислали через форму архив
		// Нужно его сохранить как next_version.zip
		copy($_FILES['file']['tmp_name'], $root.'/'.$archive);
		if(!file_exists($root.'/'.$archive)) {
			// Проверим на всякий случай
			$error='Ошибка при загрузке архива обновления';
		}
		else {
			if($_FILES['file']['size']!=filesize($root.'/'.$archive)) {
				$error='Ошибка при загрузке архива обновления';
				@unlink($root.'/'.$archive);
			}
		}
	}
	else {
		$error='Неверное расширение архива. Допускается только .zip.';
	}
}

if($error!='') {
	// Если есть ошибки пишем их в лог и сразу отправляем в агорту
	$log='Ошибка при сервисном обслуживании. Версия: '.$site_version.'. Этап '.$get['stage'].'. Ошибка: '.$error;
	$db->exec("INSERT INTO logs (txt, read, time, info_type) VALUES ('".htmlentities($log)."', 0, ".time().", 'error')");
	if($base_settings['autonomous']==0) { // Если касса неавтономная, то отправим логи в агорту
		$db->exec('UPDATE synchronization SET time_start=0');
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $host.'/api/logs');
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,  2);
		curl_setopt($ch, CURLOPT_COOKIE, 'hash='.$ini['service_hash']);
		$result = curl_exec($ch);
		curl_close($ch);
		$json=json_decode($result, true);
		if(is_array($json)) {
			if($json['success']===true) {
				// Если пришел успешный ответ, значит никаких ошибок не было, и все что нужно отсинхронизировано
				if(isset($json['next_step']) && $json['next_step']!='') {
					$next_step=$json['next_step'];
				}
				$ok=true;
			}
			else {
				if($error!='') $error.='<br>';
				$error.=$json['txt'];
			}
		}
		else {
			if($error!='') $error.='<br>';
			$error.='Ошибка отправки логов.'.$result;
		}
	}
}

class DbActualizer
{
    /** @var PDO */
    private $db;

    /** @var string */
    private $sqlFile;

    /** @var bool */
    private $dryRun;

    /** @var bool */
    private $verbose;

    /** @var array */
    private $changes = [];

    /** @var array */
    private $errors = [];

    /** @var array Таблицы, которые были (или будут) пересозданы в текущем прогоне */
    private $recreatedTables = [];

    public function __construct(string $dbPath, string $sqlFile, bool $dryRun = false, bool $verbose = false)
    {
        $this->sqlFile = $sqlFile;
        $this->dryRun  = $dryRun;
        $this->verbose = $verbose;

        $this->db = new PDO('sqlite:' . $dbPath);
        $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->db->exec('PRAGMA foreign_keys = OFF');
        $this->db->exec('PRAGMA journal_mode = WAL');
    }

    // -------------------------------------------------------------------------
    // Публичный API
    // -------------------------------------------------------------------------

    /**
     * Запустить актуализацию. Возвращает true если ошибок не было.
     */
    public function run()
    {
        $sql = file_get_contents($this->sqlFile);
        if ($sql === false) {
            $this->errors[] = "Не удалось прочитать файл: {$this->sqlFile}";
            return false;
        }

        $reference = $this->parseSQL($sql);

        $this->syncTables($reference);
        $this->syncViews($reference);
        $this->syncTriggers($reference);

        return $this->Summary();
    }

    public function getChanges(): array { return $this->changes; }
    public function getErrors(): array  { return $this->errors; }

    // -------------------------------------------------------------------------
    // Парсинг эталонного SQL
    // -------------------------------------------------------------------------

    /**
     * Разбирает SQL-файл и возвращает структуру:
     * [
     *   'tables'   => [ tableName => [ 'sql' => '...', 'columns' => [...], 'indexes' => [...] ] ],
     *   'views'    => [ viewName  => 'CREATE VIEW ...' ],
     *   'triggers' => [ trigName  => 'CREATE TRIGGER ...' ],
     *   'inserts'  => [ tableName => [ 'INSERT INTO ...' ] ],
     * ]
     */
    public function parseSQL(string $sql): array
    {
        $result = [
            'tables'   => [],
            'views'    => [],
            'triggers' => [],
            'inserts'  => [],
        ];

        // Убираем комментарии
        $sql = preg_replace('/--[^\n]*/', '', $sql);
        $sql = preg_replace('/\/\*.*?\*\//s', '', $sql);

        // Разбиваем на отдельные statements
        $statements = $this->splitStatements($sql);

        foreach ($statements as $stmt) {
            $stmt = trim($stmt);
            if ($stmt === '') continue;

            $upper = strtoupper(ltrim($stmt));

            if (strpos($upper, 'CREATE TABLE') === 0) {
                $name = $this->extractCreateTableName($stmt);
                if ($name !== null) {
                    $result['tables'][$name] = [
                        'sql'     => $stmt,
                        'columns' => $this->parseColumns($stmt),
                        'indexes' => [],
                    ];
                }
            } elseif (strpos($upper, 'CREATE UNIQUE INDEX') === 0 || strpos($upper, 'CREATE INDEX') === 0) {
                $parsed = $this->parseIndexStatement($stmt);
                if ($parsed && isset($result['tables'][$parsed['table']])) {
                    $result['tables'][$parsed['table']]['indexes'][$parsed['name']] = $parsed;
                }
            } elseif (strpos($upper, 'CREATE VIEW') === 0) {
                $name = $this->extractObjectName($stmt, 'VIEW');
                if ($name) $result['views'][$name] = $stmt;
            } elseif (strpos($upper, 'CREATE TRIGGER') === 0) {
                $name = $this->extractObjectName($stmt, 'TRIGGER');
                if ($name) $result['triggers'][$name] = $stmt;
            } elseif (strpos($upper, 'INSERT INTO') === 0) {
                $name = $this->extractInsertTableName($stmt);
                if ($name) $result['inserts'][$name][] = $stmt;
            }
        }

        return $result;
    }

    // -------------------------------------------------------------------------
    // Синхронизация таблиц
    // -------------------------------------------------------------------------

    private function syncTables(array $reference): void
    {
        $existing = $this->getExistingTables();

        // Добавляем отсутствующие таблицы
        foreach ($reference['tables'] as $name => $def) {
            if (!isset($existing[$name])) {
                $this->applyChange("CREATE TABLE `{$name}`", $def['sql']);
                // Индексы новой таблицы
                foreach ($def['indexes'] as $idx) {
                    $this->applyChange("CREATE INDEX `{$idx['name']}` ON `{$name}`", $idx['sql']);
                }
                // Начальные данные
                if (!empty($reference['inserts'][$name])) {
                    foreach ($reference['inserts'][$name] as $ins) {
                        $this->applyChange("INSERT initial data into `{$name}`", $ins);
                    }
                }
                continue;
            }

            // Таблица существует — синхронизируем столбцы и индексы
            $this->syncColumns($name, $def, $existing[$name]);
            $this->syncIndexes($name, $def['indexes']);
        }

        // Удаляем лишние таблицы
        foreach ($existing as $name => $existDef) {
            if (!isset($reference['tables'][$name])) {
                $this->applyChange("DROP TABLE `{$name}`", "DROP TABLE \"{$name}\"");
            }
        }
    }

    // -------------------------------------------------------------------------
    // Синхронизация столбцов
    // -------------------------------------------------------------------------

    private function syncColumns(string $table, array $refDef, array $existDef): void
    {
        $refCols    = $refDef['columns'];
        $existCols  = $this->getExistingColumns($table);

        $refNames   = array_column($refCols, 'name');
        $existNames = array_column($existCols, 'name');

        // Сначала определяем — нужно ли пересоздание таблицы
        $needsRecreate = false;
        $recreateReasons = [];

        foreach ($existCols as $existCol) {
            if (!in_array($existCol['name'], $refNames, true)) {
                $needsRecreate = true;
                $recreateReasons[] = "лишний столбец `{$existCol['name']}`";
            }
        }

        foreach ($refCols as $refCol) {
            $found = null;
            foreach ($existCols as $ec) {
                if ($ec['name'] === $refCol['name']) { $found = $ec; break; }
            }
            if ($found === null) continue; // новый столбец — обрабатывается ниже или через recreation

            if (!$this->columnsMatch($refCol, $found)) {
                $needsRecreate = true;
                $recreateReasons[] = "столбец `{$refCol['name']}` отличается (тип/умолчание/not null/pk)";
            }
        }

        if ($needsRecreate) {
            // Пересоздание покрывает и добавление новых столбцов — ADD COLUMN не нужен
            $reason = implode('; ', $recreateReasons);
            $this->recreateTable($table, $refDef, $reason);
            return;
        }

        // Пересоздание не нужно — добавляем только отсутствующие столбцы
        foreach ($refCols as $col) {
            if (!in_array($col['name'], $existNames, true)) {
                $colDef = $this->buildColumnDef($col);
                $this->applyChange(
                    "ADD COLUMN `{$col['name']}` to `{$table}`",
                    "ALTER TABLE \"{$table}\" ADD COLUMN {$colDef}"
                );
            }
        }
    }

    /**
     * Пересоздание таблицы (единственный способ изменить тип столбца / удалить столбец в SQLite).
     */
    private function recreateTable(string $table, array $refDef, string $reason): void
    {
        $this->logVerbose("Пересоздание таблицы `{$table}`: {$reason}");

        $tmpName = "__tmp_{$table}_" . uniqid();

        // 1. Создать временную таблицу по эталону
        $createTmp = preg_replace(
            '/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`\[]?' . preg_quote($table, '/') . '["`\]]?/i',
            "CREATE TABLE \"{$tmpName}\"",
            $refDef['sql']
        );

        $existCols = $this->getExistingColumns($table);
        $refCols   = $refDef['columns'];
        $refNames  = array_column($refCols, 'name');

        // Столбцы для копирования данных (только те, что есть в обеих таблицах)
        $copyNames = [];
        foreach ($existCols as $ec) {
            if (in_array($ec['name'], $refNames, true)) {
                $copyNames[] = '"' . $ec['name'] . '"';
            }
        }
        $colList = implode(', ', $copyNames);

        $ops = [
            ['msg' => "CREATE TEMP TABLE `{$tmpName}` (по эталону `{$table}`)", 'sql' => $createTmp],
            ['msg' => "Копирование данных из `{$table}` в `{$tmpName}`",
             'sql' => "INSERT INTO \"{$tmpName}\" ({$colList}) SELECT {$colList} FROM \"{$table}\""],
            ['msg' => "DROP TABLE `{$table}`", 'sql' => "DROP TABLE \"{$table}\""],
            ['msg' => "ALTER TABLE `{$tmpName}` RENAME TO `{$table}`",
             'sql' => "ALTER TABLE \"{$tmpName}\" RENAME TO \"{$table}\""],
        ];

        if ($this->dryRun) {
            foreach ($ops as $op) {
                $this->changes[] = "[DRY-RUN] {$op['msg']}";
                $this->logVerbose("  [DRY-RUN] {$op['sql']}");
            }
            $this->recreatedTables[] = $table;
            return;
        }

        $this->db->beginTransaction();
        try {
            foreach ($ops as $op) {
                $this->db->exec($op['sql']);
                $this->changes[] = $op['msg'];
                $this->logVerbose("  " . $op['sql']);
            }
            $this->db->commit();
            $this->recreatedTables[] = $table;
        } catch (Exception $e) {
            $this->db->rollBack();
            $this->errors[] = "Ошибка пересоздания таблицы `{$table}`: " . $e->getMessage();
        }
    }

    // -------------------------------------------------------------------------
    // Синхронизация индексов
    // -------------------------------------------------------------------------

    private function syncIndexes(string $table, array $refIndexes): void
    {
        // Если таблица была пересоздана — все старые индексы уже удалены вместе с ней
        $existingIndexes = in_array($table, $this->recreatedTables, true)
            ? []
            : $this->getExistingIndexes($table);

        // Добавляем отсутствующие
        foreach ($refIndexes as $name => $idx) {
            if (!isset($existingIndexes[$name])) {
                $this->applyChange("CREATE INDEX `{$name}` ON `{$table}`", $idx['sql']);
                continue;
            }
            // Индекс существует — проверяем соответствие
            if (!$this->indexesMatch($idx, $existingIndexes[$name])) {
                $this->applyChange("DROP INDEX `{$name}` (изменился)", "DROP INDEX \"{$name}\"");
                $this->applyChange("CREATE INDEX `{$name}` ON `{$table}` (пересоздание)", $idx['sql']);
            }
        }

        // Удаляем лишние (которых нет в эталоне)
        foreach ($existingIndexes as $name => $idx) {
            if (!isset($refIndexes[$name])) {
                $this->applyChange("DROP INDEX `{$name}` (лишний)", "DROP INDEX \"{$name}\"");
            }
        }
    }

    // -------------------------------------------------------------------------
    // Синхронизация views
    // -------------------------------------------------------------------------

    private function syncViews(array $reference): void
    {
        $existing = $this->getExistingObjects('view');

        foreach ($reference['views'] as $name => $sql) {
            if (!isset($existing[$name])) {
                $this->applyChange("CREATE VIEW `{$name}`", $sql);
            } elseif (!$this->sqlNormEqual($existing[$name], $sql)) {
                $this->applyChange("DROP VIEW `{$name}` (изменился)", "DROP VIEW \"{$name}\"");
                $this->applyChange("CREATE VIEW `{$name}` (пересоздание)", $sql);
            }
        }

        foreach ($existing as $name => $sql) {
            if (!isset($reference['views'][$name])) {
                $this->applyChange("DROP VIEW `{$name}` (лишний)", "DROP VIEW \"{$name}\"");
            }
        }
    }

    // -------------------------------------------------------------------------
    // Синхронизация триггеров
    // -------------------------------------------------------------------------

    private function syncTriggers(array $reference): void
    {
        $existing = $this->getExistingObjects('trigger');

        foreach ($reference['triggers'] as $name => $sql) {
            if (!isset($existing[$name])) {
                $this->applyChange("CREATE TRIGGER `{$name}`", $sql);
            } elseif (!$this->sqlNormEqual($existing[$name], $sql)) {
                $this->applyChange("DROP TRIGGER `{$name}` (изменился)", "DROP TRIGGER \"{$name}\"");
                $this->applyChange("CREATE TRIGGER `{$name}` (пересоздание)", $sql);
            }
        }

        foreach ($existing as $name => $sql) {
            if (!isset($reference['triggers'][$name])) {
                $this->applyChange("DROP TRIGGER `{$name}` (лишний)", "DROP TRIGGER \"{$name}\"");
            }
        }
    }

    // -------------------------------------------------------------------------
    // Применение изменения
    // -------------------------------------------------------------------------

    private function applyChange(string $description, string $sql): void
    {
        if ($this->dryRun) {
            $this->changes[] = "[DRY-RUN] {$description}";
            $this->logVerbose("  [DRY-RUN] {$sql}");
            return;
        }

        try {
            $this->db->exec($sql);
            $this->changes[] = $description;
            $this->logVerbose("  {$sql}");
        } catch (Exception $e) {
            $this->errors[] = "Ошибка при «{$description}»: " . $e->getMessage() . "\n  SQL: {$sql}";
        }
    }

    // -------------------------------------------------------------------------
    // Получение текущего состояния БД
    // -------------------------------------------------------------------------

    /**
     * Возвращает [ tableName => ['sql' => 'CREATE TABLE ...'] ]
     */
    public function getExistingTables(): array
    {
        $rows = $this->db->query(
            "SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
        )->fetchAll(PDO::FETCH_ASSOC);

        $result = [];
        foreach ($rows as $row) {
            $result[$row['name']] = ['sql' => $row['sql']];
        }
        return $result;
    }

    /**
     * Возвращает столбцы таблицы через PRAGMA table_info.
     */
    public function getExistingColumns(string $table): array
    {
        $rows = $this->db->query("PRAGMA table_info(\"{$table}\")")->fetchAll(PDO::FETCH_ASSOC);
        $cols = [];
        foreach ($rows as $r) {
            $cols[] = [
                'name'    => $r['name'],
                'type'    => strtoupper(trim($r['type'])),
                'notnull' => (int)$r['notnull'],
                'default' => $r['dflt_value'],
                'pk'      => (int)$r['pk'],
            ];
        }
        return $cols;
    }

    /**
     * Возвращает индексы таблицы [ indexName => ['name'=>..., 'unique'=>..., 'columns'=>[...], 'sql'=>...] ]
     */
    public function getExistingIndexes(string $table): array
    {
        // PRAGMA index_list даёт флаг unique; sqlite_master даёт sql
        $list = $this->db->query("PRAGMA index_list(\"{$table}\")")->fetchAll(PDO::FETCH_ASSOC);

        $result = [];
        foreach ($list as $idx) {
            // Пропускаем неявные индексы (PRIMARY KEY, UNIQUE constraint) — у них sql=NULL
            $sqlRow = $this->db->query(
                "SELECT sql FROM sqlite_master WHERE type='index' AND name='" . str_replace("'", "''", $idx['name']) . "' AND sql IS NOT NULL"
            )->fetch(PDO::FETCH_ASSOC);
            if (!$sqlRow) continue;

            $cols = $this->db->query("PRAGMA index_info(\"{$idx['name']}\")")->fetchAll(PDO::FETCH_ASSOC);
            $result[$idx['name']] = [
                'name'    => $idx['name'],
                'unique'  => (int)$idx['unique'],
                'columns' => array_column($cols, 'name'),
                'sql'     => $sqlRow['sql'],
            ];
        }
        return $result;
    }

    /**
     * Возвращает views или triggers [ name => sql ]
     */
    private function getExistingObjects(string $type): array
    {
        $rows = $this->db->query(
            "SELECT name, sql FROM sqlite_master WHERE type='{$type}' ORDER BY name"
        )->fetchAll(PDO::FETCH_ASSOC);

        $result = [];
        foreach ($rows as $r) {
            $result[$r['name']] = $r['sql'];
        }
        return $result;
    }

    // -------------------------------------------------------------------------
    // Парсинг SQL-структур
    // -------------------------------------------------------------------------

    /**
     * Разбивает SQL на отдельные statements по символу ';',
     * игнорируя ';' внутри скобок, строковых литералов и BEGIN...END блоков.
     */
    public function splitStatements(string $sql): array
    {
        $statements = [];
        $current    = '';
        $depth      = 0;    // глубина скобок
        $beginDepth = 0;    // глубина BEGIN...END (для триггеров)
        $len        = strlen($sql);
        $inStr      = false;
        $strChar    = '';

        for ($i = 0; $i < $len; $i++) {
            $ch = $sql[$i];

            if ($inStr) {
                $current .= $ch;
                if ($ch === $strChar) {
                    // Проверяем на экранирование удвоением
                    if (isset($sql[$i + 1]) && $sql[$i + 1] === $strChar) {
                        $current .= $sql[++$i];
                    } else {
                        $inStr = false;
                    }
                }
                continue;
            }

            if ($ch === "'" || $ch === '"') {
                $inStr   = true;
                $strChar = $ch;
                $current .= $ch;
                continue;
            }

            if ($ch === '(') { $depth++; $current .= $ch; continue; }
            if ($ch === ')') { $depth--; $current .= $ch; continue; }

            // Проверяем ключевые слова BEGIN и END для триггеров
            if ($depth === 0 && !$inStr) {
                $upper5 = strtoupper(substr($sql, $i, 5));
                $upper3 = strtoupper(substr($sql, $i, 3));
                $prevChar = $i > 0 ? $sql[$i - 1] : ' ';
                $nextChar = isset($sql[$i + 5]) ? $sql[$i + 5] : ' ';
                $nextChar3 = isset($sql[$i + 3]) ? $sql[$i + 3] : ' ';

                if ($upper5 === 'BEGIN' && (ctype_space($prevChar) || $prevChar === '') &&
                    (ctype_space($nextChar) || $nextChar === ';' || $nextChar === '')) {
                    $beginDepth++;
                } elseif ($upper3 === 'END' && $beginDepth > 0 &&
                    (ctype_space($prevChar) || $prevChar === '') &&
                    (ctype_space($nextChar3) || $nextChar3 === ';' || $nextChar3 === '')) {
                    $beginDepth--;
                }
            }

            if ($ch === ';' && $depth === 0 && $beginDepth === 0) {
                $statements[] = trim($current);
                $current = '';
                continue;
            }

            $current .= $ch;
        }
        if (trim($current) !== '') {
            $statements[] = trim($current);
        }

        return $statements;
    }

    /**
     * Разбирает столбцы из CREATE TABLE.
     * Возвращает массив [ ['name'=>..., 'type'=>..., 'notnull'=>..., 'default'=>..., 'pk'=>...] ]
     */
    public function parseColumns(string $createSQL): array
    {
        // Извлекаем содержимое скобок верхнего уровня
        $body = $this->extractTableBody($createSQL);
        if ($body === null) return [];

        $parts = $this->splitByCommaTopLevel($body);
        $cols  = [];

        foreach ($parts as $part) {
            $part = trim($part);
            if ($part === '') continue;

            $upper = strtoupper($part);

            // Пропускаем табличные ограничения
            if (preg_match('/^(PRIMARY\s+KEY|UNIQUE|CHECK|FOREIGN\s+KEY)/i', $part)) continue;

            $col = $this->parseColumnDef($part);
            if ($col) $cols[] = $col;
        }

        return $cols;
    }

    /**
     * Парсит определение одного столбца.
     */
    public function parseColumnDef(string $def): ?array
    {
        $def = trim($def);

        // Имя столбца (с кавычками или без)
        if (preg_match('/^["`\[]?(\w+)["`\]]?\s+(.*)$/si', $def, $m)) {
            $name = $m[1];
            $rest = trim($m[2]);
        } else {
            return null;
        }

        // Тип
        $type    = '';
        $notnull = 0;
        $default = null;
        $pk      = 0;

        // Извлекаем тип: слово(а) до ключевых слов
        if (preg_match('/^([A-Za-z_][A-Za-z0-9_ ]*?)(?:\s*\([\d,\s]+\))?\s*(?:PRIMARY|NOT|DEFAULT|UNIQUE|REFERENCES|CHECK|$)/si', $rest, $tm)) {
            $type = strtoupper(trim($tm[1]));
        } elseif (preg_match('/^([A-Za-z_][A-Za-z0-9_ ]*?)(?:\s*\([\d,\s]+\))?\s*$/si', $rest, $tm)) {
            $type = strtoupper(trim($tm[1]));
        }

        if (stripos($rest, 'NOT NULL') !== false) $notnull = 1;
        if (stripos($rest, 'PRIMARY KEY') !== false) $pk = 1;

        // DEFAULT — значение может быть в скобках или без (с пробелом или без)
        if (preg_match('/DEFAULT\s*(\(([^)]*)\)|\'[^\']*\'|\S+)/si', $rest, $dm)) {
            $default = $dm[1];
        }

        return [
            'name'    => $name,
            'type'    => $type,
            'notnull' => $notnull,
            'default' => $default,
            'pk'      => $pk,
        ];
    }

    /**
     * Сравнивает столбец эталона с существующим.
     */
    public function columnsMatch(array $ref, array $existing): bool
    {
        if ($this->normalizeType($ref['type']) !== $this->normalizeType($existing['type'])) return false;
        if ($ref['notnull'] !== $existing['notnull']) return false;
        if (!$this->defaultsMatch($ref['default'], $existing['default'])) return false;
        return true;
    }

    /**
     * Нормализует тип SQLite для сравнения.
     */
    public function normalizeType(string $type): string
    {
        $type = strtoupper(trim($type));
        // Убираем размерность: INT(1) -> INT
        $type = preg_replace('/\s*\(\s*\d+\s*\)/', '', $type);
        return trim($type);
    }

    /**
     * Сравнивает значения DEFAULT.
     */
    public function defaultsMatch(?string $ref, ?string $existing): bool
    {
        $norm = function (?string $v): string {
            if ($v === null) return '';
            $v = trim($v);
            // Убираем внешние скобки: (0) -> 0
            while (strlen($v) >= 2 && $v[0] === '(' && substr($v, -1) === ')') {
                $inner = substr($v, 1, -1);
                if ($this->isBalanced($inner)) {
                    $v = trim($inner);
                } else {
                    break;
                }
            }
            // Убираем кавычки: 'abc' -> abc
            if (strlen($v) >= 2 && $v[0] === "'" && substr($v, -1) === "'") {
                $v = substr($v, 1, -1);
            }
            return strtolower($v);
        };

        return $norm($ref) === $norm($existing);
    }

    /**
     * Проверяет что скобки в строке сбалансированы.
     */
    private function isBalanced(string $s): bool
    {
        $depth = 0;
        foreach (str_split($s) as $ch) {
            if ($ch === '(') $depth++;
            if ($ch === ')') $depth--;
            if ($depth < 0) return false;
        }
        return $depth === 0;
    }

    /**
     * Строит строку определения столбца для ALTER TABLE ADD COLUMN.
     */
    private function buildColumnDef(array $col): string
    {
        $def = '"' . $col['name'] . '" ' . $col['type'];
        if ($col['notnull']) $def .= ' NOT NULL';
        if ($col['default'] !== null) $def .= ' DEFAULT ' . $col['default'];
        return $def;
    }

    /**
     * Парсит CREATE INDEX / CREATE UNIQUE INDEX.
     */
    public function parseIndexStatement(string $sql): ?array
    {
        if (preg_match(
            '/CREATE\s+(UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`\[]?(\w+)["`\]]?\s+ON\s+["`\[]?(\w+)["`\]]?\s*\(([^)]+)\)/si',
            $sql,
            $m
        )) {
            $cols = array_map('trim', explode(',', $m[4]));
            $cols = array_map(function ($c) {
                return trim($c, '"\' `[]');
            }, $cols);
            // Убираем ASC/DESC из имён столбцов
            $cols = array_map(function ($c) {
                return preg_replace('/\s+(ASC|DESC)$/i', '', $c);
            }, $cols);

            return [
                'name'    => $m[2],
                'unique'  => (int)!empty($m[1]),
                'table'   => $m[3],
                'columns' => $cols,
                'sql'     => $sql,
            ];
        }
        return null;
    }

    /**
     * Сравнивает два индекса.
     */
    public function indexesMatch(array $ref, array $existing): bool
    {
        if ($ref['unique'] !== $existing['unique']) return false;
        if ($ref['columns'] !== $existing['columns']) return false;
        return true;
    }

    // -------------------------------------------------------------------------
    // Вспомогательные методы парсинга
    // -------------------------------------------------------------------------

    private function extractCreateTableName(string $sql): ?string
    {
        if (preg_match('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`\[]?(\w+)["`\]]?/si', $sql, $m)) {
            return $m[1];
        }
        return null;
    }

    private function extractObjectName(string $sql, string $keyword): ?string
    {
        if (preg_match('/CREATE\s+(?:TEMP\s+|TEMPORARY\s+)?' . $keyword . '\s+(?:IF\s+NOT\s+EXISTS\s+)?["`\[]?(\w+)["`\]]?/si', $sql, $m)) {
            return $m[1];
        }
        return null;
    }

    private function extractInsertTableName(string $sql): ?string
    {
        if (preg_match('/INSERT\s+(?:OR\s+\w+\s+)?INTO\s+["`\[]?(\w+)["`\]]?/si', $sql, $m)) {
            return $m[1];
        }
        return null;
    }

    /**
     * Извлекает содержимое скобок верхнего уровня CREATE TABLE (...).
     */
    public function extractTableBody(string $sql): ?string
    {
        $start = strpos($sql, '(');
        if ($start === false) return null;

        $depth  = 0;
        $len    = strlen($sql);
        $result = '';

        for ($i = $start; $i < $len; $i++) {
            $ch = $sql[$i];
            if ($ch === '(') {
                if ($depth > 0) $result .= $ch;
                $depth++;
            } elseif ($ch === ')') {
                $depth--;
                if ($depth === 0) break;
                $result .= $ch;
            } else {
                if ($depth > 0) $result .= $ch;
            }
        }

        return $result;
    }

    /**
     * Разбивает строку по запятым, игнорируя запятые внутри скобок.
     */
    public function splitByCommaTopLevel(string $s): array
    {
        $parts  = [];
        $depth  = 0;
        $current = '';
        $len    = strlen($s);
        $inStr  = false;
        $strChar = '';

        for ($i = 0; $i < $len; $i++) {
            $ch = $s[$i];

            if ($inStr) {
                $current .= $ch;
                if ($ch === $strChar) $inStr = false;
                continue;
            }
            if ($ch === "'" || $ch === '"') {
                $inStr = true; $strChar = $ch; $current .= $ch; continue;
            }

            if ($ch === '(') { $depth++; $current .= $ch; continue; }
            if ($ch === ')') { $depth--; $current .= $ch; continue; }

            if ($ch === ',' && $depth === 0) {
                $parts[] = trim($current);
                $current = '';
                continue;
            }
            $current .= $ch;
        }
        if (trim($current) !== '') $parts[] = trim($current);

        return $parts;
    }

    /**
     * Нормализует SQL для сравнения (пробелы, регистр).
     */
    private function sqlNormEqual(string $a, string $b): bool
    {
        $norm = function (string $s): string {
            $s = strtolower(trim($s));
            $s = preg_replace('/\s+/', ' ', $s);
            return $s;
        };
        return $norm($a) === $norm($b);
    }

    // -------------------------------------------------------------------------
    // Вывод
    // -------------------------------------------------------------------------

    private function logVerbose(string $msg): void
    {
        if ($this->verbose) {
            echo $msg . PHP_EOL;
        }
    }

    private function Summary()
    {
        $txt='';
        $errors='';
        $mode = $this->dryRun ? '[DRY-RUN]' : '[APPLIED]';
        $txt.=PHP_EOL . "=== Результат актуализации БД {$mode} ===" . PHP_EOL;

        if (empty($this->changes) && empty($this->errors)) {
            $txt.="Изменений не требуется. БД соответствует эталону." . PHP_EOL;
            return array(
                'success'=>true,
                'txt'=>$txt,
                'update'=>false,
            );
        }

        if (!empty($this->changes)) {
            $txt.=PHP_EOL . "Изменения (" . count($this->changes) . "):" . PHP_EOL;
            foreach ($this->changes as $ch) {
                $txt.="  + {$ch}" . PHP_EOL;
            }
        }

        if (!empty($this->errors)) {
            $errors.=PHP_EOL . "ОШИБКИ (" . count($this->errors) . "):" . PHP_EOL;
            foreach ($this->errors as $err) {
                $errors.="  ! {$err}" . PHP_EOL;
            }
        }

        return array(
            'success'=>$errors==''?true:false,
            'txt'=>$txt,
            'errors'=>$errors,
            'update'=>true
        );
    }
}

?>
<!DOCTYPE html>
<html lang="ru">
	<head>
		<meta charset="utf-8" />
		<title><?=isset($get['backup'])?'Восстановление БД':'Сервисное обслуживание'?></title>
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1, minimum-scale=1, user-scalable=no">
		<meta name="HandheldFriendly" content="true">

		<link rel="stylesheet" href="/ui/css/base.css?v=<?=$site_version?>" type="text/css" />
		<? if($base_settings['dark_theme']==1) { ?>
		<link rel="stylesheet" href="/ui/css/dark.css?v=<?=$site_version?>" type="text/css" />
		<? } ?>

		<link rel="apple-touch-icon" sizes="180x180" href="/ui/css/img/apple-touch-icon.png?v=<?=$site_version?>">
		<link rel="icon" type="image/png" sizes="32x32" href="/ui/css/img/favicon-32x32.png?v=<?=$site_version?>">
		<link rel="icon" type="image/png" sizes="16x16" href="/ui/css/img/favicon-16x16.png?v=<?=$site_version?>">
		<link rel="manifest" href="/ui/css/img/site.webmanifest?v=<?=$site_version?>">
		<link rel="mask-icon" href="/ui/css/img/safari-pinned-tab.svg?v=<?=$site_version?>" color="#5bbad5">
		<meta name="apple-mobile-web-app-title" content="Агорта.Касса">
		<meta name="application-name" content="Агорта.Касса">
		<meta name="msapplication-TileColor" content="#ffffff">
		<meta name="theme-color" content="#ffffff">

		<link rel="stylesheet" href="/ui/css/all.css">
		<link rel="stylesheet" href="/ui/css/agortaicon.css?v=<?=$site_version?>">
		<style>
			.upd-page {
				display: flex;
				align-items: flex-start;
				justify-content: center;
				padding: 32px 0 48px;
				min-height: 100%;
			}
			.upd-card {
				width: 100%;
				max-width: 520px;
				border: 1px solid #dde1e7;
				border-radius: 12px;
				box-shadow: 0 2px 12px rgba(0,0,0,.07);
				overflow: hidden;
				background: #fff;
			}
			.upd-head {
				border-bottom: 1px solid #eee;
				display: flex;
				flex-direction: column;
			}
			.upd-head-back {
				display: flex;
				align-items: center;
				gap: 10px;
				padding: 12px 16px;
				color: #555;
				text-decoration: none;
				border-bottom: 1px solid #f0f0f0;
			}
			.upd-head-back:hover { background: #f8f8f8; color: #3f51b5; }
			.upd-head-back i { font-size: 28px; }
			.upd-head-back span { font-size: 15px; font-weight: 500; }
			.upd-head-title { padding: 12px 20px 14px; text-align: center; }
			.upd-head-title h1 { font-size: 16px; font-weight: 700; margin: 0; line-height: 1.3; }
			.upd-head-version { font-size: 12px; color: #999; margin-top: 4px; }
			.upd-body { padding: 20px; }
			/* stepper */
			.stepper { list-style: none; padding: 0; margin: 0 0 4px; }
			.stepper__item { display: flex; align-items: flex-start; gap: 12px; position: relative; }
			.stepper__item:not(:last-child)::before {
				content: ''; position: absolute; left: 11px; top: 26px;
				width: 2px; bottom: 0; background: #e4e4e4; z-index: 0;
			}
			.stepper__item.s-done:not(:last-child)::before  { background: #66bb6a; }
			.stepper__item.s-active:not(:last-child)::before,
			.stepper__item.s-error:not(:last-child)::before  { background: #e4e4e4; }
			.stepper__dot {
				width: 24px; height: 24px; border-radius: 50%; flex-shrink: 0;
				display: flex; align-items: center; justify-content: center;
				font-size: 12px; font-weight: 700; position: relative; z-index: 1; margin-top: 2px;
			}
			.s-pending .stepper__dot { background: #f3f3f3; border: 2px solid #d8d8d8; }
			.s-done .stepper__dot    { background: #66bb6a; color: #fff; }
			.s-active .stepper__dot  { background: #3f51b5; color: #fff; animation: dot-pulse 1.5s ease-in-out infinite; }
			.s-error .stepper__dot   { background: #ef5350; color: #fff; }
			@keyframes dot-pulse {
				0%,100% { box-shadow: 0 0 0 0 rgba(63,81,181,.4); }
				50%      { box-shadow: 0 0 0 7px rgba(63,81,181,0); }
			}
			.stepper__label { padding-top: 3px; font-size: 13px; color: #c0c0c0; padding-bottom: 16px; line-height: 1.4; }
			.stepper__item:last-child .stepper__label { padding-bottom: 0; }
			.s-done .stepper__label   { color: #666; }
			.s-active .stepper__label { color: #3f51b5; font-weight: 600; }
			.s-error .stepper__label  { color: #ef5350; font-weight: 600; }
			/* status blocks */
			.upd-error {
				background: #fff5f5; border: 1px solid #ffcdd2; border-radius: 8px;
				padding: 12px 14px; color: #c62828; font-size: 13px; line-height: 1.5; margin-bottom: 14px;
			}
			.upd-success {
				display: flex; align-items: center; gap: 10px;
				background: #f1f8e9; border: 1px solid #c5e1a5; border-radius: 8px;
				padding: 13px 14px; color: #2e7d32; font-size: 14px; font-weight: 600; margin-bottom: 16px;
			}
			.upd-success-icon {
				width: 22px; height: 22px; border-radius: 50%; background: #4caf50;
				color: #fff; display: flex; align-items: center; justify-content: center;
				font-size: 13px; flex-shrink: 0;
			}
			.upd-new-version {
				background: #fff8e1; border: 1px solid #ffe082; border-radius: 8px;
				padding: 10px 14px; font-size: 13px; color: #e65100; margin-bottom: 12px;
			}
			/* buttons */
			.upd-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
			.upd-actions a { display: block; text-decoration: none; }
			.upd-actions form { display: block; }
			.upd-actions input[type="button"],
			.upd-actions input[type="submit"],
			.upd-actions button {
				display: block; width: 100% !important; min-width: 0 !important;
				padding: 10px 16px; font-size: 13px; border-radius: 6px;
				border: 1px solid #d8d8d8; background: #f6f6f6;
				cursor: pointer; text-align: center; box-sizing: border-box; transition: background .15s;
			}
			.upd-actions input[type="button"]:hover,
			.upd-actions input[type="submit"]:hover,
			.upd-actions button:hover { background: #ececec; }
			.upd-btn-primary {
				background: #3f51b5 !important; color: #fff !important;
				border-color: #3f51b5 !important; font-weight: 600;
			}
			.upd-btn-primary:hover { background: #303f9f !important; }
			.upd-file-label {
				display: block; width: 100%; padding: 10px 16px; font-size: 13px;
				font-family: Manrope, Verdana; border-radius: 6px; border: 1px solid #d8d8d8;
				background: #f6f6f6; cursor: pointer; text-align: center;
				box-sizing: border-box; transition: background .15s; line-height: 1.7em;
			}
			.upd-file-label:hover { background: #ececec; }
			.upd-divider { border: none; border-top: 1px solid #eee; margin: 2px 0; }
			.upd-actions.is-loading { pointer-events: none; opacity: 0.45; }
			/* backup table */
			.backup-tbl { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 14px; }
			.backup-tbl th {
				text-align: left; padding: 7px 10px; border-bottom: 2px solid #eee;
				font-weight: 600; color: #666; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
			}
			.backup-tbl td { padding: 10px; border-bottom: 1px solid #f2f2f2; vertical-align: middle; }
			.backup-tbl tr:last-child td { border-bottom: none; }
			.backup-tbl tr:hover td { background: #fafafa; }
			.backup-tbl td:last-child { text-align: right; white-space: nowrap; }
			.bt-time-sub { display: block; color: #aaa; font-size: 11px; line-height: 1.3; }
			.backup-tbl input[type="submit"] {
				padding: 5px 14px !important; font-size: 12px !important;
				min-width: 0 !important; width: auto !important; border-radius: 5px;
			}
			.upd-empty { color: #aaa; font-size: 13px; padding: 8px 0 14px; }
			/* pagination */
			.upd-pagination { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
			.upd-pagination a { text-decoration: none; }
			.upd-pagination input[type="button"] {
				min-width: 0 !important; padding: 6px 14px; font-size: 12px;
				border-radius: 5px; border: 1px solid #d8d8d8; background: #f6f6f6; cursor: pointer;
			}
			/* warnings */
			.upd-warning { font-size: 12px; font-weight: 700; color: #888; line-height: 1.6; margin-bottom: 14px; }
			.upd-warning-running {
				font-size: 13px; font-weight: 600; color: #e65100;
				text-align: center; margin-bottom: 16px; line-height: 1.7;
			}
		</style>
	</head>
	<body style="zoom: <?=isset($base_settings['zoom']) && is_numeric($base_settings['zoom'])?$base_settings['zoom']:100?>%;">
	<div id="screen">
		<div id="screen_window">
			<div class="upd-page">
				<div class="upd-card">
<?php
$_is_backup    = isset($get['backup']);
$_cur_stage    = isset($_GET['stage']) ? (int)$_GET['stage'] : 0;
$_is_done      = (($_GET['stage']??0)==13 && $error=='');
$_is_running   = isset($stage) && $stage>0 && $error=='';
$_show_stepper = ($_cur_stage > 0);
?>
				<div class="upd-head">
					<? if(!$_is_running) { ?>
					<a href="/<?=$_is_backup?'update_new.php':''?>" class="upd-head-back">
						<i class="fas fa-arrow-left"></i>
						<span>Вернуться<?=$_is_backup?' в сервисное обслуживание':''?></span>
					</a>
					<? } ?>
					<div class="upd-head-title">
						<h1><?=$_is_backup?'Восстановление БД':'Сервисное обслуживание'?></h1>
						<? if(!$_is_backup) { ?><div class="upd-head-version">Версия <?=$site_version?></div><? } ?>
					</div>
				</div>

				<div class="upd-body">

				<? if($_is_done) { ?>
				<div class="upd-success">
					<span class="upd-success-icon">&#10003;</span>
					<span><?=$_is_backup?'Восстановление БД успешно завершено!':'Сервисное обслуживание успешно завершено!'?></span>
				</div>
				<? } ?>

				<? if($_show_stepper) { ?>
				<? if($_is_running) { ?>
				<p class="upd-warning-running">Не выключайте устройство.<br>Не закрывайте приложение.<br>Дождитесь окончания процедуры.</p>
				<? } ?>
				<ul class="stepper">
				<? foreach($stage_title as $_snum=>$_stitle) {
					$_sst='pending';
					if($_is_done) { $_sst='done'; }
					elseif(isset($stage) && $stage>0) {
						if($_snum<$stage) { $_sst='done'; }
						elseif($_snum==$stage) {
							if($error!='') { $_sst='error'; }
							elseif($ok && !isset($next_step)) { $_sst='done'; }
							else { $_sst='active'; }
						}
					}
				?>
				<li class="stepper__item s-<?=$_sst?>">
					<span class="stepper__dot"><?
						if($_sst=='done') echo '&#10003;';
						elseif($_sst=='error') echo '&#10007;';
					?></span>
					<span class="stepper__label"><?=htmlspecialchars($_stitle)?></span>
				</li>
				<? } ?>
				</ul>
				<? } ?>

				<? // Авто-переход к следующему этапу — логика без изменений ?>
				<? if(isset($stage) && $stage>0 && $error=='') { ?>
				<script>
				<? if(!isset($next_step)) { ?>
				location.replace('/update_new.php?<?=isset($get['backup'])?'backup=1&':''?>stage=<?=$next_stage?>');
				<? } else { ?>
				location.replace('/update_new.php?<?=isset($get['backup'])?'backup=1&':''?>stage=<?=$stage?>&next_step=<?=urlencode($next_step)?>');
				<? } ?>
				</script>
				<? } ?>

				<? if(!$stage || $error!='') { ?>

					<? if($error!='') { ?>
					<div class="upd-error"><?=$error?></div>
					<div class="upd-actions">
						<? if(!isset($_FILES['file']['tmp_name'])) { ?>
						<a href=""><input type="button" value="Попробовать ещё раз"></a>
						<? } ?>
					</div>
					<? } ?>

					<? if($_is_done) { ?>
					<div class="upd-actions">
						<? if($_is_backup) { ?>
						<a href="/update_new.php"><input type="button" value="Вернуться в сервисное обслуживание"></a>
						<? } ?>
						<a href="/"><input type="button" class="upd-btn-primary" value="На главную"></a>
					</div>

					<? } elseif(!$_is_backup) { // раздел обновления ?>

					<p class="upd-warning">Обслуживание займёт несколько минут. После начала не выключайте устройство и не закрывайте приложение до окончания процесса.</p>
					<div class="upd-actions">
						<? if(is_array($get_update) && $get_update['success']===true) { ?>
						<? if(compare_version($site_version, $get_update['version']['version'])) { ?>
						<div class="upd-new-version">Доступна новая версия: <b><?=$get_update['version']['version']?></b></div>
						<? } ?>
						<a href="/update_new.php?stage=1" id="btn_update_start">
							<input type="button" class="upd-btn-primary" value="<?=$site_version!=$get_update['version']['version']?'Обновить до версии '.$get_update['version']['version']:'Установить последнюю версию'?>">
						</a>
						<? } elseif(is_array($get_update) && isset($get_update['error'])) { ?>
						<div class="upd-error" style="margin-bottom:0"><?=$get_update['error']?></div>
						<? } ?>
						<form action="/update_new.php" method="POST" enctype="multipart/form-data" id="input_file_form">
							<input type="file" name="file" accept=".zip" class="hide" id="input_file_input">
							<label for="input_file_input" class="upd-file-label">Обновить из файла</label>
						</form>
						<hr class="upd-divider">
						<a href="/update_new.php?backup=1">
							<input type="button" value="Восстановить БД">
						</a>
					</div>
					<script>
					function lockActions() {
						var a = document.querySelector('.upd-actions');
						if (a) a.classList.add('is-loading');
						var b = document.querySelector('.upd-head-back');
						if (b) b.style.visibility = 'hidden';
					}
					var s = document.getElementById('btn_update_start');
					if (s) s.addEventListener('click', lockActions);
					document.getElementById('input_file_input').onchange = function() {
						lockActions();
						document.getElementById('input_file_form').submit();
					};
					</script>

					<? } else { // раздел восстановления ?>

					<? if((isset($backups) && isset($backups['success']) && $backups['success']===true) || (isset($_GET['page']) && $_GET['page']>1)) { ?>
						<? if(isset($backups) && isset($backups['success']) && $backups['success']===true) { ?>
						<p class="upd-warning" style="margin-bottom:12px">Выберите резервную копию БД для восстановления:</p>
						<? if($base_settings['autonomous']!=1) { ?>
						<div class="upd-actions" style="margin-top:0;margin-bottom:14px">
							<? if($_GET['all']==1) { ?>
							<a href="/update_new.php?backup=1&all=0"><input type="button" value="Искать только для этой кассы"></a>
							<? } else { ?>
							<a href="/update_new.php?backup=1&all=1"><input type="button" value="Искать для всех касс"></a>
							<? } ?>
						</div>
						<? } ?>
						<table class="backup-tbl">
							<thead><tr>
								<th>Дата и время</th>
								<? if($base_settings['autonomous']!=1) { ?><th>Касса</th><? } ?>
								<th>Версия</th>
								<th></th>
							</tr></thead>
							<tbody>
							<? foreach($backups['data'] as $backup) { ?>
							<tr>
								<td>
									<? if($backup['time']>0) { ?>
									<?=date('d.m.Y', $backup['time'])?>
									<span class="bt-time-sub"><?=date('H:i:s', $backup['time'])?></span>
									<? } else { ?>—<? } ?>
								</td>
								<? if($base_settings['autonomous']!=1) { ?>
								<td><?=$backups['cashboxes'][$backup['rm_id']]['rm_name']??$backup['rm_id']?></td>
								<? } ?>
								<td><?=$backup['version']?></td>
								<td>
									<form action="/update_new.php" method="GET">
										<input type="hidden" name="backup" value="1">
										<input type="hidden" name="db" value="<?=$backup['file_name']?>">
										<input type="hidden" name="hash" value="<?=$backup['hash']??''?>">
										<input type="submit" value="Выбрать">
									</form>
								</td>
							</tr>
							<? } ?>
							</tbody>
						</table>
						<? } ?>
						<? if($base_settings['autonomous']!=1) { ?>
						<div class="upd-pagination">
							<? if(isset($_GET['page']) && $_GET['page']>2) { ?>
							<a href="/update_new.php?backup=1&all=<?=$_GET['all']??0?>&page=1"><input type="button" value="Страница 1"></a>
							<? } ?>
							<? if(isset($_GET['page']) && $_GET['page']>1) { ?>
							<a href="/update_new.php?backup=1&all=<?=$_GET['all']??0?>&page=<?=max(1,($_GET['page']??1)-1)?>"><input type="button" value="Назад"></a>
							<? } ?>
							<? if(!empty($backups['data']) && count($backups['data'])==$ini['lines_of_page']) { ?>
							<a href="/update_new.php?backup=1&all=<?=$_GET['all']??0?>&page=<?=($_GET['page']??1)+1?>"><input type="button" value="Дальше"></a>
							<? } ?>
						</div>
						<? } ?>
					<? } else { ?>
					<div class="upd-empty"><?=isset($backups) && isset($backups['error']) && $backups['error']!=''?$backups['error']:'Резервные копии БД отсутствуют'?></div>
						<? if($base_settings['autonomous']!=1) { ?>
					<div class="upd-actions" style="margin-top:0">
						<a href="/update_new.php?backup=1&all=1"><input type="button" value="Искать для всех касс"></a>
					</div>
						<? } ?>
					<? } ?>

					<? } // end разделы ?>

				<? } // end !$stage || $error!='' ?>

				</div><!-- .upd-body -->
				</div><!-- .upd-card -->
			</div><!-- .upd-page -->
		</div>
	</div>
	</body>
</html>
