<?php
/**
 * Setup Wizard controller.
 *
 * @package NimblyMail\SMTP
 *
 * @since 1.0.0
 */

namespace NimblyMail\SMTP\Admin;

use NimblyMail\SMTP\Options;

defined( 'ABSPATH' ) || exit;

/**
 * Class SetupWizard
 *
 * Registers a hidden admin page for the setup wizard, handles the
 * one-time activation redirect, and enqueues the Vue app assets. On
 * multisite the redirect only fires for super admins landing on the
 * Network Admin screen — sub-site admins never see the wizard.
 *
 * @since 1.0.0
 * @since 1.0.2 First-run redirect now uses the dedicated SHOULD_RUN_TRANSIENT flag consumed only by this class,
 *              and the wizard page sets the global admin title on load to fix a strip_tags( null ) deprecation.
 * @since 1.0.4 Network activation redirect now lands on the Network Settings screen instead of the
 *              wizard page, which is never registered on network_admin_menu and caused a wp_die() fatal.
 */
class SetupWizard {

    /**
     * Wizard page slug.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const SLUG = 'nimblymail-smtp-setup-wizard';

    /**
     * One-shot transient flagging that the wizard redirect should run.
     *
     * Set by Plugin::activate() (regular transient on single-site, site
     * transient on network activation) and consumed exclusively by this
     * class — no other component may read or delete it.
     *
     * @since 1.0.2
     *
     * @var string
     */
    public const SHOULD_RUN_TRANSIENT = 'nimblymail_should_run_wizard';

    /**
     * Initialize hooks.
     *
     * @since 1.0.0 Added setup wizard hooks.
     *
     * @return void
     */
    public function init(): void {
        add_action( 'admin_menu', array( $this, 'add_wizard_page' ) );
        add_action( 'admin_init', array( $this, 'maybe_redirect' ), 9999 );
        add_filter( 'admin_body_class', array( $this, 'add_body_class' ) );
    }

    /**
     * Add a body class on the wizard page for full-screen CSS targeting.
     *
     * @since 1.0.0 Added wizard body class for full-screen layout.
     *
     * @param string $classes Space-separated list of body classes.
     *
     * @return string
     */
    public function add_body_class( string $classes ): string {
        $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

        if ( self::SLUG === $page ) {
            $classes .= ' nimblymail-smtp-wizard-page';
        }

        return $classes;
    }

    /**
     * Register a hidden admin page for the wizard.
     *
     * Uses a null parent so the page doesn't appear in the sidebar menu
     * but is still accessible via direct URL.
     *
     * @since 1.0.0 Added wizard page registration.
     * @since 1.0.2 Set the global page title on load to avoid a PHP 8.1+
     *              strip_tags( null ) deprecation in admin-header.php.
     *
     * @return void
     */
    public function add_wizard_page(): void {
        $hook = add_submenu_page(
            '', // Hidden — no parent menu.
            __( 'NimblyMail Setup Wizard', 'nimblymail-smtp' ),
            __( 'Setup Wizard', 'nimblymail-smtp' ),
            'manage_options',
            self::SLUG,
            array( $this, 'render_wizard_page' )
        );

        if ( $hook ) {
            // get_admin_page_title() cannot resolve titles for pages with an
            // empty parent, leaving the global $title null when core calls
            // strip_tags( $title ) in admin-header.php.
            add_action(
                'load-' . $hook,
                static function (): void {
                    // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Core never resolves a title for parentless admin pages, and no filter runs early enough; assigning the global is the only fix.
                    $GLOBALS['title'] = __( 'NimblyMail Setup Wizard', 'nimblymail-smtp' );
                }
            );
        }
    }

    /**
     * Render the wizard Vue mount point.
     *
     * @since 1.0.0 Added wizard page renderer.
     *
     * @return void
     */
    public function render_wizard_page(): void {
        echo '<div class="wrap"><div id="nimblymail-smtp-wizard"></div></div>';
    }

