<?php

declare(strict_types=1);

namespace SecurWP\Modules\Hardening\Tweaks;

use SecurWP\Includes\DataDirectoryProtection;
use SecurWP\Includes\ServerConfigWriter;
use SecurWP\Includes\SitePathResolver;
use SecurWP\Modules\Hardening\Hardening;

if ( ! defined( 'ABSPATH' ) ) { exit; }

class SystemTweaks {

    private array $settings;

    /**
     * All PHP-related file extensions that could be executed by a web server.
     * Covers legacy and future PHP handler suffixes plus common alternate PHP
     * entry extensions and SSI-style executable templates.
     */
    private const PHP_EXTENSIONS = 'php[0-9]*|pht|phtml?|phar|phps|shtml';

    public function __construct( array $settings ) {
        $this->settings = $settings;
    }

    /**
     * Get .htaccess rules for system tweaks.
     *
     * Rules are compatible with Apache 2.4, Apache 2.2 (legacy), and
     * LiteSpeed. Directory-scoped PHP execution blocks rely on mod_rewrite at
     * the site root, while file-level denies use auth directives that map cleanly
     * across the supported .htaccess environments.
     */
    public function get_htaccess_rules(): string {
        $rules = DataDirectoryProtection::get_htaccess_rules();

        if ( ! empty( $this->settings['disable_directory_browsing'] ) ) {
            $rules .= "\n# Disable Directory Browsing\nOptions -Indexes\n";
        }

        foreach ( $this->get_php_block_directories() as $directory => $enabled ) {
            if ( $enabled ) {
                $rules .= $this->get_php_block_htaccess( $directory );
            }
        }

        // Block XML-RPC at server level (only in full disable mode - not
        // for disable_pingbacks, which still needs xmlrpc.php reachable).
        if ( 'disable' === ( $this->settings['disable_xmlrpc'] ?? Hardening::get_default_settings()['disable_xmlrpc'] ) ) {
            $rules .= $this->get_xmlrpc_block_htaccess();
        }

        if ( ! empty( $this->settings['block_sensitive_files'] ) ) {
            $rules .= $this->get_sensitive_files_htaccess();
        }

        return $rules;
    }

    /**
     * Get Nginx rules for system tweaks.
     */
    public function get_nginx_rules(): string {
        $rules = DataDirectoryProtection::get_nginx_rules();

        if ( ! empty( $this->settings['disable_directory_browsing'] ) ) {
            $rules .= "\n# Disable Directory Browsing\nautoindex off;\n";
        }

        foreach ( $this->get_php_block_directories() as $directory => $enabled ) {
            if ( $enabled ) {
                $rules .= $this->get_php_block_nginx( $directory );
            }
        }

        if ( 'disable' === ( $this->settings['disable_xmlrpc'] ?? Hardening::get_default_settings()['disable_xmlrpc'] ) ) {
            $rules .= $this->get_xmlrpc_block_nginx();
        }

        if ( ! empty( $this->settings['block_sensitive_files'] ) ) {
            $rules .= $this->get_sensitive_files_nginx();
        }

        if ( trim( $rules ) !== '' ) {
            $rules .= $this->get_nginx_activation_probe_rule();
        }

        return $rules;
    }

    /**
     * Generate the dedicated Nginx activation canary rule.
     */
    private function get_nginx_activation_probe_rule(): string {
        $path        = ServerConfigWriter::get_nginx_activation_probe_path();
        $header_name = ServerConfigWriter::get_nginx_activation_probe_header_name();
        $header_value = ServerConfigWriter::get_nginx_activation_probe_header_value();
        $body        = ServerConfigWriter::get_nginx_activation_probe_body();
        $status      = ServerConfigWriter::get_nginx_activation_probe_status();

        return implode(
            "\n",
            [
                '',
                '# SecurWP Nginx activation probe',
                "location = {$path} {",
                '  add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;',
                '  add_header Pragma "no-cache" always;',
                '  add_header Expires "0" always;',
                "  add_header {$header_name} \"{$header_value}\" always;",
                '  default_type text/plain;',
                "  return {$status} \"{$body}\";",
                '}',
                '',
            ]
        );
    }

