<?php
/**
 * DokuWiki Plugin siedeployer (Action Component)
 *
 * AJAX dispatcher para los 16 pasos de aprovisionamiento.
 * Todos los endpoints requieren superusuario + CSRF token.
 *
 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
 * @author  SIE Systems <soporte@systemsie.site>
 */

class action_plugin_siedeployer extends DokuWiki_Action_Plugin
{
    public function register(Doku_Event_Handler $controller)
    {
        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax');
    }

    public function handle_ajax(Doku_Event $event, $param)
    {
        $call = $event->data;

        if (strpos($call, 'plugin_siedeployer_') !== 0) {
            return;
        }

        $event->stopPropagation();
        $event->preventDefault();

        // Seguridad: solo superusuarios
        if (!auth_isadmin()) {
            $this->_jsonResponse(['error' => 'No autorizado - solo superusuario'], 403);
            return;
        }

        global $INPUT;

        // CSRF para POST
        if ($_SERVER['REQUEST_METHOD'] === 'POST' && !checkSecurityToken()) {
            $this->_jsonResponse(['error' => 'Token de seguridad invalido'], 403);
            return;
        }

        // CRITICO: Liberar el lock de sesion ANTES de procesar.
        // Sin esto, una operacion lenta (escaneo, limpieza, find -delete)
        // bloquea el archivo de sesion PHP y TODAS las demas peticiones AJAX
        // del mismo usuario quedan en cola esperando => TODO el panel se congela.
        session_write_close();

        /** @var helper_plugin_siedeployer $helper */
        $helper = plugin_load('helper', 'siedeployer');
        if (!$helper) {
            $this->_jsonResponse(['error' => 'Error cargando helper'], 500);
            return;
        }

        switch ($call) {
            case 'plugin_siedeployer_scan_server':
                $this->_ajaxScanServer($helper);
                break;

            case 'plugin_siedeployer_create_deployment':
                $this->_ajaxCreateDeployment($helper, $INPUT);
                break;

            case 'plugin_siedeployer_list_deployments':
                $this->_ajaxListDeployments($helper);
                break;

            case 'plugin_siedeployer_delete_deployment':
                $this->_ajaxDeleteDeployment($helper, $INPUT);
                break;

            case 'plugin_siedeployer_get_status':
                $this->_ajaxGetStatus($helper, $INPUT);
                break;

            case 'plugin_siedeployer_get_log':
                $this->_ajaxGetLog($helper, $INPUT);
                break;

            case 'plugin_siedeployer_generate_token':
                $this->_ajaxGenerateToken($helper, $INPUT);
                break;

            case 'plugin_siedeployer_reset_deployment':
                $this->_ajaxResetDeployment($helper, $INPUT);
                break;

            case 'plugin_siedeployer_delete_subdomain':
                $this->_ajaxDeleteSubdomain($helper, $INPUT);
                break;

            // --- Migracion masiva de dominios ---
            case 'plugin_siedeployer_migration_scan':
                $this->_ajaxMigrationScan($helper);
                break;

            case 'plugin_siedeployer_migration_check_domains':
                $this->_ajaxMigrationCheckDomains($helper);
                break;

            case 'plugin_siedeployer_migration_create':
                $this->_ajaxMigrationCreate($helper, $INPUT);
                break;

            case 'plugin_siedeployer_migration_delete':
                $this->_ajaxMigrationDelete($helper, $INPUT);
                break;

            case 'plugin_siedeployer_migration_ssl':
                $this->_ajaxMigrationSSL($helper);
                break;

            case 'plugin_siedeployer_request_ssl':
                $this->_ajaxRequestSSL($helper);
                break;

            case 'plugin_siedeployer_cleanup_deployment':
                $this->_ajaxCleanupDeployment($helper, $INPUT);
                break;

            // --- Descarga de archivos actuales por paso ---
            case 'plugin_siedeployer_download_step_files':
                $this->_ajaxDownloadStepFiles($helper, $INPUT);
                break;

            case 'plugin_siedeployer_list_backups':
                $this->_ajaxListBackups($helper);
                break;

            case 'plugin_siedeployer_download_backup':
                $this->_ajaxDownloadBackup($helper, $INPUT);
                break;

            case 'plugin_siedeployer_delete_backup':
                $this->_ajaxDeleteBackup($helper, $INPUT);
                break;

            case 'plugin_siedeployer_estimate_backup':
                $this->_ajaxEstimateBackup($helper, $INPUT);
                break;

            case 'plugin_siedeployer_estimate_backup_web':
                $this->_ajaxEstimateBackupWeb($helper, $INPUT);
                break;

            case 'plugin_siedeployer_backup_web':
                $this->_ajaxBackupWeb($helper, $INPUT);
                break;

            case 'plugin_siedeployer_estimate_backup_plugins':
                $this->_ajaxEstimateBackupPlugins($helper, $INPUT);
                break;

            case 'plugin_siedeployer_backup_plugins':
                $this->_ajaxBackupPlugins($helper, $INPUT);
                break;

            case 'plugin_siedeployer_estimate_backup_config':
                $this->_ajaxEstimateBackupConfig($helper, $INPUT);
                break;

            case 'plugin_siedeployer_backup_config':
                $this->_ajaxBackupConfig($helper, $INPUT);
                break;

            case 'plugin_siedeployer_doceditor_load':
                $this->_ajaxDocEditorLoad($helper, $INPUT);
                break;

            case 'plugin_siedeployer_doceditor_apply':
                $this->_ajaxDocEditorApply($helper, $INPUT);
                break;

            // --- Actualizaciones ---
            case 'plugin_siedeployer_updates_check_sistema':
                $this->_ajaxUpdatesCheckSistema($helper);
                break;

            case 'plugin_siedeployer_updates_clean_sistema':
                $this->_ajaxUpdatesCleanSistema($helper);
                break;

            case 'plugin_siedeployer_updates_upload_sistema':
                $this->_ajaxUpdatesUploadSistema($helper);
                break;

            case 'plugin_siedeployer_updates_run_patch':
                $this->_ajaxUpdatesRunPatch($helper, $INPUT);
                break;

            case 'plugin_siedeployer_updates_cleanup_patch':
                $this->_ajaxUpdatesCleanupPatch($helper);
                break;

            case 'plugin_siedeployer_individual_apply':
                $this->_ajaxIndividualApply($helper, $INPUT);
                break;

            default:
                // Ejecutar pasos 1-19
                if (preg_match('/^plugin_siedeployer_execute_step(\d+)$/', $call, $m)) {
                    $stepNum = intval($m[1]);
                    if ($stepNum >= 1 && $stepNum <= 19) {
                        $this->_ajaxExecuteStep($helper, $INPUT, $stepNum);
                    } else {
                        $this->_jsonResponse(['error' => 'Paso invalido: ' . $stepNum], 400);
                    }
                } else {
                    $this->_jsonResponse(['error' => 'Endpoint desconocido'], 404);
                }
                break;
        }
    }

