<?php
/**
 * Manual patcher for Intec Bitrix modules.
 *
 * Place intec.* module folders next to this file in intec_patcher/
 * and open /intec_patcher/ (index.php) as a Bitrix administrator.
 * User guide: intec_patcher/instruction.txt
 */

define('INTEC_PATCHER_VERSION', '1.0.1');

$documentRoot = rtrim(str_replace('\\', '/', (string)$_SERVER['DOCUMENT_ROOT']), '/');
$patchRoot = str_replace('\\', '/', __DIR__);
$backupSuffix = '.php_patcher_backup_'.date('d_m_y');

if ($documentRoot === '') {
    die('DOCUMENT_ROOT is empty');
}

$prolog = $documentRoot.'/bitrix/modules/main/include/prolog_before.php';
if (!file_exists($prolog)) {
    die('Bitrix prolog not found');
}

require_once($prolog);

global $USER;
if (!is_object($USER) || !$USER->IsAdmin()) {
    http_response_code(403);
    die('Access denied');
}

function ip_norm_path($path)
{
    return str_replace('\\', '/', (string)$path);
}

function ip_starts_with($value, $prefix)
{
    return substr($value, 0, strlen($prefix)) === $prefix;
}

function ip_relative_path($path, $base)
{
    $path = ip_norm_path($path);
    $base = rtrim(ip_norm_path($base), '/').'/';
    if (ip_starts_with($path, $base)) {
        return substr($path, strlen($base));
    }
    return $path;
}

function ip_html($value)
{
    return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}

function ip_get_sites()
{
    $sites = array();

    if (class_exists('CSite')) {
        $by = 'sort';
        $order = 'asc';
        $rsSites = CSite::GetList($by, $order);
        while ($site = $rsSites->Fetch()) {
            $lid = trim((string)$site['LID']);
            if ($lid !== '') {
                $sites[] = $lid;
            }
        }
    }

    if (empty($sites) && defined('SITE_ID')) {
        $sites[] = SITE_ID;
    }

    return array_values(array_unique($sites));
}

function ip_find_patch_modules($patchRoot)
{
    $modules = array();
    $items = scandir($patchRoot);

    if (!is_array($items)) {
        return $modules;
    }

    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }

        $path = $patchRoot.'/'.$item;
        if (is_dir($path) && preg_match('/^intec\.[a-z0-9_]+$/i', $item)) {
            $modules[] = $item;
        }
    }

    sort($modules);
    return $modules;
}

function ip_module_suffix($moduleId)
{
    if (preg_match('/^intec\.(.+)$/i', $moduleId, $matches)) {
        return $matches[1];
    }

    return '';
}

function ip_module_labels_config_path($patchRoot)
{
    return rtrim(ip_norm_path($patchRoot), '/').'/module_labels.php';
}

function ip_module_labels_config($patchRoot)
{
    static $labels = null;

    if ($labels !== null) {
        return $labels;
    }

    $labels = array();
    $configPath = ip_module_labels_config_path($patchRoot);

    if (is_file($configPath)) {
        $loaded = include $configPath;
        if (is_array($loaded)) {
            $labels = $loaded;
        }
    }

    return $labels;
}

function ip_read_lang_messages($file)
{
    if (!is_readable($file)) {
        return array();
    }

    $MESS = array();
    include $file;

    return is_array($MESS) ? $MESS : array();
}

function ip_extract_wizard_name_from_messages(array $messages, $moduleId)
{
    $suffix = ip_module_suffix($moduleId);
    $keys = array($moduleId.'.wizard.name');

    if ($suffix !== '') {
        $keys[] = 'intec.'.$suffix.'.wizard.name';
    }

    foreach ($keys as $key) {
        if (!empty($messages[$key]) && is_string($messages[$key])) {
            return trim($messages[$key]);
        }
    }

    foreach ($messages as $key => $value) {
        if (!is_string($value) || trim($value) === '') {
            continue;
        }
        if (preg_match('/\.wizard\.name$/', (string)$key)) {
            return trim($value);
        }
    }

    return '';
}

function ip_extract_module_name_from_messages(array $messages, $moduleId)
{
    foreach (array($moduleId.'.installer.name', $moduleId.'.install.name') as $key) {
        if (!empty($messages[$key]) && is_string($messages[$key])) {
            return trim($messages[$key]);
        }
    }

    foreach ($messages as $key => $value) {
        if (!is_string($value) || trim($value) === '') {
            continue;
        }
        if (preg_match('/\.installer\.name$/', (string)$key)) {
            return trim($value);
        }
    }

    return '';
}

