<?php

/**
 * Admin UI and front-end routing for static site mode.
 *
 * @package Parkstatic
 */
declare (strict_types = 1);
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
require_once PARKSTATIC_PLUGIN_DIR . 'includes/class-static-file-server.php';
/**
 * Switches the front-end between WordPress and the Parkstatic export.
 */
final class Parkstatic_Static_Site_Switcher {
    public const OPTION_STATIC_MODE_ENABLED = 'parkstatic_static_mode_enabled';

    public const OPTION_HTACCESS_STORAGE_VERSION = 'parkstatic_htaccess_storage_version';

    public const OPTION_CONTENT_SECURITY_POLICY = 'parkstatic_content_security_policy';

    public const HTACCESS_MARKER = 'Parkstatic Assets';

    public const HTACCESS_STORAGE_VERSION = 3;

    public static function register_hooks() : void {
        add_action( 'parse_request', array(self::class, 'maybe_serve_static'), 0 );
        add_action( 'wp', array(self::class, 'maybe_serve_static'), 0 );
        add_action( 'template_redirect', array(self::class, 'maybe_serve_static'), 0 );
        add_filter( 'query_vars', array(self::class, 'register_query_vars') );
        add_filter(
            'redirect_canonical',
            array(self::class, 'maybe_disable_canonical_redirect'),
            10,
            2
        );
        add_action( 'init', array(self::class, 'register_rewrite_rules') );
        add_action( 'init', array(self::class, 'maybe_refresh_htaccess_for_storage'), 1 );
    }

    public static function register_query_vars( array $vars ) : array {
        $vars[] = 'parkstatic_asset';
        return $vars;
    }

    public static function register_rewrite_rules() : void {
        if ( !self::is_enabled() ) {
            return;
        }
        add_rewrite_rule( '^assets/(.+)$', 'index.php?parkstatic_asset=$matches[1]', 'top' );
    }

    public static function is_enabled() : bool {
        return (bool) get_option( self::OPTION_STATIC_MODE_ENABLED, false );
    }

    public static function static_export_exists() : bool {
        return is_file( Parkstatic_Static_File_Server::content_root() . 'index.html' );
    }

    /**
     * @return true|\WP_Error
     */
    public static function enable() {
        if ( !self::static_export_exists() ) {
            return new WP_Error('parkstatic_static_missing', __( 'Static export not found. Deploy files to the uploads/parkstatic/static directory first.', 'parkstatic' ));
        }
        Parkstatic_Plugin::instance()->ensure_parkstatic_directories();
        update_option( self::OPTION_STATIC_MODE_ENABLED, true, false );
        self::register_rewrite_rules();
        flush_rewrite_rules();
        self::write_htaccess_rules();
        Parkstatic_Static_File_Server::detect_and_store_legacy_asset_base( true );
        return true;
    }

    public static function disable() : void {
        delete_option( self::OPTION_STATIC_MODE_ENABLED );
        delete_option( Parkstatic_Static_File_Server::OPTION_LEGACY_ASSET_BASE );
        flush_rewrite_rules();
        self::remove_htaccess_rules();
    }

    /**
     * Re-emits the asset fast-path .htaccess block after storage moves (for
     * example wp-content/parkstatic → uploads/parkstatic). Without this,
     * Apache keeps rewriting /assets/ to the old on-disk path and CSS/JS 404
     * while HTML still serves through PHP.
     */
    public static function maybe_refresh_htaccess_for_storage() : void {
        if ( !self::is_enabled() ) {
            return;
        }
        if ( (int) get_option( self::OPTION_HTACCESS_STORAGE_VERSION, 0 ) >= self::HTACCESS_STORAGE_VERSION ) {
            return;
        }
        if ( self::write_htaccess_rules() ) {
            update_option( self::OPTION_HTACCESS_STORAGE_VERSION, self::HTACCESS_STORAGE_VERSION, false );
            flush_rewrite_rules();
        }
    }

