<?php
if (!function_exists('upd_path_join')) {
function upd_path_join(string ...$parts): string {
    $clean = [];
    foreach ($parts as $i => $part) {
        if ($part === '') continue;
        if ($i === 0) {
            $clean[] = rtrim($part, DIRECTORY_SEPARATOR);
        } else {
            $clean[] = trim($part, DIRECTORY_SEPARATOR);
        }
    }
    return implode(DIRECTORY_SEPARATOR, $clean);
}
}

if (!function_exists('upd_ensure_dir')) {
function upd_ensure_dir(string $dir): bool {
    return is_dir($dir) || @mkdir($dir, 0775, true);
}
}

if (!function_exists('upd_rrmdir')) {
function upd_rrmdir(string $dir): void {
    if (!is_dir($dir)) return;
    $it = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::CHILD_FIRST
    );
    foreach ($it as $item) {
        if ($item->isDir()) {
            @rmdir($item->getPathname());
        } else {
            @unlink($item->getPathname());
        }
    }
    @rmdir($dir);
}
}

if (!function_exists('upd_rcopy')) {
function upd_rcopy(string $src, string $dst): bool {
    if (!file_exists($src)) return false;
    if (is_file($src)) {
        upd_ensure_dir(dirname($dst));
        return @copy($src, $dst);
    }
    upd_ensure_dir($dst);
    $items = scandir($src);
    if ($items === false) return false;
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;
        $from = upd_path_join($src, $item);
        $to = upd_path_join($dst, $item);
        if (is_dir($from)) {
            if (!upd_rcopy($from, $to)) return false;
        } else {
            upd_ensure_dir(dirname($to));
            if (!@copy($from, $to)) return false;
        }
    }
    return true;
}
}


if (!function_exists('upd_rcopy_filtered')) {
function upd_rcopy_filtered(string $src, string $dst, array $excludeRel = []): bool {
    $exclude = [];
    foreach ($excludeRel as $rel) {
        $rel = trim(str_replace('\\', '/', (string)$rel), '/');
        if ($rel !== '') $exclude[$rel] = true;
    }
    $base = realpath($src) ?: $src;
    $walker = function(string $from, string $to) use (&$walker, $base, $exclude): bool {
        if (!file_exists($from)) return false;
        $rel = trim(str_replace('\\', '/', substr($from, strlen($base))), '/');
        if ($rel !== '' && isset($exclude[$rel])) return true;
        foreach ($exclude as $ex => $_) {
            if ($rel !== '' && str_starts_with($rel . '/', $ex . '/')) return true;
        }
        if (is_file($from)) {
            upd_ensure_dir(dirname($to));
            return @copy($from, $to);
        }
        upd_ensure_dir($to);
        $items = scandir($from);
        if ($items === false) return false;
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') continue;
            $childFrom = upd_path_join($from, $item);
            $childTo = upd_path_join($to, $item);
            if (!$walker($childFrom, $childTo)) return false;
        }
        return true;
    };
    return $walker($src, $dst);
}
}

if (!function_exists('upd_read_version_info')) {
function upd_read_version_info(string $adminDir): array {
    $file = upd_path_join($adminDir, 'version.php');
    $info = ['version' => '0.0.0', 'build' => '', 'name' => ''];
    if (is_file($file)) {
        $ret = include $file;
        if (is_array($ret)) {
            foreach (['version','build','name'] as $k) {
                if (isset($ret[$k]) && is_scalar($ret[$k])) $info[$k] = (string)$ret[$k];
            }
        }
    }
    return $info;
}
}

if (!function_exists('upd_write_version_info')) {
function upd_write_version_info(string $adminDir, array $info): bool {
    $version = var_export((string)($info['version'] ?? '0.0.0'), true);
    $build = var_export((string)($info['build'] ?? date('Y-m-d H:i:s')), true);
    $name = var_export((string)($info['name'] ?? 'Updated Release'), true);
    $php = "<?php\nreturn [\n    'version' => {$version},\n    'build' => {$build},\n    'name' => {$name},\n];\n";
    return @file_put_contents(upd_path_join($adminDir, 'version.php'), $php) !== false;
}
}