    /**
     * Generate deny-all directives compatible with Apache 2.4, LiteSpeed,
     * and Apache 2.2 (legacy).
     *
     * Apache 2.4 uses mod_authz_core with "Require all denied".
     * LiteSpeed uses mod_litespeed with "Order/Deny" syntax.
     * Apache 2.2 falls back to "Order/Deny" when neither module is present.
     *
     * @param string $indent Whitespace prefix for each line.
     * @return string Multi-line directives.
     */
    private static function deny_all_directives( string $indent = '  ' ): string {
        return implode(
            "\n",
            [
                "{$indent}<IfModule mod_authz_core.c>",
                "{$indent}  Require all denied",
                "{$indent}</IfModule>",
                "{$indent}<IfModule mod_litespeed.c>",
                "{$indent}  Order Deny,Allow",
                "{$indent}  Deny from all",
                "{$indent}</IfModule>",
                "{$indent}<IfModule !mod_authz_core.c>",
                "{$indent}  <IfModule !mod_litespeed.c>",
                "{$indent}    Order Deny,Allow",
                "{$indent}    Deny from all",
                "{$indent}  </IfModule>",
                "{$indent}</IfModule>",
            ]
        );
    }

    /**
     * Resolve which directories should receive direct PHP execution blocking.
     *
     * Conservative defaults are handled at the settings layer; this method only
     * reflects the currently enabled rule set.
     *
     * @return array<string, bool>
     */
    private function get_php_block_directories(): array {
        return [
            'uploads'    => ! empty( $this->settings['block_php_uploads'] ),
            'plugins'    => ! empty( $this->settings['block_php_plugins'] ),
            'mu-plugins' => ! empty( $this->settings['block_php_plugins'] ),
            'themes'     => ! empty( $this->settings['block_php_themes'] ),
        ];
    }

    /**
     * Resolve the public request path for a site-root file.
     */
    private function get_site_root_file_path( string $filename ): string {
        return SitePathResolver::get_site_root_file_path( $filename );
    }

    /**
     * Resolve the public request path for a directory that may need PHP blocking.
     */
    private function get_public_directory_path( string $dir ): ?string {
        return SitePathResolver::get_public_directory_path( $dir );
    }

    /**
     * Resolve the public request path for an admin-relative path.
     */
    private function get_admin_public_path( string $relative = '' ): ?string {
        return SitePathResolver::get_admin_public_path( $relative );
    }

    /**
     * Resolve the public request path for a wp-includes-relative path.
     */
    private function get_includes_public_path( string $relative = '' ): ?string {
        return SitePathResolver::get_includes_public_path( $relative );
    }

    /**
     * Resolve the public request path for the managed nginx include when it is
     * stored inside the site's public document tree.
     */
    private function get_nginx_include_public_path(): ?string {
        if ( ! class_exists( '\SecurWP\Includes\ServerConfigWriter' ) ) {
            return null;
        }

        return $this->get_public_path_from_local_path( \SecurWP\Includes\ServerConfigWriter::get_nginx_path() );
    }

    /**
     * Convert a site-local filesystem path to its public request path.
     */
    private function get_public_path_from_local_path( string $path ): ?string {
        return SitePathResolver::get_public_path_from_local_path( $path );
    }

    /**
     * Convert an absolute request path into the path format expected by a
     * root-level .htaccess RewriteRule.
     */
    private function get_htaccess_relative_path( string $public_path ): string {
        $normalized = SitePathResolver::normalize_request_path( $public_path, true );
        $site_root  = $this->get_site_root_path();

        if ( $site_root !== '/' && str_starts_with( $normalized, $site_root ) ) {
            $normalized = '/' . ltrim( substr( $normalized, strlen( $site_root ) ), '/' );
        }

        return ltrim( $normalized, '/' );
    }

    /**
     * Get the public request path to the WordPress install root.
     */
    private function get_site_root_path(): string {
        return SitePathResolver::get_site_root_path();
    }

