<?php
/**
 * Flexia — deployment environment checks for the install wizard.
 */

/**
 * Best-effort layout for shared hosting: creates writable dirs, protective
 * .htaccess where missing, optional external key parent dirs, and master.key
 * when no encrypted store exists yet (so the wizard does not require manual mkdir).
 */
function antara_install_prepare_hosting_layout(): void
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    require_once ANTARA_ROOT . '/core/security/vault_state.php';

    AntaraPaths::ensureDirs();
    $root = AntaraPaths::root();
    $ds = DIRECTORY_SEPARATOR;

    $mkdir = static function (string $abs): void {
        if ($abs === '' || is_dir($abs)) {
            return;
        }
        @mkdir($abs, 0755, true);
    };

    $paths = [
        $root . $ds . 'media',
        $root . $ds . 'temp',
        $root . $ds . 'media' . $ds . 'explorer' . $ds . 'files',
        $root . $ds . 'media' . $ds . 'explorer' . $ds . 'trash',
        $root . $ds . 'media' . $ds . 'explorer' . $ds . 'remote-cache',
        $root . $ds . 'media' . $ds . 'explorer' . $ds . 'users',
        $root . $ds . 'media' . $ds . 'explorer' . $ds . 'departments',
        $root . $ds . 'storage' . $ds . 'users',
        $root . $ds . 'logs',
        $root . $ds . 'logos_sinapsys',
        $root . $ds . 'data',
    ];
    foreach ($paths as $p) {
        $mkdir($p);
    }

    require_once ANTARA_ROOT . '/includes/user_settings_helpers.php';
    grid_user_profiles_prepare_hosting_layout();

    $htDenySimple = <<<'HTA'
<IfModule mod_authz_core.c>
    Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
    Order deny,allow
    Deny from all
</IfModule>
Options -Indexes

HTA;

    $htStorageRoot = <<<'HTA'
# Deny ALL direct HTTP access to /storage and any subfolder.
<IfModule mod_authz_core.c>
    Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
    Order deny,allow
    Deny from all
</IfModule>

# Defensive: block by extension as well in case of misconfig.
<FilesMatch "\.(enc|key|log|json|php|bak|tmp)$">
    <IfModule mod_authz_core.c>
        Require all denied
    </IfModule>
    <IfModule !mod_authz_core.c>
        Order deny,allow
        Deny from all
    </IfModule>
</FilesMatch>

Options -Indexes

HTA;

    $htCore = <<<'HTA'
# Antara — hardening for anything deployed under /core/
Options -Indexes

<IfModule mod_authz_core.c>
  <FilesMatch "(?i)\.(enc|bak|tmp|swp|log)$">
    Require all denied
  </FilesMatch>
</IfModule>

HTA;

    $htData = <<<'HTA'
# Denegar acceso HTTP directo a datos JSON (Apache 2.2 / 2.4).
<IfModule mod_authz_core.c>
    Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
    Order deny,allow
    Deny from all
</IfModule>

HTA;

    $writeHt = static function (string $file, string $contents): void {
        if (is_file($file)) {
            return;
        }
        $dir = dirname($file);
        if (!is_dir($dir) || !is_writable($dir)) {
            return;
        }
        if (@file_put_contents($file, $contents) !== false) {
            @chmod($file, 0644);
        }
    };

    $writeHt($root . $ds . 'storage' . $ds . '.htaccess', $htStorageRoot);
    $writeHt(AntaraPaths::secure() . $ds . '.htaccess', $htDenySimple);
    $writeHt(AntaraPaths::config() . $ds . '.htaccess', $htDenySimple);
    $writeHt(AntaraPaths::logs() . $ds . '.htaccess', $htDenySimple);
    $writeHt($root . $ds . 'data' . $ds . '.htaccess', $htData);
    $writeHt($root . $ds . 'core' . $ds . '.htaccess', $htCore);

    // External master key dirs (all candidates except internal .master.key): create dir + lock down if Apache.
    foreach (AntaraVaultState::masterKeyCandidatePaths() as $keyPath) {
        if (basename($keyPath) === '.master.key') {
            continue;
        }
        $secretsDir = dirname($keyPath);
        if (!is_dir($secretsDir)) {
            @mkdir($secretsDir, 0700, true);
        }
        if (is_dir($secretsDir) && is_writable($secretsDir)) {
            $writeHt($secretsDir . $ds . '.htaccess', $htDenySimple);
        }
    }

    // Pre-create master key only when there is no ciphertext yet (installer / fresh deploy).
    if (!AntaraVaultState::hasNonEmptyStoreCiphertexts()) {
        require_once ANTARA_ROOT . '/core/security/crypto.php';
        try {
            AntaraCrypto::masterKeyPath();
        } catch (Throwable $e) {
            // Checks will surface the error if still failing.
        }
    }
}

/**
 * Nombre de carpeta externa opaco (no ligado a empresa ni a datos visibles en pantalla).
 */
function antara_install_generate_discrete_external_basename(): string
{
    return 'fx_' . bin2hex(random_bytes(8));
}

function antara_install_is_valid_external_secrets_basename(string $basename): bool
{
    if (strlen($basename) < 3 || strlen($basename) > 64 || !preg_match('/^[a-z0-9_]+$/', $basename)) {
        return false;
    }
    if (preg_match('/^fx_(site_)?[a-f0-9]{16}$/', $basename)) {
        return true;
    }
    if (str_ends_with($basename, '_secrets')) {
        return true;
    }
    return false;
}

function antara_install_https_request_context(): bool
{
    return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
        || (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https');
}

function antara_install_root_writable_for_user_ini(): bool
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    $root = AntaraPaths::root();
    return is_dir($root) && is_writable($root);
}

/**
 * @return array{ok:bool,message:string}
 */
function antara_install_apply_wizard_fix(string $fixId): array
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    require_once ANTARA_ROOT . '/core/security/crypto.php';

    if ($fixId === 'htaccess') {
        antara_install_prepare_hosting_layout();
        AntaraCrypto::resetMasterKeyPathCache();
        return [
            'ok' => true,
            'message' => 'Se volvieron a crear carpetas necesarias y archivos de protección donde el servidor lo permitió. Ejecuta el diagnóstico otra vez para ver el resultado.',
        ];
    }
    if ($fixId === 'user_ini') {
        $root = AntaraPaths::root();
        if (!is_writable($root)) {
            return ['ok' => false, 'message' => 'No se puede escribir en la carpeta de la aplicación; este ajuste no está disponible desde el instalador.'];
        }
        $path = $root . DIRECTORY_SEPARATOR . '.user.ini';
        $https = antara_install_https_request_context();
        $lines = [
            '; Flexia — valores sugeridos (algunos hostings los aplican en la siguiente petición o tras unos minutos)',
            'expose_php=Off',
            'display_errors=Off',
            'allow_url_include=Off',
            'session.cookie_httponly=1',
        ];
        if ($https) {
            $lines[] = 'session.cookie_secure=1';
        }
        $block = implode("\n", $lines) . "\n";
        $begin = '; --- Flexia installer begin ---';
        $end = '; --- Flexia installer end ---';
        $existing = is_file($path) ? (string) @file_get_contents($path) : '';
        $wrapped = $begin . "\n" . $block . $end . "\n";
        if (strpos($existing, $begin) !== false && strpos($existing, $end) !== false) {
            $existing = preg_replace(
                '/' . preg_quote($begin, '/') . '[\s\S]*?' . preg_quote($end, '/') . '\s*/',
                $wrapped,
                $existing,
                1
            );
            if (!is_string($existing)) {
                $existing = $wrapped;
            }
        } else {
            $existing = rtrim($existing) . ($existing !== '' ? "\n\n" : '') . $wrapped;
        }
        if (@file_put_contents($path, $existing) === false) {
            return ['ok' => false, 'message' => 'No se pudo guardar el archivo .user.ini en la raíz del proyecto.'];
        }
        @chmod($path, 0644);
        return [
            'ok' => true,
            'message' => 'Se actualizó .user.ini en la raíz del proyecto. Muchos hostings aplican estos valores en la siguiente visita o tras unos minutos; vuelve a ejecutar el diagnóstico. Si el aviso sigue, cámbialo desde el panel de PHP de tu proveedor.',
        ];
    }
    return ['ok' => false, 'message' => 'Esta corrección no está disponible.'];
}