if (!function_exists('upd_default_manifest')) {
function upd_default_manifest(string $stageDir, string $targetAdminDir, string $packageName = 'legacy.zip'): array {
    $root = $stageDir;
    if (is_dir(upd_path_join($stageDir, 'admin'))) {
        $root = upd_path_join($stageDir, 'admin');
    } elseif (is_dir(upd_path_join($stageDir, 'mikrotik', 'admin'))) {
        $root = upd_path_join($stageDir, 'mikrotik', 'admin');
    }

    $files = [];
    $it = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)
    );
    foreach ($it as $item) {
        if ($item->isDir()) continue;
        $rel = str_replace('\\', '/', substr($item->getPathname(), strlen($root) + 1));
        $files[] = $rel;
    }
    sort($files);

    $current = upd_read_version_info($targetAdminDir);
    return [
        'package_name' => $packageName,
        'version' => $current['version'],
        'name' => 'Legacy Admin Update Package',
        'description' => 'Auto-generated manifest for a package without manifest.json',
        'target' => 'admin',
        'root_dir' => basename($root) === 'admin' ? 'admin' : '.',
        'strategy' => 'replace',
        'files' => $files,
        'migrations' => [],
        'protected_files' => [
            'config.php',
            '../config.php',
            '../includes/db.php',
            'includes/db.php'
        ],
        'allow_legacy' => true,
    ];
}
}

if (!function_exists('upd_load_manifest')) {
function upd_load_manifest(string $stageDir, string $targetAdminDir, string $packageName = 'package.zip'): array {
    $candidates = [
        upd_path_join($stageDir, 'manifest.json'),
        upd_path_join($stageDir, 'admin', 'manifest.json'),
        upd_path_join($stageDir, 'mikrotik', 'admin', 'manifest.json'),
    ];

    foreach ($candidates as $file) {
        if (!is_file($file)) continue;
        $json = @file_get_contents($file);
        $arr = json_decode((string)$json, true);
        if (!is_array($arr)) {
            throw new RuntimeException('manifest.json غير صالح.');
        }
        $arr['_manifest_path'] = $file;
        return $arr;
    }

    return upd_default_manifest($stageDir, $targetAdminDir, $packageName);
}
}

if (!function_exists('upd_normalize_manifest')) {
function upd_normalize_manifest(array $manifest): array {
    $m = $manifest;
    $m['package_name'] = (string)($m['package_name'] ?? 'update-package');
    $m['version'] = trim((string)($m['version'] ?? '0.0.0'));
    $m['name'] = (string)($m['name'] ?? 'Update Package');
    $m['description'] = (string)($m['description'] ?? '');
    $m['target'] = (string)($m['target'] ?? 'admin');
    $m['root_dir'] = (string)($m['root_dir'] ?? '.');
    $m['strategy'] = (string)($m['strategy'] ?? 'replace');
    $m['min_supported_version'] = trim((string)($m['min_supported_version'] ?? ''));
    $m['migrations'] = array_values(array_filter((array)($m['migrations'] ?? []), 'is_string'));
    $m['files'] = array_values(array_filter((array)($m['files'] ?? []), 'is_string'));
    $m['protected_files'] = array_values(array_filter((array)($m['protected_files'] ?? []), 'is_string'));
    return $m;
}
}

if (!function_exists('upd_validate_rel_path')) {
function upd_validate_rel_path(string $path): bool {
    $path = str_replace('\\', '/', trim($path));
    if ($path === '' || $path[0] === '/') return false;
    if (strpos($path, '../') !== false || strpos($path, '..\\') !== false) return false;
    return true;
}
}

if (!function_exists('upd_compare_versions')) {
function upd_compare_versions(string $a, string $b): int {
    return version_compare($a, $b);
}
}