    // =========================================================================
    //  ENDPOINTS
    // =========================================================================

    protected function _ajaxScanServer($helper)
    {
        $result = $helper->scanServerSystems();

        // Verificar si retorno un error de debug
        if (isset($result['_error'])) {
            $this->_jsonResponse([
                'systems'   => [],
                'error'     => $result['_error'],
                'doku_inc'  => $result['_doku_inc'] ?? '',
                'base_path' => $helper->getBasePath(),
            ]);
            return;
        }

        $this->_jsonResponse([
            'systems'   => $result,
            'base_path' => $helper->getBasePath(),
        ]);
    }

    protected function _ajaxCreateDeployment($helper, $INPUT)
    {
        $name = trim($INPUT->post->str('name'));
        $sourcePath = trim($INPUT->post->str('source_path'));
        $targetPath = trim($INPUT->post->str('target_path'));
        $subdomain = trim($INPUT->post->str('subdomain'));

        if (empty($name) || empty($sourcePath) || empty($targetPath)) {
            $this->_jsonResponse(['error' => 'Nombre, ruta origen y ruta destino son obligatorios'], 400);
            return;
        }

        $basePath = $helper->getBasePath();
        if (!$helper->validatePath($sourcePath, $basePath)) {
            $this->_jsonResponse(['error' => 'Ruta origen invalida'], 400);
            return;
        }

        if (!$helper->validatePath($targetPath, $basePath)) {
            $this->_jsonResponse(['error' => 'Ruta destino invalida'], 400);
            return;
        }

        $mode = trim($INPUT->post->str('deploy_mode', 'new'));
        $user = $_SERVER['REMOTE_USER'] ?? 'unknown';
        $result = $helper->createDeployment($name, $sourcePath, $targetPath, $subdomain, $user, $mode);

        if (is_array($result) && isset($result['error'])) {
            $this->_jsonResponse(['error' => $result['error']], 400);
        } elseif ($result) {
            $this->_jsonResponse(['success' => true, 'deployment_id' => $result]);
        } else {
            $this->_jsonResponse(['error' => 'No se pudo crear el despliegue'], 500);
        }
    }