/**
 * Acción de corrección desde el instalador (solo advertencias y solo si aplica).
 */
function antara_install_wizard_fix_action_for_item(string $id, string $level, bool $ok, bool $rootWritable, bool $https): ?string
{
    if ($ok || $level !== 'warn') {
        return null;
    }
    if ($id === 'htaccess_sensitive') {
        return 'htaccess';
    }
    if (in_array($id, ['expose_php', 'display_errors', 'session_cookie_httponly'], true)) {
        return $rootWritable ? 'user_ini' : null;
    }
    if ($id === 'session_cookie_secure' && $https) {
        return $rootWritable ? 'user_ini' : null;
    }
    return null;
}

function antara_install_write_external_secrets_basename(string $basename): bool
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    AntaraPaths::ensureDirs();
    if (!antara_install_is_valid_external_secrets_basename($basename)) {
        return false;
    }
    $path = AntaraPaths::externalSecretsBasenameFile();
    $dir = dirname($path);
    if (!is_dir($dir) || !is_writable($dir)) {
        return false;
    }
    $tmp = $path . '.tmp.' . bin2hex(random_bytes(4));
    if (@file_put_contents($tmp, $basename . "\n", LOCK_EX) === false) {
        return false;
    }
    if (!@rename($tmp, $path)) {
        @unlink($tmp);
        return false;
    }
    @chmod($path, 0640);
    return true;
}

/**
 * Si la clave solo está en el cofre interno, intenta copiarla al primer candidato externo disponible.
 */
function antara_install_try_promote_master_key_to_external(): void
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    require_once ANTARA_ROOT . '/core/security/vault_state.php';
    require_once ANTARA_ROOT . '/core/security/crypto.php';

    $internal = AntaraPaths::secure() . DIRECTORY_SEPARATOR . '.master.key';
    if (!is_file($internal) || (int) @filesize($internal) < 32) {
        return;
    }
    $raw = (string) @file_get_contents($internal);
    if (strlen($raw) < 32) {
        return;
    }
    foreach (AntaraVaultState::masterKeyCandidatePaths() as $dest) {
        if (basename($dest) === '.master.key') {
            continue;
        }
        if (is_file($dest) && (int) @filesize($dest) >= 32) {
            return;
        }
        $dir = dirname($dest);
        if (!is_dir($dir) && !@mkdir($dir, 0700, true)) {
            continue;
        }
        if (!is_writable($dir)) {
            continue;
        }
        $tmp = $dest . '.tmp.' . bin2hex(random_bytes(4));
        if (@file_put_contents($tmp, substr($raw, 0, 32), LOCK_EX) === false) {
            continue;
        }
        @chmod($tmp, 0600);
        if (!@rename($tmp, $dest)) {
            @unlink($tmp);
            continue;
        }
        @chmod($dest, 0600);
        if (is_file($dest) && (int) @filesize($dest) >= 32) {
            @unlink($internal);
        }
        AntaraCrypto::resetMasterKeyPathCache();
        return;
    }
}

/**
 * Reinicia settings.json (y respaldo), personalización del explorador y caché
 * de estadísticas a valores de fábrica. Solo debe llamarse desde el asistente
 * de instalación (paso 1 GET), no durante recuperación criptográfica.
 *
 * También quita la referencia a la carpeta auxiliar fuera del sitio público e
 * intenta prepararla de nuevo en la misma petición (si el servidor lo permite).
 *
 * @return string|null Texto de recomendación si no se pudo preparar la ubicación auxiliar; null si todo bien o no aplica.
 */
function antara_install_reset_workspace_json_for_new_install(): ?string
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    $root = AntaraPaths::root();
    $ds = DIRECTORY_SEPARATOR;
    if (!is_dir($root) || !is_writable($root)) {
        return null;
    }

    require_once ANTARA_ROOT . '/includes/grid_default_settings.php';
    require_once ANTARA_ROOT . '/includes/settings_helpers.php';

    $defaults = grid_app_default_settings();
    $blank = grid_merge_settings_defaults($defaults, []);
    require_once ANTARA_ROOT . '/includes/user_settings_helpers.php';
    grid_user_profiles_wipe_all();
    grid_user_profiles_prepare_hosting_layout();
    $blank = grid_workspace_settings_only($blank);
    $sf = $root . $ds . 'settings.json';
    $bk = $root . $ds . 'settings.json.bak';
    grid_persist_settings($sf, $bk, $blank);

    if (!defined('GRID_ROOT')) {
        define('GRID_ROOT', $root);
    }
    require_once ANTARA_ROOT . '/includes/explorer_customization.php';
    $ecPath = grid_explorer_customization_config_path();
    $ecDir = dirname($ecPath);
    if (!is_dir($ecDir)) {
        @mkdir($ecDir, 0755, true);
    }
    if (is_dir($ecDir) && is_writable($ecDir)) {
        $ec = grid_explorer_customization_defaults();
        $jsonEc = json_encode($ec, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        if ($jsonEc !== false) {
            grid_atomic_write_file($ecPath, $jsonEc . "\n");
        }
    }

    $stats = $root . $ds . 'temp' . $ds . 'explorer_storage_stats.v1.json';
    if (is_file($stats)) {
        @unlink($stats);
    }

    antara_install_clear_host_logos();

    return antara_install_reset_external_aux_and_try_auto();
}

/**
 * Quita la referencia a la carpeta auxiliar (coherente con un escritorio formateado)
 * e intenta crear otra referencia opaca y directorios en la misma petición.
 *
 * @return string|null Recomendación para el usuario si no se pudo escribir la nueva referencia.
 */
function antara_install_reset_external_aux_and_try_auto(): ?string
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    require_once ANTARA_ROOT . '/core/security/vault_state.php';
    require_once ANTARA_ROOT . '/core/security/crypto.php';

    $cfgDir = AntaraPaths::config();
    if (!is_dir($cfgDir) || !is_writable($cfgDir)) {
        return 'Al formatear el escritorio desde este paso, la indicación de la carpeta auxiliar fuera del sitio público no pudo regenerarse: la carpeta de configuración no es escribible. Puedes seguir con la instalación; Flexia usará la ubicación interna protegida. Si tu plan de hosting lo permite más adelante, vuelve a abrir esta página.';
    }

    $f = AntaraPaths::externalSecretsBasenameFile();
    if (is_file($f)) {
        @unlink($f);
    }
    AntaraCrypto::resetMasterKeyPathCache();

    $slug = antara_install_generate_discrete_external_basename();
    if (!antara_install_write_external_secrets_basename($slug)) {
        return 'Al formatear el escritorio, la referencia a la carpeta auxiliar fuera del sitio público se restableció y no se pudo volver a crear (permisos en la configuración del servidor). Puedes continuar sin problema usando solo la zona interna. Cuando el hosting permita escribir en la carpeta de configuración del proyecto, recarga esta página para que Flexia lo intente de nuevo solo.';
    }

    antara_install_prepare_hosting_layout();
    antara_install_try_promote_master_key_to_external();
    try {
        AntaraCrypto::masterKeyPath();
    } catch (Throwable $e) {
        // El diagnóstico de entorno reflejará el estado de la clave.
    }

    return null;
}

/**
 * @return list<array{id:string,ok:bool,level:string,message:string}>
 */