if (!function_exists('upd_collect_writable_issues')) {
function upd_collect_writable_issues(array $paths): array {
    $issues = [];
    foreach ($paths as $label => $path) {
        if (!file_exists($path)) {
            $parent = dirname($path);
            if (!is_dir($parent) || !is_writable($parent)) {
                $issues[] = $label . ' غير قابل للإنشاء أو الكتابة: ' . $path;
            }
            continue;
        }
        if (!is_writable($path)) {
            $issues[] = $label . ' غير قابل للكتابة: ' . $path;
        }
    }
    return $issues;
}
}

if (!function_exists('upd_extract_zip_safely')) {
function upd_extract_zip_safely(string $zipPath, string $stageDir): void {
    if (!class_exists('ZipArchive')) {
        throw new RuntimeException('امتداد ZipArchive غير متاح على السيرفر.');
    }
    $zip = new ZipArchive();
    if ($zip->open($zipPath) !== true) {
        throw new RuntimeException('تعذر فتح ملف التحديث ZIP.');
    }

    upd_rrmdir($stageDir);
    upd_ensure_dir($stageDir);

    for ($i = 0; $i < $zip->numFiles; $i++) {
        $name = $zip->getNameIndex($i);
        if ($name === false) continue;
        $name = str_replace('\\', '/', $name);
        if ($name === '' || str_starts_with($name, '/') || strpos($name, '../') !== false) {
            $zip->close();
            throw new RuntimeException('ملف التحديث يحتوي مسارًا غير آمن: ' . $name);
        }
    }

    if (!$zip->extractTo($stageDir)) {
        $zip->close();
        throw new RuntimeException('فشل فك ضغط الحزمة.');
    }
    $zip->close();
}
}

if (!function_exists('upd_find_package_root')) {
function upd_find_package_root(string $stageDir, array $manifest): string {
    $rootDir = trim((string)($manifest['root_dir'] ?? '.'));
    if ($rootDir === '.' || $rootDir === '') {
        if (is_dir(upd_path_join($stageDir, 'admin'))) return upd_path_join($stageDir, 'admin');
        if (is_dir(upd_path_join($stageDir, 'mikrotik', 'admin'))) return upd_path_join($stageDir, 'mikrotik', 'admin');
        return $stageDir;
    }
    $candidate = upd_path_join($stageDir, $rootDir);
    if (!is_dir($candidate)) {
        throw new RuntimeException('المجلد المحدد في root_dir غير موجود داخل الحزمة.');
    }
    return $candidate;
}
}

if (!function_exists('upd_backup_directory')) {
function upd_backup_directory(string $sourceAdminDir, string $backupsDir): string {
    $name = 'admin_' . date('Ymd_His');
    $dest = upd_path_join($backupsDir, $name);
    $snapshot = upd_path_join($dest, 'admin_snapshot');
    if (!upd_ensure_dir($dest)) {
        throw new RuntimeException('تعذر إنشاء مجلد النسخة الاحتياطية.');
    }
    if (!upd_rcopy_filtered($sourceAdminDir, $snapshot, ['updates'])) {
        throw new RuntimeException('فشل إنشاء النسخة الاحتياطية.');
    }
    return $dest;
}
}

