<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Redirect;

use PrettyLinks\Pro\Options\Store as ProOptionsStore;

/**
 * Optional MU-plugin that dispatches pretty-link redirects earlier in the
 * WordPress lifecycle (before `init`), bypassing the bulk of plugin and theme
 * loading for the hot path.
 *
 * Drops a file into wp-content/mu-plugins/ when enabled. Idempotent.
 *
 * Pro-only: this is the "redirect turbo mode" that ships with Pretty Links
 * Pro. Lite has no knowledge of it — the toggle, endpoint, and self-heal
 * hook are all registered from Pro's Bootstrap.
 */
class MuPluginInstaller
{
    private const FILENAME = '00000001-pretty-link-dispatch.php';

    /**
     * Absolute filesystem path to the plugin base.
     *
     * @var string
     */
    private string $basePath;

    /**
     * Public URL to the plugin base.
     *
     * @var string
     */
    private string $baseUrl;

    /**
     * Store the plugin base path and URL.
     *
     * @param string $basePath Absolute filesystem path to the plugin base.
     * @param string $baseUrl  Public URL to the plugin base.
     */
    public function __construct(string $basePath, string $baseUrl)
    {
        $this->basePath = $basePath;
        $this->baseUrl  = $baseUrl;
    }

    /**
     * Write the dispatch mu-plugin stub into the mu-plugins directory.
     *
     * @return boolean True on success, false on failure.
     */
    public function install(): bool
    {
        $dir = $this->muDir();
        if (!wp_mkdir_p($dir)) {
            return false;
        }
        $path = $dir . '/' . self::FILENAME;
        $code = $this->generateStub();
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents -- MU-plugin installer.
        $written = file_put_contents($path, $code);
        return $written !== false;
    }

    /**
     * Remove the dispatch mu-plugin stub if present.
     *
     * @return boolean True on success, false on failure.
     */
    public function uninstall(): bool
    {
        return self::removeIfExists();
    }

    /**
     * Delete the mu-plugin stub file if it exists.
     *
     * @return boolean True when the file is absent or removed.
     */
    public static function removeIfExists(): bool
    {
        $dir  = defined('WPMU_PLUGIN_DIR') ? (string) WPMU_PLUGIN_DIR : WP_CONTENT_DIR . '/mu-plugins';
        $path = $dir . '/' . self::FILENAME;
        if (file_exists($path)) {
            return (bool) wp_delete_file($path);
        }
        return true;
    }

    /**
     * Whether the dispatch mu-plugin stub is currently installed.
     *
     * @return boolean True when the stub file exists.
     */
    public function isInstalled(): bool
    {
        return file_exists($this->muDir() . '/' . self::FILENAME);
    }

    /**
     * Self-heal after a mu-plugin dispatch crash.
     *
     * The stub's top-level try/catch writes `prli_mu_dispatch_error` when it
     * catches a Throwable. On the next front-end request, the main plugin
     * loads normally on `init` and this runs at priority 0: remove the
     * mu-plugin file, flip the `mu_dispatch` setting back off, and delete
     * the trigger flag. Silent — the crash detail is only in the PHP error
     * log when WP_DEBUG_LOG is on.
     */
    public static function maybeSelfHeal(): void
    {
        if (empty(get_option('prli_mu_dispatch_error'))) {
            return;
        }
        self::removeIfExists();
        (new ProOptionsStore())->set('mu_dispatch', false);
        delete_option('prli_mu_dispatch_error');
    }

    /**
     * Resolve the mu-plugins directory path.
     *
     * @return string Absolute path to the mu-plugins directory.
     */
    private function muDir(): string
    {
        if (defined('WPMU_PLUGIN_DIR')) {
            return (string) WPMU_PLUGIN_DIR;
        }
        return WP_CONTENT_DIR . '/mu-plugins';
    }

    /**
     * Build the mu-plugin stub contents from the bundled template.
     *
     * @return string The rendered mu-plugin PHP source.
     */
    private function generateStub(): string
    {
        $template = (string) file_get_contents(__DIR__ . '/mu-plugin-template.php.stub');
        $basename = plugin_basename($this->basePath . 'pretty-link.php');
        // Edition is baked in (per-build slug, can't change at
        // runtime). Version is NOT baked in — the mu-plugin reads it
        // from the main plugin file's `Version:` header via
        // get_file_data() on every request, so the mu-plugin doesn't
        // go stale when the plugin is updated and the user never has
        // to reinstall it.
        $edition      = defined('PRLI_EDITION')
            ? (string) PRLI_EDITION
            : 'pretty-link-lite';
        $editionLabel = ucwords(str_replace('-', ' ', $edition));
        // No geo-endpoint placeholder: the dispatcher no longer makes an
        // outbound geo call. It reads the shared cache and queues anything it
        // can't answer for GeoBackfillJob.
        return strtr($template, [
            '%%PRLI_PLUGIN_BASENAME%%' => $basename,
            '%%PRLI_EDITION_LABEL%%'   => $editionLabel,
        ]);
    }
}
