<?php
/**
 * Public Free ↔ Pro extension API catalog (versioned).
 *
 * This is the stable surface Pro and other trusted extensions may rely on.
 * Anything not listed here is internal and may change without notice.
 *
 * @package SEOvaultSEO\Extension
 */

namespace SEOvaultSEO\Extension;

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

/**
 * Catalog of documented hooks, assets, UI tokens, and public classes.
 */
class ExtensionApi {

	const VERSION = '1.2';

	const HANDLE_ADMIN_FOUNDATION  = 'seovaultseo-admin-foundation';
	const HANDLE_ADMIN_COMPONENTS  = 'seovaultseo-admin-components';
	const HANDLE_ADMIN_LEGACY      = 'seovaultseo-admin';
	const LOCALIZE_ADMIN           = 'seovaultseo_admin';

	const FILTER_ADMIN_SUBMENUS           = 'seovaultseo_admin_submenus';
	const FILTER_MAIN_MENU_CAPABILITY     = 'seovaultseo_main_menu_capability';
	const FILTER_GENERAL_SETTINGS_TABS    = 'seovaultseo_general_settings_tabs';
	const FILTER_DASHBOARD_CARDS          = 'seovaultseo_dashboard_cards';
	const FILTER_EDITOR_PANELS            = 'seovaultseo_editor_panels';
	const FILTER_MODULES                  = 'seovaultseo_modules';
	const FILTER_ACTION_CENTER_ITEMS      = 'seovaultseo_action_center_items';
	const FILTER_CAPABILITIES             = 'seovaultseo_capabilities';

	const ACTION_LOADED                   = 'seovaultseo_extension_api_loaded';
	const ACTION_REGISTER_MODULES         = 'seovaultseo_register_modules';
	const ACTION_GENERAL_TAB_NAV          = 'seovaultseo_general_settings_tab_nav';
	const ACTION_GENERAL_TAB_PANELS       = 'seovaultseo_general_settings_tab_panels';
	const ACTION_DASHBOARD_SECTIONS       = 'seovaultseo_dashboard_sections';
	const ACTION_EDITOR_META_BOX_PANELS   = 'seovaultseo_editor_meta_box_panels';
	const ACTION_ENQUEUE_ADMIN_ASSETS     = 'seovaultseo_enqueue_admin_assets';
	const ACTION_ANALYTICS_FIELDS_BEFORE_MEASUREMENT = 'seovaultseo_analytics_fields_before_measurement';
	const ACTION_ANALYTICS_FIELDS_AFTER_MEASUREMENT  = 'seovaultseo_analytics_fields_after_measurement';
	const ACTION_ANALYTICS_SETTINGS_SAVED            = 'seovaultseo_analytics_settings_saved';
	const ACTION_ANALYTICS_OUTPUT                    = 'seovaultseo_analytics_output';
	const FILTER_ANALYTICS_OUTPUT_MODE               = 'seovaultseo_analytics_output_mode';

	/**
	 * Bootstrap extension API (register assets early, fire lifecycle action).
	 *
	 * @return void
	 */
	public static function init() {
		Assets::init();
		Capabilities::init();

		/**
		 * Fires once the Free extension API is ready.
		 *
		 * Pro should hook here (or on plugins_loaded after Free) to register
		 * modules, menus, and assets against the documented contracts.
		 *
		 * @param string $version Extension API version (SEOVAULT_SEO_EXTENSION_API_VERSION).
		 */
		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Hook name is the prefixed public API constant declared above.
		do_action( self::ACTION_LOADED, self::version() );
		/**
		 * Fires so extensions can register module descriptors.
		 *
		 * Prefer also returning descriptors via {@see self::FILTER_MODULES}.
		 */
		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Hook name is the prefixed public API constant declared above.
		do_action( self::ACTION_REGISTER_MODULES );
	}

	/**
	 * Current extension API version string.
	 *
	 * @return string
	 */
	public static function version() {
		if ( defined( 'SEOVAULT_SEO_EXTENSION_API_VERSION' ) ) {
			return (string) SEOVAULT_SEO_EXTENSION_API_VERSION;
		}
		return self::VERSION;
	}