if (!function_exists('upd_backup_db_if_possible')) {
function upd_backup_db_if_possible(string $backupDir): array {
    $result = ['ok' => false, 'file' => null, 'message' => ''];
    $rootConfig = realpath(__DIR__ . '/..') ? upd_path_join(realpath(__DIR__ . '/..'), 'config.php') : null;
    if (!$rootConfig || !is_file($rootConfig)) {
        $result['message'] = 'لم يتم العثور على config.php الخاص بالمشروع.';
        return $result;
    }

    $config = @file_get_contents($rootConfig);
    if (!is_string($config)) {
        $result['message'] = 'تعذر قراءة config.php.';
        return $result;
    }

    $patterns = [
        'host' => '/(?:db_host|DB_HOST)\s*[=,]\s*[\"\']([^\"\']+)[\"\']/i',
        'user' => '/(?:db_user|DB_USER)\s*[=,]\s*[\"\']([^\"\']+)[\"\']/i',
        'pass' => '/(?:db_pass|db_password|DB_PASS|DB_PASSWORD)\s*[=,]\s*[\"\']([^\"\']*)[\"\']/i',
        'name' => '/(?:db_name|DB_NAME)\s*[=,]\s*[\"\']([^\"\']+)[\"\']/i',
        'port' => '/(?:db_port|DB_PORT)\s*[=,]\s*[\"\']?([0-9]+)[\"\']?/i',
    ];
    $creds = ['host' => '127.0.0.1', 'port' => 3306, 'user' => '', 'pass' => '', 'name' => ''];
    foreach ($patterns as $k => $regex) {
        if (preg_match($regex, $config, $m)) $creds[$k] = $m[1];
    }
    if ($creds['user'] === '' || $creds['name'] === '') {
        $result['message'] = 'لم أستطع استخراج بيانات قاعدة البيانات من config.php.';
        return $result;
    }
    if (!class_exists('mysqli')) {
        $result['message'] = 'امتداد mysqli غير متاح.';
        return $result;
    }
    mysqli_report(MYSQLI_REPORT_OFF);
    $db = @new mysqli($creds['host'], $creds['user'], $creds['pass'], $creds['name'], (int)$creds['port']);
    if ($db->connect_errno) {
        $result['message'] = 'تعذر الاتصال بقاعدة البيانات لأخذ نسخة SQL.';
        return $result;
    }
    $db->set_charset('utf8mb4');
    $dump = "-- Admin local updater database backup\n-- Generated: " . date('Y-m-d H:i:s') . "\n\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS=0;\n\n";
    $tables = [];
    $res = $db->query('SHOW TABLES');
    while ($res && ($row = $res->fetch_array(MYSQLI_NUM))) {
        $tables[] = $row[0];
    }
    foreach ($tables as $table) {
        $tableEsc = '`' . str_replace('`', '``', $table) . '`';
        $cr = $db->query('SHOW CREATE TABLE ' . $tableEsc);
        $createRow = $cr ? $cr->fetch_assoc() : null;
        if ($createRow) {
            $createSql = $createRow['Create Table'] ?? array_values($createRow)[1] ?? '';
            $dump .= "DROP TABLE IF EXISTS {$tableEsc};\n{$createSql};\n\n";
        }
        $data = $db->query('SELECT * FROM ' . $tableEsc);
        if ($data && $data->num_rows > 0) {
            while ($r = $data->fetch_assoc()) {
                $cols = [];
                $vals = [];
                foreach ($r as $col => $val) {
                    $cols[] = '`' . str_replace('`', '``', (string)$col) . '`';
                    $vals[] = is_null($val) ? 'NULL' : "'" . $db->real_escape_string((string)$val) . "'";
                }
                $dump .= 'INSERT INTO ' . $tableEsc . ' (' . implode(',', $cols) . ') VALUES (' . implode(',', $vals) . ");\n";
            }
            $dump .= "\n";
        }
    }
    $dump .= "SET FOREIGN_KEY_CHECKS=1;\n";
    $file = upd_path_join($backupDir, 'database_backup.sql');
    if (@file_put_contents($file, $dump) === false) {
        $result['message'] = 'فشل حفظ نسخة قاعدة البيانات.';
        return $result;
    }
    $result['ok'] = true;
    $result['file'] = $file;
    $result['message'] = 'تم حفظ نسخة SQL بنجاح.';
    return $result;
}
}

if (!function_exists('upd_run_sql_file')) {
function upd_run_sql_file(mysqli $db, string $file): void {
    $sql = @file_get_contents($file);
    if (!is_string($sql) || trim($sql) === '') return;
    if (!$db->multi_query($sql)) {
        throw new RuntimeException('فشل تنفيذ migration: ' . basename($file) . ' - ' . $db->error);
    }
    do {
        if ($res = $db->store_result()) {
            $res->free();
        }
    } while ($db->more_results() && $db->next_result());
}
}