function antara_install_environment_checks(): array
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    antara_install_prepare_hosting_layout();

    require_once ANTARA_ROOT . '/core/security/paths.php';
    require_once ANTARA_ROOT . '/core/security/crypto.php';

    $root = AntaraPaths::root();
    $checks = [];

    $opensslOk = extension_loaded('openssl') && function_exists('openssl_encrypt');
    $checks[] = [
        'id' => 'openssl',
        'ok' => $opensslOk,
        'level' => $opensslOk ? 'ok' : 'error',
        'message' => $opensslOk
            ? 'Cifrado del sistema disponible en el servidor.'
            : 'Falta soporte de cifrado en el servidor; no se pueden proteger bien los datos sensibles.',
    ];

    $dirs = [
        ['id' => 'storage', 'path' => AntaraPaths::storage(), 'label' => 'Zona interna de datos'],
        ['id' => 'storage_config', 'path' => AntaraPaths::config(), 'label' => 'Ajustes internos'],
        ['id' => 'storage_secure', 'path' => AntaraPaths::secure(), 'label' => 'Cofre cifrado interno'],
        ['id' => 'storage_logs', 'path' => AntaraPaths::logs(), 'label' => 'Registro de actividad'],
        ['id' => 'storage_users', 'path' => $root . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'users', 'label' => 'Perfiles de usuario'],
        ['id' => 'media', 'path' => $root . DIRECTORY_SEPARATOR . 'media', 'label' => 'Archivos y medios'],
        ['id' => 'temp', 'path' => $root . DIRECTORY_SEPARATOR . 'temp', 'label' => 'Espacio temporal'],
    ];
    foreach ($dirs as $d) {
        $p = $d['path'];
        $writable = is_dir($p) ? is_writable($p) : false;
        if (!is_dir($p)) {
            $writable = @mkdir($p, 0755, true) && is_writable($p);
        }
        $checks[] = [
            'id' => $d['id'],
            'ok' => $writable,
            'level' => $writable ? 'ok' : 'error',
            'message' => $writable
                ? $d['label'] . ': en orden para el esquema de seguridad esencial.'
                : $d['label'] . ': falta permiso o espacio; revisa con quien administra el hosting.',
        ];
    }

    $settingsPath = $root . DIRECTORY_SEPARATOR . 'settings.json';
    $settingsDirOk = is_writable(dirname($settingsPath));
    $checks[] = [
        'id' => 'settings_writable',
        'ok' => $settingsDirOk,
        'level' => $settingsDirOk ? 'ok' : 'error',
        'message' => $settingsDirOk
            ? 'La configuración del sitio puede guardarse correctamente.'
            : 'No hay permiso para guardar la configuración del sitio en la carpeta del proyecto.',
    ];

    $htStorage = AntaraPaths::storage() . DIRECTORY_SEPARATOR . '.htaccess';
    $htCore = $root . DIRECTORY_SEPARATOR . 'core' . DIRECTORY_SEPARATOR . '.htaccess';
    $htOk = is_file($htStorage) && is_file($htCore);
    $checks[] = [
        'id' => 'htaccess_sensitive',
        'ok' => $htOk,
        'level' => $htOk ? 'ok' : 'warn',
        'message' => $htOk
            ? 'Protección básica del servidor contra acceso web indebido: lista.'
            : 'Falta un refuerzo recomendado del servidor; el paquete incluye reglas para proteger zonas sensibles.',
    ];

    $keyOk = false;
    $keyMsg = '';
    try {
        AntaraCrypto::masterKeyPath();
        $keyOk = true;
        $keyMsg = 'Esquema de seguridad esencial accesible.';
    } catch (Throwable $e) {
        $keyMsg = 'No se pudo crear o leer la clave maestra: ' . $e->getMessage();
    }
    $checks[] = [
        'id' => 'master_key',
        'ok' => $keyOk,
        'level' => $keyOk ? 'ok' : 'error',
        'message' => $keyOk
            ? 'Esquema de seguridad esencial listo: el cifrado puede operar. Si restauras desde una copia, usa la misma clave y datos que van juntos en esa copia.'
            : $keyMsg,
    ];

    $critical = [
        'index.php',
        'includes/api.php',
        'includes/bootstrap.php',
        'includes/grid_default_settings.php',
        'includes/settings_helpers.php',
        'core/auth/api.php',
        'core/auth/bootstrap_auth.php',
        'core/auth/store.php',
        'core/security/crypto.php',
    ];
    $filesOk = true;
    foreach ($critical as $rel) {
        if (!is_file($root . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $rel))) {
            $filesOk = false;
            break;
        }
    }
    $checks[] = [
        'id' => 'core_files',
        'ok' => $filesOk,
        'level' => $filesOk ? 'ok' : 'error',
        'message' => $filesOk
            ? 'Archivos principales del proyecto presentes.'
            : 'Faltan piezas esenciales del programa en el servidor.',
    ];

    $docRoot = isset($_SERVER['DOCUMENT_ROOT']) ? realpath((string)$_SERVER['DOCUMENT_ROOT']) : false;
    $appRoot = realpath($root);
    if ($docRoot && $appRoot) {
        $basenames = array_unique([
            AntaraVaultState::defaultExternalSecretsDirBasename(),
            'antara_secrets',
        ]);
        $parents = [dirname($root), dirname($root, 2)];
        foreach ($parents as $parent) {
            foreach ($basenames as $bn) {
                $auxDir = $parent . DIRECTORY_SEPARATOR . $bn;
                if (!is_dir($auxDir)) {
                    continue;
                }
                $secretsReal = realpath($auxDir);
                if ($secretsReal === false) {
                    continue;
                }
                $underDoc = stripos(str_replace('\\', '/', $secretsReal), str_replace('\\', '/', $docRoot)) === 0;
                if ($underDoc) {
                    $checks[] = [
                        'id' => 'external_aux_under_docroot',
                        'ok' => false,
                        'level' => 'warn',
                        'message' => 'Una zona auxiliar de claves parece quedar expuesta como web pública. En producción conviene que quien administra el hosting la deje fuera del sitio visitable.',
                    ];
                    break 2;
                }
            }
        }
    }

    return $checks;
}

/**
 * Contexto del hosting (solo lectura, textos seguros para mostrar en el instalador).
 *
 * @return array<string, string>
 */
function antara_install_host_context_snapshot(): array
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    $root = AntaraPaths::root();

    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
        || (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https');
    $host = (string)($_SERVER['HTTP_HOST'] ?? '—');
    $sw = (string)($_SERVER['SERVER_SOFTWARE'] ?? '—');
    if (strlen($sw) > 80) {
        $sw = substr($sw, 0, 77) . '…';
    }
    $doc = isset($_SERVER['DOCUMENT_ROOT']) ? (string)$_SERVER['DOCUMENT_ROOT'] : '';
    $docR = $doc !== '' && ($rp = @realpath($doc)) ? $rp : $doc;
    if (strlen($docR) > 96) {
        $docR = '…' . substr($docR, -92);
    }
    $appR = @realpath($root) ?: $root;
    if (strlen($appR) > 96) {
        $appR = '…' . substr($appR, -92);
    }

    return [
        'scheme' => $https ? 'https' : 'http',
        'host' => $host,
        'php' => PHP_VERSION,
        'server_software' => $sw,
        'document_root' => $docR !== '' ? str_replace('\\', '/', $docR) : '—',
        'app_root' => str_replace('\\', '/', $appR),
    ];
}

/**
 * Comprobaciones adicionales de endurecimiento (complementan el entorno base).
 *
 * @return list<array{id:string,ok:bool,level:string,message:string,recommendation:string}>
 */
