<?php

namespace GOSP\GravityOps\Core\SuiteCore\Admin;

use GFAPI;
use GOSP\GravityOps\Core\SuiteCore\SuiteCore;

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

/**
 * GravityOps AdminShell (root library version)
 *
 * Centralizes a branded header + pill tabs for all GravityOps plugins while
 * keeping the left admin menu clean (submenus removed after registration).
 */
class AdminShell {

    /**
     * SuiteCore instance.
     *
     * @var SuiteCore
     */
    private SuiteCore $core;

    /**
     * Tracks whether the shell has already booted (registered hooks).
     *
     * @var bool
     */
    private bool $did_boot = false;

    /**
     * An array of registered plugin pages.
     *
     * @var array<string,array>
     */
    private array $pages = [];

    /**
     * Extra admin page slugs that should be treated as shell pages.
     *
     * @var array<int,string>
     */
    private array $extra_shell_pages = [];

    /**
     * Current help tab catalog slug while rendering a page.
     *
     * @var string
     */
    private string $current_help_slug = '';

    // Note: We render the header inside the page callback to control vertical order
    // and then relocate notices via JS. No special wrapper tracking is needed.

    /**
     * Initialize the class by setting up WordPress actions related to admin menus and assets.
     *
     * @param SuiteCore $core SuiteCore instance.
     *
     * @return void
     */
    public function __construct( SuiteCore $core ) {
        $this->core = $core;
    }

    /**
     * Check if a plugin page with the given slug has been registered in the shell.
     *
     * Works across namespaces/copies via the shared instance filter.
     *
     * @param string $slug Page slug.
     * @return bool True if the page exists.
     */
    public function has_page( $slug ) {
        return isset( $this->pages[ $slug ] );
    }

    /**
     * Build standard Freemius link tabs for a given plugin slug.
     *
     * Convention: pages are "{plugin_slug}-account", "{plugin_slug}-affiliation", "{plugin_slug}-pricing".
     *
     * Usage:
     *   $tabs = array_merge(
     *       [ 'overview' => [ 'label' => 'Overview', 'type' => 'render', 'callback' => ... ] ],
     *       SuiteCore::instance()->shell()->freemius_tabs( 'plugin-slug' )
     *   );
     *
     * @param string               $plugin_slug Base plugin slug used for admin pages.
     * @param array<string,string> $labels Optional custom labels for the three tabs: [ 'account' => 'Account', 'affiliation' => 'Affiliation', 'pricing' => 'Pricing' ].
     * @param string               $freemius_slug Optional Freemius base slug override.
     * @param bool                 $show_freemius_affiliation Whether to show the affiliation tab. Defaults to true.
     * @param object|null          $freemius Freemius SDK instance used to enforce registration and white-label policy.
     *
     * @return array<string,array<string,string>> Tabs config array suitable to merge with register_plugin_page() tabs.
     */
    public function freemius_tabs( $plugin_slug, $labels = [], $freemius_slug = '', $show_freemius_affiliation = true, $freemius = null ) {
        $default_labels = [
            'account'     => 'Account',
            'affiliation' => 'Affiliation',
            'pricing'     => 'Pricing',
        ];
        $labels         = array_merge( $default_labels, $labels );
        // Allow the Freemius base slug to be overridden/mapped for cases where the AdminShell page slug
        // differs from the SDK menu slug (e.g., hyphenated vs underscored).
        $fs_slug = $freemius_slug ?: $plugin_slug;

        $tabs = [
			'account' => [
				'label' => $labels['account'],
				'type'  => 'link',
				'url'   => admin_url( 'admin.php?page=' . $fs_slug . '-account' ),
			],
			'pricing' => [
				'label' => $labels['pricing'],
				'type'  => 'link',
				'url'   => admin_url( 'admin.php?page=' . $fs_slug . '-pricing' ),
			],
        ];

        if ( $show_freemius_affiliation ) {
            $tabs['affiliation'] = [
				'label' => $labels['affiliation'],
				'type'  => 'link',
				'url'   => admin_url( 'admin.php?page=' . $fs_slug . '-affiliation' ),
            ];
        } else {
            $tabs['affiliation'] = [
				'label' => 'Affiliation',
				'type'  => 'link',
				'url'   => 'https://brightleafdigital.io/affiliate/',
            ];
        }

        if ( is_object( $freemius ) ) {
            if ( method_exists( $freemius, 'is_registered' ) && ! $freemius->is_registered() ) {
                unset( $tabs['account'] );
            }

            $is_whitelabeled = method_exists( $freemius, 'is_whitelabeled' ) && $freemius->is_whitelabeled();
            $is_agency       = method_exists( $freemius, 'is_plan_or_trial' ) && $freemius->is_plan_or_trial( 'agency' );
            if ( $is_whitelabeled || $is_agency ) {
                unset( $tabs['pricing'], $tabs['affiliation'] );
            }
        }

        return $tabs;
    }