function ip_wizard_lang_candidates($documentRoot, $patchRoot, $moduleId)
{
    $suffix = ip_module_suffix($moduleId);
    if ($suffix === '') {
        return array();
    }

    $relative = 'wizards/intec/'.$suffix.'/lang/ru/.description.php';
    $candidates = array(
        $patchRoot.'/'.$moduleId.'/install/'.$relative,
        $documentRoot.'/bitrix/'.$relative,
        $documentRoot.'/local/'.$relative,
        $documentRoot.'/bitrix/modules/'.$moduleId.'/install/'.$relative,
        $documentRoot.'/local/modules/'.$moduleId.'/install/'.$relative,
    );

    $enRelative = 'wizards/intec/'.$suffix.'/lang/en/.description.php';
    $candidates[] = $patchRoot.'/'.$moduleId.'/install/'.$enRelative;
    $candidates[] = $documentRoot.'/bitrix/'.$enRelative;
    $candidates[] = $documentRoot.'/bitrix/modules/'.$moduleId.'/install/'.$enRelative;

    return $candidates;
}

function ip_module_install_lang_candidates($documentRoot, $moduleId)
{
    $relative = 'lang/ru/install/index.php';

    return array(
        $documentRoot.'/bitrix/modules/'.$moduleId.'/'.$relative,
        $documentRoot.'/local/modules/'.$moduleId.'/'.$relative,
        $documentRoot.'/bitrix/modules/'.$moduleId.'/lang/en/install/index.php',
    );
}

function ip_discover_module_label($documentRoot, $patchRoot, $moduleId)
{
    foreach (ip_wizard_lang_candidates($documentRoot, $patchRoot, $moduleId) as $file) {
        $name = ip_extract_wizard_name_from_messages(ip_read_lang_messages($file), $moduleId);
        if ($name !== '') {
            return $name;
        }
    }

    foreach (ip_module_install_lang_candidates($documentRoot, $moduleId) as $file) {
        $name = ip_extract_module_name_from_messages(ip_read_lang_messages($file), $moduleId);
        if ($name !== '') {
            return $name;
        }
    }

    $config = ip_module_labels_config($patchRoot);
    if (!empty($config[$moduleId]) && is_string($config[$moduleId])) {
        return trim($config[$moduleId]);
    }

    $suffix = ip_module_suffix($moduleId);
    if ($suffix !== '') {
        return ucfirst(str_replace(array('_', '.'), array(' ', ' '), $suffix));
    }

    return $moduleId;
}

function ip_module_has_wizard($patchRoot, $moduleId)
{
    $suffix = ip_module_suffix($moduleId);
    if ($suffix === '') {
        return false;
    }

    return is_dir($patchRoot.'/'.$moduleId.'/install/wizards/intec/'.$suffix)
        || is_dir($patchRoot.'/'.$moduleId.'/install/wizards/intec/'.$suffix.'/site');
}

function ip_module_label_kind($documentRoot, $patchRoot, $moduleId)
{
    if (ip_module_has_wizard($patchRoot, $moduleId)) {
        return 'solution';
    }

    $suffix = ip_module_suffix($moduleId);
    $utilitySuffixes = array('core', 'regionality', 'startshop', 'constructorlite');
    if ($suffix !== '' && in_array($suffix, $utilitySuffixes, true)) {
        return 'utility';
    }

    foreach (ip_wizard_lang_candidates($documentRoot, $patchRoot, $moduleId) as $file) {
        if (is_file($file)) {
            return 'solution';
        }
    }

    return 'module';
}

function ip_resolve_module_labels($documentRoot, $patchRoot, array $moduleIds)
{
    $labels = array();

    foreach ($moduleIds as $moduleId) {
        $labels[$moduleId] = array(
            'title' => ip_discover_module_label($documentRoot, $patchRoot, $moduleId),
            'kind' => ip_module_label_kind($documentRoot, $patchRoot, $moduleId),
        );
    }

    return $labels;
}

function ip_module_label_title(array $moduleLabels, $moduleId)
{
    return isset($moduleLabels[$moduleId]['title'])
        ? $moduleLabels[$moduleId]['title']
        : $moduleId;
}