if (!function_exists('upd_connect_db')) {
function upd_connect_db(): ?mysqli {
    $conn = $GLOBALS['conn'] ?? null;
    if ($conn instanceof mysqli) {
        return $conn;
    }

    $bootstrap = upd_path_join(__DIR__, 'db_bootstrap.php');
    if (is_file($bootstrap)) {
        try {
            @include_once $bootstrap;
            if (function_exists('ensure_db')) {
                $safe = @ensure_db(false);
                if ($safe instanceof mysqli && empty($safe->connect_errno)) {
                    $GLOBALS['conn'] = $safe;
                    return $safe;
                }
            }
        } catch (Throwable $e) {
            return null;
        }

        $conn = $GLOBALS['conn'] ?? null;
        if ($conn instanceof mysqli && empty($conn->connect_errno)) {
            return $conn;
        }
    }

    return null;
}
}

if (!function_exists('upd_apply_package')) {
function upd_apply_package(string $sourceRoot, string $targetAdminDir, array $manifest, string $backupDir, array &$applied = []): void {
    $protected = array_map(static function($v){ return ltrim(str_replace('\\','/',trim((string)$v)), '/'); }, (array)$manifest['protected_files']);
    $files = (array)$manifest['files'];
    if (empty($files)) {
        $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($sourceRoot, FilesystemIterator::SKIP_DOTS));
        foreach ($it as $item) {
            if ($item->isDir()) continue;
            $files[] = str_replace('\\','/', substr($item->getPathname(), strlen($sourceRoot) + 1));
        }
        sort($files);
    }

    foreach ($files as $rel) {
        $rel = ltrim(str_replace('\\','/',trim((string)$rel)), '/');
        if (!upd_validate_rel_path($rel)) {
            throw new RuntimeException('الحزمة تحتوي مسار ملف غير آمن: ' . $rel);
        }
        if (in_array($rel, $protected, true)) {
            continue;
        }
        $src = upd_path_join($sourceRoot, $rel);
        $dst = upd_path_join($targetAdminDir, $rel);
        if (!file_exists($src)) {
            throw new RuntimeException('الملف المذكور في manifest غير موجود: ' . $rel);
        }
        if (is_dir($src)) {
            upd_ensure_dir($dst);
            continue;
        }
        upd_ensure_dir(dirname($dst));
        if (file_exists($dst)) {
            $shadow = upd_path_join($backupDir, '__files_before_apply', $rel);
            upd_ensure_dir(dirname($shadow));
            @copy($dst, $shadow);
        }
        if (!@copy($src, $dst)) {
            throw new RuntimeException('فشل نسخ الملف: ' . $rel);
        }
        $applied[] = $rel;
    }
}
}

if (!function_exists('upd_log_append')) {
function upd_log_append(string $updatesDir, array $row): void {
    upd_ensure_dir($updatesDir);
    $file = upd_path_join($updatesDir, 'update_log.jsonl');
    $row['time'] = $row['time'] ?? date('Y-m-d H:i:s');
    @file_put_contents($file, json_encode($row, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL, FILE_APPEND);
}
}

if (!function_exists('upd_log_read')) {
function upd_log_read(string $updatesDir, int $limit = 50): array {
    $file = upd_path_join($updatesDir, 'update_log.jsonl');
    if (!is_file($file)) return [];
    $lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
    $lines = array_slice($lines, -1 * abs($limit));
    $rows = [];
    foreach ($lines as $line) {
        $row = json_decode((string)$line, true);
        if (is_array($row)) $rows[] = $row;
    }
    return array_reverse($rows);
}
}

if (!function_exists('upd_read_package_info')) {
function upd_read_package_info(string $zipPath, string $stageDir, string $adminDir): array {
    upd_extract_zip_safely($zipPath, $stageDir);
    $manifest = upd_normalize_manifest(upd_load_manifest($stageDir, $adminDir, basename($zipPath)));
    $sourceRoot = upd_find_package_root($stageDir, $manifest);
    if ($manifest['version'] === '') {
        throw new RuntimeException('الحزمة لا تحدد رقم إصدار صالح.');
    }
    return [$manifest, $sourceRoot];
}
}