function antara_install_extra_security_checks(): array
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    $root = AntaraPaths::root();
    $out = [];

    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
        || (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https');
    $out[] = [
        'id' => 'transport_https',
        'ok' => $https,
        'level' => $https ? 'ok' : 'warn',
        'message' => $https
            ? 'La solicitud llega por HTTPS (cifrado en tránsito).'
            : 'La instalación se está sirviendo por HTTP: las credenciales viajan sin cifrar entre el navegador y el servidor.',
        'recommendation' => $https
            ? 'Mantén un certificado TLS válido y fuerza HTTPS en el virtual host o proxy.'
            : 'Activa TLS en Apache/Nginx/IIS o detrás de un proxy (Cloudflare, etc.) y redirige HTTP→HTTPS. En hosting compartido suele haber una opción «Forzar HTTPS».',
    ];

    $phpOk = version_compare(PHP_VERSION, '8.0.0', '>=');
    $out[] = [
        'id' => 'php_version',
        'ok' => $phpOk,
        'level' => $phpOk ? 'ok' : 'warn',
        'message' => $phpOk
            ? 'Versión de PHP ' . PHP_VERSION . ' (compatible con Flexia).'
            : 'PHP ' . PHP_VERSION . ' está por debajo de 8.0; puede haber riesgos de compatibilidad y menor soporte de seguridad.',
        'recommendation' => $phpOk
            ? 'Actualiza PHP cuando tu proveedor ofrezca versiones superiores (8.2+ recomendado).'
            : 'En el panel del hosting elige PHP 8.1 o superior para este dominio o carpeta.',
    ];

    $expose = strtolower((string)ini_get('expose_php'));
    $exposeOn = ($expose === '1' || $expose === 'on');
    $out[] = [
        'id' => 'expose_php',
        'ok' => !$exposeOn,
        'level' => $exposeOn ? 'warn' : 'ok',
        'message' => $exposeOn
            ? 'expose_php está activo: el servidor puede filtrar la versión de PHP en cabeceras HTTP.'
            : 'expose_php desactivado o no expone la versión de PHP.',
        'recommendation' => $exposeOn
            ? 'En php.ini o en la configuración del hosting: expose_php = Off. Reduce la superficie de información útil para atacantes.'
            : '—',
    ];

    $disp = strtolower((string)ini_get('display_errors'));
    $dispOn = ($disp === '1' || $disp === 'on');
    $out[] = [
        'id' => 'display_errors',
        'ok' => !$dispOn,
        'level' => $dispOn ? 'warn' : 'ok',
        'message' => $dispOn
            ? 'display_errors está activo: los errores PHP podrían mostrarse a los visitantes.'
            : 'display_errors desactivado para entorno público (recomendado).',
        'recommendation' => $dispOn
            ? 'En producción: display_errors=Off, log_errors=On y revisa los logs del servidor. En hosting, desactiva «mostrar errores» en el selector PHP.'
            : 'Mantén el registro de errores activo y revísalo en el panel del hosting o con quien administra el servidor.',
    ];

    $aui = strtolower((string)ini_get('allow_url_include'));
    $auiOn = ($aui === '1' || $aui === 'on');
    $out[] = [
        'id' => 'allow_url_include',
        'ok' => !$auiOn,
        'level' => $auiOn ? 'error' : 'ok',
        'message' => $auiOn
            ? 'allow_url_include está activo: riesgo crítico (inclusión remota de código).'
            : 'allow_url_include desactivado (correcto).',
        'recommendation' => $auiOn
            ? 'Desactiva allow_url_include en php.ini de inmediato. Ninguna aplicación web moderna debería usarlo.'
            : '—',
    ];

    $ch = ini_get('session.cookie_httponly');
    $chStr = strtolower((string)$ch);
    $httponly = ($ch === false || $ch === '' || $chStr === '1' || $chStr === 'on');
    $out[] = [
        'id' => 'session_cookie_httponly',
        'ok' => $httponly,
        'level' => $httponly ? 'ok' : 'warn',
        'message' => $httponly
            ? 'session.cookie_httponly activo (mitiga robo de sesión vía JavaScript).'
            : 'session.cookie_httponly desactivado explícitamente: las cookies de sesión son más accesibles desde JS.',
        'recommendation' => $httponly
            ? '—'
            : 'En php.ini: session.cookie_httponly = 1. Flexia también envía cabeceras de seguridad desde PHP.',
    ];

    if ($https) {
        $cs = strtolower((string)ini_get('session.cookie_secure'));
        $secureOk = ($cs === '1' || $cs === 'on');
        $out[] = [
            'id' => 'session_cookie_secure',
            'ok' => $secureOk,
            'level' => $secureOk ? 'ok' : 'warn',
            'message' => $secureOk
                ? 'session.cookie_secure activo acorde a HTTPS.'
                : 'HTTPS detectado pero session.cookie_secure no está activo: la cookie de sesión podría enviarse también por HTTP.',
            'recommendation' => $secureOk
                ? '—'
                : 'En php.ini: session.cookie_secure = 1 cuando todo el sitio sea HTTPS.',
        ];
    }

    $free = @disk_free_space($root);
    if ($free !== false) {
        $low = $free < (80 * 1024 * 1024);
        $out[] = [
            'id' => 'disk_free',
            'ok' => !$low,
            'level' => $low ? 'warn' : 'ok',
            'message' => $low
                ? 'Poco espacio libre en disco en la ruta del proyecto (< 80 MB aprox.).'
                : 'Espacio en disco suficiente para operar y cifrar datos.',
            'recommendation' => $low
                ? 'Libera espacio o amplía el almacenamiento: backups y cifrado fallan si el disco se llena.'
                : 'Programa copias de seguridad de esta instalación y de la base de datos si aplica.',
        ];
    }

    return $out;
}

/**
 * Recomendación breve para cada comprobación estándar del entorno (por id).
 */
function antara_install_recommendation_for_env_check(array $c): string
{
    $id = (string)($c['id'] ?? '');
    $map = [
        'openssl' => 'En el panel del hosting habilita el soporte de cifrado que pide PHP (openssl) o pide ayuda al proveedor.',
        'storage' => 'Comprueba permisos de carpetas y que el usuario del servidor web tenga escritura donde corresponde.',
        'storage_config' => 'Revisa permisos y que la zona de ajustes internos no sea accesible desde el navegador.',
        'storage_secure' => 'El cofre cifrado interno debe ser escribible solo por el sistema; no enlaces esa zona como web pública.',
        'storage_logs' => 'Activa un registro de actividad escribible para auditoría (permisos del hosting).',
        'storage_users' => 'Los perfiles personales (escritorio, notas) se guardan aquí; debe ser escribible solo por el servidor.',
        'media' => 'Asegura permisos de escritura para archivos y medios que suben los usuarios.',
        'temp' => 'Asegura permisos de escritura para cachés temporales del sistema.',
        'settings_writable' => 'Ajusta permisos de la carpeta del proyecto para que se pueda guardar la configuración.',
        'htaccess_sensitive' => 'Vuelve a desplegar las reglas de protección incluidas en el paquete o configura el equivalente en tu servidor (Apache, Nginx, IIS).',
        'master_key' => 'Revisa permisos y espacio en disco; si hay clave auxiliar fuera del proyecto, también sus permisos en el servidor.',
        'core_files' => 'Vuelve a subir el paquete completo del proyecto sin sobrescribir solo partes.',
        'external_aux_under_docroot' => 'Revisa con el proveedor que la zona auxiliar de claves no quede dentro del sitio web público. Los traslados entre servidores se harán con el flujo de migración previsto en Flexia.',
    ];
    if (($c['ok'] ?? false) && ($c['level'] ?? '') !== 'warn') {
        return '—';
    }
    return $map[$id] ?? 'Consulta con tu proveedor de hosting o el administrador del servidor.';
}

/**
 * Diagnóstico de seguridad del entorno + puntuación para el asistente web.
 *
 * @return array{
 *   items: list<array{id:string,ok:bool,level:string,message:string,recommendation:string}>,
 *   score: int,
 *   tier: string,
 *   tier_label: string,
 *   host: array<string, string>,
 *   blocking: bool
 * }
 */