function ip_module_label_kind_value(array $moduleLabels, $moduleId)
{
    return isset($moduleLabels[$moduleId]['kind'])
        ? $moduleLabels[$moduleId]['kind']
        : 'module';
}

function ip_is_ignored_patch_relative($relative)
{
    return (bool)preg_match('#^(?:\d+\.){2,}\d+(?:\.\d+)?(?:/|$)#', $relative);
}

function ip_scan_files($dir)
{
    $files = array();

    if (!is_dir($dir)) {
        return $files;
    }

    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
    );

    foreach ($iterator as $file) {
        if (!$file->isFile()) {
            continue;
        }

        $path = ip_norm_path($file->getPathname());
        $name = basename($path);

        if ($name === 'index.php' || $name === 'intec_patcher.php' || $name === 'updater.php'
            || $name === 'instruction.txt' || $name === 'module_labels.php') {
            continue;
        }

        if (preg_match('/\.php_patcher_backup_\d{2}_\d{2}_\d{2}(?:_\d{6})?$/', $name)) {
            continue;
        }

        $files[] = $path;
    }

    sort($files);
    return $files;
}

function ip_template_names($templateName, $sites)
{
    $names = array($templateName);

    foreach ($sites as $siteId) {
        $names[] = $templateName.'_'.$siteId;
    }

    return array_values(array_unique($names));
}

function ip_add_bitrix_local_targets(array &$targets, $section, $documentRoot, $relativePath)
{
    $targets[] = array(
        'section' => $section,
        'path' => $documentRoot.'/bitrix/'.$relativePath,
    );
    $targets[] = array(
        'section' => $section,
        'path' => $documentRoot.'/local/'.$relativePath,
    );
}

function ip_resolve_targets($documentRoot, $moduleId, $relative, $sites)
{
    $relative = ip_norm_path($relative);
    $targets = array();
    $wizardName = ip_module_suffix($moduleId);

    if (ip_starts_with($relative, 'install/components/')) {
        $sub = substr($relative, strlen('install/'));
        ip_add_bitrix_local_targets($targets, 'components', $documentRoot, $sub);
        return $targets;
    }

    if ($wizardName !== '') {
        $wizardTemplatesPrefix = 'install/wizards/intec/'.$wizardName.'/site/templates/';
        if (ip_starts_with($relative, $wizardTemplatesPrefix)) {
            $sub = substr($relative, strlen($wizardTemplatesPrefix));
            $parts = explode('/', $sub, 2);
            $templateName = isset($parts[0]) ? trim($parts[0]) : '';
            $templateFile = isset($parts[1]) ? $parts[1] : '';

            if ($templateName !== '' && $templateFile !== '') {
                foreach (ip_template_names($templateName, $sites) as $activeTemplateName) {
                    ip_add_bitrix_local_targets(
                        $targets,
                        'templates',
                        $documentRoot,
                        'templates/'.$activeTemplateName.'/'.$templateFile
                    );
                }
            }

            return $targets;
        }
    }

    if (ip_starts_with($relative, 'install/wizards/')) {
        $sub = substr($relative, strlen('install/'));
        ip_add_bitrix_local_targets($targets, 'wizards', $documentRoot, $sub);
        return $targets;
    }

    if (ip_starts_with($relative, 'install/tools/')) {
        $sub = substr($relative, strlen('install/'));
        ip_add_bitrix_local_targets($targets, 'tools', $documentRoot, $sub);
        return $targets;
    }

    ip_add_bitrix_local_targets(
        $targets,
        'module',
        $documentRoot,
        'modules/'.$moduleId.'/'.$relative
    );

    return $targets;
}

function ip_hash_file($path)
{
    if (!is_file($path)) {
        return '';
    }

    $hash = @hash_file('sha256', $path);
    return is_string($hash) ? $hash : '';
}