    /**
     * Render a standard Feeds tab list used by multiple plugins.
     *
     * Each item shows:
     * - Feed name (link to feed editor)
     * - Badge Active/Inactive
     * - Form name (link to form editor)
     * - A right-aligned Activate/Deactivate button that posts to admin-post.php with provided action.
     *
     * @param array  $feeds_and_forms  Array of [ 'feed' => array, 'form' => array ] rows.
     * @param string $gf_subview_slug  The subview slug used by the add-on in GF settings (usually $this->_slug).
     * @param string $plugin_short_title Short plugin title used for the header, e.g., "Asana Integration".
     * @param string $toggle_action     The admin_post action to handle toggle, used for form action and nonce prefix.
     * @param array  $workflow_steps    Optional array of workflow steps to display in the feed list.
     *
     * @return void
     */
    public function render_feeds_list( $feeds_and_forms, $gf_subview_slug, $plugin_short_title, $toggle_action, $workflow_steps = [] ) {
        echo '<div class="gops-card">';
        echo '<h2 class="gops-title" style="margin:0 0 10px;">' . esc_html( $plugin_short_title ) . ' Feeds</h2>';

        if ( empty( $feeds_and_forms ) ) {
            echo '<p>No feeds found. Create one from a form’s settings.</p>';
        } else {
            echo '<ul style="margin:0; padding-left:18px;">';
            foreach ( $feeds_and_forms as $feed_and_form ) {
                $feed      = $feed_and_form['feed'];
                $form      = $feed_and_form['form'];
                $form_id   = $form['id'];
                $form_name = $form['title'];
                $feed_id   = $feed['id'];
                $is_active = (bool) $feed['is_active'];
                $name      = $feed['meta']['feedName'];

                $edit_form = admin_url( 'admin.php?page=gf_edit_forms&id=' . $form_id );
                $edit_feed = add_query_arg(
                    [
                        'page'    => 'gf_edit_forms',
                        'view'    => 'settings',
                        'subview' => $gf_subview_slug,
                        'fid'     => $feed_id,
                        'id'      => $form_id,
                    ],
                    admin_url( 'admin.php' )
                );

                echo '<li style="margin-bottom:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">';
                echo '<a class="gops-link" target="_blank" rel="noopener" href="' . esc_url( $edit_feed ) . '"><strong>' . esc_html( (string) $name ) . '</strong></a>';
                echo ' — ' . ( $is_active ? '<span class="gops-badge gops-badge--ok">Active</span>' : '<span class="gops-badge gops-badge--warn">Inactive</span>' );
                echo ' &nbsp; <a class="gops-link" target="_blank" rel="noopener" href="' . esc_url( $edit_form ) . '">' . esc_html( $form_name ) . '</a>';
                // Toggle button
                echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline-block; margin-left:8px;">';
                echo '<input type="hidden" name="action" value="' . esc_attr( $toggle_action ) . '" />';
                echo '<input type="hidden" name="return_url" value="' . esc_attr( 'admin.php?page=' . $gf_subview_slug . '&tab=feeds' ) . '" />';
                echo '<input type="hidden" name="feed_id" value="' . esc_attr( (string) $feed_id ) . '" />';
                echo '<input type="hidden" name="_wpnonce" value="' . esc_attr( wp_create_nonce( $toggle_action . '_' . $feed_id ) ) . '" />';
                $label = $is_active ? 'Deactivate' : 'Activate';
                $cls   = $is_active ? 'button' : 'button button-primary';
                echo '<button class="' . esc_attr( $cls ) . '" type="submit">' . esc_html( $label ) . '</button>';
                echo '</form>';
                echo '</li>';
            }
            echo '</ul>';
        }
        echo '</div>';

        if ( ! empty( $workflow_steps ) ) {
            echo '<div class="gops-card" style="margin-top:20px;">';
            echo '<h2 class="gops-title" style="margin:0 0 10px;">Workflow Steps</h2>';
            echo '<ul style="margin:0; padding-left:18px;">';
			$workflow_toggle_action = str_replace( '_toggle_feed', '_toggle_workflow_step', $toggle_action );
			if ( $workflow_toggle_action === $toggle_action ) {
				$workflow_toggle_action = 'gravityops_toggle_workflow_step';
			}

            foreach ( $workflow_steps as $item ) {
                $step      = $item['step'];
                $form      = $item['form'];
                $form_id   = $form['id'];
                $form_name = $form['title'];
                $step_id   = $step['id'];
                $is_active = (bool) ( $step['is_active'] ?? true );
                $name      = $step['step_label'] ?? ( 'Step #' . $step_id );

                $edit_step = add_query_arg(
                    [
                        'page'    => 'gf_edit_forms',
                        'view'    => 'settings',
                        'subview' => 'gravityflow',
                        'fid'     => $step_id,
                        'id'      => $form_id,
                    ],
                    admin_url( 'admin.php' )
                );

                echo '<li style="margin-bottom:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">';
                echo '<a class="gops-link" target="_blank" rel="noopener" href="' . esc_url( $edit_step ) . '"><strong>' . esc_html( (string) $name ) . '</strong></a>';
                echo ' — ' . ( $is_active ? '<span class="gops-badge gops-badge--ok">Active</span>' : '<span class="gops-badge gops-badge--warn">Inactive</span>' );
                echo ' &nbsp; <a class="gops-link" target="_blank" rel="noopener" href="' . esc_url( admin_url( 'admin.php?page=gf_edit_forms&id=' . $form_id ) ) . '">' . esc_html( $form_name ) . '</a>';
                // Toggle button for workflow step
                echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline-block; margin-left:8px;">';
                echo '<input type="hidden" name="action" value="' . esc_attr( $workflow_toggle_action ) . '" />';
                echo '<input type="hidden" name="feed_id" value="' . esc_attr( (string) $step_id ) . '" />';
                echo '<input type="hidden" name="_wpnonce" value="' . esc_attr( wp_create_nonce( $workflow_toggle_action . '_' . $step_id ) ) . '" />';

                echo '<input type="hidden" name="return_url" value="' . esc_attr( 'admin.php?page=' . $gf_subview_slug . '&tab=feeds' ) . '" />';
                $step_label = $is_active ? 'Deactivate' : 'Activate';
                $step_cls   = $is_active ? 'button' : 'button button-primary';
                echo '<button class="' . esc_attr( $step_cls ) . '" type="submit">' . esc_html( $step_label ) . '</button>';
                echo '</form>';

                echo '</li>';
            }
            echo '</ul>';
            echo '</div>';
        }
    }