function antara_install_security_wizard_data(): array
{
    $host = antara_install_host_context_snapshot();
    $base = antara_install_environment_checks();
    $items = [];
    foreach ($base as $c) {
        $items[] = [
            'id' => (string)($c['id'] ?? ''),
            'ok' => !empty($c['ok']),
            'level' => (string)($c['level'] ?? 'ok'),
            'message' => (string)($c['message'] ?? ''),
            'recommendation' => antara_install_recommendation_for_env_check($c),
        ];
    }
    foreach (antara_install_extra_security_checks() as $e) {
        $items[] = $e;
    }

    $score = 100;
    foreach ($items as $it) {
        $lv = $it['level'] ?? 'ok';
        $ok = !empty($it['ok']);
        if ($lv === 'error' && !$ok) {
            $score -= 28;
        } elseif ($lv === 'warn' && !$ok) {
            $score -= 10;
        }
    }
    if ($score < 0) {
        $score = 0;
    }
    if ($score > 100) {
        $score = 100;
    }

    $blocking = antara_install_checks_have_blocking_errors($base);
    if ($blocking) {
        $score = min($score, 38);
    }

    $allowUrlIncBad = false;
    foreach ($items as $it) {
        if (($it['id'] ?? '') === 'allow_url_include' && empty($it['ok'])) {
            $allowUrlIncBad = true;
            break;
        }
    }
    if ($allowUrlIncBad) {
        $score = min($score, 25);
    }

    $tier = 'medium';
    $tierLabel = 'Nivel medio — puedes seguir; mira las advertencias';
    if ($score >= 78 && !$blocking) {
        $tier = 'high';
        $tierLabel = 'Nivel alto — buen punto de partida';
    } else    if ($score < 48 || $blocking) {
        $tier = 'low';
        $tierLabel = $blocking
            ? 'Nivel bajo — primero los puntos en rojo'
            : 'Nivel bajo — mejora el entorno antes de datos delicados';
    }

    $rootW = antara_install_root_writable_for_user_ini();
    $https = antara_install_https_request_context();
    foreach ($items as $idx => $it) {
        $items[$idx]['fix_action'] = antara_install_wizard_fix_action_for_item(
            (string)($it['id'] ?? ''),
            (string)($it['level'] ?? ''),
            !empty($it['ok']),
            $rootW,
            $https
        );
    }

    return [
        'items' => $items,
        'score' => $score,
        'tier' => $tier,
        'tier_label' => $tierLabel,
        'host' => $host,
        'blocking' => $blocking,
    ];
}

function antara_install_checks_have_blocking_errors(array $checks): bool
{
    foreach ($checks as $c) {
        if (($c['level'] ?? '') === 'error' && empty($c['ok'])) {
            return true;
        }
    }
    return false;
}

function antara_fs_remove_recursive(string $path): void
{
    $path = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path), DIRECTORY_SEPARATOR);
    if ($path === '' || !file_exists($path)) {
        return;
    }
    if (is_file($path) || is_link($path)) {
        @unlink($path);
        return;
    }
    if (!is_dir($path)) {
        return;
    }
    $items = @scandir($path);
    if (is_array($items)) {
        foreach ($items as $it) {
            if ($it === '.' || $it === '..') {
                continue;
            }
            antara_fs_remove_recursive($path . DIRECTORY_SEPARATOR . $it);
        }
    }
    @rmdir($path);
}

require_once __DIR__ . '/flexia_disk.php';

/** @deprecated Use GRID_FLEXIA_DISK_DIRNAME */
const ANTARA_INSTALL_FLEXIA_BACKUP_DIRNAME = GRID_FLEXIA_DISK_DIRNAME;

/**
 * Mueve documentos del explorador (comunitario, departamentos y por usuario) a flexia disk/{fecha}/.
 *
 * @return array{ok:bool,path?:string,error?:string}
 */
function antara_install_backup_explorer_documents_to_flexia_backup(): array
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    $root = AntaraPaths::root();
    $ds = DIRECTORY_SEPARATOR;
    $explorer = $root . $ds . 'media' . $ds . 'explorer';
    if (!is_dir($explorer)) {
        return ['ok' => true, 'path' => ''];
    }

    $stamp = gmdate('Y-m-d_His');
    $backupRoot = $root . $ds . GRID_FLEXIA_DISK_DIRNAME . $ds . $stamp;
    if (!is_dir($backupRoot) && !@mkdir($backupRoot, 0755, true)) {
        return ['ok' => false, 'error' => 'No se pudo crear la carpeta de respaldo.'];
    }

    $moveDir = static function (string $src, string $dest) use ($ds): bool {
        if (!is_dir($src)) {
            return true;
        }
        $items = @scandir($src);
        if (!is_array($items) || count(array_diff($items, ['.', '..'])) === 0) {
            return true;
        }
        if (is_dir($dest)) {
            foreach ($items as $it) {
                if ($it === '.' || $it === '..') {
                    continue;
                }
                $from = $src . $ds . $it;
                $to = $dest . $ds . $it;
                if (is_dir($from)) {
                    if (!is_dir($to) && !@mkdir($to, 0755, true)) {
                        return false;
                    }
                    if (!antara_install_backup_merge_dir($from, $to)) {
                        return false;
                    }
                    antara_fs_remove_recursive($from);
                } elseif (is_file($from) || is_link($from)) {
                    if (is_file($to) || is_dir($to)) {
                        @unlink($to);
                    }
                    if (!@rename($from, $to)) {
                        return false;
                    }
                }
            }

            return true;
        }
        $parent = dirname($dest);
        if (!is_dir($parent) && !@mkdir($parent, 0755, true)) {
            return false;
        }

        return @rename($src, $dest);
    };

    $communitySrc = $explorer . $ds . 'files';
    $communityDest = $backupRoot . $ds . 'espacio_comunitario';
    if (!$moveDir($communitySrc, $communityDest)) {
        return ['ok' => false, 'error' => 'No se pudo respaldar el espacio comunitario.'];
    }
    if (!is_dir($communitySrc)) {
        @mkdir($communitySrc, 0755, true);
    }

    $usersRoot = $explorer . $ds . 'users';
    if (is_dir($usersRoot)) {
        $userIds = @scandir($usersRoot);
        if (is_array($userIds)) {
            foreach ($userIds as $uid) {
                if ($uid === '.' || $uid === '..') {
                    continue;
                }
                $userDir = $usersRoot . $ds . $uid;
                if (!is_dir($userDir)) {
                    continue;
                }
                $destUser = $backupRoot . $ds . 'usuarios' . $ds . $uid;
                if (!$moveDir($userDir, $destUser)) {
                    return ['ok' => false, 'error' => 'No se pudo respaldar documentos del usuario.'];
                }
            }
        }
    }

    $deptRoot = $explorer . $ds . 'departments';
    if (is_dir($deptRoot)) {
        $deptIds = @scandir($deptRoot);
        if (is_array($deptIds)) {
            foreach ($deptIds as $deptId) {
                if ($deptId === '.' || $deptId === '..') {
                    continue;
                }
                $deptDir = $deptRoot . $ds . $deptId;
                if (!is_dir($deptDir)) {
                    continue;
                }
                $destDept = $backupRoot . $ds . 'departamentos' . $ds . $deptId;
                if (!$moveDir($deptDir, $destDept)) {
                    return ['ok' => false, 'error' => 'No se pudo respaldar carpetas de departamento.'];
                }
            }
        }
    }

    $rel = GRID_FLEXIA_DISK_DIRNAME . '/' . $stamp;

    return ['ok' => true, 'path' => $rel];
}

/**
 * Fusiona el contenido de $src dentro de $dest (ambos directorios).
 */