	/**
	 * Documented public hooks and their contracts.
	 *
	 * @return array<string,array{type:string,purpose:string,payload:string,notes:string}>
	 */
	public static function documented_hooks() {
		return array(
			self::FILTER_ADMIN_SUBMENUS         => array(
				'type'     => 'filter',
				'purpose'  => 'Add or modify SEOvault AI admin submenus under the Free top-level menu.',
				'payload'  => 'array<int,array{id:string,page_title:string,callback?:callable,capability?:string}>',
				'notes'    => 'Do not register a separate top-level Pro menu. Duplicate ids are skipped.',
			),
			self::FILTER_MAIN_MENU_CAPABILITY   => array(
				'type'     => 'filter',
				'purpose'  => 'Override the capability required for the Free top-level menu.',
				'payload'  => 'string capability (default manage_options)',
				'notes'    => 'Pro should prefer granular Capabilities::CAP_* checks on its own pages.',
			),
			self::FILTER_GENERAL_SETTINGS_TABS  => array(
				'type'     => 'filter',
				'purpose'  => 'Register additional General Settings vertical tabs.',
				'payload'  => 'array<string,array{id:string,label:string,icon?:string,callback?:callable}> keyed by id',
				'notes'    => 'Ids must be sanitize_key-safe and should use a vtab-pro-* prefix. Built-in ids cannot be replaced.',
			),
			self::FILTER_DASHBOARD_CARDS        => array(
				'type'     => 'filter',
				'purpose'  => 'Register optional dashboard card descriptors.',
				'payload'  => 'array<int,array{id:string,title:string,callback:callable,priority?:int}>',
				'notes'    => 'Cards are rendered via ACTION_DASHBOARD_SECTIONS; invalid entries are dropped.',
			),
			self::FILTER_EDITOR_PANELS          => array(
				'type'     => 'filter',
				'purpose'  => 'Register optional post-editor panel descriptors.',
				'payload'  => 'array<int,array{id:string,title?:string,callback:callable}>',
				'notes'    => 'Prefer ACTION_EDITOR_META_BOX_PANELS for simple output. Keep enqueue-only assets.',
			),
			self::FILTER_MODULES                => array(
				'type'     => 'filter',
				'purpose'  => 'Declare installed extension modules to Free (observability / future UI).',
				'payload'  => 'array<int,array{id:string,title:string,version?:string}>',
				'notes'    => 'Free does not execute module business logic from this list.',
			),
			self::FILTER_ACTION_CENTER_ITEMS    => array(
				'type'     => 'filter',
				'purpose'  => 'Add Action Center items on General/Status sidebars and dashboard.',
				'payload'  => 'array of Action Center item arrays (re-validated by Free)',
				'notes'    => 'Existing Free contract; invalid items are omitted.',
			),
			self::FILTER_CAPABILITIES           => array(
				'type'     => 'filter',
				'purpose'  => 'Map or extend documented SEOvault capability names.',
				'payload'  => 'array<string,string> capability_key => WP capability',
				'notes'    => 'See Capabilities::defaults().',
			),
			self::ACTION_LOADED                 => array(
				'type'     => 'action',
				'purpose'  => 'Extension API ready lifecycle signal.',
				'payload'  => 'string $version',
				'notes'    => 'Safe point for Pro bootstrap after Free constants and registries exist.',
			),
			self::ACTION_REGISTER_MODULES       => array(
				'type'     => 'action',
				'purpose'  => 'Register modules / side effects that need Free to be loaded.',
				'payload'  => 'void',
				'notes'    => 'Also populate FILTER_MODULES for discovery.',
			),
			self::ACTION_GENERAL_TAB_NAV        => array(
				'type'     => 'action',
				'purpose'  => 'Render extra General Settings tab nav links after built-ins.',
				'payload'  => 'string $active_tab',
				'notes'    => 'Prefer FILTER_GENERAL_SETTINGS_TABS; this action is for advanced markup.',
			),
			self::ACTION_GENERAL_TAB_PANELS     => array(
				'type'     => 'action',
				'purpose'  => 'Render extra General Settings tab panels after built-ins.',
				'payload'  => 'string $active_tab',
				'notes'    => 'Callbacks from FILTER_GENERAL_SETTINGS_TABS are invoked automatically.',
			),
			self::ACTION_DASHBOARD_SECTIONS     => array(
				'type'     => 'action',
				'purpose'  => 'Render extra dashboard sections after Free dashboard content.',
				'payload'  => 'void',
				'notes'    => 'FILTER_DASHBOARD_CARDS callbacks are invoked by Free before this action ends.',
			),
			self::ACTION_EDITOR_META_BOX_PANELS => array(
				'type'     => 'action',
				'purpose'  => 'Render extra panels inside the SEOvault post meta box.',
				'payload'  => '\WP_Post $post',
				'notes'    => 'Enqueue scripts via ACTION_ENQUEUE_ADMIN_ASSETS / editor hooks — no inline script tags.',
			),
			self::ACTION_ENQUEUE_ADMIN_ASSETS   => array(
				'type'     => 'action',
				'purpose'  => 'Enqueue extension assets on SEOvault admin pages after Free shell assets.',
				'payload'  => 'string $hook, string $page',
				'notes'    => 'Depend on HANDLE_ADMIN_FOUNDATION / HANDLE_ADMIN_COMPONENTS. Never echo script/style tags.',
			),
			self::ACTION_ANALYTICS_FIELDS_BEFORE_MEASUREMENT => array(
				'type'     => 'action',
				'purpose'  => 'Render trusted extension-owned Analytics fields before the Free Measurement ID field.',
				'payload'  => 'void',
				'notes'    => 'Only trusted extensions should register callbacks. Output escaped form controls; the enclosing Free form supplies the nonce.',
			),
			self::ACTION_ANALYTICS_FIELDS_AFTER_MEASUREMENT => array(
				'type'     => 'action',
				'purpose'  => 'Render trusted extension-owned Analytics fields after the Free Measurement ID field.',
				'payload'  => 'void',
				'notes'    => 'Only trusted extensions should register callbacks. Output escaped form controls; the enclosing Free form supplies the nonce.',
			),
			self::ACTION_ANALYTICS_SETTINGS_SAVED => array(
				'type'     => 'action',
				'purpose'  => 'Persist extension-owned Analytics settings after Free verifies the form nonce and capability.',
				'payload'  => 'array $request (verified admin settings request)',
				'notes'    => 'Extension callbacks must sanitize their own fields and enforce capability checks defensively.',
			),
			self::FILTER_ANALYTICS_OUTPUT_MODE => array(
				'type'     => 'filter',
				'purpose'  => 'Select generated Free Analytics output or a trusted extension-owned output handler.',
				'payload'  => "string mode: 'gtag' or 'extension'",
				'notes'    => 'Unknown values fall back to generated gtag output. An extension mode must pair with ACTION_ANALYTICS_OUTPUT.',
			),
			self::ACTION_ANALYTICS_OUTPUT => array(
				'type'     => 'action',
				'purpose'  => 'Let a trusted extension emit its own Analytics script after Free applies visitor eligibility checks.',
				'payload'  => 'void',
				'notes'    => 'Extensions must enqueue a registered handle and attach code with wp_add_inline_script(); Free never accepts or emits arbitrary code.',
			),
		);
	}