function ip_build_manifest($documentRoot, $patchRoot)
{
    $sites = ip_get_sites();
    $modules = ip_find_patch_modules($patchRoot);
    $moduleLabels = ip_resolve_module_labels($documentRoot, $patchRoot, $modules);
    $manifest = array();

    foreach ($modules as $moduleId) {
        $modulePatchRoot = $patchRoot.'/'.$moduleId;
        $patchFiles = ip_scan_files($modulePatchRoot);

        foreach ($patchFiles as $patchFile) {
            $relative = ip_relative_path($patchFile, $modulePatchRoot);

            if (ip_is_ignored_patch_relative($relative)) {
                continue;
            }

            $targets = ip_resolve_targets($documentRoot, $moduleId, $relative, $sites);

            foreach ($targets as $target) {
                $targetPath = ip_norm_path($target['path']);
                if (!is_file($targetPath)) {
                    continue;
                }

                $patchHash = ip_hash_file($patchFile);
                $targetHash = ip_hash_file($targetPath);
                $manifest[] = array(
                    'id' => count($manifest),
                    'module' => $moduleId,
                    'solution' => ip_module_label_title($moduleLabels, $moduleId),
                    'solution_kind' => ip_module_label_kind_value($moduleLabels, $moduleId),
                    'section' => $target['section'],
                    'patch' => $patchFile,
                    'target' => $targetPath,
                    'relative' => $relative,
                    'patch_hash' => $patchHash,
                    'target_hash' => $targetHash,
                    'same' => ($patchHash !== '' && $patchHash === $targetHash),
                    'writable' => is_writable($targetPath) && is_writable(dirname($targetPath)),
                );
            }
        }
    }

    usort($manifest, function ($a, $b) {
        $left = $a['module'].'/'.$a['section'].'/'.$a['relative'].'/'.$a['target'];
        $right = $b['module'].'/'.$b['section'].'/'.$b['relative'].'/'.$b['target'];
        return strcmp($left, $right);
    });

    foreach ($manifest as $idx => $item) {
        $manifest[$idx]['id'] = $idx;
    }

    $manifest = ip_annotate_manifest_conflicts($manifest);

    return array('items' => $manifest, 'sites' => $sites, 'module_labels' => $moduleLabels);
}

function ip_annotate_manifest_conflicts(array $manifest)
{
    $byTarget = array();

    foreach ($manifest as $idx => $item) {
        $byTarget[$item['target']][] = $idx;
    }

    foreach ($byTarget as $indices) {
        if (count($indices) < 2) {
            continue;
        }

        $patchHashes = array();
        foreach ($indices as $idx) {
            $hash = $manifest[$idx]['patch_hash'];
            if ($hash !== '') {
                $patchHashes[$hash] = true;
            }
        }

        if (count($patchHashes) < 2) {
            continue;
        }

        foreach ($indices as $idx) {
            $manifest[$idx]['patch_conflict'] = true;
        }
    }

    return $manifest;
}

function ip_make_backup_path($targetPath, $backupSuffix)
{
    $backupPath = $targetPath.$backupSuffix;
    if (!file_exists($backupPath)) {
        return $backupPath;
    }

    return $backupPath.'_'.date('His');
}

function ip_json_response($data, $code = 200)
{
    http_response_code($code);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    die();
}

function ip_manifest_stats($manifest)
{
    $stats = array(
        'all' => count($manifest),
        'same' => 0,
    );

    foreach ($manifest as $item) {
        if ($item['same']) {
            $stats['same']++;
        }
    }

    return $stats;
}

function ip_find_manifest_items_by_target($manifest, $targetPath)
{
    $items = array();

    foreach ($manifest as $item) {
        if ($item['target'] === $targetPath) {
            $items[] = $item;
        }
    }

    return $items;
}

function ip_manifest_item_payload($item)
{
    return array(
        'id' => (int)$item['id'],
        'module' => $item['module'],
        'solution' => isset($item['solution']) ? $item['solution'] : $item['module'],
        'solution_kind' => isset($item['solution_kind']) ? $item['solution_kind'] : 'module',
        'same' => (bool)$item['same'],
        'writable' => (bool)$item['writable'],
        'patch_conflict' => !empty($item['patch_conflict']),
        'target' => $item['target'],
    );
}

function ip_patch_file($item, $backupSuffix)
{
    if (!is_file($item['patch'])) {
        return array(false, 'Файл патча не найден');
    }

    if (!is_file($item['target'])) {
        return array(false, 'Файл на сайте не найден');
    }

    if (!$item['writable']) {
        return array(false, 'Нет прав на запись в файл или папку');
    }

    if ($item['same']) {
        return array(false, 'Файл уже совпадает с патчем');
    }

    $backupPath = ip_make_backup_path($item['target'], $backupSuffix);
    if (!@copy($item['target'], $backupPath)) {
        return array(false, 'Не удалось создать бэкап: '.$backupPath);
    }

    $oldPerms = @fileperms($item['target']);
    if (!@copy($item['patch'], $item['target'])) {
        return array(false, 'Бэкап создан, но файл не удалось заменить: '.$backupPath);
    }

    if ($oldPerms !== false) {
        @chmod($item['target'], $oldPerms & 0777);
    }

    return array(true, 'Обновлено. Бэкап: '.$backupPath);
}