function antara_install_backup_merge_dir(string $src, string $dest): bool
{
    $ds = DIRECTORY_SEPARATOR;
    if (!is_dir($src)) {
        return true;
    }
    if (!is_dir($dest) && !@mkdir($dest, 0755, true)) {
        return false;
    }
    $items = @scandir($src);
    if (!is_array($items)) {
        return false;
    }
    foreach ($items as $it) {
        if ($it === '.' || $it === '..') {
            continue;
        }
        $from = $src . $ds . $it;
        $to = $dest . $ds . $it;
        if (is_dir($from)) {
            if (!antara_install_backup_merge_dir($from, $to)) {
                return false;
            }
            antara_fs_remove_recursive($from);
        } elseif (is_file($from) || is_link($from)) {
            if (is_file($to) || is_dir($to)) {
                @unlink($to);
            }
            if (!@rename($from, $to)) {
                return false;
            }
        }
    }

    return true;
}

/**
 * Vacía media/ para dejar el proyecto listo para un nuevo paso 1 del instalador.
 *
 * @param bool $keepExplorerDocuments Si es true, conserva el árbol media/explorer/files (documentos del explorador).
 */
function antara_install_clear_media_for_reinstall(bool $keepExplorerDocuments): void
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    $root = AntaraPaths::root();
    $ds = DIRECTORY_SEPARATOR;
    $media = $root . $ds . 'media';
    if (!is_dir($media)) {
        return;
    }
    $items = @scandir($media);
    if (!is_array($items)) {
        return;
    }
    $filesRoot = $media . $ds . 'explorer' . $ds . 'files';

    foreach ($items as $it) {
        if ($it === '.' || $it === '..') {
            continue;
        }
        $full = $media . $ds . $it;
        if ($keepExplorerDocuments && strcasecmp($it, 'explorer') === 0 && is_dir($full)) {
            $sub = @scandir($full);
            if (!is_array($sub)) {
                continue;
            }
            foreach ($sub as $s) {
                if ($s === '.' || $s === '..') {
                    continue;
                }
                if (strcasecmp($s, 'files') === 0) {
                    continue;
                }
                antara_fs_remove_recursive($full . $ds . $s);
            }
            continue;
        }
        antara_fs_remove_recursive($full);
    }
}

/**
 * Elimina todos los logos e imágenes de perfil subidos en logos_sinapsys/
 * (empresa, sucursales, primer admin, avatares profile_*, etc.).
 */
function antara_install_clear_host_logos(): void
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    $dir = AntaraPaths::root() . DIRECTORY_SEPARATOR . 'logos_sinapsys';
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);

        return;
    }
    $ds = DIRECTORY_SEPARATOR;
    $items = @scandir($dir);
    if (!is_array($items)) {
        return;
    }
    foreach ($items as $it) {
        if ($it === '.' || $it === '..') {
            continue;
        }
        $full = $dir . $ds . $it;
        if (is_file($full) || is_link($full)) {
            @unlink($full);
        } elseif (is_dir($full)) {
            antara_fs_remove_recursive($full);
        }
    }
}

/** @deprecated Use antara_install_clear_host_logos() */
function antara_install_clear_client_logos(): void
{
    antara_install_clear_host_logos();
}

/** Nombre del fichero persistente de formateos (no se borra al formatear). */
const ANTARA_INSTALL_FORMAT_COUNT_BASENAME = 'flexia_format_count.json';

/**
 * @return string Ruta absoluta a storage/config/flexia_format_count.json
 */
function antara_install_format_count_path(): string
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/core/security/paths.php';
    AntaraPaths::ensureDirs();

    return AntaraPaths::config() . DIRECTORY_SEPARATOR . ANTARA_INSTALL_FORMAT_COUNT_BASENAME;
}

/**
 * Crea el fichero de conteo con 0 si no existe (sin error al usuario).
 */
function antara_install_ensure_format_count_file(): void
{
    $path = antara_install_format_count_path();
    if (is_file($path)) {
        return;
    }
    $dir = dirname($path);
    if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
        return;
    }
    require_once ANTARA_ROOT . '/includes/settings_helpers.php';
    $payload = [
        'count' => 0,
        'created_at' => gmdate('c'),
        'updated_at' => gmdate('c'),
    ];
    $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    if ($json !== false) {
        grid_atomic_write_file($path, $json . "\n");
    }
}

/**
 * Veces que se ha formateado Flexia en este servidor (0 si nunca o fichero nuevo).
 */
function antara_install_read_format_count(): int
{
    antara_install_ensure_format_count_file();
    $path = antara_install_format_count_path();
    if (!is_file($path)) {
        return 0;
    }
    $raw = @file_get_contents($path);
    if ($raw === false || $raw === '') {
        return 0;
    }
    $data = json_decode($raw, true);
    if (!is_array($data)) {
        return 0;
    }

    return max(0, (int) ($data['count'] ?? 0));
}

/**
 * Registra un formateo / reinicio del asistente. Devuelve el total acumulado.
 */
function antara_install_increment_format_count(): int
{
    $count = antara_install_read_format_count() + 1;
    $path = antara_install_format_count_path();
    $dir = dirname($path);
    if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
        return $count;
    }
    require_once ANTARA_ROOT . '/includes/settings_helpers.php';
    $payload = [
        'count' => $count,
        'updated_at' => gmdate('c'),
    ];
    if (!is_file($path)) {
        $payload['created_at'] = gmdate('c');
    } else {
        $raw = @file_get_contents($path);
        $prev = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null;
        if (is_array($prev) && isset($prev['created_at'])) {
            $payload['created_at'] = $prev['created_at'];
        } else {
            $payload['created_at'] = gmdate('c');
        }
    }
    $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    if ($json !== false) {
        grid_atomic_write_file($path, $json . "\n");
    }

    return $count;
}

/**
 * Texto para el subtítulo del instalador según el conteo de formateos.
 */
function antara_install_format_count_caption(): string
{
    $n = antara_install_read_format_count();
    if ($n <= 0) {
        return 'Sin formateos previos registrados en este servidor.';
    }
    if ($n === 1) {
        return 'Este servidor ha registrado 1 formateo de Flexia.';
    }

    return 'Este servidor ha registrado ' . $n . ' formateos de Flexia.';
}

/**
 * Borrador, sesión del wizard, settings, logos del host y media/ para empezar en paso 1.
 *
 * @param bool $keepExplorerDocuments Si true, conserva media/explorer/files
 */
function antara_install_begin_fresh_wizard(bool $keepExplorerDocuments = false): void
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    $formatCount = antara_install_increment_format_count();
    require_once ANTARA_ROOT . '/includes/flexia_instance.php';
    antara_flexia_regenerate_instance('format', $formatCount);
    require_once ANTARA_ROOT . '/includes/install_wizard_state.php';
    antara_install_wizard_clear_draft();
    antara_install_wizard_clear_install_session();
    antara_install_reset_workspace_json_for_new_install();
    antara_install_clear_media_for_reinstall($keepExplorerDocuments);
}

/**
 * Reinicia escritorio y explorador, vacía media (opcionalmente conservando documentos),
 * elimina todos los logos/imágenes del host y vacía usuarios y credenciales WebAuthn, y quita install.lock
 * para reabrir /install/.
 *
 * @return array{ok:bool,error?:string}
 */