	/**
	 * Stable shared asset handles Pro may enqueue.
	 *
	 * @return array<string,array{type:string,src:string,deps:string[],localize?:string,keys?:string[]}>
	 */
	public static function documented_asset_handles() {
		return array(
			self::HANDLE_ADMIN_FOUNDATION => array(
				'type' => 'style',
				'src'  => 'assets/css/admin.css',
				'deps' => array(),
			),
			self::HANDLE_ADMIN_COMPONENTS => array(
				'type'     => 'script',
				'src'      => 'assets/js/admin.js',
				'deps'     => array( 'jquery' ),
				'localize' => self::LOCALIZE_ADMIN,
				'keys'     => array(
					'ajax_url',
					'nonce',
					'site_key',
					'rest_url',
					'version',
					'is_connected',
					'cache_notice',
					// Plus i18n strings merged by I18n::merge_admin_script_strings().
				),
			),
			self::HANDLE_ADMIN_LEGACY     => array(
				'type' => 'alias',
				'src'  => '',
				'deps' => array( self::HANDLE_ADMIN_FOUNDATION, self::HANDLE_ADMIN_COMPONENTS ),
			),
		);
	}

	/**
	 * Stable CSS custom properties Pro may use in module stylesheets.
	 *
	 * @return string[]
	 */
	public static function documented_css_variables() {
		return array(
			'--seovaultseo-primary',
			'--seovaultseo-primary-hover',
			'--seovaultseo-primary-light',
			'--seovaultseo-success',
			'--seovaultseo-warning',
			'--seovaultseo-error',
			'--seovaultseo-info',
			'--seovaultseo-muted',
			'--seovaultseo-bg',
			'--seovaultseo-card-bg',
			'--seovaultseo-border',
			'--seovaultseo-radius',
			'--seovaultseo-shadow',
		);
	}

