<?php
class NexoraSecurity {
    const CSRF_SESSION_KEY = 'nexora_csrf_token';

    private static $config;
    private static $managedSecrets;

    public static function config($key = null, $default = null) {
        if(!is_array(self::$config)) {
            $path = __PATH_CONFIGS__ . 'nexora.security.php';
            self::$config = is_file($path) ? include($path) : array();
            $local = nexoraLocalConfig();
            if(isset($local['NEXORA_PLATFORM_ENABLED'])) self::$config['platform_enabled'] = (bool)$local['NEXORA_PLATFORM_ENABLED'];
            if(isset($local['NEXORA_PLATFORM_DB']) && $local['NEXORA_PLATFORM_DB'] !== '') self::$config['platform_database'] = $local['NEXORA_PLATFORM_DB'];
            if(isset($local['NEXORA_TRUST_PROXY_HEADERS'])) self::$config['trust_proxy_headers'] = (bool)$local['NEXORA_TRUST_PROXY_HEADERS'];
            if(isset($local['NEXORA_TRUSTED_PROXIES']) && is_array($local['NEXORA_TRUSTED_PROXIES'])) self::$config['trusted_proxies'] = $local['NEXORA_TRUSTED_PROXIES'];
        }
        if($key === null) return self::$config;
        return array_key_exists($key, self::$config) ? self::$config[$key] : $default;
    }

    public static function csrfToken() {
        if(session_status() !== PHP_SESSION_ACTIVE) return '';
        if(empty($_SESSION[self::CSRF_SESSION_KEY]) || !is_string($_SESSION[self::CSRF_SESSION_KEY])) {
            $_SESSION[self::CSRF_SESSION_KEY] = bin2hex(random_bytes(32));
        }
        return $_SESSION[self::CSRF_SESSION_KEY];
    }