function antara_install_prepare_reopen_wizard(bool $keepExplorerDocuments): array
{
    if (!defined('ANTARA_ROOT')) {
        define('ANTARA_ROOT', dirname(__DIR__));
    }
    require_once ANTARA_ROOT . '/includes/install_public.php';
    require_once ANTARA_ROOT . '/core/auth/store.php';
    require_once ANTARA_ROOT . '/core/security/logger.php';
    try {
        AntaraStore::resetUsersAndCredentials();
        AntaraLogger::log('install.reopen_wizard_cleared_auth', []);
    } catch (\Throwable $e) {
        AntaraLogger::log('install.reopen_wizard_clear_auth_failed', ['msg' => $e->getMessage()]);

        return ['ok' => false, 'error' => 'No se pudo vaciar el almacén de usuarios para el reinstalador. Revisa permisos y el cifrado de datos.'];
    }
    if ($keepExplorerDocuments) {
        $backup = antara_install_backup_explorer_documents_to_flexia_backup();
        if (empty($backup['ok'])) {
            return ['ok' => false, 'error' => (string) ($backup['error'] ?? 'No se pudo guardar una copia de los documentos en flexia disk.')];
        }
    }
    antara_install_begin_fresh_wizard(false);
    if (!antara_install_remove_lock()) {
        return ['ok' => false, 'error' => 'No se pudo completar el reinicio del instalador: revisa permisos con quien administra el servidor.'];
    }
    antara_install_prepare_hosting_layout();
    return ['ok' => true];
}

/**
 * Parse sucursales desde POST (arrays paralelos por índice).
 *
 * @param list<array<string, mixed>> $existingBranches IDs existentes para conservar al reenviar
 * @return list<array{
 *   id:string,
 *   name:string,
 *   code:string,
 *   address:string,
 *   email:string,
 *   phone:string,
 *   useCompanyContact:bool,
 *   usePrimaryLogo:bool,
 *   logoRel:string
 * }>
 */
function antara_install_parse_branches_post(array $body, array $existingBranches = []): array
{
    require_once dirname(__DIR__) . '/includes/install_geo.php';

    $names = $body['branch_name'] ?? [];
    $codes = $body['branch_code'] ?? [];
    $addresses = $body['branch_address'] ?? [];
    $locationJsons = $body['branch_location_json'] ?? [];
    $emails = $body['branch_email'] ?? [];
    $phones = $body['branch_phone'] ?? [];
    $usePrimaryLogo = $body['branch_use_primary_logo'] ?? [];
    $idsIn = $body['branch_id'] ?? [];
    if (!is_array($names)) {
        $names = [];
    }
    if (!is_array($codes)) {
        $codes = [];
    }
    if (!is_array($addresses)) {
        $addresses = [];
    }
    if (!is_array($locationJsons)) {
        $locationJsons = [];
    }
    if (!is_array($emails)) {
        $emails = [];
    }
    if (!is_array($phones)) {
        $phones = [];
    }
    if (!is_array($usePrimaryLogo)) {
        $usePrimaryLogo = [];
    }
    if (!is_array($idsIn)) {
        $idsIn = [];
    }

    $branches = [];
    foreach ($names as $i => $nameRaw) {
        $name = trim((string) $nameRaw);
        if ($name === '') {
            continue;
        }
        if (function_exists('mb_strlen') && mb_strlen($name, 'UTF-8') > 120) {
            $name = (string) mb_substr($name, 0, 120, 'UTF-8');
        } elseif (strlen($name) > 120) {
            $name = substr($name, 0, 120);
        }
        $code = '';

        $id = '';
        if (isset($idsIn[$i]) && is_string($idsIn[$i])) {
            $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $idsIn[$i]);
        }
        $existingAdditional = array_values(array_slice($existingBranches, 1));
        if ($id === '' && isset($existingAdditional[$i]) && is_array($existingAdditional[$i])) {
            $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($existingAdditional[$i]['id'] ?? ''));
        }
        if ($id === '') {
            try {
                $id = 'br_' . bin2hex(random_bytes(12));
            } catch (\Throwable $e) {
                $rnd = function_exists('openssl_random_pseudo_bytes') ? openssl_random_pseudo_bytes(12) : null;
                $id = 'br_' . bin2hex(is_string($rnd) && strlen($rnd) >= 12 ? substr($rnd, 0, 12) : (string) microtime(true));
            }
        }

        $upl = !empty($usePrimaryLogo[$i]);

        $locJson = isset($locationJsons[$i]) ? (string) $locationJsons[$i] : '';
        $merged = antara_install_merge_location_into_address(antara_install_parse_location_json($locJson));
        $location = $merged['location'];
        $address = trim((string) ($addresses[$i] ?? ''));
        if ($address === '') {
            $address = $merged['address'];
        }
        $email = trim((string) ($emails[$i] ?? ''));
        $phone = trim((string) ($phones[$i] ?? ''));

        if (strlen($address) > 500) {
            $address = function_exists('mb_substr') ? (string) mb_substr($address, 0, 500, 'UTF-8') : substr($address, 0, 500);
        }
        if (strlen($email) > 200) {
            $email = function_exists('mb_substr') ? (string) mb_substr($email, 0, 200, 'UTF-8') : substr($email, 0, 200);
        }
        if (strlen($phone) > 40) {
            $phone = function_exists('mb_substr') ? (string) mb_substr($phone, 0, 40, 'UTF-8') : substr($phone, 0, 40);
        }

        $logoRel = '';
        if (isset($existingAdditional[$i]) && is_array($existingAdditional[$i])) {
            $logoRel = (string) ($existingAdditional[$i]['logoRel'] ?? '');
        }

        $branches[] = [
            'id' => $id,
            'name' => $name,
            'code' => $code,
            'address' => $address,
            'location' => $location,
            'email' => $email,
            'phone' => $phone,
            'useCompanyContact' => false,
            'usePrimaryLogo' => $upl,
            'logoRel' => $logoRel,
            'departments' => [],
        ];
    }

    return $branches;
}

/**
 * Sucursal matriz (misma empresa): se crea en servidor, no en el formulario de sucursales.
 *
 * @param list<array<string, mixed>> $existingBranches
 * @return array<string, mixed>
 */
function antara_install_build_matrix_branch(string $companyName, string $matrixBranchId, array $existingBranches = []): array
{
    require_once dirname(__DIR__) . '/includes/install_geo.php';

    $id = preg_replace('/[^a-zA-Z0-9_]/', '', $matrixBranchId);
    if ($id === '' && isset($existingBranches[0]) && is_array($existingBranches[0])) {
        $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($existingBranches[0]['id'] ?? ''));
    }
    if ($id === '') {
        try {
            $id = 'br_' . bin2hex(random_bytes(12));
        } catch (\Throwable $e) {
            $rnd = function_exists('openssl_random_pseudo_bytes') ? openssl_random_pseudo_bytes(12) : null;
            $id = 'br_' . bin2hex(is_string($rnd) && strlen($rnd) >= 12 ? substr($rnd, 0, 12) : (string) microtime(true));
        }
    }
    $name = trim($companyName);
    if ($name === '') {
        $name = 'Matriz';
    }

    return [
        'id' => $id,
        'name' => $name,
        'code' => 'MATRIZ',
        'address' => '',
        'location' => antara_install_empty_location(),
        'email' => '',
        'phone' => '',
        'useCompanyContact' => true,
        'usePrimaryLogo' => true,
        'logoRel' => '',
        'departments' => [],
    ];
}

/**
 * @param list<array<string, mixed>> $additional
 * @param list<array<string, mixed>> $existingBranches
 * @return list<array<string, mixed>>
 */
function antara_install_merge_matrix_and_branches(array $matrix, array $additional, array $existingBranches = []): array
{
    if ($matrix === []) {
        return $additional;
    }

    return array_merge([$matrix], $additional);
}

/**
 * Aplica datos de empresa y logos a las sucursales tras el POST company.
 *
 * @param list<array<string, mixed>> $branches
 * @param array{companyAddress:string,companyEmail:string,companyPhone:string,companyLocation?:array<string,mixed>} $company
 * @return array{errors: list<string>, branches: list<array<string, mixed>>}
 */