$data = ip_build_manifest($documentRoot, $patchRoot);
$manifest = $data['items'];
$sites = $data['sites'];
$moduleLabels = isset($data['module_labels']) ? $data['module_labels'] : array();
$patchModules = ip_find_patch_modules($patchRoot);

if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['ajax'])) {
    if (!check_bitrix_sessid()) {
        ip_json_response(array('ok' => false, 'message' => 'Неверный sessid'), 403);
    }

    $action = isset($_POST['action']) ? (string)$_POST['action'] : '';

    if ($action === 'patch_one') {
        $id = isset($_POST['id']) ? (int)$_POST['id'] : -1;

        if (!isset($manifest[$id])) {
            ip_json_response(array('ok' => false, 'message' => 'Файл не найден в манифесте'));
        }

        $targetPath = $manifest[$id]['target'];
        list($ok, $message) = ip_patch_file($manifest[$id], $backupSuffix);

        if ($ok && function_exists('BXClearCache')) {
            BXClearCache(true, '/');
        }

        $data = ip_build_manifest($documentRoot, $patchRoot);
        $manifest = $data['items'];
        $updatedItems = ip_find_manifest_items_by_target($manifest, $targetPath);

        if ($ok && count($updatedItems) > 1) {
            $stillDiff = 0;
            foreach ($updatedItems as $updatedItem) {
                if (!$updatedItem['same']) {
                    $stillDiff++;
                }
            }
            if ($stillDiff > 0) {
                $message .= ' На сайте один файл, но в intec_patcher '
                    .$stillDiff.' других модулей предлагают для него другую версию.';
            }
        }

        $itemPayloads = array();
        foreach ($updatedItems as $updatedItem) {
            $itemPayloads[] = ip_manifest_item_payload($updatedItem);
        }

        ip_json_response(array(
            'ok' => $ok,
            'message' => $message,
            'stats' => ip_manifest_stats($manifest),
            'items' => $itemPayloads,
            'item' => !empty($itemPayloads) ? $itemPayloads[0] : null,
        ));
    }

    ip_json_response(array('ok' => false, 'message' => 'Неизвестное действие'));
}

$stats = ip_manifest_stats($manifest);

$sections = array(
    'module' => 'Модуль',
    'components' => 'Компоненты',
    'wizards' => 'Wizards',
    'tools' => 'Tools',
    'templates' => 'Шаблоны',
);