    /**
     * Redirect to the wizard on first activation.
     *
     * On single-site installs the super admin on the freshly-activated
     * site is redirected to the per-site wizard once. On multisite the
     * redirect only fires when the super admin lands in Network Admin
     * after network activation, and lands on the Network Settings screen
     * — sub-site admins never see the wizard.
     *
     * Skips bulk activations and AJAX requests regardless of context.
     *
     * @since 1.0.0
     * @since 1.0.4 Documents the multisite branch landing on Network Settings rather than the wizard.
     *
     * @return void
     */
    public function maybe_redirect(): void {
        if ( wp_doing_ajax() || isset( $_GET['activate-multi'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            return;
        }

        if ( is_multisite() ) {
            $this->maybe_redirect_network();
            return;
        }

        $this->maybe_redirect_site();
    }

    /**
     * Single-site first-run redirect.
     *
     * Guard stored in a site option since there is no network layer.
     *
     * @since 1.0.0
     * @since 1.0.2 Consumes the dedicated SHOULD_RUN_TRANSIENT flag instead of the shared activation transient.
     *
     * @return void
     */
    private function maybe_redirect_site(): void {
        if ( get_option( 'nimblymail_wizard_redirect_done' ) ) {
            return;
        }

        if ( ! get_transient( self::SHOULD_RUN_TRANSIENT ) ) {
            return;
        }

        delete_transient( self::SHOULD_RUN_TRANSIENT );

        update_option( 'nimblymail_wizard_redirect_done', true, false );

        wp_safe_redirect( admin_url( 'admin.php?page=' . self::SLUG ) );
        exit;
    }

    /**
     * Multisite first-run redirect.
     *
     * Redirects the super admin to the Network Settings screen after network
     * activation, exactly once. Redirect guard lives in the network option
     * so a sub-site admin activating the plugin again (if ever possible)
     * cannot re-trigger the redirect.
     *
     * @since 1.0.0
     * @since 1.0.2 Consumes the dedicated SHOULD_RUN_TRANSIENT flag instead of the shared activation transient.
     * @since 1.0.4 Redirect target changed from the wizard page (never registered on
     *              network_admin_menu, causing a wp_die() fatal) to the existing Network
     *              Settings screen.
     *
     * @return void
     */
    private function maybe_redirect_network(): void {
        if ( ! is_network_admin() ) {
            return;
        }

        if ( ! current_user_can( 'manage_network_options' ) ) {
            return;
        }

        $network = get_site_option( Options::NETWORK_OPTION_KEY, array() );
        if ( is_array( $network ) && ! empty( $network['wizard_completed'] ) ) {
            return;
        }

        if ( ! get_site_transient( self::SHOULD_RUN_TRANSIENT ) ) {
            return;
        }

        delete_site_transient( self::SHOULD_RUN_TRANSIENT );

        wp_safe_redirect( network_admin_url( 'admin.php?page=' . NetworkArea::SETTINGS_SLUG ) );
        exit;
    }

    /**
     * Get the wizard page URL.
     *
     * @since 1.0.0 Added wizard URL helper.
     *
     * @return string
     */
    public static function get_url(): string {
        return admin_url( 'admin.php?page=' . self::SLUG );
    }

    /**
     * Record wizard completion.
     *
     * On multisite this writes the flag into the network option so a fresh
     * sub-site never re-triggers the wizard; on single-site it stays in the
     * regular option, preserving existing behavior.
     *
     * @since 1.0.0
     *
     * @return void
     */
    public static function mark_complete(): void {
        if ( is_multisite() ) {
            $network = get_site_option( Options::NETWORK_OPTION_KEY, array() );
            if ( ! is_array( $network ) ) {
                $network = array();
            }
            $network['wizard_completed'] = time();
            update_site_option( Options::NETWORK_OPTION_KEY, $network );
            return;
        }

        update_option( 'nimblymail_wizard_completed', time(), false );
    }

    /**
     * Build the ordered list of wizard step descriptors.
     *
     * Each descriptor is an associative array with `name`, `path`,
     * `component`, and `title` keys. The Vue wizard iterates this list to
     * build router routes and the progress indicator. The
     * `nimblymail_wizard_steps` filter lets Pro add-ons splice in extra
     * steps (e.g. License, Backup, Reports) before the final Complete step.
     *
     * @since 1.0.0
     *
     * @return array<int, array<string, string>> Ordered step descriptors.
     */
    public static function get_steps_schema(): array {
        $steps = array(
            array(
                'name'      => 'welcome',
                'path'      => '/welcome',
                'component' => 'core/WelcomeStep',
                'title'     => __( 'Welcome', 'nimblymail-smtp' ),
            ),
            array(
                'name'      => 'sender',
                'path'      => '/sender',
                'component' => 'core/SenderStep',
                'title'     => __( 'Sender', 'nimblymail-smtp' ),
            ),
            array(
                'name'      => 'mailer',
                'path'      => '/mailer',
                'component' => 'core/MailerStep',
                'title'     => __( 'Mailer', 'nimblymail-smtp' ),
            ),
            array(
                'name'      => 'configure',
                'path'      => '/configure',
                'component' => 'core/ConfigureStep',
                'title'     => __( 'Configure', 'nimblymail-smtp' ),
            ),
            array(
                'name'      => 'test',
                'path'      => '/test',
                'component' => 'core/TestStep',
                'title'     => __( 'Test', 'nimblymail-smtp' ),
            ),
            array(
                'name'      => 'verify',
                'path'      => '/verify',
                'component' => 'core/VerifyDeliverabilityStep',
                'title'     => __( 'Verify', 'nimblymail-smtp' ),
            ),
            array(
                'name'      => 'complete',
                'path'      => '/complete',
                'component' => 'core/CompleteStep',
                'title'     => __( 'Done', 'nimblymail-smtp' ),
            ),
        );

        /**
         * Filter the wizard step descriptors before they are sent to the
         * Vue app.
         *
         * Pro add-ons should splice their steps in before the final
         * `complete` step. Each step requires `name`, `path`, `component`,
         * and `title` keys. The `component` key is a string ID that the JS
         * registry uses to look up the actual Vue component (Pro must
         * register the component at the same ID via
         * `window.nimblymailSmtpWizard.registerStep()`).
         *
         * @since 1.0.0
         *
         * @param array<int, array<string, string>> $steps Ordered step descriptors.
         */
        $steps = apply_filters( 'nimblymail_wizard_steps', $steps );

        if ( ! is_array( $steps ) ) {
            return array();
        }

        return array_values(
            array_filter(
                $steps,
                static function ( $step ) {
                    return is_array( $step )
                        && ! empty( $step['name'] )
                        && ! empty( $step['path'] )
                        && ! empty( $step['component'] )
                        && isset( $step['title'] );
                }
            )
        );
    }
}