    /**
     * Processes the feed toggle action for a Gravity Forms add-on feed.
     * Validates the request, checks permissions, and toggles the activation state of the feed.
     *
     * @param string $action_prefix Prefix used for nonce generation and verification.
     * @param string $return_url URL to redirect to after processing the toggle action.
     *
     * @return void
     */
    public function process_feed_toggle( $action_prefix, $return_url ) {
        if ( empty( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) || 'POST' !== ( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ?? '' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
            wp_safe_redirect( admin_url( $return_url ) );
            exit;
        }

        $fid             = isset( $_POST['feed_id'] ) ? (int) $_POST['feed_id'] : 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
        $post_return_url = isset( $_POST['return_url'] ) ? sanitize_text_field( wp_unslash( $_POST['return_url'] ) ) : '';
        if ( ! empty( $post_return_url ) ) {
            $return_url = $post_return_url;
        }

        $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['_wpnonce'] ) ) : '';
        if ( ! $fid || ! wp_verify_nonce( $nonce, $action_prefix . '_' . $fid ) ) {
            wp_safe_redirect( admin_url( $return_url ) );
            exit;
        }

        if ( ! current_user_can( $this->core->suite_menu()->get_parent_capability() ) ) {
            wp_safe_redirect( admin_url( $return_url ) );
            exit;
        }