    /**
     * Generate .htaccess rules to block PHP execution in a directory.
     *
     * At the site root, path-scoped execution blocking requires mod_rewrite.
     * A FilesMatch-only fallback would either over-block the whole site or
     * depend on Apache 2.4 expression syntax that is not available on legacy
     * Apache 2.2 hosts, so SecurWP intentionally emits rewrite rules only.
     *
     * Covers legacy, current, and future PHP handler suffixes (.php, .php5,
     * .php74, .php83, .php9), plus alternate PHP entry extensions such as
     * .pht, .phtm, .phtml, and .phar.
     * Compatible with Apache 2.4, LiteSpeed, and Apache 2.2 when mod_rewrite
     * is available, which is the standard WordPress hosting baseline.
     *
     * @param string $dir 'uploads', 'plugins', 'mu-plugins', or 'themes'.
     * @return string .htaccess rules.
     */
    private function get_php_block_htaccess( string $dir ): string {
        $public_path = $this->get_public_directory_path( $dir );
        if ( $public_path === null ) {
            return '';
        }

        $relative       = $this->get_htaccess_relative_path( $public_path );
        if ( $relative === '' ) {
            return '';
        }

        $relative_regex = preg_quote( $relative, '#' );
        $exts           = self::PHP_EXTENSIONS;

        return implode(
            "\n",
            [
                '',
                "# Block PHP execution in {$dir} directory",
                '<IfModule mod_rewrite.c>',
                'RewriteEngine On',
                "RewriteRule ^{$relative_regex}.*\\.({$exts})$ - [F,L,NC]",
                '</IfModule>',
                '',
            ]
        );
    }

    /**
     * Generate Nginx rules to block PHP execution in a directory.
     */
    private function get_php_block_nginx( string $dir ): string {
        $public_path = $this->get_public_directory_path( $dir );
        if ( $public_path === null ) {
            return '';
        }

        $relative_regex = preg_quote( SitePathResolver::normalize_request_path( $public_path, true ), '#' );
        $nginx_exts     = self::PHP_EXTENSIONS;

        return implode(
            "\n",
            [
                '',
                "# Block PHP execution in {$dir} directory",
                "location ~* ^{$relative_regex}.*\\.(?:{$nginx_exts})$ {",
                '  deny all;',
                '}',
                '',
            ]
        );
    }

    /**
     * Generate .htaccess rules to block xmlrpc.php at server level.
     *
     * This prevents the PHP interpreter from even loading for XML-RPC
     * requests, protecting against DDoS and brute-force amplification.
     * Compatible with Apache 2.4, LiteSpeed, and Apache 2.2.
     */
    private function get_xmlrpc_block_htaccess(): string {
        $deny = self::deny_all_directives( '  ' );

        return implode(
            "\n",
            [
                '',
                '# Block XML-RPC at server level',
                '<Files "xmlrpc.php">',
                $deny,
                '</Files>',
                '',
            ]
        );
    }

    /**
     * Generate Nginx rules to block xmlrpc.php.
     */
    private function get_xmlrpc_block_nginx(): string {
        $xmlrpc_path = $this->get_site_root_file_path( 'xmlrpc.php' );

        return implode(
            "\n",
            [
                '',
                '# Block XML-RPC at server level',
                "location = {$xmlrpc_path} {",
                '  deny all;',
                '}',
                '',
            ]
        );
    }

    /**
     * Generate .htaccess rules to block sensitive files.
     *
     * Blocks: wp-config.php, readme.html, wp-admin/install.php, debug/error
     * logs, lock files, hidden dotfiles such as .env, .htaccess, and version
     * control dirs.
     * Compatible with Apache 2.4, LiteSpeed, and Apache 2.2.
     */
    private function get_sensitive_files_htaccess(): string {
        $deny = self::deny_all_directives( '  ' );
        $lines = [
            '',
            '# Block Sensitive Files',
            '<FilesMatch "^(wp-config\.php|readme\.html|readme\.txt|wp-config-sample\.php|php\.ini|\.user\.ini|error_log)$">',
            $deny,
            '</FilesMatch>',
            '',
            '# Block log and lock files',
            '<FilesMatch "\.(log|lock)$">',
            $deny,
            '</FilesMatch>',
            '',
            '# Block .htaccess and .htpasswd',
            '<FilesMatch "^\.ht">',
            $deny,
            '</FilesMatch>',
            '',
            '# Block hidden dotfiles (.env, .git, .svn, etc.) while preserving .well-known',
            '<IfModule mod_rewrite.c>',
            'RewriteEngine On',
            'RewriteRule (^|/)\.(?!well-known(?:/|$)) - [F,L]',
            '</IfModule>',
            '',
            '# Block version control directories',
            '<IfModule mod_rewrite.c>',
            'RewriteEngine On',
            'RewriteRule ^(.*/)?\.git(/.*)?$ - [F,L]',
            'RewriteRule ^(.*/)?\.svn(/.*)?$ - [F,L]',
            '</IfModule>',
            '',
            '# Block installer access',
            '<IfModule mod_rewrite.c>',
            'RewriteRule ^wp-admin/install\.php$ - [F,L]',
            '</IfModule>',
            '',
            '# Block wp-admin/includes direct access',
            '<IfModule mod_rewrite.c>',
            'RewriteRule ^wp-admin/includes/ - [F,L]',
            '</IfModule>',
            '',
            '# Block wp-includes PHP files',
            '<IfModule mod_rewrite.c>',
        ];

        if ( is_multisite() && get_site_option( 'ms_files_rewriting' ) ) {
            $lines[] = 'RewriteRule ^wp-includes/ms-files\.php$ - [S=4]';
        }

        $lines = array_merge(
            $lines,
            [
                'RewriteRule !^wp-includes/ - [S=3]',
                'RewriteRule ^wp-includes/[^/]+\.php$ - [F,L]',
                'RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L]',
                'RewriteRule ^wp-includes/theme-compat/ - [F,L]',
                '</IfModule>',
                '',
            ]
        );

        return implode( "\n", $lines );
    }