	/**
	 * Public classes / APIs Pro may call. Everything else under includes/ is @internal.
	 *
	 * @return string[]
	 */
	public static function documented_public_classes() {
		return array(
			__CLASS__,
			Assets::class,
			Capabilities::class,
			ConnectorAuth::class,
			\SEOvaultSEO\Core\Options::class,
			\SEOvaultSEO\Core\PostMeta::class,
			\SEOvaultSEO\Core\TermMeta::class,
			\SEOvaultSEO\Schema\SchemaExtensionApi::class,
			\SEOvaultSEO\Schema\SchemaExtensionContributor::class,
			\SEOvaultSEO\Schema\SchemaIntegrationContributor::class,
		);
	}

	/**
	 * Sanitize extension-registered general tabs.
	 *
	 * Built-in tab ids cannot be overridden. Ids must match vtab-[a-z0-9\-]+.
	 *
	 * @param mixed    $tabs     Filter return.
	 * @param string[] $built_in Built-in tab ids.
	 * @return array<string,array{id:string,label:string,icon:string,callback:?callable}>
	 */
	public static function sanitize_general_tabs( $tabs, array $built_in ) {
		if ( ! is_array( $tabs ) ) {
			return array();
		}

		$clean = array();
		foreach ( $tabs as $key => $tab ) {
			if ( ! is_array( $tab ) ) {
				continue;
			}

			$id = isset( $tab['id'] ) ? sanitize_key( (string) $tab['id'] ) : sanitize_key( (string) $key );
			if ( '' === $id || isset( $clean[ $id ] ) ) {
				continue;
			}
			if ( in_array( $id, $built_in, true ) ) {
				continue;
			}
			if ( ! preg_match( '/^vtab-[a-z0-9\-]+$/', $id ) ) {
				continue;
			}

			$label = isset( $tab['label'] ) ? sanitize_text_field( (string) $tab['label'] ) : '';
			if ( '' === $label ) {
				continue;
			}

			$icon = isset( $tab['icon'] ) ? sanitize_html_class( (string) $tab['icon'] ) : 'dashicons-admin-generic';
			if ( '' === $icon ) {
				$icon = 'dashicons-admin-generic';
			}

			$callback = null;
			if ( isset( $tab['callback'] ) && is_callable( $tab['callback'] ) ) {
				$callback = $tab['callback'];
			}

			$clean[ $id ] = array(
				'id'       => $id,
				'label'    => $label,
				'icon'     => $icon,
				'callback' => $callback,
			);
		}

		return $clean;
	}

	/**
	 * Sanitize dashboard card descriptors.
	 *
	 * @param mixed $cards Filter return.
	 * @return array<int,array{id:string,title:string,callback:callable,priority:int}>
	 */
	public static function sanitize_dashboard_cards( $cards ) {
		if ( ! is_array( $cards ) ) {
			return array();
		}

		$clean = array();
		foreach ( $cards as $card ) {
			if ( ! is_array( $card ) ) {
				continue;
			}
			$id = isset( $card['id'] ) ? sanitize_key( (string) $card['id'] ) : '';
			if ( '' === $id || ! isset( $card['callback'] ) || ! is_callable( $card['callback'] ) ) {
				continue;
			}
			$title = isset( $card['title'] ) ? sanitize_text_field( (string) $card['title'] ) : $id;
			$prio  = isset( $card['priority'] ) ? (int) $card['priority'] : 10;
			$clean[] = array(
				'id'       => $id,
				'title'    => $title,
				'callback' => $card['callback'],
				'priority' => $prio,
			);
		}

		usort(
			$clean,
			static function ( $a, $b ) {
				return $a['priority'] <=> $b['priority'];
			}
		);

		return $clean;
	}
}