    /**
     * Stops WordPress from 302-redirecting /assets/ requests to the homepage
     * when a host cannot serve the uploads path directly and the request would
     * otherwise fall through to redirect_canonical().
     *
     * @param string|false $redirect_url  Canonical redirect target.
     * @param string       $requested_url Requested URL.
     * @return string|false
     */
    public static function maybe_disable_canonical_redirect( $redirect_url, $requested_url ) {
        if ( !self::is_enabled() ) {
            return $redirect_url;
        }
        if ( !is_string( $requested_url ) || '' === $requested_url ) {
            return $redirect_url;
        }
        $path = wp_parse_url( $requested_url, PHP_URL_PATH );
        if ( !is_string( $path ) || '' === $path ) {
            return $redirect_url;
        }
        $path = Parkstatic_Static_File_Server::normalize_uri( $path );
        if ( 0 === strpos( $path, '/assets/' ) ) {
            return false;
        }
        return $redirect_url;
    }

    /**
     * Installs an .htaccess block at the WordPress root that routes
     * `/assets/...` through `index.php?parkstatic_asset=` so the PHP static
     * file server can read from the uploads-based export. We intentionally
     * do not rewrite straight to the uploads tree: many hosts (including
     * TasteWP) block or redirect direct HTTP access to files under
     * wp-content/uploads, which yields 302 → homepage and MIME errors for
     * CSS/JS module scripts.
     *
     * The block is prepended above WordPress's own `# BEGIN WordPress` block
     * so the `[L]` flag short-circuits before WP's catch-all runs. If the
     * file isn't writable, the registered WP rewrite rule + the PHP static-
     * file server still serve the request.
     *
     * No-ops on nginx (mod_rewrite / IfModule blocks are simply ignored when
     * the host doesn't read .htaccess).
     */
    public static function write_htaccess_rules() : bool {
        $path = self::htaccess_path();
        if ( null === $path ) {
            return false;
        }
        $block = array(
            '# Routes /assets/<file> through WordPress so PHP can serve the',
            '# uploads-based static export (direct uploads rewrites 302 on',
            '# some hosts). Hashed assets still get long-cache headers below.',
            '<IfModule mod_rewrite.c>',
            '  RewriteEngine On',
            '  RewriteRule ^assets/(.+)$ index.php?parkstatic_asset=$1 [L,QSA]',
            '</IfModule>',
            '<IfModule mod_expires.c>',
            '  ExpiresActive On',
            '</IfModule>',
            '<IfModule mod_headers.c>',
            '  <FilesMatch "-[A-Za-z0-9_]{6,}\\.[A-Za-z0-9]+$">',
            '    Header set Cache-Control "public, max-age=31536000, immutable"',
            '  </FilesMatch>',
            '</IfModule>'
        );
        return self::replace_marker_block( $path, $block );
    }

    public static function remove_htaccess_rules() : bool {
        $path = self::htaccess_path();
        if ( null === $path || !file_exists( $path ) ) {
            return true;
        }
        return self::replace_marker_block( $path, array() );
    }

    /**
     * Resolves the path to the .htaccess file that handles requests for the
     * site root. Returns null when WordPress hasn't loaded the helper or the
     * directory isn't writable.
     */
    private static function htaccess_path() : ?string {
        if ( !function_exists( 'get_home_path' ) ) {
            $file = ABSPATH . 'wp-admin/includes/file.php';
            if ( !is_readable( $file ) ) {
                return null;
            }
            require_once $file;
        }
        $home_path = get_home_path();
        if ( !is_string( $home_path ) || '' === $home_path ) {
            return null;
        }
        $candidate = rtrim( $home_path, '/\\' ) . '/.htaccess';
        $dir = dirname( $candidate );
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
        if ( !is_dir( $dir ) || !is_writable( $dir ) ) {
            return null;
        }
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
        if ( file_exists( $candidate ) && !is_writable( $candidate ) ) {
            return null;
        }
        return $candidate;
    }