    /**
     * Generate Nginx rules to block sensitive files.
     */
    private function get_sensitive_files_nginx(): string {
        $root_sensitive_files = array_filter(
            [
                $this->get_site_root_file_path( 'readme.html' ),
                $this->get_site_root_file_path( 'readme.txt' ),
                $this->get_site_root_file_path( 'wp-config.php' ),
                $this->get_site_root_file_path( 'wp-config-sample.php' ),
                $this->get_site_root_file_path( 'php.ini' ),
                $this->get_site_root_file_path( '.user.ini' ),
                $this->get_site_root_file_path( 'error_log' ),
                $this->get_nginx_include_public_path(),
            ],
            static fn( $path ) => is_string( $path ) && $path !== ''
        );
        $root_sensitive_files = array_values( array_unique( $root_sensitive_files ) );

        $lines = [
            '',
            '# Block Sensitive Files',
        ];

        foreach ( $root_sensitive_files as $path ) {
            $lines[] = "location = {$path} {";
            $lines[] = '  deny all;';
            $lines[] = '}';
        }

        $install_path = $this->get_admin_public_path( 'install.php' );
        if ( $install_path !== null ) {
            $lines[] = '';
            $lines[] = '# Block installer access';
            $lines[] = "location = {$install_path} {";
            $lines[] = '  deny all;';
            $lines[] = '}';
        }

        $lines = array_merge(
            $lines,
            [
                '',
                '# Block log and lock files',
                sprintf(
                    'location ~* ^%s(?:.+/)?[^/]+\.(log|lock)$ {',
                    preg_quote( SitePathResolver::normalize_request_path( $this->get_site_root_path(), true ), '#' )
                ),
                '  deny all;',
                '}',
                '',
                '# Block dotfiles (.htaccess, .htpasswd, .git, .svn, etc.) while preserving /.well-known/',
                sprintf(
                    'location ~* ^%s(?:.+/)?\.(?!well-known(?:/|$)) {',
                    preg_quote( SitePathResolver::normalize_request_path( $this->get_site_root_path(), true ), '#' )
                ),
                '  deny all;',
                '}',
                '',
                '# Block wp-admin/includes direct access',
            ]
        );

        $admin_includes_path = $this->get_admin_public_path( 'includes/' );
        if ( $admin_includes_path !== null ) {
            $admin_includes_regex = preg_quote( SitePathResolver::normalize_request_path( $admin_includes_path, true ), '#' );
            $lines[] = "location ~* ^{$admin_includes_regex} {";
            $lines[] = '  deny all;';
            $lines[] = '}';
        }

        $lines[] = '';
        $lines[] = '# Block wp-includes PHP files';

        $includes_path = $this->get_includes_public_path();
        if ( $includes_path === null ) {
            return implode( "\n", $lines ) . "\n";
        }

        $includes_regex = preg_quote( SitePathResolver::normalize_request_path( $includes_path, true ), '#' );
        if ( ! is_multisite() || ! get_site_option( 'ms_files_rewriting' ) ) {
            $lines[] = "location ~* ^{$includes_regex}[^/]+\\.php$ {";
            $lines[] = '  deny all;';
            $lines[] = '}';
        }

        $lines = array_merge(
            $lines,
            [
                "location ~* ^{$includes_regex}js/tinymce/langs/.+\\.php$ {",
                '  deny all;',
                '}',
                "location ~* ^{$includes_regex}theme-compat/ {",
                '  deny all;',
                '}',
                '',
            ]
        );

        return implode( "\n", $lines );
    }
}