function antara_install_finalize_company_branches(array $branches, array $company): array
{
    require_once dirname(__DIR__) . '/includes/install_branch_logos.php';
    require_once dirname(__DIR__) . '/includes/install_company_logo.php';
    require_once dirname(__DIR__) . '/includes/install_geo.php';

    $errors = [];
    $addr = trim((string) ($company['companyAddress'] ?? ''));
    $email = trim((string) ($company['companyEmail'] ?? ''));
    $phone = trim((string) ($company['companyPhone'] ?? ''));
    $companyLoc = isset($company['companyLocation']) && is_array($company['companyLocation'])
        ? antara_install_normalize_location($company['companyLocation'])
        : antara_install_empty_location();
    if ($addr === '' && !antara_install_location_is_empty($companyLoc)) {
        $addr = antara_install_compose_address($companyLoc);
    }

    $logoResult = antara_install_company_logo_apply_upload();
    if ($logoResult['errors'] !== []) {
        $errors = array_merge($errors, $logoResult['errors']);
    }

    $primaryRel = antara_install_primary_company_logo_rel();

    $logoFileIndex = 0;
    foreach ($branches as $i => &$br) {
        if (!is_array($br)) {
            continue;
        }
        if ($i === 0) {
            $br['address'] = $addr;
            $br['location'] = $companyLoc;
            $br['email'] = $email;
            $br['phone'] = $phone;
            $br['useCompanyContact'] = true;
            $br['usePrimaryLogo'] = true;
            if ($primaryRel !== '') {
                $br['logoRel'] = $primaryRel;
            } else {
                $bid = (string) ($br['id'] ?? '');
                if ($bid !== '') {
                    $copied = antara_install_branch_copy_primary_logo_to_branch($bid);
                    if ($copied !== '') {
                        $br['logoRel'] = $copied;
                    }
                }
            }
            continue;
        }
        $bid = (string) ($br['id'] ?? '');
        if (!empty($br['usePrimaryLogo'])) {
            $br['logoRel'] = $primaryRel !== '' ? $primaryRel : (string) ($branches[0]['logoRel'] ?? '');
            continue;
        }
        $upload = antara_install_branch_logo_apply_upload($bid, $logoFileIndex);
        $logoFileIndex++;
        if ($upload['errors'] !== []) {
            $errors = array_merge($errors, $upload['errors']);
        } elseif ($upload['rel'] !== '') {
            $br['logoRel'] = $upload['rel'];
        }
    }
    unset($br);

    return ['errors' => $errors, 'branches' => $branches];
}

/**
 * Valida cuentas opcionales por sucursal adicional en el POST del paso empresa.
 *
 * @param list<array<string, mixed>> $additionalBranches Sucursales adicionales (sin matriz)
 * @return list<string>
 */
function antara_install_validate_branch_users_post(array $body, array $additionalBranches): array
{
    require_once dirname(__DIR__) . '/core/auth/store.php';

    $errors = [];
    $creates = $body['branch_user_create'] ?? [];
    $usernames = $body['branch_user_username'] ?? [];
    $emails = $body['branch_user_email'] ?? [];
    $phones = $body['branch_user_phone'] ?? [];
    $passwords = $body['branch_user_password'] ?? [];
    $confirms = $body['branch_user_password_confirm'] ?? [];
    $roles = $body['branch_user_role'] ?? [];
    if (!is_array($creates)) {
        $creates = [];
    }
    if (!is_array($usernames)) {
        $usernames = [];
    }
    if (!is_array($emails)) {
        $emails = [];
    }
    if (!is_array($phones)) {
        $phones = [];
    }
    if (!is_array($passwords)) {
        $passwords = [];
    }
    if (!is_array($confirms)) {
        $confirms = [];
    }
    if (!is_array($roles)) {
        $roles = [];
    }

    foreach ($additionalBranches as $i => $br) {
        if (!is_array($br)) {
            continue;
        }
        $bLabel = (string) ($br['name'] ?? ('Sucursal ' . ($i + 1)));
        if (empty($creates[$i]) || (string) $creates[$i] === '0') {
            continue;
        }
        $username = trim((string) ($usernames[$i] ?? ''));
        $email = trim((string) ($emails[$i] ?? ''));
        $phone = trim((string) ($phones[$i] ?? ''));
        $password = (string) ($passwords[$i] ?? '');
        $confirm = (string) ($confirms[$i] ?? '');
        $roleRaw = (string) ($roles[$i] ?? 'admin');
        if ($username === '') {
            $errors[] = 'Indica el nombre completo de la cuenta de «' . $bLabel . '».';
        }
        if ($email === '') {
            $errors[] = 'Indica el correo de la cuenta de «' . $bLabel . '».';
        } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $errors[] = 'El correo de la cuenta de «' . $bLabel . '» no es válido.';
        }
        if ($phone === '') {
            $errors[] = 'Indica el teléfono de la cuenta de «' . $bLabel . '».';
        }
        if (strlen($password) < 8) {
            $errors[] = 'La contraseña de la cuenta de «' . $bLabel . '» debe tener al menos 8 caracteres.';
        }
        if ($password !== $confirm) {
            $errors[] = 'Las contraseñas de la cuenta de «' . $bLabel . '» no coinciden.';
        }
        if ($password !== '' && !preg_match('/[A-Za-z]/', $password)) {
            $errors[] = 'La contraseña de «' . $bLabel . '» debe incluir al menos una letra.';
        }
        if ($password !== '' && !preg_match('/[0-9]/', $password)) {
            $errors[] = 'La contraseña de «' . $bLabel . '» debe incluir al menos un número.';
        }
        if (!in_array($roleRaw, ['admin', 'usuario', AntaraStore::ROLE_ADMIN, AntaraStore::ROLE_USER], true)) {
            $errors[] = 'El rol de la cuenta de «' . $bLabel . '» no es válido.';
        }
    }

    return $errors;
}

/**
 * @param list<array<string, mixed>> $allBranches [matriz, ...adicionales]
 * @return list<array{branchId:string,email:string,username:string,phone:string,password:string,role:string}>
 */
function antara_install_parse_branch_users_post(array $body, array $allBranches): array
{
    require_once dirname(__DIR__) . '/core/auth/store.php';

    $creates = $body['branch_user_create'] ?? [];
    $usernames = $body['branch_user_username'] ?? [];
    $emails = $body['branch_user_email'] ?? [];
    $phones = $body['branch_user_phone'] ?? [];
    $passwords = $body['branch_user_password'] ?? [];
    $roles = $body['branch_user_role'] ?? [];
    if (!is_array($creates)) {
        $creates = [];
    }
    if (!is_array($usernames)) {
        $usernames = [];
    }
    if (!is_array($emails)) {
        $emails = [];
    }
    if (!is_array($phones)) {
        $phones = [];
    }
    if (!is_array($passwords)) {
        $passwords = [];
    }
    if (!is_array($roles)) {
        $roles = [];
    }

    $additional = count($allBranches) > 1 ? array_values(array_slice($allBranches, 1)) : [];
    $pending = [];
    foreach ($additional as $i => $br) {
        if (!is_array($br) || empty($creates[$i]) || (string) $creates[$i] === '0') {
            continue;
        }
        $branchId = (string) ($br['id'] ?? '');
        if ($branchId === '') {
            continue;
        }
        $roleRaw = (string) ($roles[$i] ?? 'admin');
        $role = ($roleRaw === AntaraStore::ROLE_USER || $roleRaw === 'usuario')
            ? AntaraStore::ROLE_USER
            : AntaraStore::ROLE_ADMIN;
        $pending[] = [
            'branchId' => $branchId,
            'email' => trim((string) ($emails[$i] ?? '')),
            'username' => trim((string) ($usernames[$i] ?? '')),
            'phone' => trim((string) ($phones[$i] ?? '')),
            'password' => (string) ($passwords[$i] ?? ''),
            'role' => $role,
        ];
    }

    return $pending;
}