    /**
     * Replaces the Parkstatic-managed marker block at the top of the file.
     * Unlike WP's `insert_with_markers()` we prepend (not append) so our
     * rules run before the WordPress block's catch-all `index.php` rewrite.
     *
     * Passing an empty $block removes the block entirely.
     */
    private static function replace_marker_block( string $path, array $block ) : bool {
        $existing = '';
        if ( is_readable( $path ) ) {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
            $existing = (string) file_get_contents( $path );
        }
        $begin = '# BEGIN ' . self::HTACCESS_MARKER;
        $end = '# END ' . self::HTACCESS_MARKER;
        $stripped = preg_replace( '/' . preg_quote( $begin, '/' ) . '.*?' . preg_quote( $end, '/' ) . "\r?\n?/s", '', $existing );
        if ( !is_string( $stripped ) ) {
            $stripped = $existing;
        }
        if ( empty( $block ) ) {
            $contents = $stripped;
        } else {
            $prefix = $begin . "\n" . implode( "\n", $block ) . "\n" . $end . "\n";
            $contents = $prefix . $stripped;
        }
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_put_contents_file_put_contents
        $bytes = @file_put_contents( $path, $contents, LOCK_EX );
        return false !== $bytes;
    }

    /**
     * Home URL path prefix for subdirectory installs, e.g. /site/s-qw4lsvtdp8nbm.
     */
    public static function get_home_path() : string {
        $home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
        $home_path = ( is_string( $home_path ) ? '/' . trim( $home_path, '/' ) : '/' );
        if ( '/' === $home_path ) {
            return '';
        }
        return $home_path;
    }

    public static function is_subdirectory_install() : bool {
        return '' !== self::get_home_path();
    }

    /**
     * Resolves the front-end path to serve, relative to the WordPress home URL.
     */
    public static function resolve_serve_uri() : string {
        $asset = get_query_var( 'parkstatic_asset' );
        if ( (!is_string( $asset ) || '' === $asset) && !empty( $_GET['parkstatic_asset'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public asset URL routing.
            $asset = sanitize_text_field( wp_unslash( $_GET['parkstatic_asset'] ) );
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public asset URL routing; no form is processed.
        }
        if ( is_string( $asset ) && '' !== $asset ) {
            return '/assets/' . ltrim( rawurldecode( str_replace( '\\', '/', $asset ) ), '/' );
        }
        return Parkstatic_Static_File_Server::normalize_uri( self::extract_request_path() );
    }

    /**
     * Reads the request path from server variables.
     */
    private static function extract_request_path() : string {
        $candidates = array(( isset( $_SERVER['REQUEST_URI'] ) ? sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '' ), ( isset( $_SERVER['REDIRECT_URL'] ) ? sanitize_url( wp_unslash( $_SERVER['REDIRECT_URL'] ) ) : '' ));
        foreach ( $candidates as $candidate ) {
            if ( !is_string( $candidate ) || '' === $candidate ) {
                continue;
            }
            $path = wp_parse_url( $candidate, PHP_URL_PATH );
            if ( is_string( $path ) && '' !== $path ) {
                return rawurldecode( $path );
            }
        }
        return '/';
    }

    public static function maybe_serve_static() : void {
        // Hooked to both `wp` and `template_redirect` so we kick in as early
        // as possible across themes/plugins, but the body should run at most
        // once per request — `serve.php` either exits or returns, and the
        // second fire would just re-evaluate the same bail-out conditions.
        static $already_ran = false;
        if ( $already_ran ) {
            return;
        }
        if ( is_admin() ) {
            return;
        }
        if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
            return;
        }
        if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
            return;
        }
        if ( !self::is_enabled() ) {
            return;
        }
        $already_ran = true;
        if ( !defined( 'PARKSTATIC_SITE_ROOT' ) ) {
            define( 'PARKSTATIC_SITE_ROOT', ABSPATH );
        }
        if ( !defined( 'PARKSTATIC_REQUEST_URI' ) ) {
            define( 'PARKSTATIC_REQUEST_URI', self::resolve_serve_uri() );
        }
        require PARKSTATIC_PLUGIN_DIR . 'serve.php';
    }

}