        global $wpdb;
        $table = $wpdb->prefix . 'gf_addon_feed';
        // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT is_active FROM $table WHERE id = %d", $fid ) );
        if ( $row ) {
            $new = ( (int) $row->is_active ) ? 0 : 1;
            $wpdb->query( $wpdb->prepare( "UPDATE $table SET is_active = %d WHERE id = %d", $new, $fid ) );
        }
        // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        wp_safe_redirect( admin_url( $return_url ) );
        exit;
    }

    /**
     * Helper to fetch workflow steps for specific types across all forms.
     *
     * @param string|array $step_types The step type(s) to filter by (e.g. 'iawgf_update_task' or ['type1', 'type2']).
     * @return array Array of items, each having 'step' (array) and 'form' (array).
     */
    public function get_workflow_steps_by_type( $step_types ) {
        if ( ! function_exists( 'gravity_flow' ) || ! class_exists( 'GFAPI' ) ) {
            return [];
        }

        $step_types = (array) $step_types;

        global $wpdb;
        $table_name = $wpdb->prefix . 'gf_addon_feed';

        // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        // Check if table exists
        $has_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name;
        if ( ! $has_table ) {
            return [];
        }

        // Workflow steps in Gravity Flow are stored as feeds for the 'gravityflow' addon.
        $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name WHERE addon_slug = %s", 'gravityflow' ), ARRAY_A );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        if ( empty( $results ) ) {
            return [];
        }

        $steps = [];
        foreach ( $results as $row ) {
            $meta = json_decode( (string) $row['meta'], true );
            if ( ! is_array( $meta ) ) {
                continue;
            }

            $current_type = $meta['step_type'] ?? '';
            if ( in_array( $current_type, $step_types, true ) ) {
                $form_id = isset( $row['form_id'] ) ? (int) $row['form_id'] : 0;
                if ( ! $form_id ) {
                    continue;
                }
                $form = GFAPI::get_form( $form_id );
                if ( $form ) {
                    $steps[] = [
                        'step' => [
                            'id'         => $row['id'],
                            'form_id'    => $row['form_id'],
                            'is_active'  => $row['is_active'],
                            'step_label' => $meta['step_name'] ?? ( 'Step #' . $row['id'] ),
                        ],
                        'form' => $form,
                    ];
                }
            }
        }

        return $steps;
    }

    /**
     * Render a standard Help tab card.
     *
     * Legacy callers may still pass a link array; user-defined PHP methods allow
     * extra arguments, and this method intentionally ignores them.
     *
     * @return void
     */
    public function render_help_tab() {
        $links = $this->build_help_links();

        echo '<div class="gops-card">';
        echo '<h2 class="gops-title" style="margin:0 0 8px;">Help</h2>';
        echo '<ul style="margin:0; padding-left:18px; line-height:1.7;">';
        foreach ( $links as $label => $url ) {
            if ( empty( $url ) ) {
                continue;
            }
            echo '<li><a class="gops-link" target="_blank" href="' . esc_url( $url ) . '">' . esc_html( $label ) . '</a></li>';
        }
        echo '</ul>';
        echo '<div style="margin-top: 20px;">';
        echo '<a class="button button-primary" href="' . esc_url( admin_url( 'admin.php?page=gravity_ops_support' ) ) . '">Grant Support Access</a>';
        echo '</div>';
        echo '</div>';
    }

    /**
     * Build standard help links for the current plugin page.
     *
     * @return array<string,string>
     */
    private function build_help_links() {
        $links = [
			'Docs'                   => 'https://brightleafdigital.io/bld-docs/',
			'Community forum'        => 'https://brightleafdigital.io/community/',
			'Open a support request' => 'https://brightleafdigital.io/support/',
			'Join the community'     => 'https://brightleafdigital.io/plugintomember',
        ];

        $slug = $this->current_help_slug;
        if ( '' === $slug && isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $slug = sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        }

        $item = $this->core->catalog()->find( $slug );
        if ( ! $item || empty( $item['marketing_url'] ) ) {
            return $links;
        }

        return array_merge(
            [
				'Learn More' => (string) $item['marketing_url'],
            ],
            $links
        );
    }

    /**
     * Show coupons on the GravityOps dashboard for plugins that aren't installed as premium.
     *
     * @param string $content Existing content.
     * @param array  $item    Plugin registry item.
     * @return string
     */
    public function maybe_render_dashboard_coupon( $content, $item ) {
        // Folders and Search are free-only, no coupons.
        if ( ! empty( $item['is_free'] ) ) {
            return $content;
        }

        // If premium version is installed, don't show coupon.
        if ( ! empty( $item['is_installed_prem'] ) ) {
            return $content;
        }

        $coupons = $this->core->config()->get_coupon_codes();

        $slug   = $item['slug'] ?? '';
        $coupon = $coupons[ $slug ] ?? '';

        if ( $coupon ) {
            $content .= '<div class="gops-tile-coupon" style="margin-top:10px; font-size: 0.7em; color: #2271b1; font-weight: 500;">';
            $content .= '<p><small>Special Gift: Get 10% Off Premium!</small></p>';
            $content .= '<p><small>Use coupon code <code><small>' . esc_html( $coupon ) . '</small></code> at checkout.</small></p>';
            $content .= '</div>';
        }

        return $content;
    }

    /**
     * Register a plugin page to be rendered under the GravityOps parent menu.
     *
     * @param string $slug Unique slug for the submenu page.
     * @param array  $args Page configuration.
     */
    public function register_plugin_page( $slug, $args ) {
        $defaults = [
            'slug'          => $slug,
            'title'         => $slug,
            'menu_title'    => $slug,
            'capability'    => 'manage_options',
            'subtitle'      => '',
            'links'         => [],
            'tabs'          => [], // array id => [ 'label' => '', 'type' => 'render|link|help', 'callback' => callable, 'url' => '' ]
            'render'        => null, // callable to render the whole page content (used if no tabs)
            'freemius_slug' => '',
            'hide_header'   => false,
            'help'          => false,
            'docs_path'     => '',
            'docs_after'    => 'overview',
        ];
        $merged   = array_merge( $defaults, $args );
        if ( empty( $merged['freemius_slug'] ) ) {
            $merged['freemius_slug'] = $slug;
        }
        if ( ! is_array( $merged['tabs'] ) ) {
            $merged['tabs'] = [];
        }
        $this->maybe_add_docs_tab( $merged );
        if ( ! empty( $merged['help'] ) && empty( $merged['tabs']['help'] ) ) {
            $merged['tabs']['help'] = [
				'label' => 'Help',
				'type'  => 'help',
            ];
        } elseif ( isset( $merged['tabs']['help'] ) && true === $merged['tabs']['help'] ) {
            $merged['tabs']['help'] = [
				'label' => 'Help',
				'type'  => 'help',
            ];
        }
        $this->pages[ $slug ] = $merged;
    }

    /**
     * Add a runtime Markdown docs tab when a docs directory is registered.
     *
     * @param array $args Page configuration, passed by reference.
     * @return void
     */
    private function maybe_add_docs_tab( array &$args ): void {
        $docs_path = is_string( $args['docs_path'] ?? '' ) ? untrailingslashit( $args['docs_path'] ) : '';
        if ( '' === $docs_path || ! is_dir( $docs_path ) || isset( $args['tabs']['docs'] ) ) {
            return;
        }

        $capability = is_string( $args['capability'] ?? '' ) ? $args['capability'] : 'manage_options';
        $page_slug  = is_string( $args['slug'] ?? '' ) ? $args['slug'] : '';

        $docs_tab = [
            'label'    => 'Docs',
            'type'     => 'render',
            'callback' => static function () use ( $docs_path, $page_slug, $capability ): void {
                ( new DocsRenderer( $docs_path, $page_slug, $capability ) )->render();
            },
        ];

        $tabs          = $args['tabs'];
        $docs_after    = is_string( $args['docs_after'] ?? '' ) ? $args['docs_after'] : 'overview';
        $ordered_tabs  = [];
        $inserted_docs = false;
        foreach ( $tabs as $id => $tab ) {
            $ordered_tabs[ $id ] = $tab;
            if ( $docs_after === $id ) {
                $ordered_tabs['docs'] = $docs_tab;
                $inserted_docs        = true;
            }
        }

        if ( ! $inserted_docs ) {
            $ordered_tabs = [ 'docs' => $docs_tab ] + $tabs;
        }

        $args['tabs'] = $ordered_tabs;
    }

    /**
     * Register an admin page slug that should be treated as a shell page for assets.
     *
     * @param string $slug Page slug.
     * @return void
     */
    public function register_shell_page( string $slug ): void {
        $slug = sanitize_key( $slug );
        if ( '' === $slug ) {
            return;
        }
        if ( ! in_array( $slug, $this->extra_shell_pages, true ) ) {
            $this->extra_shell_pages[] = $slug;
        }
    }

    /**
     * Add the submenu pages under the shared GravityOps parent.
     */
    public function register_menus() {
        $this->core->suite_menu()->ensure_parent_menu();

        foreach ( $this->pages as $slug => $args ) {
            // Child pages automatically inherit the parent menu capability unless explicitly provided.
            $cap = 'manage_options';
            // Ensure parent is registered and capability resolved.
            $this->core->suite_menu()->ensure_parent_menu();
            $cap = $this->core->suite_menu()->get_parent_capability();
            if ( ! empty( $args['capability'] ) && is_string( $args['capability'] ) ) {
                // Respect an explicit capability if provided by the caller.
                $cap = $args['capability'];
            }

            add_submenu_page(
                null,
                $args['title'] ?? $slug,
                $args['menu_title'] ?? ( $args['title'] ?? $slug ),
                $cap,
                $slug,
                function () use ( $slug, $args ) {
                    $this->render_page( $slug, $args );
                }
            );
        }
    }

    /**
     * Enqueue scoped assets only on our pages (and optionally Freemius pages).
     *
     * @param string $hook Current admin page hook.
     */
    public function enqueue_assets( $hook ) {
        $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

        $pages_lower_case = array_change_key_case( $this->pages );
        $is_shell_page    = isset( $pages_lower_case[ $page ] ) || 'toplevel_page_gravity_ops' === $hook;
        if ( ! $is_shell_page && ! empty( $this->extra_shell_pages ) && in_array( $page, $this->extra_shell_pages, true ) ) {
            $is_shell_page = true;
        }

        // Optionally style Freemius pages in-place.
        $style_freemius = $this->core->config()->style_freemius_screens();
        $is_freemius    = false;
        if ( $style_freemius && ! $is_shell_page && ! empty( $page ) ) {
            // Match our standardized Freemius pages by hyphenated suffix to reduce false positives.
            $is_freemius = ( false !== strpos( $page, '-account' ) ) || ( false !== strpos( $page, '-pricing' ) ) || ( false !== strpos( $page, '-affiliation' ) );
        }

        $is_gf_page = ( 0 === strpos( $page, 'gf_' ) ) || ( false !== strpos( (string) $hook, 'forms_page_gf' ) );

        $this->core->admin_assets()->enqueue( $is_shell_page, $is_freemius, $is_gf_page );

        // We only need the JS config on our shell pages or Freemius pages where we inject tabs.
        if ( ! $is_shell_page && ! $is_freemius ) {
            return;
        }

        // Identify the plugin and pass its config to JS
        $config       = null;
        $current_slug = '';

        if ( $is_freemius ) {
            foreach ( $this->pages as $slug => $args ) {
                $fs_slug = $args['freemius_slug'] ?? $slug;

                $normalized_page = str_replace( '_', '-', $page );
                $bases           = [
					str_replace( '_', '-', strtolower( $fs_slug ) ),
					str_replace( '_', '-', strtolower( $slug ) ),
                ];

                $suffixes      = [ '-account', '-pricing', '-affiliation' ];
                $valid_targets = array_merge(
                        ...array_map( fn( $b ) => array_map( fn( $s ) => $b . $s, $suffixes ), $bases )
                );

                if ( in_array( $normalized_page, $valid_targets, true ) ) {
                    $current_slug = $slug;
                    break;
                }
            }
        } elseif ( $is_shell_page && isset( $this->pages[ $page ] ) ) {
            $current_slug = $page;
        }

        if ( $current_slug && isset( $this->pages[ $current_slug ] ) ) {
            $args     = $this->pages[ $current_slug ];
            $tabs     = [];
            $base_url = admin_url( 'admin.php?page=' . $current_slug );

            foreach ( ( $args['tabs'] ?? [] ) as $id => $tab ) {
                $tabs[] = [
                    'id'    => $id,
                    'label' => $tab['label'] ?? $id,
                    'url'   => ( ( $tab['type'] ?? 'render' ) === 'link' ) ? ( $tab['url'] ?? '#' ) : add_query_arg( 'tab', $id, $base_url ),
                ];
            }

            $config = [
				'slug'  => $current_slug,
				'title' => $args['title'] ?? '',
				'tabs'  => $tabs,
			];
        }

        if ( $config && wp_script_is( 'gravityops-admin', 'registered' ) ) {
            wp_localize_script( 'gravityops-admin', 'gops_admin_config', $config );
        }
    }

    /**
     * Boot the chosen provider (register side‑effect hooks). Called once.
     *
     * @return void
     */
    public function boot() {
        if ( $this->did_boot ) {
            return;
        }
        // Defer adding menus until after all plugins have registered.
        add_action( 'admin_menu', [ $this, 'register_menus' ], 99 );
        add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
        add_action( 'wp_ajax_gops_load_doc', [ $this, 'ajax_load_doc' ] );

        // Initialize TrustedLogin on all page loads (for auth redirects and background tasks).
        $this->core->trusted_login();

        // Register the shared Support page.
        $this->register_support_page();

        $this->did_boot = true;
    }

    /**
     * Load a single Markdown docs article for a registered shell page.
     *
     * @return void
     */
    public function ajax_load_doc(): void {
        $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['nonce'] ) ) : '';
        if ( ! wp_verify_nonce( $nonce, 'gops_admin' ) ) {
            wp_send_json_error( [ 'message' => 'Security check failed. Refresh and try again.' ], 403 );
        }

        $page  = isset( $_POST['page'] ) ? sanitize_key( wp_unslash( $_POST['page'] ) ) : '';
        $doc   = isset( $_POST['doc'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['doc'] ) ) : '';
        $token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['token'] ) ) : '';

        $context = $this->get_docs_ajax_context( $token, $page );
        if ( empty( $doc ) || empty( $context['docs_path'] ) ) {
            wp_send_json_error( [ 'message' => 'The requested doc could not be found.' ], 404 );
        }

        if ( ! current_user_can( $context['capability'] ) ) {
            wp_send_json_error( [ 'message' => 'You do not have permission to view this documentation.' ], 403 );
        }

        $docs_path = $context['docs_path'];
        if ( '' === $docs_path || ! is_dir( $docs_path ) ) {
            wp_send_json_error( [ 'message' => 'Documentation is not available for this plugin.' ], 404 );
        }

        $article = ( new DocsRenderer( $docs_path ) )->get_article( $doc );
        if ( empty( $article ) ) {
            wp_send_json_error( [ 'message' => 'The requested doc could not be loaded.' ], 404 );
        }

        wp_send_json_success( $article );
    }

    /**
     * Resolve docs AJAX context from a render-time token, with page registry fallback.
     *
     * @param string $token Docs access token.
     * @param string $page Page slug.
     * @return array{docs_path:string,capability:string}
     */
    private function get_docs_ajax_context( string $token, string $page ): array {
        if ( '' !== $token ) {
            $stored = get_transient( 'gops_docs_' . $token );
            if ( is_array( $stored ) && ! empty( $stored['docs_path'] ) ) {
                return [
                    'docs_path'  => is_string( $stored['docs_path'] ) ? $stored['docs_path'] : '',
                    'capability' => is_string( $stored['capability'] ?? '' ) ? $stored['capability'] : 'manage_options',
                ];
            }
        }

        if ( '' !== $page && ! empty( $this->pages[ $page ] ) ) {
            $args = $this->pages[ $page ];
            return [
                'docs_path'  => is_string( $args['docs_path'] ?? '' ) ? $args['docs_path'] : '',
                'capability' => is_string( $args['capability'] ?? '' ) ? $args['capability'] : 'manage_options',
            ];
        }

        return [
            'docs_path'  => '',
            'capability' => 'manage_options',
        ];
    }

    /**
     * Register the shared Support page under the GravityOps menu.
     *
     * @return void
     */
    private function register_support_page() {
        $this->register_plugin_page(
            'gravity_ops_support',
            [
                'title'       => 'Grant Support Access',
                'menu_title'  => 'Support Access',
                'capability'  => $this->core->suite_menu()->get_parent_capability(),
                'hide_header' => true,
                'render'      => [ $this->core->trusted_login(), 'render_support_screen' ],
            ]
        );
    }

    /**
     * Render the complete page including header, pills, and tab content.
     *
     * @param string $slug Page slug.
     * @param array  $args Page config.
     */
    public function render_page( $slug, $args ) {
        if ( ! current_user_can( $args['capability'] ?? 'manage_options' ) ) {
            return;
        }

        $tabs   = is_array( $args['tabs'] ?? [] ) ? $args['tabs'] : [];
        $active = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if ( empty( $active ) || ! isset( $tabs[ $active ] ) ) {
            $active = array_key_first( $tabs );
        }

        $base_url    = menu_page_url( $slug, false );
        $hide_header = ! empty( $args['hide_header'] );

        // Always render full wrapper + header here; notices will be relocated via JS beneath the header.
        echo '<div class="wrap gops-admin' . ( $hide_header ? ' gops-hide-header' : '' ) . '" data-gops-nonce="' . esc_attr( wp_create_nonce( 'gops_admin' ) ) . '" data-gops-ajax="' . esc_attr( admin_url( 'admin-ajax.php' ) ) . '">';
        if ( ! $hide_header ) {
            $this->render_header_only( $args );
        }
        // Dedicated notices container prevents margin-collapsing into the header
        echo '<div class="gops-notices" aria-live="polite"></div>';

        if ( ! empty( $tabs ) ) {
            echo '<nav class="gops-tabs">';
            foreach ( $tabs as $id => $tab ) {
                $visible = ! isset( $tab['visible'] ) || $tab['visible'];
                if ( ! $visible ) {
                    continue;
                }
                $label     = $tab['label'] ?? ucfirst( $id );
                $is_active = ( $active === $id );
                $tab_type  = $tab['type'] ?? 'render';
                $href      = ( 'link' === $tab_type ) ? ( $tab['url'] ?? '#' ) : add_query_arg( 'tab', $id, $base_url );

                echo '<a class="gops-tab' . ( $is_active ? ' is-active' : '' ) . '" href="' . esc_url( $href ) . '">' . esc_html( $label ) . '</a>';
            }
            echo '</nav>';
        }

        echo '<section class="gops-content">';
        if ( ! empty( $tabs ) && isset( $tabs[ $active ] ) ) {
            $tab  = $tabs[ $active ];
            $type = $tab['type'] ?? 'render';
            if ( 'help' === $type ) {
                $previous_help_slug      = $this->current_help_slug;
                $this->current_help_slug = $tab['slug'] ?? ( $args['slug'] ?? $slug );
                $this->render_help_tab();
                $this->current_help_slug = $previous_help_slug;
            } elseif ( 'render' === $type && isset( $tab['callback'] ) && is_callable( $tab['callback'] ) ) {
                call_user_func( $tab['callback'] );
            }
        } elseif ( isset( $args['render'] ) && is_callable( $args['render'] ) ) {
            call_user_func( $args['render'] );
        }
        echo '</section>';

        echo '</div>';
    }

    /**
     * Output only the branded header (no wrapper open/close, no tabs).
     *
     * @param array $args Page config.
     *
     * @return void
     */
    public function render_header_only( $args ) {
        $icon_url = '';
        if ( ! empty( $args['slug'] ) ) {
            $item = $this->core->catalog()->find( (string) $args['slug'] );
            if ( $item ) {
                $icon_url = $this->core->suite_menu()->resolve_plugin_icon_url( $item );
            }
        }
        ?>
        <div class="gops-header-wrapper">
            <?php if ( $icon_url ) : ?>
                <img class="gops-header-icon" src="<?php echo esc_url( $icon_url ); ?>" alt="" width="80" height="80" />
            <?php endif; ?>
            <header class="gops-header">
                <div class="gops-header__left">
                    <a class="gops-logo-chip" href="<?php echo esc_url( admin_url( 'admin.php?page=gravity_ops' ) ); ?>">GravityOps</a>
                    <div class="gops-headings">
                        <h1 class="gops-title"><?php echo esc_html( $args['title'] ?? '' ); ?></h1>
                        <?php if ( ! empty( $args['subtitle'] ) ) : ?>
                            <p class="gops-subtitle"><?php echo esc_html( $args['subtitle'] ); ?></p>
                        <?php endif; ?>
                    </div>
                </div>
                <?php if ( ! empty( $args['links'] ) && is_array( $args['links'] ) ) : ?>
                    <div class="gops-header__right">
                        <?php foreach ( $args['links'] as $label => $url ) : ?>
                            <a class="gops-link" target="_blank" href="<?php echo esc_url( $url ); ?>"><?php echo esc_html( $label ); ?></a>
                        <?php endforeach; ?>
                    </div>
                <?php endif; ?>
            </header>
        </div>
        <?php
    }

    // Previous in_admin_header header injection removed in favor of rendering within page callback
    // and relocating notices via JS for consistent vertical ordering.

    /**
     * Resolve assets URLs from the registered assets base URL.
     *
     * @return array{css:string,menu_css:string,js:string,base_url:string}
     */
    public function resolve_assets_urls() {
        $base_url = $this->core->assets_base_url();
        if ( '' === $base_url ) {
            return [
				'css'      => '',
				'menu_css' => '',
				'js'       => '',
				'base_url' => '',
			];
        }

        $base_url = trailingslashit( $base_url );
        return [
			'css'      => $base_url . 'css/admin.css',
			'menu_css' => $base_url . 'css/menu.css',
			'js'       => $base_url . 'js/admin.js',
			'base_url' => $base_url,
		];
    }
}
