<?php
/**
 * Admin menu + asset enqueue.
 *
 * @package Backreload
 */

namespace Backreload;

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

final class Admin {

	const MENU_SLUG = 'backreload';

	public function init(): void {
		add_action( 'admin_menu',           [ $this, 'register_menu' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_chrome' ] );
		add_action( 'in_admin_header',       [ $this, 'suppress_admin_notices' ], 1000 );
		add_action( 'admin_bar_menu',        [ $this, 'admin_bar_node' ], 80 );

		// Add a "Settings" quick link to the plugin's row on the Plugins
		// screen, matching the convention other plugins use.
		add_filter(
			'plugin_action_links_' . plugin_basename( BACKRELOAD_FILE ),
			[ $this, 'add_action_links' ]
		);
	}

	/**
	 * Enqueue the small inline CSS/JS that keeps our admin-menu icon and
	 * toolbar node looking right. Uses the WordPress enqueue API (registered
	 * dependency-only handles + wp_add_inline_style / wp_add_inline_script)
	 * rather than echoing <style>/<script> directly. Runs on every admin page
	 * because the menu icon and toolbar appear on all of them.
	 */
	public function enqueue_chrome(): void {
		$css = $this->menu_icon_css_rules() . $this->admin_bar_css_rules();
		wp_register_style( 'backreload-chrome', false, [], BACKRELOAD_VERSION );
		wp_enqueue_style( 'backreload-chrome' );
		wp_add_inline_style( 'backreload-chrome', $css );

		$js = $this->menu_icon_js_code();
		if ( '' !== $js ) {
			wp_register_script( 'backreload-chrome', false, [], BACKRELOAD_VERSION, true );
			wp_enqueue_script( 'backreload-chrome' );
			wp_add_inline_script( 'backreload-chrome', $js );
		}
	}

	/**
	 * CSS rules that keep the brand menu icon in its original colours.
	 *
	 * WordPress and some admin themes dim menu icons with `opacity` or
	 * desaturate them with a CSS `filter`. We undo both on our icon only, so
	 * the brand green + white shows at full strength. (Icon *replacement* by
	 * aggressive admin skinners / browser extensions is handled separately in
	 * menu_icon_js_code(), because those set the inline style with `!important`,
	 * which no stylesheet rule can override.) The colours live in the SVG
	 * itself, so the icon reads correctly on both dark and light admin schemes.
	 *
	 * @return string CSS (no <style> wrapper — enqueued via wp_add_inline_style).
	 */
	private function menu_icon_css_rules(): string {
		$id = '#adminmenu #toplevel_page_' . self::MENU_SLUG . ' .wp-menu-image';
		return $id . ',' . $id . ' img'
			. '{opacity:1!important;filter:none!important;background-size:24px auto!important;}';
	}

	/**
	 * JS that guarantees the brand-coloured menu icon survives icon
	 * "uniformisers" — admin-skin plugins or browser extensions that rewrite
	 * every menu icon's *inline* style to a flat white copy. Because they use
	 * `!important` on the inline declaration, CSS can't win; only re-applying
	 * the original colours in JS does. Such tools run once at load and don't
	 * re-watch, so a single re-apply (guarded by a MutationObserver in case
	 * they do) keeps our icon in place. On a normal site this is a no-op.
	 *
	 * @return string JS (no <script> wrapper — enqueued via wp_add_inline_script).
	 */
	private function menu_icon_js_code(): string {
		$uri = self::icon_data_uri();
		if ( '' === $uri || ! preg_match( '#^data:image/svg\+xml;base64,[A-Za-z0-9+/=]+$#', $uri ) ) {
			return ''; // No bundled icon → nothing to defend.
		}
		return '(function(){'
			. 'var u=' . wp_json_encode( $uri ) . ',s="#toplevel_page_' . esc_js( self::MENU_SLUG ) . ' .wp-menu-image";'
			. 'function a(){var e=document.querySelector(s);if(!e)return;'
			. 'if(e.style.backgroundImage.indexOf(u)===-1){e.style.setProperty("background-image","url(\'"+u+"\')","important");}}'
			. 'function go(){var e=document.querySelector(s);if(!e){return void setTimeout(go,50);}a();'
			. 'try{new MutationObserver(a).observe(e,{attributes:true,attributeFilter:["style"]});}catch(x){}}'
			. 'if(document.readyState!=="loading"){go();}else{document.addEventListener("DOMContentLoaded",go);}'
			. '})();';
	}

	/**
	 * Prepend a "Settings" quick link to the plugin's action row on the
	 * Plugins list table (wp-admin/plugins.php).
	 *
	 * @param array<string,string> $links Existing action links (Deactivate, …).
	 * @return array<string,string>
	 */
	public function add_action_links( array $links ): array {
		$custom = [
			'settings' => sprintf(
				'<a href="%s">%s</a>',
				esc_url( admin_url( 'admin.php?page=backreload-settings' ) ),
				esc_html__( 'Settings', 'backreload' )
			),
		];
		return array_merge( $custom, $links );
	}

	/**
	 * Submenu items shown under the top-level "BackReload" entry in the WP
	 * admin sidebar. Each one opens the same single-page React app; the
	 * key in this map flows through to window.Backreload.initialPage and
	 * the React app uses it as the starting <Layout page=…> state.
	 *
	 * Adding a new entry here is a 4-step process:
	 *   1. Add `slug => page_key` to this map
	 *   2. The submenu auto-registers from register_menu()
	 *   3. PageKey in src/App.tsx must include the page_key
	 *   4. The React Page component must exist + be wired in App.tsx
	 *
	 * @var array<string,string> slug → React PageKey
	 */
	private const SUBMENUS = [
		'backreload'              => 'dashboard',
		'backreload-backups'      => 'backups',
		'backreload-restore'      => 'restore',
		'backreload-migrate'      => 'migrate',
		'backreload-schedule'     => 'schedule',
		'backreload-destinations' => 'destinations',
		'backreload-notifications'=> 'notifications',
		'backreload-logs'         => 'logs',
		'backreload-system'       => 'system',
		'backreload-settings'     => 'settings',
	];

	public function register_menu(): void {
		// Top-level item. Clicking the top-level row OR the first submenu
		// both land on the Dashboard. WordPress automatically duplicates
		// the menu_page entry as the first submenu, so we relabel that
		// duplicate to "Dashboard" rather than the default "BackReload".
		add_menu_page(
			__( 'BackReload', 'backreload' ),
			__( 'BackReload', 'backreload' ),
			'manage_options',
			self::MENU_SLUG,
			[ $this, 'render_app' ],
			self::menu_icon(),
			80
		);

		// Submenu rows for the important pages. Each renders the same
		// React app — initialPage routing happens via wp_localize_script
		// in enqueue_assets(), driven by $_GET['page'].
		$labels = self::submenu_labels();
		foreach ( self::SUBMENUS as $slug => $_page_key ) {
			add_submenu_page(
				self::MENU_SLUG,
				$labels[ $slug ] ?? $slug,
				$labels[ $slug ] ?? $slug,
				'manage_options',
				$slug,
				[ $this, 'render_app' ]
			);
		}
	}

	/**
	 * Human labels for each admin page, keyed by menu slug. Shared by the
	 * sidebar submenu and the admin-bar (toolbar) node so both stay in sync.
	 *
	 * @return array<string,string> slug → translated label
	 */
	private static function submenu_labels(): array {
		return [
			'backreload'               => __( 'Dashboard',     'backreload' ),
			'backreload-backups'       => __( 'Backups',       'backreload' ),
			'backreload-restore'       => __( 'Restore',       'backreload' ),
			'backreload-migrate'       => __( 'Migrate',       'backreload' ),
			'backreload-schedule'      => __( 'Schedule',      'backreload' ),
			'backreload-destinations'  => __( 'Destinations',  'backreload' ),
			'backreload-notifications' => __( 'Notifications', 'backreload' ),
			'backreload-logs'          => __( 'Logs',          'backreload' ),
			'backreload-system'        => __( 'System',        'backreload' ),
			'backreload-settings'      => __( 'Settings',      'backreload' ),
		];
	}

	/**
	 * CSS rules that keep the toolbar icon + label on one line. Many themes
	 * set `img{display:block}`, which would push our label onto a second row
	 * in the admin bar; we force the icon back to an inline element. Scoped to
	 * our node only, so it's inert when the toolbar isn't shown.
	 *
	 * @return string CSS (no <style> wrapper — enqueued via wp_add_inline_style).
	 */
	private function admin_bar_css_rules(): string {
		return '#wpadminbar #wp-admin-bar-backreload .ab-item img{'
			. 'display:inline-block!important;width:16px;height:16px;'
			. 'vertical-align:middle!important;margin:0 6px 0 0!important;float:none!important;}'
			. '#wpadminbar #wp-admin-bar-backreload>.ab-item{white-space:nowrap;}';
	}

	/**
	 * Add a "BackReload" node to the WordPress admin toolbar, with the same
	 * pages as the sidebar as quick-access children. Restricted to the admin
	 * area (not the front-end) for users who can manage the plugin, so a
	 * backup/restore is one click away from any wp-admin screen.
	 *
	 * @param \WP_Admin_Bar $bar The toolbar instance.
	 */
	public function admin_bar_node( $bar ): void {
		if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) {
			return;
		}

		// Brand icon (small) before the label. The data URI is validated in
		// icon_data_uri()/below; fall back to a dashicon glyph if missing.
		$uri   = self::icon_data_uri();
		$valid = '' !== $uri && preg_match( '#^data:image/svg\+xml;base64,[A-Za-z0-9+/=]+$#', $uri );
		$icon  = $valid
			? '<img src="' . $uri . '" alt="" style="width:16px;height:16px;vertical-align:text-bottom;margin-right:6px" />'
			: '<span class="ab-icon dashicons dashicons-backup" style="margin-right:6px"></span>';

		$bar->add_node(
			[
				'id'    => 'backreload',
				'title' => $icon . '<span class="ab-label">' . esc_html__( 'BackReload', 'backreload' ) . '</span>',
				'href'  => admin_url( 'admin.php?page=' . self::MENU_SLUG ),
			]
		);

		foreach ( self::submenu_labels() as $slug => $label ) {
			$bar->add_node(
				[
					'parent' => 'backreload',
					'id'     => 'backreload-bar-' . $slug,
					'title'  => esc_html( $label ),
					'href'   => admin_url( 'admin.php?page=' . $slug ),
				]
			);
		}
	}

	/**
	 * Admin-menu icon. Returns the bundled brand SVG inlined as a base64
	 * data URI so it renders without an extra HTTP request and survives in
	 * locked-down admin areas. Falls back to a core dashicon if the file is
	 * ever missing (e.g. a partial install).
	 *
	 * @return string Data URI or dashicon name.
	 */
	private static function menu_icon(): string {
		$uri = self::icon_data_uri();
		return '' !== $uri ? $uri : 'dashicons-backup';
	}

	/**
	 * Base64 data URI for the bundled brand menu icon, or '' if unreadable.
	 * Result is cached for the request so we don't re-read/re-encode the
	 * file for both add_menu_page() and the CSS guard below.
	 *
	 * @return string
	 */
	private static function icon_data_uri(): string {
		static $cache = null;
		if ( null !== $cache ) {
			return $cache;
		}
		$cache = '';
		$path  = BACKRELOAD_DIR . 'assets/menu-icon.svg';
		if ( is_readable( $path ) ) {
			$svg = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a bundled plugin asset, not a remote URL.
			if ( false !== $svg && '' !== $svg ) {
				$cache = 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Standard technique for inlining an admin-menu SVG icon.
			}
		}
		return $cache;
	}

	/**
	 * Hooks reported by WordPress for our admin pages. The top-level slug
	 * uses the `toplevel_page_` prefix; submenus use `backreload_page_`.
	 * We hide admin notices on all of them so third-party "rate us" /
	 * "welcome to plugin X" banners don't crash into our app layout.
	 *
	 * @return string[]
	 */
	private function admin_page_hooks(): array {
		$hooks = [ 'toplevel_page_' . self::MENU_SLUG ];
		foreach ( self::SUBMENUS as $slug => $_p ) {
			// Skip the dashboard submenu — it shares the toplevel hook.
			if ( $slug === self::MENU_SLUG ) continue;
			$hooks[] = self::MENU_SLUG . '_page_' . $slug;
		}
		return $hooks;
	}

	public function suppress_admin_notices(): void {
		$screen = get_current_screen();
		if ( $screen && in_array( $screen->id, $this->admin_page_hooks(), true ) ) {
			remove_all_actions( 'admin_notices' );
			remove_all_actions( 'all_admin_notices' );
			remove_all_actions( 'network_admin_notices' );
			remove_all_actions( 'user_admin_notices' );
		}
	}

	public function render_app(): void {
		echo '<div id="backreload-root" class="backreload-root"></div>';
	}

	public function enqueue_assets( string $hook ): void {
		if ( ! in_array( $hook, $this->admin_page_hooks(), true ) ) {
			return;
		}

		$dist      = BACKRELOAD_DIR . 'assets/dist/';
		$dist_url  = BACKRELOAD_URL . 'assets/dist/';
		$manifest  = $dist . '.vite/manifest.json';

		if ( ! file_exists( $manifest ) ) {
			add_action( 'admin_notices', function () {
				echo '<div class="notice notice-warning"><p><strong>BackReload:</strong> Build assets not found. Run <code>npm install &amp;&amp; npm run build</code> inside the plugin directory.</p></div>';
			} );
			return;
		}

		$mani  = json_decode( file_get_contents( $manifest ), true );
		$entry = $mani['src/main.tsx'] ?? null;
		if ( ! $entry ) {
			return;
		}

		wp_enqueue_script(
			'backreload-app',
			$dist_url . $entry['file'],
			// `wp-i18n` exposes window.wp.i18n.__() to our bundle so the JS
			// shim in src/lib/i18n.ts can translate strings without bundling
			// a separate i18n library.
			[ 'wp-i18n' ],
			BACKRELOAD_VERSION,
			true
		);

		// Pair the script handle with our text domain so translations from
		// languages/backreload-{locale}.json are loaded into
		// wp.i18n. Strings in the React bundle are then translatable via the
		// standard WP flow.
		if ( function_exists( 'wp_set_script_translations' ) ) {
			wp_set_script_translations(
				'backreload-app',
				'backreload',
				BACKRELOAD_DIR . 'languages'
			);
		}

		add_filter( 'script_loader_tag', function ( $tag, $handle ) {
			if ( 'backreload-app' === $handle ) {
				return str_replace( '<script ', '<script type="module" ', $tag );
			}
			return $tag;
		}, 10, 2 );

		if ( ! empty( $entry['css'] ) ) {
			foreach ( $entry['css'] as $i => $css ) {
				wp_enqueue_style( 'backreload-app-' . $i, $dist_url . $css, [], BACKRELOAD_VERSION );
			}
		}

		// Map the currently-loaded admin slug to a React PageKey so the
		// app boots straight to the user's chosen submenu. Falls back to
		// 'dashboard' when the slug isn't in our map (defensive — should
		// never happen since the enqueue gate above checks the hook).
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only: the admin page slug only decides which React view the SPA opens on; no state is changed.
		$current_slug = isset( $_GET['page'] )
			? sanitize_key( wp_unslash( (string) $_GET['page'] ) )
			: self::MENU_SLUG;
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
		$initial_page = self::SUBMENUS[ $current_slug ] ?? 'dashboard';

		wp_localize_script( 'backreload-app', 'Backreload', [
			'apiUrl'         => esc_url_raw( rest_url( 'backreload/v1/' ) ),
			'nonce'          => wp_create_nonce( 'wp_rest' ),
			'version'        => BACKRELOAD_VERSION,
			'siteUrl'        => home_url(),
			// Data-only build? When true the Restore wizard hides plugins/themes
			// (restore brings back content only). Default full build = false.
			'dataOnly'       => \Backreload\Core\RestoreEngine::data_only_mode(),
			// Which page to open on app boot. Driven by WP admin submenu
			// click — see SUBMENUS map above.
			'initialPage'    => $initial_page,
			// Absolute URLs to the plugin's brand marks, served as static
			// assets so the React UI renders them as <img> without bundling
			// into the JS payload (and a designer can swap the files without
			// rebuilding). Two variants:
			//   logoUrl      — dark wordmark, for LIGHT backgrounds (the
			//                  sidebar + topbar, which are white).
			//   logoWhiteUrl — white wordmark, for DARK backgrounds (the
			//                  maintenance "be right back" page, future dark UI).
			'logoUrl'        => plugins_url( 'assets/logo.svg', BACKRELOAD_FILE ),
			'logoWhiteUrl'   => plugins_url( 'assets/logo-white.svg', BACKRELOAD_FILE ),
			// Effective host limit for a single HTTP upload — useful for the
			// Import card's "your host caps uploads at X" hint. Our chunked
			// transport stays well under this, so it isn't actually a ceiling
			// on the import; it's only shown so the user understands the
			// environment they're working in.
			'maxUploadBytes' => (int) wp_max_upload_size(),
		] );
	}
}