    protected function _ajaxListDeployments($helper)
    {
        $deployments = $helper->listDeployments();
        $this->_jsonResponse(['deployments' => $deployments]);
    }

    protected function _ajaxDeleteDeployment($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }

        $helper->deleteDeployment($id);
        $this->_jsonResponse(['success' => true]);
    }

    protected function _ajaxGetStatus($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }

        $dep = $helper->getDeployment($id);
        if (!$dep) {
            $this->_jsonResponse(['error' => 'Despliegue no encontrado'], 404);
            return;
        }

        $steps = $helper->getStepsStatus($id);

        $this->_jsonResponse([
            'deployment' => $dep,
            'steps'      => $steps,
        ]);
    }

    protected function _ajaxGetLog($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        $step = $INPUT->post->int('step_number');

        if (!$id || !$step) {
            $this->_jsonResponse(['error' => 'deployment_id y step_number requeridos'], 400);
            return;
        }

        $logs = $helper->getStepLogs($id, $step);
        $this->_jsonResponse(['logs' => $logs]);
    }

    protected function _ajaxResetDeployment($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }

        $helper->resetDeploymentSteps($id);
        $this->_jsonResponse(['success' => true, 'message' => 'Todos los pasos reiniciados']);
    }

    protected function _ajaxGenerateToken($helper, $INPUT)
    {
        $projectName = trim($INPUT->post->str('project_name'));
        if (empty($projectName)) {
            $this->_jsonResponse(['error' => 'Nombre del proyecto requerido'], 400);
            return;
        }

        $result = $helper->generateToken($projectName);
        if ($result['error']) {
            $this->_jsonResponse(['error' => $result['error']]);
        } else {
            $this->_jsonResponse(['success' => true, 'token' => $result['token']]);
        }
    }

    protected function _ajaxDeleteSubdomain($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }

        $dep = $helper->getDeployment($id);
        if (!$dep) {
            $this->_jsonResponse(['error' => 'Despliegue no encontrado'], 404);
            return;
        }

        $user = $_SERVER['REMOTE_USER'] ?? 'unknown';
        $startTime = microtime(true);

        $helper->clearStepLogs($id, 16);
        $helper->markStepRunning($id, 16, $user);
        $helper->addLog($id, 16, 'Iniciando eliminacion de subdominio...', 'info');

        @set_time_limit(120);
        $result = $helper->deleteSubdomain($id);

        $duration = intval(microtime(true) - $startTime);

        if ($result === true) {
            $helper->addLog($id, 16, 'Subdominio eliminado exitosamente', 'success');
            $helper->markStepCompleted($id, 16, 'Subdominio eliminado', $duration);
            $this->_jsonResponse([
                'success'  => true,
                'message'  => 'Subdominio eliminado en ' . $duration . ' segundos. La carpeta del sistema no fue afectada.',
                'duration' => $duration,
            ]);
        } else {
            $errorMsg = is_string($result) ? $result : 'Error desconocido';
            $helper->addLog($id, 16, 'Error: ' . $errorMsg, 'error');
            $helper->markStepFailed($id, 16, $errorMsg);
            $this->_jsonResponse([
                'success' => false,
                'error'   => $errorMsg,
            ]);
        }
    }

    // =========================================================================
    //  MIGRACION MASIVA DE DOMINIOS
    // =========================================================================

    protected function _ajaxMigrationScan($helper)
    {
        @set_time_limit(60);
        $result = $helper->scanAllSubdomains();
        $this->_jsonResponse($result);
    }

    protected function _ajaxMigrationCheckDomains($helper)
    {
        $result = $helper->listConfiguredDomains();
        $this->_jsonResponse($result);
    }

    protected function _ajaxMigrationCreate($helper, $INPUT)
    {
        $subdomain    = trim($INPUT->post->str('subdomain'));
        $targetDomain = trim($INPUT->post->str('target_domain'));
        $docRoot      = trim($INPUT->post->str('doc_root'));
        $systemPath   = trim($INPUT->post->str('system_path'));
        $updateBaseurl = $INPUT->post->bool('update_baseurl');

        if (empty($subdomain) || empty($targetDomain) || empty($docRoot)) {
            $this->_jsonResponse(['error' => 'subdomain, target_domain y doc_root son requeridos'], 400);
            return;
        }

        // Validar dominio destino
        if (!in_array($targetDomain, helper_plugin_siedeployer::$availableDomains)) {
            $this->_jsonResponse(['error' => 'Dominio destino no valido: ' . $targetDomain], 400);
            return;
        }

        @set_time_limit(120);

        $result = $helper->migrateCreateSubdomain($subdomain, $targetDomain, $docRoot);

        if (is_string($result)) {
            $this->_jsonResponse(['success' => false, 'error' => $result]);
            return;
        }

        $response = $result;

        // Actualizar baseurl si se solicita y es sistema SIE
        if ($updateBaseurl && !empty($systemPath)) {
            $baseurlResult = $helper->migrateUpdateBaseurl($systemPath, $subdomain, $targetDomain);
            if (is_string($baseurlResult)) {
                $response['baseurl_warning'] = $baseurlResult;
            } else {
                $response['baseurl_updated'] = $baseurlResult;
            }
        }

        $this->_jsonResponse($response);
    }

    protected function _ajaxMigrationDelete($helper, $INPUT)
    {
        $subdomain    = trim($INPUT->post->str('subdomain'));
        $sourceDomain = trim($INPUT->post->str('source_domain'));

        if (empty($subdomain) || empty($sourceDomain)) {
            $this->_jsonResponse(['error' => 'subdomain y source_domain son requeridos'], 400);
            return;
        }

        @set_time_limit(120);

        $result = $helper->migrateDeleteSubdomain($subdomain, $sourceDomain);

        if (is_string($result)) {
            $this->_jsonResponse(['success' => false, 'error' => $result]);
            return;
        }

        $this->_jsonResponse($result);
    }

    protected function _ajaxMigrationSSL($helper)
    {
        @set_time_limit(120);
        $result = $helper->requestAutoSSL();
        $this->_jsonResponse($result);
    }

    protected function _ajaxRequestSSL($helper)
    {
        @set_time_limit(120);
        $result = $helper->requestAutoSSL();
        $this->_jsonResponse($result);
    }

    protected function _ajaxCleanupDeployment($helper, $INPUT)
    {
        @set_time_limit(120);

        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }

        $options = [
            'delete_subdomain' => $INPUT->post->bool('delete_subdomain'),
            'delete_folder'    => $INPUT->post->bool('delete_folder'),
            'delete_record'    => $INPUT->post->bool('delete_record'),
        ];

        $result = $helper->fullCleanup($id, $options);
        $this->_jsonResponse($result);
    }

    // =========================================================================
    //  EJECUTAR PASOS
    // =========================================================================

    protected function _ajaxExecuteStep($helper, $INPUT, $stepNum)
    {
        $deploymentId = $INPUT->post->int('deployment_id');
        if (!$deploymentId) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }

        $dep = $helper->getDeployment($deploymentId);
        if (!$dep) {
            $this->_jsonResponse(['error' => 'Despliegue no encontrado'], 404);
            return;
        }

        $user = $_SERVER['REMOTE_USER'] ?? 'unknown';
        $startTime = microtime(true);

        // Limpiar logs previos y marcar como ejecutando
        $helper->clearStepLogs($deploymentId, $stepNum);
        $helper->markStepRunning($deploymentId, $stepNum, $user);
        $helper->addLog($deploymentId, $stepNum, 'Iniciando paso ' . $stepNum . '...', 'info');

        // Ejecutar paso
        $result = $this->_dispatchStep($helper, $INPUT, $deploymentId, $stepNum);

        $duration = intval(microtime(true) - $startTime);

        if ($result === true) {
            $helper->addLog($deploymentId, $stepNum, 'Paso completado exitosamente', 'success');
            $helper->markStepCompleted($deploymentId, $stepNum, 'OK', $duration);

            // Paso 8 tambien cubre pasos 11 (favicon 20cones) y 13 (favicon 21cones)
            if ($stepNum === 8) {
                $helper->markStepCompleted($deploymentId, 11, 'Incluido en Paso 8', 0);
                $helper->addLog($deploymentId, 11, 'Favicon copiado automaticamente por Paso 8', 'success');
                $helper->markStepCompleted($deploymentId, 13, 'Incluido en Paso 8', 0);
                $helper->addLog($deploymentId, 13, 'Favicon copiado automaticamente por Paso 8', 'success');
            }

            $this->_jsonResponse([
                'success'  => true,
                'message'  => 'Paso ' . $stepNum . ' completado en ' . $duration . ' segundos',
                'duration' => $duration,
            ]);
        } else {
            $errorMsg = is_string($result) ? $result : 'Error desconocido';
            $helper->addLog($deploymentId, $stepNum, 'Error: ' . $errorMsg, 'error');
            $helper->markStepFailed($deploymentId, $stepNum, $errorMsg);
            $this->_jsonResponse([
                'success' => false,
                'error'   => $errorMsg,
            ]);
        }
    }

    protected function _dispatchStep($helper, $INPUT, $deploymentId, $stepNum)
    {
        switch ($stepNum) {
            case 1:
                @set_time_limit(600);
                return $helper->executeStep1($deploymentId);

            case 2:
                $title = trim($INPUT->post->str('title'));
                $baseurl = trim($INPUT->post->str('baseurl'));
                $template = trim($INPUT->post->str('template'));
                if (empty($title) || empty($baseurl)) {
                    return 'Titulo y URL son obligatorios';
                }
                // Validar template contra valores permitidos
                if (!in_array($template, ['20cones', '21cones'], true)) {
                    $template = '20cones';
                }
                $helper->saveStepInput($deploymentId, 2, ['title' => $title, 'baseurl' => $baseurl, 'template' => $template]);
                return $helper->executeStep2($deploymentId, $title, $baseurl, $template);

            case 3:
                $email = trim($INPUT->post->str('email'));
                if (empty($email)) return 'Email es obligatorio';
                $helper->saveStepInput($deploymentId, 3, ['email' => $email]);
                return $helper->executeStep3($deploymentId, $email);

            case 4:
                $token = trim($INPUT->post->str('token_ia'));
                if (empty($token)) return 'Token es obligatorio';
                $helper->saveStepInput($deploymentId, 4, ['token_ia' => '***']);
                return $helper->executeStep4($deploymentId, $token);

            case 5:
                @set_time_limit(300);
                return $helper->executeStep5($deploymentId);

            case 6:
                $logoPng = $_FILES['logo_png'] ?? null;
                $logoGif = $_FILES['logo_gif'] ?? null;
                return $helper->executeStep6($deploymentId, $logoPng, $logoGif);

            case 7:
                @set_time_limit(300);
                return $helper->executeStep7($deploymentId);

            case 8:
                $dokuwikiPng = $_FILES['dokuwiki_png'] ?? null;
                $faviconIco = $_FILES['favicon_ico'] ?? null;
                return $helper->executeStep8($deploymentId, $dokuwikiPng, $faviconIco);

            case 9:
                $aclContent = $INPUT->post->str('acl_content');
                if (empty($aclContent)) $aclContent = $helper->getDefaultACL();
                $helper->saveStepInput($deploymentId, 9, ['acl_length' => strlen($aclContent)]);
                return $helper->executeStep9($deploymentId, $aclContent);

            case 10:
                $usersContent = $INPUT->post->str('users_content');
                if (empty($usersContent)) $usersContent = $helper->getDefaultUsers();
                $helper->saveStepInput($deploymentId, 10, ['users_length' => strlen($usersContent)]);
                return $helper->executeStep10($deploymentId, $usersContent);

            case 11:
                $favicon = $_FILES['favicon_ico'] ?? null;
                return $helper->executeStep11($deploymentId, $favicon);

            case 12:
                $files = $this->_collectUploadedFiles();
                return $helper->executeStep12($deploymentId, $files);

            case 13:
                $favicon = $_FILES['favicon_ico'] ?? null;
                return $helper->executeStep13($deploymentId, $favicon);

            case 14:
                $files = $this->_collectUploadedFiles();
                return $helper->executeStep14($deploymentId, $files);

            case 15:
                $mode = trim($INPUT->post->str('step15_mode', 'individual'));
                return $helper->executeStep15($deploymentId, $_FILES, $mode);

            case 16:
                @set_time_limit(120);
                $rootDomain = trim($INPUT->post->str('root_domain', ''));
                return $helper->executeStep16($deploymentId, $rootDomain);

            case 17:
                @set_time_limit(600);
                return $helper->executeStep17($deploymentId);

            case 18:
                // Zona de Peligro: no tiene executeStep, usa endpoint cleanup_deployment
                return 'Paso 18 se gestiona via Zona de Peligro (endpoint cleanup_deployment)';

            case 19:
                // Editor Documental: no tiene executeStep, usa endpoints doceditor_load/apply
                return 'Paso 19 se gestiona via endpoints doceditor_load y doceditor_apply';

            default:
                return 'Paso no implementado';
        }
    }

    // =========================================================================
    //  EDITOR DOCUMENTAL (Paso 19)
    // =========================================================================

    /**
     * AJAX: Cargar datos del manual de calidad del sistema desplegado
     */
    protected function _ajaxDocEditorLoad($helper, $INPUT)
    {
        $deploymentId = $INPUT->post->int('deployment_id');
        if (!$deploymentId) {
            $this->_jsonResponse(['error' => 'Falta deployment_id'], 400);
            return;
        }

        $result = $helper->loadDocEditorData($deploymentId);

        if (isset($result['error'])) {
            $this->_jsonResponse(['success' => false, 'error' => $result['error']]);
            return;
        }

        // Formato v2: resultado tiene 'fields' y '_debug' separados
        if (isset($result['_format']) && $result['_format'] === 'v2') {
            $this->_jsonResponse([
                'success' => true,
                'fields'  => $result['fields'],
                'debug'   => isset($result['_debug']) ? $result['_debug'] : [],
            ]);
        } else {
            // Compatibilidad anterior
            $this->_jsonResponse(['success' => true, 'fields' => $result]);
        }
    }

    /**
     * AJAX: Aplicar cambios masivos del editor documental
     */
    protected function _ajaxDocEditorApply($helper, $INPUT)
    {
        @set_time_limit(300);

        $deploymentId = $INPUT->post->int('deployment_id');
        $changesJson  = $INPUT->post->str('changes', '');

        if (!$deploymentId || !$changesJson) {
            $this->_jsonResponse(['error' => 'Parametros faltantes'], 400);
            return;
        }

        $changes = json_decode($changesJson, true);
        if (!is_array($changes) || empty($changes)) {
            $this->_jsonResponse(['error' => 'No se recibieron cambios validos'], 400);
            return;
        }

        $result = $helper->applyDocEditorChanges($deploymentId, $changes);

        $this->_jsonResponse([
            'success'           => true,
            'files_processed'   => $result['files_processed'] ?? 0,
            'files_modified'    => $result['files_modified'] ?? 0,
            'folders_scanned'   => $result['folders_scanned'] ?? 0,
            'folders_skipped'   => $result['folders_skipped'] ?? [],
            'replacements'      => $result['replacements'] ?? 0,
            'errors'            => $result['errors'] ?? [],
            'detail'            => $result['detail'] ?? [],
        ]);
    }

    protected function _ajaxListBackups($helper)
    {
        $backups = $helper->listBackups();
        $this->_jsonResponse(['success' => true, 'backups' => $backups, 'backup_dir' => $helper->getBackupDir()]);
    }

    protected function _ajaxDownloadBackup($helper, $INPUT)
    {
        $filename = trim($INPUT->str('filename'));
        if (empty($filename)) {
            $this->_jsonResponse(['error' => 'Nombre de archivo requerido'], 400);
            return;
        }

        // Sanitizar: solo permitir nombre de archivo sin path traversal
        $filename = basename($filename);
        if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/i', $filename)) {
            $this->_jsonResponse(['error' => 'Nombre de archivo no valido'], 400);
            return;
        }

        $backupDir = $helper->getBackupDir();
        $filePath = $backupDir . '/' . $filename;

        if (!file_exists($filePath)) {
            $this->_jsonResponse(['error' => 'Archivo no encontrado: ' . $filename], 404);
            return;
        }

        $fileSize = filesize($filePath);

        // Enviar archivo como descarga directa
        header('Content-Type: application/zip');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Content-Length: ' . $fileSize);
        header('Cache-Control: no-cache, no-store, must-revalidate');
        header('Pragma: no-cache');

        // Usar readfile para streaming eficiente
        readfile($filePath);
        exit;
    }

    protected function _ajaxDeleteBackup($helper, $INPUT)
    {
        $filename = trim($INPUT->post->str('filename'));
        if (empty($filename)) {
            $this->_jsonResponse(['error' => 'Nombre de archivo requerido'], 400);
            return;
        }

        $result = $helper->deleteBackup($filename);
        if ($result === true) {
            $this->_jsonResponse(['success' => true, 'message' => 'Backup eliminado: ' . basename($filename)]);
        } else {
            $this->_jsonResponse(['success' => false, 'error' => $result]);
        }
    }

    protected function _ajaxEstimateBackup($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }

        $dep = $helper->getDeployment($id);
        if (!$dep) {
            $this->_jsonResponse(['error' => 'Despliegue no encontrado'], 404);
            return;
        }

        $size = $helper->estimateBackupSize($dep['target_path']);
        $this->_jsonResponse([
            'success'  => true,
            'size'     => $size,
            'size_mb'  => round($size / 1048576, 1),
            'size_gb'  => round($size / 1073741824, 2),
            'exceeds'  => $size > helper_plugin_siedeployer::BACKUP_MAX_SIZE,
        ]);
    }

    protected function _ajaxEstimateBackupWeb($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }
        $dep = $helper->getDeployment($id);
        if (!$dep) {
            $this->_jsonResponse(['error' => 'Despliegue no encontrado'], 404);
            return;
        }

        $result = $helper->estimateBackupSizeWeb($dep['target_path']);
        $size = $result['size'];
        $this->_jsonResponse([
            'success'  => true,
            'exists'   => $result['exists'],
            'size'     => $size,
            'size_mb'  => round($size / 1048576, 1),
            'size_gb'  => round($size / 1073741824, 2),
            'exceeds'  => $size > helper_plugin_siedeployer::BACKUP_MAX_SIZE,
        ]);
    }

    protected function _ajaxBackupWeb($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }
        @set_time_limit(600);
        $result = $helper->executeBackupWeb($id);
        if ($result === true) {
            $this->_jsonResponse(['success' => true, 'message' => 'Backup de web/ creado exitosamente']);
        } else {
            $this->_jsonResponse(['success' => false, 'error' => $result]);
        }
    }

    protected function _ajaxEstimateBackupPlugins($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }
        $dep = $helper->getDeployment($id);
        if (!$dep) {
            $this->_jsonResponse(['error' => 'Despliegue no encontrado'], 404);
            return;
        }

        $result = $helper->estimateBackupSizePlugins($dep['target_path']);
        $size = $result['size'];
        $this->_jsonResponse([
            'success'  => true,
            'exists'   => $result['exists'],
            'size'     => $size,
            'size_mb'  => round($size / 1048576, 1),
            'size_gb'  => round($size / 1073741824, 2),
            'exceeds'  => $size > helper_plugin_siedeployer::BACKUP_MAX_SIZE,
        ]);
    }

    protected function _ajaxBackupPlugins($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }
        @set_time_limit(600);
        $result = $helper->executeBackupPlugins($id);
        if ($result === true) {
            $this->_jsonResponse(['success' => true, 'message' => 'Backup de plugins creado exitosamente']);
        } else {
            $this->_jsonResponse(['success' => false, 'error' => $result]);
        }
    }

    protected function _ajaxEstimateBackupConfig($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }
        $dep = $helper->getDeployment($id);
        if (!$dep) {
            $this->_jsonResponse(['error' => 'Despliegue no encontrado'], 404);
            return;
        }

        $result = $helper->estimateBackupSizeConfig($dep['target_path']);
        $size = $result['size'];
        $this->_jsonResponse([
            'success'  => true,
            'exists'   => $result['exists'],
            'size'     => $size,
            'size_mb'  => round($size / 1048576, 1),
            'size_gb'  => round($size / 1073741824, 2),
            'exceeds'  => $size > helper_plugin_siedeployer::BACKUP_MAX_SIZE,
        ]);
    }

    protected function _ajaxBackupConfig($helper, $INPUT)
    {
        $id = $INPUT->post->int('deployment_id');
        if (!$id) {
            $this->_jsonResponse(['error' => 'deployment_id requerido'], 400);
            return;
        }
        @set_time_limit(600);
        $result = $helper->executeBackupConfig($id);
        if ($result === true) {
            $this->_jsonResponse(['success' => true, 'message' => 'Backup de configuracion creado exitosamente']);
        } else {
            $this->_jsonResponse(['success' => false, 'error' => $result]);
        }
    }

    protected function _collectUploadedFiles()
    {
        $files = [];
        foreach ($_FILES as $key => $file) {
            if ($file['error'] === UPLOAD_ERR_OK) {
                $files[$key] = $file;
            }
        }
        return $files;
    }

    // =========================================================================
    //  DESCARGA DE ARCHIVOS POR PASO
    // =========================================================================

    protected function _ajaxDownloadStepFiles($helper, $INPUT)
    {
        // Validar CSRF tambien en GET para descargas
        if (!checkSecurityToken()) {
            $this->_jsonResponse(['error' => 'Token de seguridad invalido'], 403);
            return;
        }

        $deploymentId = $INPUT->int('deployment_id');
        $stepNumber   = $INPUT->int('step_number');

        if (!$deploymentId || !$stepNumber) {
            $this->_jsonResponse(['error' => 'deployment_id y step_number requeridos'], 400);
            return;
        }

        $result = $helper->downloadStepFiles($deploymentId, $stepNumber);

        if (isset($result['error'])) {
            $this->_jsonResponse(['error' => $result['error']], 400);
            return;
        }

        // Enviar archivo como descarga directa
        header('Content-Type: ' . $result['mime']);
        header('Content-Disposition: attachment; filename="' . $result['filename'] . '"');
        header('Content-Length: ' . strlen($result['content']));
        header('Cache-Control: no-cache, no-store, must-revalidate');
        echo $result['content'];
        exit;
    }

    // =========================================================================
    //  ACTUALIZACIONES - ENDPOINTS
    // =========================================================================

    protected function _ajaxUpdatesCheckSistema($helper)
    {
        global $INPUT;
        $subpath = $INPUT->post->str('subpath', '');
        $result = $helper->checkSistemaFolder($subpath);
        $this->_jsonResponse($result);
    }

    protected function _ajaxUpdatesCleanSistema($helper)
    {
        $result = $helper->cleanSistemaFolder();
        $this->_jsonResponse($result);
    }

    protected function _ajaxUpdatesUploadSistema($helper)
    {
        if (!isset($_FILES['sistema_zip']) || $_FILES['sistema_zip']['error'] !== UPLOAD_ERR_OK) {
            $this->_jsonResponse(['error' => 'No se recibio archivo ZIP valido'], 400);
            return;
        }

        $result = $helper->uploadSistemaZip($_FILES['sistema_zip']);
        $this->_jsonResponse($result);
    }

    protected function _ajaxUpdatesRunPatch($helper, $INPUT)
    {
        $patchName = $INPUT->post->str('patch_name');
        if (empty($patchName)) {
            $this->_jsonResponse(['error' => 'patch_name requerido'], 400);
            return;
        }

        $options = [
            'action'       => $INPUT->post->str('patch_action', ''),
            'exclude_list' => $INPUT->post->str('exclude_list', ''),
        ];

        $result = $helper->deployPatch($patchName, $options);
        $this->_jsonResponse($result);
    }

    protected function _ajaxUpdatesCleanupPatch($helper)
    {
        $result = $helper->cleanupPatch();
        $this->_jsonResponse($result);
    }

    // =========================================================================
    //  AJUSTE INDIVIDUAL
    // =========================================================================

    protected function _ajaxIndividualApply($helper, $INPUT)
    {
        $targetPath = $INPUT->post->str('target_path');
        if (empty($targetPath)) {
            $this->_jsonResponse(['error' => 'target_path requerido'], 400);
            return;
        }
        $result = $helper->applyIndividualUpdate($targetPath);
        $this->_jsonResponse($result);
    }

    // =========================================================================
    //  UTILIDADES
    // =========================================================================

    protected function _jsonResponse($data, $status = 200)
    {
        if ($status !== 200) {
            http_response_code($status);
        }
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE);
        exit;
    }
}
