<?php
/**
 * Admin: Sites registry screen, guide Targeting metabox, Audience list column.
 */

defined( 'ABSPATH' ) || exit;

class BW_Guides_Server_Admin {

	const CAP        = 'manage_options';
	const SITES_PAGE = 'bw-guides-sites';

	public function register() {
		add_action( 'admin_menu', array( $this, 'register_menu' ) );
		add_action( 'add_meta_boxes_bw_guide', array( $this, 'add_targeting_metabox' ) );
		add_action( 'add_meta_boxes_bw_guide', array( $this, 'add_keywords_metabox' ) );
		add_action( 'save_post_bw_guide', array( $this, 'save_targeting' ), 10, 2 );
		add_action( 'save_post_bw_guide', array( $this, 'save_keywords' ), 10, 2 );
		add_filter( 'manage_bw_guide_posts_columns', array( $this, 'guide_columns' ) );
		add_action( 'manage_bw_guide_posts_custom_column', array( $this, 'render_guide_column' ), 10, 2 );
		add_action( 'admin_post_bw_guides_server_add_site', array( $this, 'handle_add_site' ) );
		add_action( 'admin_post_bw_guides_server_site_action', array( $this, 'handle_site_action' ) );
		add_action( 'admin_post_bw_guides_server_set_client', array( $this, 'handle_set_client' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
		add_action( 'admin_notices', array( $this, 'render_keyword_mismatch_notice' ) );
	}

	public function register_menu() {
		add_submenu_page(
			'edit.php?post_type=' . BW_Guides_Server_CPT::POST_TYPE,
			__( 'Sites', 'bw-guides-server' ),
			__( 'Sites', 'bw-guides-server' ),
			self::CAP,
			self::SITES_PAGE,
			array( $this, 'render_sites_page' )
		);
	}

	public function enqueue_assets( $hook ) {
		if ( 'bw_guide_page_' . self::SITES_PAGE !== $hook ) {
			return;
		}
		wp_enqueue_style( 'bw-guides-server-admin', BW_GUIDES_SERVER_URL . 'assets/css/admin.css', array(), BW_GUIDES_SERVER_VERSION );
		wp_enqueue_script( 'bw-guides-server-admin', BW_GUIDES_SERVER_URL . 'assets/js/admin.js', array(), BW_GUIDES_SERVER_VERSION, true );
	}

	/* ---------------------------------------------------------------------
	 * Sites screen
	 * ------------------------------------------------------------------- */

	public function render_sites_page() {
		if ( ! current_user_can( self::CAP ) ) {
			return;
		}

		echo '<div class="wrap bw-gs-wrap">';
		echo '<h1>' . esc_html__( 'BW Guides — Sites', 'bw-guides-server' ) . '</h1>';

		$this->render_new_key_notice();
		$this->render_action_notice();
		$this->render_client_slug_datalist();

		$sites = BW_Guides_Server_Sites::all();

		echo '<h2>' . esc_html__( 'Registered Sites', 'bw-guides-server' ) . '</h2>';
		if ( empty( $sites ) ) {
			echo '<p>' . esc_html__( 'No sites registered yet. Add one below to generate its key.', 'bw-guides-server' ) . '</p>';
		} else {
			$this->render_sites_table( $sites );
		}

		$this->render_add_site_form();
		echo '</div>';
	}

	private function render_new_key_notice() {
		$transient_key = 'bw_guides_server_new_key_' . get_current_user_id();
		$payload       = get_transient( $transient_key );
		if ( ! is_array( $payload ) || empty( $payload['key'] ) ) {
			return;
		}
		delete_transient( $transient_key );

		echo '<div class="notice notice-success bw-gs-key-notice"><p><strong>'
			. esc_html( sprintf( __( 'Site key for “%s” — copy it now, it will not be shown again:', 'bw-guides-server' ), $payload['name'] ) )
			. '</strong></p><p><input type="text" readonly class="bw-gs-key-value large-text code" value="' . esc_attr( $payload['key'] ) . '"></p>'
			. '<p>' . esc_html__( 'Paste this into the client site at Guides → Settings → Site key.', 'bw-guides-server' ) . '</p></div>';
	}

	private function render_action_notice() {
		if ( empty( $_GET['bw_notice'] ) ) {
			return;
		}
		$notice   = sanitize_key( wp_unslash( $_GET['bw_notice'] ) );
		$messages = array(
			'revoked'        => __( 'Site key revoked. The site can no longer sync.', 'bw-guides-server' ),
			'reactivated'    => __( 'Site reactivated. Its existing key works again.', 'bw-guides-server' ),
			'deleted'        => __( 'Site deleted.', 'bw-guides-server' ),
			'client_updated' => __( 'Client updated.', 'bw-guides-server' ),
			'error'          => __( 'Something went wrong — the action was not applied.', 'bw-guides-server' ),
		);
		if ( ! isset( $messages[ $notice ] ) ) {
			return;
		}
		$class = ( 'error' === $notice ) ? 'notice-error' : 'notice-success';
		echo '<div class="notice ' . esc_attr( $class ) . '"><p>' . esc_html( $messages[ $notice ] ) . '</p></div>';
	}

	private function render_sites_table( $sites ) {
		echo '<table class="wp-list-table widefat fixed striped bw-gs-table">';
		echo '<thead><tr>';
		echo '<th>' . esc_html__( 'Site', 'bw-guides-server' ) . '</th>';
		echo '<th>' . esc_html__( 'URL', 'bw-guides-server' ) . '</th>';
		echo '<th>' . esc_html__( 'Key', 'bw-guides-server' ) . '</th>';
		echo '<th>' . esc_html__( 'Status', 'bw-guides-server' ) . '</th>';
		echo '<th>' . esc_html__( 'Last Sync', 'bw-guides-server' ) . '</th>';
		echo '<th>' . esc_html__( 'Client', 'bw-guides-server' ) . '</th>';
		echo '<th>' . esc_html__( 'Plugin Ver.', 'bw-guides-server' ) . '</th>';
		echo '<th>' . esc_html__( 'Actions', 'bw-guides-server' ) . '</th>';
		echo '</tr></thead><tbody>';

		foreach ( $sites as $site ) {
			$last_sync = $site->last_sync_at
				? sprintf( __( '%s ago', 'bw-guides-server' ), human_time_diff( strtotime( $site->last_sync_at . ' UTC' ) ) )
				: __( 'never', 'bw-guides-server' );
			$status_class = ( 'active' === $site->status ) ? 'bw-gs-active' : 'bw-gs-revoked';

			echo '<tr>';
			echo '<td><strong>' . esc_html( $site->site_name ) . '</strong></td>';
			echo '<td>' . esc_html( $site->site_url ) . '</td>';
			echo '<td><code>…' . esc_html( $site->key_hint ) . '</code></td>';
			echo '<td><span class="' . esc_attr( $status_class ) . '">' . esc_html( $site->status ) . '</span></td>';
			echo '<td>' . esc_html( $last_sync ) . ( $site->last_sync_ip ? ' <span class="bw-gs-muted">(' . esc_html( $site->last_sync_ip ) . ')</span>' : '' ) . '</td>';
			echo '<td>' . $this->render_client_cell( $site ) . '</td>'; // Escaped inside render_client_cell().
			echo '<td>' . esc_html( $site->last_client_version ? $site->last_client_version : '—' ) . '</td>';
			echo '<td>' . $this->row_actions( $site ) . '</td>'; // Links are escaped inside row_actions().
			echo '</tr>';
		}

		echo '</tbody></table>';
	}

	private function row_actions( $site ) {
		$actions = array();

		$actions[] = $this->action_link(
			(int) $site->id,
			'regenerate',
			__( 'Regenerate key', 'bw-guides-server' ),
			__( 'Regenerate the key? The current key stops working immediately and the site cannot sync until the new key is entered there.', 'bw-guides-server' )
		);

		if ( 'active' === $site->status ) {
			$actions[] = $this->action_link(
				(int) $site->id,
				'revoke',
				__( 'Revoke', 'bw-guides-server' ),
				__( 'Revoke this site’s key? The site will no longer be able to sync guides.', 'bw-guides-server' )
			);
		} else {
			$actions[] = $this->action_link( (int) $site->id, 'reactivate', __( 'Reactivate', 'bw-guides-server' ), '' );
		}

		$actions[] = $this->action_link(
			(int) $site->id,
			'delete',
			__( 'Delete', 'bw-guides-server' ),
			__( 'Delete this site entry? Its key stops working and any guides targeted at it will no longer include it.', 'bw-guides-server' )
		);

		return implode( ' | ', $actions );
	}

	private function action_link( $site_id, $do, $label, $confirm ) {
		$url = wp_nonce_url(
			add_query_arg(
				array(
					'action'  => 'bw_guides_server_site_action',
					'do'      => $do,
					'site_id' => $site_id,
				),
				admin_url( 'admin-post.php' )
			),
			'bw_guides_server_site_action_' . $site_id
		);
		$confirm_attr = $confirm ? ' class="bw-gs-confirm" data-confirm="' . esc_attr( $confirm ) . '"' : '';
		return '<a href="' . esc_url( $url ) . '"' . $confirm_attr . '>' . esc_html( $label ) . '</a>';
	}

	private function render_add_site_form() {
		echo '<h2>' . esc_html__( 'Add Site', 'bw-guides-server' ) . '</h2>';
		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="bw-gs-add-form">';
		echo '<input type="hidden" name="action" value="bw_guides_server_add_site">';
		wp_nonce_field( 'bw_guides_server_add_site' );
		echo '<table class="form-table"><tbody>';
		echo '<tr><th scope="row"><label for="bw-gs-site-name">' . esc_html__( 'Site name', 'bw-guides-server' ) . '</label></th>';
		echo '<td><input required type="text" id="bw-gs-site-name" name="site_name" class="regular-text" placeholder="Client Site"></td></tr>';
		echo '<tr><th scope="row"><label for="bw-gs-site-url">' . esc_html__( 'Site URL', 'bw-guides-server' ) . '</label></th>';
		echo '<td><input type="url" id="bw-gs-site-url" name="site_url" class="regular-text" placeholder="https://example.com"></td></tr>';
		echo '<tr><th scope="row"><label for="bw-gs-site-client">' . esc_html__( 'Client slug', 'bw-guides-server' ) . '</label></th>';
		echo '<td><input type="text" id="bw-gs-site-client" name="client_slug" list="bw-gs-client-slugs-list" class="regular-text" placeholder="e.g. brentwood">';
		echo '<p class="description">' . esc_html__( 'Optional. Groups this site under a client so "Specific client" guide targeting reaches it — including guides already targeted at that client before this site existed.', 'bw-guides-server' ) . '</p></td></tr>';
		echo '</tbody></table>';
		submit_button( __( 'Add Site & Generate Key', 'bw-guides-server' ) );
		echo '</form>';
	}

	/**
	 * Shared datalist of known client slugs — referenced by the Add Site
	 * field and every row's inline client-slug edit form.
	 */
	private function render_client_slug_datalist() {
		echo '<datalist id="bw-gs-client-slugs-list">';
		foreach ( BW_Guides_Server_Sites::client_slugs() as $slug ) {
			echo '<option value="' . esc_attr( $slug ) . '">';
		}
		echo '</datalist>';
	}

	/**
	 * Small inline form: edit a single site's client-group slug in place.
	 * "Simplest robust UI wins" — a text field + Save button, no JS.
	 */
	private function render_client_cell( $site ) {
		$html  = '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="bw-gs-client-form">';
		$html .= '<input type="hidden" name="action" value="bw_guides_server_set_client">';
		$html .= '<input type="hidden" name="site_id" value="' . (int) $site->id . '">';
		$html .= wp_nonce_field( 'bw_guides_server_set_client_' . $site->id, 'bw_guides_set_client_nonce', true, false );
		$html .= '<input type="text" name="client_slug" list="bw-gs-client-slugs-list" value="' . esc_attr( $site->client_slug ) . '" class="small-text" placeholder="—">';
		$html .= ' <button type="submit" class="button button-small">' . esc_html__( 'Save', 'bw-guides-server' ) . '</button>';
		$html .= '</form>';
		return $html;
	}

	public function handle_add_site() {
		if ( ! current_user_can( self::CAP ) ) {
			wp_die( 'Insufficient permissions' );
		}
		check_admin_referer( 'bw_guides_server_add_site' );

		$name        = isset( $_POST['site_name'] ) ? sanitize_text_field( wp_unslash( $_POST['site_name'] ) ) : '';
		$url         = isset( $_POST['site_url'] ) ? esc_url_raw( wp_unslash( $_POST['site_url'] ) ) : '';
		$client_slug = isset( $_POST['client_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['client_slug'] ) ) : '';

		if ( '' === $name ) {
			$this->redirect_back( array( 'bw_notice' => 'error' ) );
		}

		$result = BW_Guides_Server_Sites::create( $name, $url, $client_slug );
		if ( is_wp_error( $result ) ) {
			$this->redirect_back( array( 'bw_notice' => 'error' ) );
		}

		// The raw key travels via a short-lived transient (never in a URL).
		set_transient(
			'bw_guides_server_new_key_' . get_current_user_id(),
			array(
				'name' => $name,
				'key'  => $result['key'],
			),
			120
		);
		$this->redirect_back();
	}

	public function handle_site_action() {
		if ( ! current_user_can( self::CAP ) ) {
			wp_die( 'Insufficient permissions' );
		}
		$site_id = isset( $_GET['site_id'] ) ? absint( $_GET['site_id'] ) : 0;
		check_admin_referer( 'bw_guides_server_site_action_' . $site_id );

		$do   = isset( $_GET['do'] ) ? sanitize_key( wp_unslash( $_GET['do'] ) ) : '';
		$site = BW_Guides_Server_Sites::get( $site_id );
		if ( ! $site ) {
			$this->redirect_back( array( 'bw_notice' => 'error' ) );
		}

		switch ( $do ) {
			case 'regenerate':
				$key = BW_Guides_Server_Sites::regenerate_key( $site_id );
				if ( is_wp_error( $key ) ) {
					$this->redirect_back( array( 'bw_notice' => 'error' ) );
				}
				set_transient(
					'bw_guides_server_new_key_' . get_current_user_id(),
					array(
						'name' => $site->site_name,
						'key'  => $key,
					),
					120
				);
				$this->redirect_back();
				break;
			case 'revoke':
				BW_Guides_Server_Sites::set_status( $site_id, 'revoked' );
				$this->redirect_back( array( 'bw_notice' => 'revoked' ) );
				break;
			case 'reactivate':
				BW_Guides_Server_Sites::set_status( $site_id, 'active' );
				$this->redirect_back( array( 'bw_notice' => 'reactivated' ) );
				break;
			case 'delete':
				BW_Guides_Server_Sites::delete( $site_id );
				$this->redirect_back( array( 'bw_notice' => 'deleted' ) );
				break;
			default:
				$this->redirect_back( array( 'bw_notice' => 'error' ) );
		}
	}

	public function handle_set_client() {
		if ( ! current_user_can( self::CAP ) ) {
			wp_die( 'Insufficient permissions' );
		}
		$site_id = isset( $_POST['site_id'] ) ? absint( $_POST['site_id'] ) : 0;
		check_admin_referer( 'bw_guides_server_set_client_' . $site_id, 'bw_guides_set_client_nonce' );

		$site = BW_Guides_Server_Sites::get( $site_id );
		if ( ! $site ) {
			$this->redirect_back( array( 'bw_notice' => 'error' ) );
		}

		$client_slug = isset( $_POST['client_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['client_slug'] ) ) : '';
		BW_Guides_Server_Sites::set_client_slug( $site_id, $client_slug );
		$this->redirect_back( array( 'bw_notice' => 'client_updated' ) );
	}

	private function redirect_back( $args = array() ) {
		$url = add_query_arg(
			$args,
			admin_url( 'edit.php?post_type=' . BW_Guides_Server_CPT::POST_TYPE . '&page=' . self::SITES_PAGE )
		);
		wp_safe_redirect( $url );
		exit;
	}

	/* ---------------------------------------------------------------------
	 * Targeting metabox
	 * ------------------------------------------------------------------- */

	public function add_targeting_metabox() {
		add_meta_box(
			'bw_guides_targeting',
			__( 'BW Guides — Targeting', 'bw-guides-server' ),
			array( $this, 'render_targeting_metabox' ),
			BW_Guides_Server_CPT::POST_TYPE,
			'side',
			'high'
		);
	}

	public function render_targeting_metabox( $post ) {
		wp_nonce_field( 'bw_guides_targeting_' . $post->ID, 'bw_guides_targeting_nonce' );

		$audience      = get_post_meta( $post->ID, '_bw_guides_audience', true );
		$audience      = in_array( $audience, array( 'selected', 'client' ), true ) ? $audience : 'all';
		$targets       = array_map( 'intval', (array) get_post_meta( $post->ID, '_bw_guides_target_sites', true ) );
		$target_client = (string) get_post_meta( $post->ID, '_bw_guides_target_client', true );
		$sites         = BW_Guides_Server_Sites::all();
		$client_slugs  = BW_Guides_Server_Sites::client_slugs();

		echo '<p class="description">' . esc_html__( 'Author with core blocks only — client sites do not have Kadence Blocks.', 'bw-guides-server' ) . '</p>';

		echo '<p><label><input type="radio" name="bw_guides_audience" value="all" ' . checked( $audience, 'all', false ) . '> '
			. esc_html__( 'All sites', 'bw-guides-server' ) . '</label></p>';
		echo '<p><label><input type="radio" name="bw_guides_audience" value="selected" ' . checked( $audience, 'selected', false ) . '> '
			. esc_html__( 'Selected sites only', 'bw-guides-server' ) . '</label></p>';
		echo '<p><label><input type="radio" name="bw_guides_audience" value="client" ' . checked( $audience, 'client', false ) . '> '
			. esc_html__( 'Specific client', 'bw-guides-server' ) . '</label></p>';

		echo '<p style="margin:4px 0 12px 24px;">';
		echo '<input type="text" name="bw_guides_target_client" list="bw-gs-client-slugs-list-metabox" value="' . esc_attr( $target_client ) . '" class="widefat" placeholder="' . esc_attr__( 'client slug, e.g. brentwood', 'bw-guides-server' ) . '">';
		echo '<datalist id="bw-gs-client-slugs-list-metabox">';
		foreach ( $client_slugs as $slug ) {
			echo '<option value="' . esc_attr( $slug ) . '">';
		}
		echo '</datalist>';
		echo '<span class="description">' . esc_html__( 'Used only when "Specific client" is selected above. Every active site whose Client (set on Guides → Sites) matches this slug will receive the guide — including sites added after this guide is published.', 'bw-guides-server' ) . '</span>';
		echo '</p>';

		if ( empty( $sites ) ) {
			echo '<p class="description">' . esc_html__( 'No sites registered yet — add them under Guides → Sites.', 'bw-guides-server' ) . '</p>';
			return;
		}

		echo '<div class="bw-gs-target-list">';
		foreach ( $sites as $site ) {
			$label = $site->site_name . ( 'revoked' === $site->status ? ' (' . __( 'revoked', 'bw-guides-server' ) . ')' : '' );
			echo '<label style="display:block;margin:2px 0;"><input type="checkbox" name="bw_guides_target_sites[]" value="' . (int) $site->id . '" '
				. checked( in_array( (int) $site->id, $targets, true ), true, false ) . '> ' . esc_html( $label ) . '</label>';
		}
		echo '</div>';
	}

	public function save_targeting( $post_id, $post ) {
		// The nonce is absent on autosaves/REST saves that don't submit metaboxes —
		// bail without touching existing meta.
		if ( ! isset( $_POST['bw_guides_targeting_nonce'] ) ) {
			return;
		}
		if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['bw_guides_targeting_nonce'] ) ), 'bw_guides_targeting_' . $post_id ) ) {
			return;
		}
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}
		if ( wp_is_post_revision( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		$audience = isset( $_POST['bw_guides_audience'] ) ? sanitize_key( wp_unslash( $_POST['bw_guides_audience'] ) ) : 'all';
		$audience = in_array( $audience, array( 'selected', 'client' ), true ) ? $audience : 'all';
		update_post_meta( $post_id, '_bw_guides_audience', $audience );

		$valid_ids = array_map( 'intval', wp_list_pluck( BW_Guides_Server_Sites::all(), 'id' ) );
		$targets   = isset( $_POST['bw_guides_target_sites'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['bw_guides_target_sites'] ) ) : array();
		$targets   = array_values( array_intersect( $targets, $valid_ids ) );
		update_post_meta( $post_id, '_bw_guides_target_sites', $targets );

		$target_client = isset( $_POST['bw_guides_target_client'] ) ? sanitize_text_field( wp_unslash( $_POST['bw_guides_target_client'] ) ) : '';
		update_post_meta( $post_id, '_bw_guides_target_client', BW_Guides_Server_Sites::normalize_client_slug( $target_client ) );
	}

	/* ---------------------------------------------------------------------
	 * Guide list column
	 * ------------------------------------------------------------------- */

	public function guide_columns( $columns ) {
		$new = array();
		foreach ( $columns as $key => $label ) {
			$new[ $key ] = $label;
			if ( 'title' === $key ) {
				$new['bw_guides_audience'] = __( 'Audience', 'bw-guides-server' );
			}
		}
		return $new;
	}

	public function render_guide_column( $column, $post_id ) {
		if ( 'bw_guides_audience' !== $column ) {
			return;
		}
		$audience = get_post_meta( $post_id, '_bw_guides_audience', true );
		if ( 'client' === $audience ) {
			$target_client = (string) get_post_meta( $post_id, '_bw_guides_target_client', true );
			if ( '' === $target_client ) {
				echo '<span style="color:#b32d2e;">' . esc_html__( 'Client — none set!', 'bw-guides-server' ) . '</span>';
				return;
			}
			echo esc_html( sprintf( __( 'Client: %s', 'bw-guides-server' ), $target_client ) );
			return;
		}
		if ( 'selected' !== $audience ) {
			echo esc_html__( 'All sites', 'bw-guides-server' );
			return;
		}
		$targets = array_map( 'intval', (array) get_post_meta( $post_id, '_bw_guides_target_sites', true ) );
		$names   = array();
		foreach ( $targets as $site_id ) {
			$site = BW_Guides_Server_Sites::get( $site_id );
			if ( $site ) {
				$names[] = $site->site_name;
			}
		}
		if ( empty( $names ) ) {
			echo '<span style="color:#b32d2e;">' . esc_html__( 'Selected — no sites!', 'bw-guides-server' ) . '</span>';
			return;
		}
		echo esc_html( implode( ', ', $names ) );
	}

	/* ---------------------------------------------------------------------
	 * Search keywords & category metabox
	 * ------------------------------------------------------------------- */

	public function add_keywords_metabox() {
		add_meta_box(
			'bw_guides_keywords',
			__( 'BW Guides — Search keywords & category', 'bw-guides-server' ),
			array( $this, 'render_keywords_metabox' ),
			BW_Guides_Server_CPT::POST_TYPE,
			'normal',
			'default'
		);
	}

	public function render_keywords_metabox( $post ) {
		wp_nonce_field( 'bw_guides_keywords_' . $post->ID, 'bw_guides_keywords_nonce' );

		$category = (string) get_post_meta( $post->ID, '_bw_guides_category', true );
		$rows     = (array) get_post_meta( $post->ID, '_bw_guides_section_keywords', true );
		$lines    = array();
		foreach ( $rows as $row ) {
			if ( is_array( $row ) && isset( $row['heading'], $row['keywords'] ) ) {
				$lines[] = $row['heading'] . ' :: ' . $row['keywords'];
			}
		}

		echo '<p><label for="bw-gs-category"><strong>' . esc_html__( 'Category', 'bw-guides-server' ) . '</strong></label><br>';
		echo '<input type="text" id="bw-gs-category" name="bw_guides_category" list="bw-gs-category-list" value="' . esc_attr( $category ) . '" class="widefat">';
		echo '<datalist id="bw-gs-category-list">';
		foreach ( $this->existing_categories() as $cat ) {
			echo '<option value="' . esc_attr( $cat ) . '">';
		}
		echo '</datalist></p>';

		echo '<p><label for="bw-gs-section-keywords"><strong>' . esc_html__( 'Section keywords', 'bw-guides-server' ) . '</strong></label><br>';
		echo '<span class="description">' . esc_html__( 'One rule per line: Heading text :: keyword, keyword, keyword. Use * as the heading for guide-level keywords (not tied to a section).', 'bw-guides-server' ) . '</span><br>';
		echo '<textarea id="bw-gs-section-keywords" name="bw_guides_section_keywords" rows="6" class="widefat code" placeholder="'
			. esc_attr__( "Changing a page's web address :: url, permalink, slug, link\n* :: general, misc", 'bw-guides-server' )
			. '">' . esc_textarea( implode( "\n", $lines ) ) . '</textarea></p>';
		echo '<p class="description">' . esc_html__( 'Heading text must match a current heading exactly (case-insensitive) or you\'ll see a warning after saving — it still saves either way.', 'bw-guides-server' ) . '</p>';
	}

	/**
	 * Distinct non-empty categories already in use, for the datalist.
	 *
	 * @return string[]
	 */
	private function existing_categories() {
		global $wpdb;
		$categories = $wpdb->get_col(
			"SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_bw_guides_category' AND meta_value != '' ORDER BY meta_value ASC"
		);
		return array_values( array_filter( (array) $categories ) );
	}

	public function save_keywords( $post_id, $post ) {
		// Same guard shape as save_targeting(): absent on autosaves/REST-only
		// saves that don't submit metaboxes — bail without touching existing meta.
		if ( ! isset( $_POST['bw_guides_keywords_nonce'] ) ) {
			return;
		}
		if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['bw_guides_keywords_nonce'] ) ), 'bw_guides_keywords_' . $post_id ) ) {
			return;
		}
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}
		if ( wp_is_post_revision( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		$category = isset( $_POST['bw_guides_category'] ) ? sanitize_text_field( wp_unslash( $_POST['bw_guides_category'] ) ) : '';
		update_post_meta( $post_id, '_bw_guides_category', $category );

		// NOTE: editing _bw_guides_category / _bw_guides_section_keywords via
		// `wp eval`/wp-cli directly (rather than through this metabox) does
		// NOT bump post_modified_gmt, so clients relying on modified_gmt to
		// decide whether to re-pull a guide won't see the change. A metabox
		// save always goes through wp_update_post() (via the block-editor
		// metabox-compat submission) and bumps it correctly. If content is
		// ever edited via wp-cli alone, follow with
		// `wp post update <ID> --post_title="$(wp post get <ID> --field=post_title)"`
		// (or any no-op wp_update_post( array( 'ID' => <ID> ) ) call) to touch it.
		$raw_lines = isset( $_POST['bw_guides_section_keywords'] ) ? (string) wp_unslash( $_POST['bw_guides_section_keywords'] ) : '';
		$rows      = array();
		foreach ( preg_split( '/\r\n|\r|\n/', $raw_lines ) as $line ) {
			$line = trim( $line );
			if ( '' === $line ) {
				continue;
			}
			$parts = explode( '::', $line, 2 );
			if ( count( $parts ) < 2 ) {
				continue; // No "::" separator — nothing to reliably parse, skip the line.
			}
			$heading  = trim( $parts[0] );
			$keywords = trim( $parts[1] );
			if ( '' === $heading ) {
				continue;
			}
			$rows[] = array(
				'heading'  => $heading,
				'keywords' => $keywords,
			);
		}
		update_post_meta( $post_id, '_bw_guides_section_keywords', $rows );

		$this->check_keyword_heading_mismatches( $post_id, $post, $rows );
	}

	/**
	 * Flag (but never block on) keyword lines whose heading no longer
	 * exists in the guide's content — usually because a heading was
	 * reworded after the keyword line was written.
	 */
	private function check_keyword_heading_mismatches( $post_id, $post, $rows ) {
		$current_headings = array();
		if ( preg_match_all( '#<(h[23])\b[^>]*>(.*?)</\1>#is', (string) $post->post_content, $m ) ) {
			foreach ( $m[2] as $h ) {
				$current_headings[] = trim( wp_strip_all_tags( $h ) );
			}
		}
		$current_lc = array_map( 'strtolower', $current_headings );

		$mismatches = array();
		foreach ( $rows as $row ) {
			if ( '*' === $row['heading'] ) {
				continue; // Guide-level keywords are not tied to a heading.
			}
			if ( ! in_array( strtolower( $row['heading'] ), $current_lc, true ) ) {
				$mismatches[] = $row['heading'];
			}
		}

		if ( ! empty( $mismatches ) ) {
			set_transient( 'bw_guides_server_kw_mismatch_' . $post_id, $mismatches, 60 );
		} else {
			delete_transient( 'bw_guides_server_kw_mismatch_' . $post_id );
		}
	}

	public function render_keyword_mismatch_notice() {
		global $pagenow;
		if ( 'post.php' !== $pagenow || empty( $_GET['post'] ) ) {
			return;
		}
		$post_id = absint( $_GET['post'] );
		if ( BW_Guides_Server_CPT::POST_TYPE !== get_post_type( $post_id ) ) {
			return;
		}
		$mismatches = get_transient( 'bw_guides_server_kw_mismatch_' . $post_id );
		if ( empty( $mismatches ) || ! is_array( $mismatches ) ) {
			return;
		}
		delete_transient( 'bw_guides_server_kw_mismatch_' . $post_id );

		echo '<div class="notice notice-warning"><p><strong>'
			. esc_html__( 'BW Guides: some keyword lines reference headings that don\'t currently exist in this guide:', 'bw-guides-server' )
			. '</strong></p><ul style="list-style:disc;margin-left:20px;">';
		foreach ( $mismatches as $h ) {
			echo '<li>' . esc_html( $h ) . '</li>';
		}
		echo '</ul><p>' . esc_html__( 'The guide still saved — check for a renamed or removed heading.', 'bw-guides-server' ) . '</p></div>';
	}
}