?><!doctype html>
<html lang="ru">
<head>
    <meta charset="utf-8">
    <title>Intec Patcher</title>
    <style>
        * { box-sizing: border-box; }
        body { margin: 0; padding: 20px; font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; color: #1f2328; background: #eef1f4; }
        .wrap { max-width: 1400px; margin: 0 auto; }
        .hero { display: flex; gap: 16px; align-items: stretch; justify-content: space-between; flex-wrap: wrap; margin-bottom: 16px; }
        .hero-main { flex: 1 1 420px; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); color: #fff; border-radius: 12px; padding: 22px 24px; }
        .hero-main h1 { margin: 0 0 6px; font-size: 26px; font-weight: 700; }
        .hero-main p { margin: 0; opacity: 0.9; max-width: 640px; }
        .hero-stats { display: flex; gap: 12px; flex-wrap: wrap; align-items: stretch; }
        .stat-card { min-width: 140px; background: #fff; border-radius: 12px; padding: 16px 18px; border: 1px solid #d8dee4; box-shadow: 0 1px 2px rgba(0,0,0,.04); }
        .stat-card b { display: block; font-size: 28px; line-height: 1.1; margin-top: 4px; color: #1a3a5c; }
        .stat-label { font-size: 12px; color: #656d76; text-transform: uppercase; letter-spacing: .03em; }
        .grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 14px; margin-bottom: 16px; }
        .panel { background: #fff; border: 1px solid #d8dee4; border-radius: 12px; padding: 16px 18px; margin-bottom: 16px; box-shadow: 0 1px 2px rgba(0,0,0,.04); }
        .panel h3 { margin: 0 0 10px; font-size: 15px; }
        .panel ul { margin: 0; padding-left: 18px; }
        .panel li { margin: 5px 0; color: #424a53; }
        .panel-warn { background: #fffbeb; border-color: #f0d060; }
        .toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; }
        .msg { padding: 10px 14px; border-radius: 8px; margin-bottom: 8px; }
        .ok { background: #dafbe1; border: 1px solid #4ac26b; }
        .error { background: #ffebe9; border: 1px solid #ff8182; }
        .section-block { margin-bottom: 18px; }
        .section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 10px; }
        .section-head h2 { margin: 0; font-size: 17px; font-weight: 600; }
        .section-count { font-size: 12px; color: #656d76; background: #f6f8fa; border: 1px solid #d8dee4; border-radius: 999px; padding: 3px 10px; }
        table { width: 100%; border-collapse: separate; border-spacing: 0; background: #fff; border: 1px solid #d8dee4; border-radius: 10px; overflow: hidden; }
        th, td { padding: 10px 12px; border-bottom: 1px solid #eaeef2; vertical-align: top; }
        th { text-align: left; background: #f6f8fa; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: #656d76; font-weight: 600; }
        tr:last-child td { border-bottom: none; }
        tr.same td { background: #f6ffed; }
        tr.diff td { background: #fffbeb; }
        tr.readonly td { background: #fff1f0; }
        code { font-family: Consolas, "Courier New", monospace; font-size: 12px; word-break: break-all; color: #24292f; }
        .path-patch code { color: #0550ae; }
        .badge { display: inline-block; padding: 3px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; border: 1px solid transparent; white-space: nowrap; }
        .badge-ok { background: #dafbe1; border-color: #4ac26b; color: #116329; }
        .badge-warn { background: #fff8c5; border-color: #d4a72c; color: #7d4e00; }
        .badge-error { background: #ffebe9; border-color: #ff8182; color: #cf222e; }
        .badge-info { background: #ddf4ff; border-color: #54aeff; color: #0969da; }
        tr.conflict td { box-shadow: inset 3px 0 0 #cf222e; }
        .solution-title { display: block; font-weight: 600; color: #1a3a5c; font-size: 13px; line-height: 1.35; }
        .solution-meta { display: block; margin-top: 2px; font-size: 11px; color: #656d76; }
        .badge-solution { background: #f0f6fc; border-color: #8bb9e8; color: #1a3a5c; }
        .badge-utility { background: #f6f8fa; border-color: #d0d7de; color: #57606a; }
        button { cursor: pointer; padding: 7px 12px; border: 1px solid #2d5a87; background: #2d5a87; color: #fff; border-radius: 8px; font-size: 13px; }
        button:hover { background: #1a3a5c; }
        button:disabled { opacity: 0.65; cursor: wait; }
        button.secondary { border-color: #d0d7de; background: #fff; color: #24292f; }
        .muted { color: #8c959f; }
        a { color: #0969da; }
    </style>
</head>
<body>
<div class="wrap">
    <div class="hero">
        <div class="hero-main">
            <h1>Intec Patcher</h1>
            <p>Точечное обновление файлов модулей Intec на сайте. Перед заменой создаётся бэкап с суффиксом <code style="color:#fff;"><?php echo ip_html($backupSuffix); ?></code>. <a href="https://intecweb.ru/company/news/obnovlenie_bezopasnosti_resheniy_intec/" style="color:#fff;text-decoration:underline;" target="_blank" rel="noopener">Инструкция</a> (или скачайте
                <a style="color: white" href="instruction.txt" download="">файл</a>)</p>
        </div>
        <div class="hero-stats">
            <div class="stat-card">
                <span class="stat-label">Найдено</span>
                <b id="ip-stat-all"><?php echo (int)$stats['all']; ?></b>
            </div>
            <div class="stat-card">
                <span class="stat-label">Совпадает</span>
                <b id="ip-stat-same"><?php echo (int)$stats['same']; ?></b>
            </div>
        </div>
    </div>

    <div class="toolbar">
        <button type="button" class="secondary" onclick="location.reload()">Пересканировать</button>
        <?php if (!empty($patchModules)) { ?>
            <span class="muted">Модули: <?php echo ip_html(implode(', ', $patchModules)); ?></span>
        <?php } ?>
    </div>

    <div id="ip-messages"></div>

    <div class="grid-2">
        <div class="panel panel-warn">
            <h3>Штатное обновление</h3>
            <ul>
                <li><strong>Модули</strong> — удалите и установите заново через <a href="https://marketplace.1c-bitrix.ru/" target="_blank" rel="noopener">Маркетплейс</a>.</li>
                <li><strong>Готовые сайты</strong> — после обновления модулей переустановите решение в режиме <em>обновления</em> через мастер.</li>
            </ul>
        </div>
        <div class="panel">
            <h3>Куда попадают файлы</h3>
            <ul>
                <li>модуль → <code>/bitrix/modules/</code>, <code>/local/modules/</code></li>
                <li><code>install/components/</code> → <code>/bitrix/components/</code></li>
                <li><code>install/wizards/</code> → <code>/bitrix/wizards/</code></li>
                <li>шаблоны визарда → <code>/bitrix/templates/</code></li>
            </ul>
        </div>
    </div>

    <?php foreach ($sections as $sectionCode => $sectionTitle) {
        $sectionItems = array();
        foreach ($manifest as $item) {
            if ($item['section'] === $sectionCode) {
                $sectionItems[] = $item;
            }
        }
        if (empty($sectionItems)) {
            continue;
        }
        ?>
        <div class="section-block panel">
            <div class="section-head">
                <h2><?php echo ip_html($sectionTitle); ?></h2>
                <span class="section-count"><?php echo count($sectionItems); ?> файлов</span>
            </div>
            <table>
                <thead>
                <tr>
                    <th style="width: 16%;">Решение</th>
                    <th style="width: 30%;">Файл в патче</th>
                    <th>Файл на сайте</th>
                    <th style="width: 110px;">Статус</th>
                    <th style="width: 110px;">Действие</th>
                </tr>
                </thead>
                <tbody>
                <?php foreach ($sectionItems as $item) {
                    $rowClass = $item['same'] ? 'same' : 'diff';
                    if (!$item['writable']) {
                        $rowClass .= ' readonly';
                    }
                    if (!empty($item['patch_conflict'])) {
                        $rowClass .= ' conflict';
                    }
                    $patchPath = ip_relative_path($item['patch'], $patchRoot);
                    ?>
                    <tr class="<?php echo ip_html($rowClass); ?>" data-item-id="<?php echo (int)$item['id']; ?>" data-module="<?php echo ip_html($item['module']); ?>" data-target="<?php echo ip_html($item['target']); ?>">
                        <td class="solution-cell">
                            <span class="solution-title" title="<?php echo ip_html($item['module']); ?>"><?php echo ip_html($item['solution']); ?></span>
                            <span class="solution-meta"><code><?php echo ip_html($item['module']); ?></code></span>
                            <?php if ($item['solution_kind'] === 'utility') { ?>
                                <span class="solution-meta">общий модуль</span>
                            <?php } elseif ($item['solution_kind'] === 'solution') { ?>
                                <span class="solution-meta">готовое решение</span>
                            <?php } ?>
                        </td>
                        <td class="path-patch">
                            <code><?php echo ip_html($patchPath); ?></code>
                            <?php if (!empty($item['patch_conflict'])) { ?>
                                <br><span class="badge badge-info" title="Несколько модулей в intec_patcher ведут на этот файл с разным содержимым">разные патчи</span>
                            <?php } ?>
                        </td>
                        <td><code><?php echo ip_html(ip_relative_path($item['target'], $documentRoot)); ?></code></td>
                        <td class="ip-status-cell">
                            <?php if ($item['same']) { ?>
                                <span class="badge badge-ok">совпадает</span>
                            <?php } elseif (!$item['writable']) { ?>
                                <span class="badge badge-error">нет записи</span>
                            <?php } else { ?>
                                <span class="badge badge-warn">отличается</span>
                            <?php } ?>
                        </td>
                        <td class="ip-action-cell">
                            <?php if (!$item['same'] && $item['writable']) { ?>
                                <button type="button" class="ip-patch-btn" data-id="<?php echo (int)$item['id']; ?>">Обновить</button>
                            <?php } else { ?>
                                <span class="muted">-</span>
                            <?php } ?>
                        </td>
                    </tr>
                <?php } ?>
                </tbody>
            </table>
        </div>
    <?php } ?>
</div>
<script>
(function () {
    var sessid = <?php echo json_encode(bitrix_sessid()); ?>;

    function showMessage(type, text) {
        var box = document.getElementById('ip-messages');
        if (!box) {
            return;
        }

        var msg = document.createElement('div');
        msg.className = 'msg ' + type;
        msg.textContent = text;
        box.appendChild(msg);

        setTimeout(function () {
            if (msg.parentNode) {
                msg.parentNode.removeChild(msg);
            }
        }, 8000);
    }

    function updateStats(stats) {
        if (!stats) {
            return;
        }

        var allEl = document.getElementById('ip-stat-all');
        var sameEl = document.getElementById('ip-stat-same');

        if (allEl) {
            allEl.textContent = stats.all;
        }
        if (sameEl) {
            sameEl.textContent = stats.same;
        }
    }

    function renderStatus(item) {
        if (item.same) {
            return '<span class="badge badge-ok">совпадает</span>';
        }
        if (!item.writable) {
            return '<span class="badge badge-error">нет записи</span>';
        }
        return '<span class="badge badge-warn">отличается</span>';
    }

    function renderPatchConflictBadge(item) {
        if (!item.patch_conflict) {
            return '';
        }
        return '<br><span class="badge badge-info" title="Несколько модулей в intec_patcher ведут на этот файл с разным содержимым">разные патчи</span>';
    }

    function renderAction(item) {
        if (!item.same && item.writable) {
            return '<button type="button" class="ip-patch-btn" data-id="' + item.id + '">Обновить</button>';
        }
        return '<span class="muted">-</span>';
    }

    function updateRow(row, item) {
        if (!row) {
            return;
        }

        row.setAttribute('data-item-id', String(item.id));
        if (item.module) {
            row.setAttribute('data-module', item.module);
        }
        if (item.target) {
            row.setAttribute('data-target', item.target);
        }
        row.className = item.same ? 'same' : 'diff';
        if (!item.writable) {
            row.className += ' readonly';
        }
        if (item.patch_conflict) {
            row.className += ' conflict';
        }

        var statusCell = row.querySelector('.ip-status-cell');
        var actionCell = row.querySelector('.ip-action-cell');
        var patchCell = row.querySelector('.path-patch');

        if (statusCell) {
            statusCell.innerHTML = renderStatus(item);
        }
        if (actionCell) {
            actionCell.innerHTML = renderAction(item);
        }
        if (patchCell) {
            var code = patchCell.querySelector('code');
            if (code) {
                var badge = patchCell.querySelector('.badge-info');
                if (item.patch_conflict && !badge) {
                    patchCell.insertAdjacentHTML('beforeend', renderPatchConflictBadge(item));
                } else if (!item.patch_conflict && badge) {
                    badge.remove();
                }
            }
        }
    }

    function findManifestRow(item) {
        if (!item || !item.module || !item.target) {
            return null;
        }

        var selector = 'tr[data-module="' + CSS.escape(item.module) + '"][data-target="' + CSS.escape(item.target) + '"]';
        return document.querySelector(selector);
    }

    function updateRowsForItems(items) {
        if (!items || !items.length) {
            return;
        }

        items.forEach(function (item) {
            updateRow(findManifestRow(item), item);
        });
    }

    function patchFile(id, button) {
        var row = button.closest('tr');
        button.disabled = true;
        button.textContent = 'Обновление...';

        var body = new FormData();
        body.append('sessid', sessid);
        body.append('action', 'patch_one');
        body.append('id', String(id));
        body.append('ajax', '1');

        fetch(window.location.href, {
            method: 'POST',
            body: body,
            credentials: 'same-origin'
        })
            .then(function (response) {
                return response.json().then(function (data) {
                    if (!response.ok && !data.message) {
                        throw new Error('Ошибка сервера');
                    }
                    return data;
                });
            })
            .then(function (data) {
                showMessage(data.ok ? 'ok' : 'error', data.message || 'Неизвестная ошибка');

                if (data.stats) {
                    updateStats(data.stats);
                }
                if (data.ok && data.items && data.items.length) {
                    updateRowsForItems(data.items);
                } else if (data.ok && data.item) {
                    updateRow(row, data.item);
                } else {
                    button.disabled = false;
                    button.textContent = 'Обновить';
                }
            })
            .catch(function () {
                showMessage('error', 'Не удалось выполнить запрос');
                button.disabled = false;
                button.textContent = 'Обновить';
            });
    }

    document.addEventListener('click', function (event) {
        var button = event.target.closest('.ip-patch-btn');
        if (!button || button.disabled) {
            return;
        }

        var id = parseInt(button.getAttribute('data-id'), 10);
        if (isNaN(id)) {
            return;
        }

        patchFile(id, button);
    });
})();
</script>
</body>
</html>