    public static function enforceCsrf() {
        if(!self::config('csrf_enabled', true)) return;
        if(session_status() !== PHP_SESSION_ACTIVE) return;
        if(($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') return;
        if(defined('access') && in_array(access, array('api','cron','install'), true)) return;
        $provided = isset($_POST['__nexora_csrf']) ? (string)$_POST['__nexora_csrf'] : '';
        $expected = self::csrfToken();
        if($provided === '' || $expected === '' || !hash_equals($expected, $provided)) {
            http_response_code(419);
            throw new RuntimeException('La sesión de seguridad expiró. Recarga la página y vuelve a intentarlo.');
        }
    }

    public static function installFormTokenInjector() {
        if(!self::config('csrf_enabled', true)) return;
        if(session_status() !== PHP_SESSION_ACTIVE) return;
        if(defined('access') && !in_array(access, array('index','admincp'), true)) return;
        ob_start(array(__CLASS__, 'injectCsrfFields'));
    }

    public static function injectCsrfFields($html) {
        if(!is_string($html) || stripos($html, '<form') === false) return $html;
        $token = htmlspecialchars(self::csrfToken(), ENT_QUOTES, 'UTF-8');
        return preg_replace_callback('/<form\b([^>]*)>/i', function($match) use ($token) {
            $tag = $match[0];
            $attrs = $match[1];
            if(!preg_match('/\bmethod\s*=\s*(["\']?)post\1/i', $attrs)) return $tag;
            if(stripos($attrs, 'data-nexora-no-csrf') !== false) return $tag;
            return $tag . '<input type="hidden" name="__nexora_csrf" value="'.$token.'">';
        }, $html);
    }

    public static function headers() {
        if(headers_sent() || !self::config('security_headers', true)) return;
        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: SAMEORIGIN');
        header('Referrer-Policy: strict-origin-when-cross-origin');
        header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
        header("Content-Security-Policy: frame-ancestors 'self'");
    }

    public static function clientIp() {
        $ip = isset($_SERVER['REMOTE_ADDR']) ? trim((string)$_SERVER['REMOTE_ADDR']) : '';
        return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0';
    }

    public static function secret($name, $legacyConfigKey = null) {
        $info = self::secretInfo($name, $legacyConfigKey);
        return $info['value'];
    }

    public static function secretInfo($name, $legacyConfigKey = null) {
        $value = '';
        $source = 'missing';

        $local = nexoraLocalConfig();
        if(isset($local[$name]) && is_string($local[$name]) && trim($local[$name]) !== '') {
            $value = trim($local[$name]);
            $source = 'nexora.local.php';
        }

        $env = getenv($name);
        if($value === '' && $env !== false && trim((string)$env) !== '') {
            $value = trim((string)$env);
            $source = 'environment';
        }

        if($value === '') {
            $managed = self::managedSecrets();
            if(isset($managed[$name]) && is_string($managed[$name]) && trim($managed[$name]) !== '') {
                $value = trim($managed[$name]);
                $source = 'managed';
            }
        }

        if($value === '' && $legacyConfigKey !== null) {
            $legacy = config($legacyConfigKey, true);
            if(is_string($legacy) && trim($legacy) !== '') {
                $value = trim($legacy);
                $source = 'legacy';
            }
        }

        $strong = strlen($value) >= 24 && $value !== 'CHANGE_ME' && stripos($value, 'CHANGE_ME') === false;
        return array(
            'value' => $value,
            'source' => $source,
            'configured' => $value !== '',
            'strong' => $strong,
            'length' => strlen($value),
            'fingerprint' => $value === '' ? '' : strtoupper(substr(hash('sha256', $value), 0, 12)),
        );
    }

    public static function requireStrongSecret($name, $legacyConfigKey = null) {
        $secret = self::secret($name, $legacyConfigKey);
        if(strlen($secret) < 24 || stripos($secret, 'CHANGE_ME') !== false) throw new RuntimeException('Falta configurar un secreto seguro para '.$name.'.');
        return $secret;
    }

    public static function managedSecretsPath() {
        return __PATH_CONFIGS__ . 'nexora.secrets.php';
    }

    public static function managedSecrets() {
        if(is_array(self::$managedSecrets)) return self::$managedSecrets;
        self::$managedSecrets = array();
        $path = self::managedSecretsPath();
        if(is_file($path) && is_readable($path)) {
            $loaded = include($path);
            if(is_array($loaded)) self::$managedSecrets = $loaded;
        }
        return self::$managedSecrets;
    }

    public static function writeManagedSecret($name, $value) {
        $allowed = array('NEXORA_PAYMENT_SIGNING_KEY', 'NEXORA_CRON_API_KEY');
        if(!in_array($name, $allowed, true)) throw new InvalidArgumentException('El secreto solicitado no puede gestionarse desde AdminCP.');
        if(!is_string($value) || strlen($value) < 24) throw new InvalidArgumentException('El secreto generado no cumple la longitud mínima.');

        $path = self::managedSecretsPath();
        $directory = dirname($path);
        if(!is_dir($directory) || !is_writable($directory)) {
            throw new RuntimeException('La carpeta includes/config no permite crear el archivo privado de secretos. Usa nexora.local.php o una variable de entorno.');
        }

        $data = self::managedSecrets();
        $data[$name] = $value;
        ksort($data);

        $body = "<?php\n/**\n * Nexora managed secrets. Generated from authenticated AdminCP.\n * Never publish or copy this file outside the server.\n */\nreturn " . var_export($data, true) . ";\n";
        $tmp = $path.'.tmp.'.bin2hex(random_bytes(6));
        if(@file_put_contents($tmp, $body, LOCK_EX) === false) throw new RuntimeException('No se pudo escribir el archivo temporal de secretos.');
        @chmod($tmp, 0600);
        if(!@rename($tmp, $path)) {
            @unlink($tmp);
            throw new RuntimeException('No se pudo publicar el archivo privado de secretos.');
        }
        @chmod($path, 0600);
        self::$managedSecrets = $data;
        clearstatcache(true, $path);
        return true;
    }

    public static function generateManagedSecret($name) {
        $value = bin2hex(random_bytes(32));
        self::writeManagedSecret($name, $value);
        return $value;
    }

    public static function expectedHeadersPresent() {
        if(!self::config('security_headers', true)) return false;
        if(!function_exists('headers_list')) return true;
        $headers = headers_list();
        if(!is_array($headers) || count($headers) === 0) return true;
        $required = array('x-content-type-options:', 'x-frame-options:', 'referrer-policy:', 'permissions-policy:', 'content-security-policy:');
        $found = array_fill_keys($required, false);
        foreach($headers as $header) {
            $lower = strtolower(trim((string)$header));
            foreach($required as $prefix) if(strpos($lower, $prefix) === 0) $found[$prefix] = true;
        }
        foreach($found as $ok) if(!$ok) return false;
        return true;
    }

    public static function proxyTrustStatus() {
        $enabled = (bool)self::config('trust_proxy_headers', false);
        $trusted = self::config('trusted_proxies', array());
        if(!$enabled) return array('safe'=>true, 'enabled'=>false, 'label'=>'Ignorados · seguro por defecto', 'count'=>0);
        $count = is_array($trusted) ? count(array_filter($trusted, 'strlen')) : 0;
        if($count < 1) return array('safe'=>false, 'enabled'=>true, 'label'=>'Activo sin allowlist', 'count'=>0);
        return array('safe'=>true, 'enabled'=>true, 'label'=>'Restringido a '.$count.' proxy'.($count === 1 ? '' : 's'), 'count'=>$count);
    }
}
