<?php
/**
 * PS Local SEO — Auto-generates indexable pickup point pages in the store.
 *
 * Creates /pickup-points/{city}/ and /punkty-odbioru/{city}/ pages that
 * list all pickup points in a given city with Schema.org LocalBusiness
 * JSON-LD markup. Fully server-rendered HTML — zero JS required for content.
 *
 * Data source: PARCSELECT_API_BASE/api/points/{country_code} (defaults parcelselector.com)
 * Cached via WordPress transients (12 hours).
 *
 * @package ParcelSelector
 * @since   3.4.0
 */

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

/**
 * Class PS_Local_SEO
 *
 * Registers rewrite rules, handles template rendering, and provides
 * an admin toggle in WooCommerce > Parcel Selector > Settings.
 */
class Parcselect_Local_SEO {

	/**
	 * API base URL for fetching pickup points.
	 *
	 * @var string
	 */
	// v4.2.3 — API_BASE deprecated, use api_base() method instead (deferred to PARCSELECT_API_BASE)
	private static function api_base() {
		return ( defined( 'PARCSELECT_API_BASE' ) ? PARCSELECT_API_BASE : 'https://parcelselector.com' ) . '/api/points/';
	}

	/**
	 * Transient cache TTL in seconds (12 hours).
	 *
	 * @var int
	 */
	const CACHE_TTL = 43200;

	/**
	 * Option key for enabling/disabling Local SEO pages.
	 *
	 * @var string
	 */
	const OPTION_ENABLED = 'parcselect_local_seo_enabled';

	/**
	 * Option key to track rewrite rules flush state.
	 *
	 * @var string
	 */
	const OPTION_FLUSH = 'parcselect_local_seo_flush_v2';

	/**
	 * Brand color (orange).
	 *
	 * @var string
	 */
	const BRAND_COLOR = '#f97316';

	/**
	 * Initialize hooks.
	 *
	 * @return void
	 */
	public static function init() {
		add_action( 'init', array( __CLASS__, 'register_rewrite_rules' ) );
		add_action( 'init', array( __CLASS__, 'maybe_flush_rules' ), 20 );
		add_filter( 'query_vars', array( __CLASS__, 'register_query_vars' ) );
		add_action( 'template_redirect', array( __CLASS__, 'handle_request' ) );

		// Admin: register setting and show notice.
		add_action( 'admin_init', array( __CLASS__, 'register_setting' ) );
		add_action( 'admin_notices', array( __CLASS__, 'admin_notice' ) );
	}

	// =========================================================================
	// Rewrite Rules
	// =========================================================================

	/**
	 * Register rewrite rules for English and Polish URL slugs.
	 *
	 * @return void
	 */
	public static function register_rewrite_rules() {
		if ( 'yes' !== get_option( self::OPTION_ENABLED, 'no' ) ) {
			return;
		}

		// /pickup-points/ → index page.
		add_rewrite_rule(
			'^pickup-points/?$',
			'index.php?parcselect_local_seo=index',
			'top'
		);
		// /pickup-points/{city}/{point-slug}/ → single point page.
		add_rewrite_rule(
			'^pickup-points/([^/]+)/([^/]+)/?$',
			'index.php?parcselect_local_seo=point&parcselect_seo_city=$matches[1]&parcselect_seo_point=$matches[2]',
			'top'
		);
		// /pickup-points/{city}/ → city page.
		add_rewrite_rule(
			'^pickup-points/([^/]+)/?$',
			'index.php?parcselect_local_seo=city&parcselect_seo_city=$matches[1]',
			'top'
		);
		// /pickup-points-sitemap.xml → XML sitemap.
		add_rewrite_rule(
			'^pickup-points-sitemap\.xml$',
			'index.php?parcselect_local_seo=sitemap',
			'top'
		);

		// Polish slugs.
		add_rewrite_rule(
			'^punkty-odbioru/?$',
			'index.php?parcselect_local_seo=index',
			'top'
		);
		add_rewrite_rule(
			'^punkty-odbioru/([^/]+)/([^/]+)/?$',
			'index.php?parcselect_local_seo=point&parcselect_seo_city=$matches[1]&parcselect_seo_point=$matches[2]',
			'top'
		);
		add_rewrite_rule(
			'^punkty-odbioru/([^/]+)/?$',
			'index.php?parcselect_local_seo=city&parcselect_seo_city=$matches[1]',
			'top'
		);
		add_rewrite_rule(
			'^punkty-odbioru-sitemap\.xml$',
			'index.php?parcselect_local_seo=sitemap',
			'top'
		);
	}

	/**
	 * One-time flush after rules are registered or setting is toggled.
	 *
	 * @return void
	 */
	public static function maybe_flush_rules() {
		if ( get_option( self::OPTION_FLUSH ) ) {
			return;
		}
		if ( 'yes' !== get_option( self::OPTION_ENABLED, 'no' ) ) {
			return;
		}
		flush_rewrite_rules( false );
		update_option( self::OPTION_FLUSH, '1' );
	}

	/**
	 * Register custom query variables.
	 *
	 * @param  string[] $vars Existing query vars.
	 * @return string[]
	 */
	public static function register_query_vars( $vars ) {
		$vars[] = 'parcselect_local_seo';
		$vars[] = 'parcselect_seo_city';
		$vars[] = 'parcselect_seo_point';
		return $vars;
	}

	// =========================================================================
	// Request Handler
	// =========================================================================

	/**
	 * Intercept matching requests and render the appropriate page.
	 *
	 * @return void
	 */
	public static function handle_request() {
		$page = get_query_var( 'parcselect_local_seo', '' );
		if ( ! $page ) {
			return;
		}

		if ( 'yes' !== get_option( self::OPTION_ENABLED, 'no' ) ) {
			return;
		}

		// Enqueue Local SEO styles via wp_head() inside get_header().
		// This hook fires during wp_head(), which is AFTER template_redirect but BEFORE get_header() output.
		if ( 'sitemap' !== $page ) {
			add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_seo_styles' ) );
		}

		$country_code = self::get_store_country();

		if ( 'sitemap' === $page ) {
			self::render_sitemap( $country_code );
			return;
		}

		$points = self::get_points( $country_code );

		if ( 'point' === $page ) {
			$city_slug  = sanitize_title( get_query_var( 'parcselect_seo_city', '' ) );
			$point_slug = sanitize_title( get_query_var( 'parcselect_seo_point', '' ) );
			if ( $city_slug && $point_slug ) {
				self::render_point( $point_slug, $city_slug, $points, $country_code );
				return;
			}
		}

		if ( 'city' === $page ) {
			$city_slug = sanitize_title( get_query_var( 'parcselect_seo_city', '' ) );
			if ( ! $city_slug ) {
				self::render_index( $points, $country_code );
				return;
			}
			self::render_city( $city_slug, $points, $country_code );
			return;
		}

		self::render_index( $points, $country_code );
	}

	// =========================================================================
	// Data Layer
	// =========================================================================

	/**
	 * Get the two-letter country code from WooCommerce settings.
	 *
	 * @return string Lowercase two-letter country code.
	 */
	public static function get_store_country() {
		$wc_country = get_option( 'woocommerce_default_country', 'PL' );
		// WooCommerce stores "PL" or "US:CA" — take the first part.
		$parts = explode( ':', $wc_country );
		return strtolower( $parts[0] );
	}

	/**
	 * Fetch pickup points from the API with transient caching.
	 *
	 * @param  string $country_code Two-letter country code.
	 * @return array  Array of point objects.
	 */
	public static function get_points( $country_code ) {
		$transient_key = 'parcselect_seo_points_' . sanitize_key( $country_code );
		$cached        = get_transient( $transient_key );

		if ( false !== $cached ) {
			return $cached;
		}

		$url      = self::api_base() . rawurlencode( $country_code );
		$response = wp_remote_get( $url, array( 'timeout' => 15 ) );

		if ( is_wp_error( $response ) ) {
			return array();
		}

		$code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $code ) {
			return array();
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

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

		set_transient( $transient_key, $data, self::CACHE_TTL );
		return $data;
	}

	/**
	 * Group pickup points by city.
	 *
	 * @param  array $points Array of point objects.
	 * @return array Associative array: city_slug => array( 'name' => city_name, 'points' => [...] ).
	 */
	public static function group_by_city( $points ) {
		$cities = array();

		foreach ( $points as $point ) {
			$city_name = isset( $point['city'] ) ? trim( $point['city'] ) : '';
			if ( '' === $city_name ) {
				continue;
			}
			$city_slug = sanitize_title( $city_name );
			if ( ! isset( $cities[ $city_slug ] ) ) {
				$cities[ $city_slug ] = array(
					'name'   => $city_name,
					'points' => array(),
				);
			}
			$cities[ $city_slug ]['points'][] = $point;
		}

		ksort( $cities );
		return $cities;
	}

	// =========================================================================
	// Rendering — Index Page
	// =========================================================================

	/**
	 * Render the pickup points index page (list of cities).
	 *
	 * @param  array  $points       All pickup points.
	 * @param  string $country_code Store country code.
	 * @return void
	 */
	public static function render_index( $points, $country_code ) {
		$cities      = self::group_by_city( $points );
		$total       = count( $points );
		$base_slug   = self::get_base_slug();
		$search_term = isset( $_GET['parcselect_search'] ) ? sanitize_text_field( wp_unslash( $_GET['parcselect_search'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public search form, no side effects.

		// Filter cities by search term.
		if ( '' !== $search_term ) {
			$search_lower = mb_strtolower( $search_term );
			$cities       = array_filter(
				$cities,
				function ( $city_data ) use ( $search_lower ) {
					return false !== mb_strpos( mb_strtolower( $city_data['name'] ), $search_lower );
				}
			);
		}

		status_header( 200 );
		header( 'Content-Type: text/html; charset=utf-8' );

		/* translators: %d: total number of pickup points available */
		$page_title = sprintf( esc_html__( 'Pickup Points (%d)', 'parcel-selector' ), absint( $total ) );

		get_header();
		?>

		<?php self::render_styles(); ?>

		<div class="ps-seo-wrap">

			<?php self::render_breadcrumbs(); ?>

			<h1 class="ps-seo-title"><?php echo esc_html( $page_title ); ?></h1>

			<p class="ps-seo-subtitle">
				<?php
				/* translators: %s: country code in uppercase (e.g. "PL", "DE") */
				$_ps_subtitle = sprintf( esc_html__( 'Browse all pickup points and parcel lockers in %s. Choose a city to see available locations.', 'parcel-selector' ), esc_html( strtoupper( $country_code ) ) );
				echo esc_html( $_ps_subtitle );
				?>
			</p>

			<?php self::render_search_form( $search_term ); ?>

			<?php if ( empty( $cities ) ) : ?>
				<p class="ps-seo-empty">
					<?php esc_html_e( 'No pickup points found. Try a different search term.', 'parcel-selector' ); ?>
				</p>
			<?php else : ?>
				<div class="ps-seo-city-grid">
					<?php foreach ( $cities as $slug => $city_data ) : ?>
						<a href="<?php echo esc_url( home_url( $base_slug . '/' . $slug . '/' ) ); ?>" class="ps-seo-city-card">
							<span class="ps-seo-city-name"><?php echo esc_html( $city_data['name'] ); ?></span>
							<span class="ps-seo-city-count">
								<?php
								/* translators: %d: number of pickup points in this city */
								$_ps_count = sprintf( esc_html__( '%d points', 'parcel-selector' ), count( $city_data['points'] ) );
								echo esc_html( $_ps_count );
								?>
							</span>
						</a>
					<?php endforeach; ?>
				</div>
			<?php endif; ?>

			<?php self::render_footer_credit(); ?>

		</div>

		<?php
		get_footer();
		exit;
	}

	// =========================================================================
	// Rendering — City Page
	// =========================================================================

	/**
	 * Render a single city page with all pickup points.
	 *
	 * @param  string $city_slug    Sanitized city slug from URL.
	 * @param  array  $points       All pickup points.
	 * @param  string $country_code Store country code.
	 * @return void
	 */
	public static function render_city( $city_slug, $points, $country_code ) {
		$cities = self::group_by_city( $points );

		if ( ! isset( $cities[ $city_slug ] ) ) {
			// Try to find a partial match (handles encoding differences).
			$found = false;
			foreach ( $cities as $slug => $data ) {
				if ( sanitize_title( $slug ) === $city_slug ) {
					$city_slug = $slug;
					$found     = true;
					break;
				}
			}
			if ( ! $found ) {
				status_header( 404 );
				get_header();
				self::render_styles();
				echo '<div class="ps-seo-wrap">';
				self::render_breadcrumbs();
				echo '<h1 class="ps-seo-title">' . esc_html__( 'City Not Found', 'parcel-selector' ) . '</h1>';
				echo '<p>' . esc_html__( 'No pickup points found for this city.', 'parcel-selector' ) . '</p>';
				echo '<p><a href="' . esc_url( home_url( self::get_base_slug() . '/' ) ) . '">';
				echo esc_html__( 'Back to all cities', 'parcel-selector' );
				echo '</a></p>';
				self::render_footer_credit();
				echo '</div>';
				get_footer();
				exit;
			}
		}

		$city_data   = $cities[ $city_slug ];
		$city_name   = $city_data['name'];
		$city_points = $city_data['points'];
		$base_slug   = self::get_base_slug();

		status_header( 200 );
		header( 'Content-Type: text/html; charset=utf-8' );

		/* translators: 1: city name, 2: number of pickup points */
		$page_title = sprintf( esc_html__( 'Pickup Points in %1$s (%2$d)', 'parcel-selector' ), esc_html( $city_name ), count( $city_points ) );

		get_header();
		?>

		<?php self::render_styles(); ?>

		<?php self::render_city_jsonld( $city_points, $city_name, $country_code ); ?>

		<div class="ps-seo-wrap">

			<?php self::render_breadcrumbs( $city_name ); ?>

			<h1 class="ps-seo-title"><?php echo esc_html( $page_title ); ?></h1>

			<p class="ps-seo-subtitle">
				<?php
				/* translators: 1: city name, 2: country code in uppercase */
				$_ps_city_sub = sprintf( esc_html__( 'All available pickup points and parcel lockers in %1$s, %2$s. Click on a point for details and directions.', 'parcel-selector' ), esc_html( $city_name ), esc_html( strtoupper( $country_code ) ) );
				echo esc_html( $_ps_city_sub );
				?>
			</p>

			<?php self::render_search_form(); ?>

			<div class="ps-seo-points-list">
				<?php foreach ( $city_points as $point ) : ?>
					<?php self::render_point_card( $point, $country_code, $city_slug ); ?>
				<?php endforeach; ?>
			</div>

			<p class="ps-seo-back">
				<a href="<?php echo esc_url( home_url( $base_slug . '/' ) ); ?>">
					<?php esc_html_e( 'Back to all cities', 'parcel-selector' ); ?>
				</a>
			</p>

			<?php self::render_footer_credit(); ?>

		</div>

		<?php
		get_footer();
		exit;
	}

	// =========================================================================
	// Rendering — Point Card
	// =========================================================================

	/**
	 * Render a single pickup point card.
	 *
	 * @param  array  $point        Single point data from API.
	 * @param  string $country_code Store country code.
	 * @return void
	 */
	public static function render_point_card( $point, $country_code, $city_slug = '' ) {
		$name    = isset( $point['n'] ) ? $point['n'] : ( isset( $point['name'] ) ? $point['name'] : '' );
		$address = isset( $point['a'] ) ? $point['a'] : ( isset( $point['address'] ) ? $point['address'] : '' );
		$carrier = isset( $point['c'] ) ? $point['c'] : ( isset( $point['carrier'] ) ? $point['carrier'] : '' );
		$hours   = isset( $point['hours'] ) ? $point['hours'] : '';
		$lat     = isset( $point['lat'] ) ? $point['lat'] : ( isset( $point['y'] ) ? $point['y'] : '' );
		$lng     = isset( $point['lng'] ) ? $point['lng'] : ( isset( $point['x'] ) ? $point['x'] : '' );

		$point_slug = self::make_point_slug( $point );
		$base_slug  = self::get_base_slug();

		// Local point page (on this store).
		$detail_url = '';
		if ( $city_slug && $point_slug ) {
			$detail_url = home_url( $base_slug . '/' . $city_slug . '/' . $point_slug . '/' );
		}
		// Fallback: link to parcelselector.com.
		$ps_url = 'https://parcelselector.com/point/' . rawurlencode( $country_code ) . '/' . rawurlencode( $point_slug ) . '/';
		?>
		<div class="ps-seo-point-card">
			<div class="ps-seo-point-header">
				<?php if ( '' !== $detail_url ) : ?>
					<a href="<?php echo esc_url( $detail_url ); ?>" class="ps-seo-point-name">
						<?php echo esc_html( $name ); ?>
					</a>
				<?php else : ?>
					<span class="ps-seo-point-name"><?php echo esc_html( $name ); ?></span>
				<?php endif; ?>

				<?php if ( '' !== $carrier ) : ?>
					<span class="ps-seo-point-carrier"><?php echo esc_html( $carrier ); ?></span>
				<?php endif; ?>
			</div>

			<?php if ( '' !== $address ) : ?>
				<p class="ps-seo-point-address"><?php echo esc_html( $address ); ?></p>
			<?php endif; ?>

			<?php if ( '' !== $hours ) : ?>
				<p class="ps-seo-point-hours">
					<?php
					/* translators: %s: opening hours of the pickup point */
					$_ps_hours = sprintf( esc_html__( 'Hours: %s', 'parcel-selector' ), esc_html( $hours ) );
					echo esc_html( $_ps_hours );
					?>
				</p>
			<?php endif; ?>

			<div class="ps-seo-point-meta">
				<?php if ( '' !== $lat && '' !== $lng ) : ?>
					<span class="ps-seo-point-coords">
						<?php
						/* translators: 1: latitude, 2: longitude */
						$_ps_coords = sprintf( esc_html__( 'GPS: %1$s, %2$s', 'parcel-selector' ), esc_attr( $lat ), esc_attr( $lng ) );
						echo esc_html( $_ps_coords );
						?>
					</span>
				<?php endif; ?>

				<?php if ( '' !== $detail_url ) : ?>
					<a href="<?php echo esc_url( $detail_url ); ?>" class="ps-seo-point-link">
						<?php esc_html_e( 'View details', 'parcel-selector' ); ?>
					</a>
				<?php endif; ?>
				<a href="<?php echo esc_url( $ps_url ); ?>" class="ps-seo-point-link" style="margin-left:8px;opacity:.7">
					<?php esc_html_e( 'Map', 'parcel-selector' ); ?> →
				</a>
			</div>
		</div>
		<?php
	}

	// =========================================================================
	// Rendering — Components
	// =========================================================================

	/**
	 * Get the URL base slug (English or Polish based on locale).
	 *
	 * @return string Base slug without leading/trailing slashes.
	 */
	public static function get_base_slug() {
		$locale = determine_locale();
		if ( 0 === strpos( $locale, 'pl' ) ) {
			return 'punkty-odbioru';
		}
		return 'pickup-points';
	}

	/**
	 * Render breadcrumb navigation.
	 *
	 * @param  string $city_name Optional city name for city-level breadcrumb.
	 * @return void
	 */
	public static function render_breadcrumbs( $city_name = '', $point_name = '' ) {
		$base_slug  = self::get_base_slug();
		$store_name = get_bloginfo( 'name' );

		// Schema.org BreadcrumbList — Google shows in search results.
		$crumbs = array(
			array( 'name' => $store_name, 'url' => home_url( '/' ) ),
			array( 'name' => __( 'Pickup Points', 'parcel-selector' ), 'url' => home_url( $base_slug . '/' ) ),
		);
		if ( $city_name ) {
			$crumbs[] = array( 'name' => $city_name, 'url' => home_url( $base_slug . '/' . sanitize_title( $city_name ) . '/' ) );
		}
		if ( $point_name ) {
			$crumbs[] = array( 'name' => $point_name, 'url' => '' );
		}

		$bc_schema = array(
			'@context'        => 'https://schema.org',
			'@type'           => 'BreadcrumbList',
			'itemListElement' => array(),
		);
		foreach ( $crumbs as $i => $c ) {
			$item = array(
				'@type'    => 'ListItem',
				'position' => $i + 1,
				'name'     => $c['name'],
			);
			if ( $c['url'] ) {
				$item['item'] = $c['url'];
			}
			$bc_schema['itemListElement'][] = $item;
		}
		echo '<script type="application/ld+json">' . wp_json_encode( $bc_schema, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP ) . '</script>' . "\n"; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript -- JSON-LD structured data, not executable JS.
		?>
		<nav class="ps-seo-breadcrumbs" aria-label="<?php esc_attr_e( 'Breadcrumb', 'parcel-selector' ); ?>">
			<a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php echo esc_html( $store_name ); ?></a>
			<span class="ps-seo-sep"> / </span>

			<?php if ( '' !== $city_name ) : ?>
				<a href="<?php echo esc_url( home_url( $base_slug . '/' ) ); ?>">
					<?php esc_html_e( 'Pickup Points', 'parcel-selector' ); ?>
				</a>
				<span class="ps-seo-sep"> / </span>
				<?php if ( '' !== $point_name ) : ?>
					<a href="<?php echo esc_url( home_url( $base_slug . '/' . sanitize_title( $city_name ) . '/' ) ); ?>">
						<?php echo esc_html( $city_name ); ?>
					</a>
					<span class="ps-seo-sep"> / </span>
					<span class="ps-seo-current"><?php echo esc_html( $point_name ); ?></span>
				<?php else : ?>
					<span class="ps-seo-current"><?php echo esc_html( $city_name ); ?></span>
				<?php endif; ?>
			<?php else : ?>
				<span class="ps-seo-current"><?php esc_html_e( 'Pickup Points', 'parcel-selector' ); ?></span>
			<?php endif; ?>
		</nav>
		<?php
	}

	/**
	 * Render the search form (GET method — indexable).
	 *
	 * @param  string $current_search Current search query.
	 * @return void
	 */
	public static function render_search_form( $current_search = '' ) {
		$base_slug = self::get_base_slug();
		?>
		<form class="ps-seo-search" method="get" action="<?php echo esc_url( home_url( $base_slug . '/' ) ); ?>">
			<label for="ps-search-input" class="screen-reader-text">
				<?php esc_html_e( 'Search pickup points', 'parcel-selector' ); ?>
			</label>
			<input
				type="search"
				id="ps-search-input"
				name="parcselect_search"
				value="<?php echo esc_attr( $current_search ); ?>"
				placeholder="<?php esc_attr_e( 'Search by city name...', 'parcel-selector' ); ?>"
				class="ps-seo-search-input"
			/>
			<button type="submit" class="ps-seo-search-btn">
				<?php esc_html_e( 'Search', 'parcel-selector' ); ?>
			</button>
		</form>
		<?php
	}

	/**
	 * Render footer credit with dofollow link.
	 *
	 * @return void
	 */
	public static function render_footer_credit() {
		// WP.org guideline: attribution only with admin opt-in.
		if ( get_option( 'parcselect_show_powered_by', 'no' ) !== 'yes' ) {
			return;
		}
		$country = self::get_store_country();
		?>
		<footer class="ps-seo-footer">
			<p>
				<?php
				/* translators: %s: link to ParcelSelector.com */
				$_ps_credit = sprintf( esc_html__( 'Pickup point map powered by %s', 'parcel-selector' ), '<a href="https://parcelselector.com/">Parcel Selector — parcel locker map for WooCommerce</a>' );
				echo wp_kses_post( $_ps_credit );
				?>
			</p>
			<p style="font-size:0.85em;margin-top:4px">
				<a href="<?php echo esc_url( 'https://parcelselector.com/points/' . $country . '/' ); ?>"><?php esc_html_e( 'Browse all pickup points', 'parcel-selector' ); ?></a>
				&middot;
				<a href="https://parcelselector.com/en/parcelselector-demo/"><?php esc_html_e( 'Interactive map demo', 'parcel-selector' ); ?></a>
				&middot;
				<a href="https://wordpress.org/plugins/parcel-selector/"><?php esc_html_e( 'Free WooCommerce plugin', 'parcel-selector' ); ?></a>
			</p>
		</footer>
		<?php
	}

	// =========================================================================
	// Schema.org JSON-LD
	// =========================================================================

	/**
	 * Render Schema.org LocalBusiness JSON-LD for all points in a city.
	 *
	 * @param  array  $points       Points in this city.
	 * @param  string $city_name    City display name.
	 * @param  string $country_code Store country code.
	 * @return void
	 */
	public static function render_city_jsonld( $points, $city_name, $country_code ) {
		$items = array();

		foreach ( $points as $point ) {
			$name    = isset( $point['name'] ) ? $point['name'] : '';
			$address = isset( $point['address'] ) ? $point['address'] : '';
			$lat     = isset( $point['lat'] ) ? $point['lat'] : '';
			$lng     = isset( $point['lng'] ) ? $point['lng'] : '';
			$slug    = isset( $point['slug'] ) ? $point['slug'] : '';
			$hours   = isset( $point['hours'] ) ? $point['hours'] : '';

			$item = array(
				'@type'   => 'LocalBusiness',
				'name'    => $name,
				'address' => array(
					'@type'           => 'PostalAddress',
					'streetAddress'   => $address,
					'addressLocality' => $city_name,
					'addressCountry'  => strtoupper( $country_code ),
				),
			);

			if ( '' !== $lat && '' !== $lng ) {
				$item['geo'] = array(
					'@type'     => 'GeoCoordinates',
					'latitude'  => (float) $lat,
					'longitude' => (float) $lng,
				);
			}

			if ( '' !== $slug ) {
				$item['url'] = 'https://parcelselector.com/point/' . rawurlencode( $country_code ) . '/' . rawurlencode( $slug ) . '/';
			}

			if ( '' !== $hours ) {
				$item['openingHours'] = $hours;
			}

			$items[] = $item;
		}

		$schema = array(
			'@context'        => 'https://schema.org',
			'@type'           => 'ItemList',
			'name'            => 'Pickup Points in ' . $city_name,
			'numberOfItems'   => count( $items ),
			'itemListElement' => array(),
		);

		$position = 1;
		foreach ( $items as $item ) {
			$schema['itemListElement'][] = array(
				'@type'    => 'ListItem',
				'position' => $position,
				'item'     => $item,
			);
			++$position;
		}

		echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP ) . '</script>' . "\n"; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript -- JSON-LD structured data, not executable JS.
	}

	// =========================================================================
	// Helper — Point Slug Generator
	// =========================================================================

	/**
	 * Generate a human-readable, SEO-friendly slug from point data.
	 *
	 * Format: {carrier}-{point_id}-{street-name}
	 * Example: inpost-POP-WAW698-pulawska-25
	 *
	 * @param  array $point Point data from API.
	 * @return string URL-safe slug.
	 */
	public static function make_point_slug( $point ) {
		$id      = isset( $point['n'] ) ? $point['n'] : ( isset( $point['name'] ) ? $point['name'] : 'unknown' );
		$carrier = isset( $point['c'] ) ? strtolower( $point['c'] ) : ( isset( $point['carrier'] ) ? strtolower( $point['carrier'] ) : 'inpost' );
		$address = isset( $point['a'] ) ? $point['a'] : ( isset( $point['address'] ) ? $point['address'] : '' );

		// Extract street (first part before comma).
		$street = '';
		if ( $address ) {
			$parts  = explode( ',', $address );
			$street = trim( $parts[0] );
			$street = preg_replace( '/^\d{2,5}[\s\-]?/', '', $street ); // strip postal.
		}

		$slug = $carrier . '-' . $id;
		if ( $street ) {
			$slug .= '-' . sanitize_title( $street );
		}
		return $slug;
	}

	/**
	 * Find a point by its slug within a city's points.
	 *
	 * @param  string $slug   Point slug to find.
	 * @param  array  $points All points.
	 * @param  string $city_slug City slug to filter.
	 * @return array|null Point data or null.
	 */
	public static function find_point_by_slug( $slug, $points, $city_slug ) {
		foreach ( $points as $p ) {
			$p_city = isset( $p['city'] ) ? sanitize_title( $p['city'] ) : '';
			if ( $p_city !== $city_slug ) {
				continue;
			}
			if ( self::make_point_slug( $p ) === $slug ) {
				return $p;
			}
		}
		// Fallback: try matching by point ID only.
		$slug_upper = strtoupper( $slug );
		foreach ( $points as $p ) {
			$pid = isset( $p['n'] ) ? strtoupper( $p['n'] ) : ( isset( $p['name'] ) ? strtoupper( $p['name'] ) : '' );
			if ( false !== strpos( $slug_upper, $pid ) && '' !== $pid ) {
				return $p;
			}
		}
		return null;
	}

	// =========================================================================
	// Rendering — Single Point Page
	// =========================================================================

	/**
	 * Render a single pickup point page with full address, map, Schema.org.
	 *
	 * URL: /pickup-points/{city}/{carrier-ID-street}/
	 * This creates a unique indexable page for every pickup point.
	 *
	 * @param  string $point_slug  Point slug from URL.
	 * @param  string $city_slug   City slug from URL.
	 * @param  array  $points      All points.
	 * @param  string $country_code Store country code.
	 * @return void
	 */
	public static function render_point( $point_slug, $city_slug, $points, $country_code ) {
		$point = self::find_point_by_slug( $point_slug, $points, $city_slug );

		if ( ! $point ) {
			status_header( 404 );
			get_header();
			self::render_styles();
			echo '<div class="ps-seo-wrap">';
			self::render_breadcrumbs();
			echo '<h1 class="ps-seo-title">' . esc_html__( 'Pickup Point Not Found', 'parcel-selector' ) . '</h1>';
			echo '<p><a href="' . esc_url( home_url( self::get_base_slug() . '/' ) ) . '">';
			echo esc_html__( 'Browse all pickup points', 'parcel-selector' );
			echo '</a></p>';
			self::render_footer_credit();
			echo '</div>';
			get_footer();
			exit;
		}

		$name    = isset( $point['n'] ) ? $point['n'] : ( isset( $point['name'] ) ? $point['name'] : '' );
		$address = isset( $point['a'] ) ? $point['a'] : ( isset( $point['address'] ) ? $point['address'] : '' );
		$carrier = isset( $point['c'] ) ? $point['c'] : ( isset( $point['carrier'] ) ? $point['carrier'] : 'inpost' );
		$lat     = isset( $point['lat'] ) ? floatval( $point['lat'] ) : ( isset( $point['y'] ) ? floatval( $point['y'] ) : 0 );
		$lon     = isset( $point['lng'] ) ? floatval( $point['lng'] ) : ( isset( $point['x'] ) ? floatval( $point['x'] ) : 0 );
		$city    = isset( $point['city'] ) ? $point['city'] : ucfirst( str_replace( '-', ' ', $city_slug ) );
		$hours   = isset( $point['hours'] ) ? $point['hours'] : '';

		$carrier_names = array(
			'inpost' => 'InPost',
		);
		$carrier_label = isset( $carrier_names[ strtolower( $carrier ) ] ) ? $carrier_names[ strtolower( $carrier ) ] : ucfirst( $carrier );
		$store_name    = get_bloginfo( 'name' );
		$base_slug     = self::get_base_slug();
		$ps_url        = 'https://parcelselector.com/point/' . rawurlencode( $country_code ) . '/' . rawurlencode( self::make_point_slug( $point ) ) . '/';

		/* translators: 1: carrier name, 2: point ID, 3: city name */
		$page_title = sprintf( esc_html__( '%1$s %2$s — Pickup Point in %3$s', 'parcel-selector' ), $carrier_label, $name, $city );

		status_header( 200 );
		header( 'Content-Type: text/html; charset=utf-8' );

		// Schema.org JSON-LD.
		$schema = array(
			'@context'    => 'https://schema.org',
			'@type'       => 'LocalBusiness',
			'name'        => $carrier_label . ' ' . $name,
			'description' => $carrier_label . ' pickup point at ' . $address . ', ' . $city . '. GPS: ' . round( $lat, 5 ) . ', ' . round( $lon, 5 ) . '. Find on Parcel Selector map.',
			'address'     => array(
				'@type'           => 'PostalAddress',
				'streetAddress'   => $address,
				'addressLocality' => $city,
				'addressCountry'  => strtoupper( $country_code ),
			),
			'url'   => $ps_url,
			'brand' => array( '@type' => 'Brand', 'name' => $carrier_label ),
			'isAccessibleForFree' => true,
		);
		if ( $lat && $lon ) {
			$schema['geo'] = array(
				'@type'     => 'GeoCoordinates',
				'latitude'  => $lat,
				'longitude' => $lon,
			);
			$schema['hasMap'] = $ps_url;
		}

		// Inject geo meta tags + FAQ schema into <head> via wp_head hook.
		$_lat = $lat;
		$_lon = $lon;
		$_carrier_label = $carrier_label;
		$_name = $name;
		$_address = $address;
		$_city = $city;
		$_country_code = $country_code;
		$_ps_url = $ps_url;
		$_base_slug = $base_slug;
		$_city_slug = $city_slug;

		add_action( 'wp_head', function() use ( $_lat, $_lon, $_carrier_label, $_name, $_address, $_city, $_country_code, $_ps_url, $_base_slug, $_city_slug ) {
			// Geo meta tags — read by Bing, Yandex, legacy crawlers.
			if ( $_lat && $_lon ) {
				echo '<meta name="geo.position" content="' . esc_attr( $_lat . ';' . $_lon ) . '">' . "\n";
				echo '<meta name="ICBM" content="' . esc_attr( $_lat . ', ' . $_lon ) . '">' . "\n";
				echo '<meta name="geo.placename" content="' . esc_attr( $_city . ', ' . strtoupper( $_country_code ) ) . '">' . "\n";
				echo '<meta name="geo.region" content="' . esc_attr( strtoupper( $_country_code ) ) . '">' . "\n";
			}

			// FAQPage Schema — Google shows FAQ rich snippets in SERP (2x more space!).
			$faq = array(
				'@context' => 'https://schema.org',
				'@type'    => 'FAQPage',
				'mainEntity' => array(
					array(
						'@type' => 'Question',
						'name'  => 'Where is ' . $_carrier_label . ' ' . $_name . ' located?',
						'acceptedAnswer' => array(
							'@type' => 'Answer',
							'text'  => $_carrier_label . ' ' . $_name . ' is located at ' . $_address . ', ' . $_city . ', ' . strtoupper( $_country_code ) . '.' . ( $_lat ? ' GPS coordinates: ' . round( $_lat, 5 ) . ', ' . round( $_lon, 5 ) . '.' : '' ),
						),
					),
					array(
						'@type' => 'Question',
						'name'  => 'What are the opening hours of ' . $_carrier_label . ' ' . $_name . '?',
						'acceptedAnswer' => array(
							'@type' => 'Answer',
							'text'  => 'Most ' . $_carrier_label . ' pickup points operate 24/7 with self-service access. Check the location details for specific hours.',
						),
					),
					array(
						'@type' => 'Question',
						'name'  => 'How do I send a parcel to ' . $_carrier_label . ' ' . $_name . '?',
						'acceptedAnswer' => array(
							'@type' => 'Answer',
							'text'  => 'Use Parcel Selector plugin in any WooCommerce store — customers select ' . $_carrier_label . ' ' . $_name . ' on the interactive map at checkout. No API key or carrier account required. Install free at wordpress.org/plugins/parcel-selector/',
						),
					),
					array(
						'@type' => 'Question',
						'name'  => 'How many pickup points are there in ' . $_city . '?',
						'acceptedAnswer' => array(
							'@type' => 'Answer',
							'text'  => 'Browse all InPost Paczkomaty pickup points in ' . $_city . ' at ' . home_url( $_base_slug . '/' . $_city_slug . '/' ) . '.',
						),
					),
				),
			);
			echo '<script type="application/ld+json">' . wp_json_encode( $faq, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP ) . '</script>' . "\n"; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript -- JSON-LD structured data, not executable JS.
		}, 5 );

		get_header();
		?>

		<?php self::render_styles(); ?>

		<?php // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript -- JSON-LD structured data, not executable JS. ?>
		<script type="application/ld+json">
		<?php
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode() output inside JSON-LD script tag, not HTML context.
		echo wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_PRETTY_PRINT );
		?>
		</script>

		<div class="ps-seo-wrap">

			<?php // Breadcrumbs: Home > Pickup Points > City > Point ?>
			<nav class="ps-seo-breadcrumbs" aria-label="<?php esc_attr_e( 'Breadcrumb', 'parcel-selector' ); ?>">
				<a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php echo esc_html( $store_name ); ?></a>
				<span class="ps-seo-sep"> / </span>
				<a href="<?php echo esc_url( home_url( $base_slug . '/' ) ); ?>"><?php esc_html_e( 'Pickup Points', 'parcel-selector' ); ?></a>
				<span class="ps-seo-sep"> / </span>
				<a href="<?php echo esc_url( home_url( $base_slug . '/' . esc_attr( $city_slug ) . '/' ) ); ?>"><?php echo esc_html( $city ); ?></a>
				<span class="ps-seo-sep"> / </span>
				<span class="ps-seo-current"><?php echo esc_html( $carrier_label . ' ' . $name ); ?></span>
			</nav>

			<h1 class="ps-seo-title"><?php echo esc_html( $page_title ); ?></h1>

			<div class="ps-seo-point-card" style="margin-bottom:24px">
				<div class="ps-seo-point-header">
					<span class="ps-seo-point-name" style="font-size:1.3em"><?php echo esc_html( $name ); ?></span>
					<span class="ps-seo-point-carrier"><?php echo esc_html( $carrier_label ); ?></span>
				</div>

				<?php if ( $address ) : ?>
					<p class="ps-seo-point-address" style="font-size:1.1em;margin:8px 0"><?php echo esc_html( $address ); ?></p>
				<?php endif; ?>

				<p style="color:#666;margin:4px 0">
					<?php echo esc_html( $city . ', ' . strtoupper( $country_code ) ); ?>
				</p>

				<?php if ( $lat && $lon ) : ?>
					<p class="ps-seo-point-coords" style="margin:4px 0">
						GPS: <?php echo esc_html( round( $lat, 6 ) . ', ' . round( $lon, 6 ) ); ?>
					</p>
				<?php endif; ?>

				<?php if ( $hours ) : ?>
					<p class="ps-seo-point-hours" style="margin:4px 0">
						<?php
						/* translators: %s: opening hours */
						$_ps_hours = sprintf( esc_html__( 'Hours: %s', 'parcel-selector' ), esc_html( $hours ) );
						echo esc_html( $_ps_hours );
						?>
					</p>
				<?php endif; ?>

				<p style="margin-top:12px">
					<a href="<?php echo esc_url( $ps_url ); ?>" style="color:<?php echo esc_attr( self::BRAND_COLOR ); ?>;font-weight:600">
						<?php esc_html_e( 'View on Parcel Selector map', 'parcel-selector' ); ?> →
					</a>
				</p>
			</div>

			<?php // Static map image for Google Images indexing ?>
			<?php if ( $lat && $lon ) : ?>
			<div style="margin-bottom:24px">
				<img
					src="<?php echo esc_url( sprintf( 'https://staticmap.openstreetmap.de/staticmap.php?center=%s,%s&zoom=16&size=600x300&maptype=mapnik&markers=%s,%s,red-pushpin', $lat, $lon, $lat, $lon ) ); ?>"
					alt="<?php echo esc_attr( $carrier_label . ' ' . $name . ' — ' . $address . ', ' . $city . ' — GPS ' . round( $lat, 5 ) . ', ' . round( $lon, 5 ) . ' — Parcel Selector' ); ?>"
					title="<?php echo esc_attr( $carrier_label . ' ' . $name . ' pickup point location — ' . $city ); ?>"
					width="600" height="300" loading="lazy"
					style="width:100%;height:auto;border-radius:12px;border:1px solid #e5e7eb"
				>
			</div>
			<?php endif; ?>

			<?php // Nearby points in same city — real links ?>
			<?php
			$cities     = self::group_by_city( $points );
			$nearby     = array();
			$count      = 0;
			if ( isset( $cities[ $city_slug ] ) ) {
				foreach ( $cities[ $city_slug ]['points'] as $np ) {
					$np_id = isset( $np['n'] ) ? $np['n'] : ( isset( $np['name'] ) ? $np['name'] : '' );
					if ( $np_id !== $name && $count < 20 ) {
						$nearby[] = $np;
						$count++;
					}
				}
			}
			?>
			<?php if ( ! empty( $nearby ) ) : ?>
			<h2 style="font-size:1.3em;margin:24px 0 12px">
				<?php
				/* translators: %s: city name */
				$_ps_nearby = sprintf( esc_html__( 'Other pickup points in %s', 'parcel-selector' ), esc_html( $city ) );
				echo esc_html( $_ps_nearby );
				?>
			</h2>
			<div class="ps-seo-points-list">
				<?php foreach ( $nearby as $np ) :
					$np_name    = isset( $np['n'] ) ? $np['n'] : ( isset( $np['name'] ) ? $np['name'] : '' );
					$np_addr    = isset( $np['a'] ) ? $np['a'] : ( isset( $np['address'] ) ? $np['address'] : '' );
					$np_carrier = isset( $np['c'] ) ? $np['c'] : ( isset( $np['carrier'] ) ? strtolower( $np['carrier'] ) : 'inpost' );
					$np_clabel  = isset( $carrier_names[ strtolower( $np_carrier ) ] ) ? $carrier_names[ strtolower( $np_carrier ) ] : ucfirst( $np_carrier );
					$np_slug    = self::make_point_slug( $np );
					$np_url     = home_url( $base_slug . '/' . esc_attr( $city_slug ) . '/' . esc_attr( $np_slug ) . '/' );
				?>
				<div class="ps-seo-point-card">
					<div class="ps-seo-point-header">
						<a href="<?php echo esc_url( $np_url ); ?>" class="ps-seo-point-name"><?php echo esc_html( $np_name ); ?></a>
						<span class="ps-seo-point-carrier"><?php echo esc_html( $np_clabel ); ?></span>
					</div>
					<?php if ( $np_addr ) : ?>
						<p class="ps-seo-point-address"><?php echo esc_html( $np_addr ); ?></p>
					<?php endif; ?>
				</div>
				<?php endforeach; ?>
			</div>
			<?php endif; ?>

			<p class="ps-seo-back" style="margin-top:20px">
				<a href="<?php echo esc_url( home_url( $base_slug . '/' . esc_attr( $city_slug ) . '/' ) ); ?>">
					<?php
					/* translators: %s: city name */
					$_ps_back = sprintf( esc_html__( 'All pickup points in %s', 'parcel-selector' ), esc_html( $city ) );
					echo esc_html( $_ps_back );
					?>
				</a>
			</p>

			<?php self::render_footer_credit(); ?>
		</div>

		<?php
		get_footer();
		exit;
	}

	// =========================================================================
	// XML Sitemap — for Google to discover all point pages
	// =========================================================================

	/**
	 * Render XML sitemap with all pickup point URLs.
	 *
	 * @param  string $country_code Store country code.
	 * @return void
	 */
	public static function render_sitemap( $country_code ) {
		$points    = self::get_points( $country_code );
		$cities    = self::group_by_city( $points );
		$base_slug = self::get_base_slug();

		header( 'Content-Type: application/xml; charset=UTF-8' );
		echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
		echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";

		// Index page.
		echo '<url><loc>' . esc_url( home_url( $base_slug . '/' ) ) . '</loc><changefreq>weekly</changefreq><priority>0.9</priority></url>' . "\n";

		// City pages.
		foreach ( $cities as $slug => $data ) {
			echo '<url><loc>' . esc_url( home_url( $base_slug . '/' . $slug . '/' ) ) . '</loc><changefreq>weekly</changefreq><priority>0.8</priority></url>' . "\n";

			// Point pages per city (limit 500 per city for performance).
			$count = 0;
			foreach ( $data['points'] as $p ) {
				if ( $count >= 500 ) break;
				$p_slug = self::make_point_slug( $p );
				echo '<url><loc>' . esc_url( home_url( $base_slug . '/' . $slug . '/' . $p_slug . '/' ) ) . '</loc><changefreq>monthly</changefreq><priority>0.6</priority></url>' . "\n";
				$count++;
			}
		}

		echo '</urlset>';
		exit;
	}

	// =========================================================================
	// Styles
	// =========================================================================

	/**
	 * Enqueue Local SEO page styles via wp_enqueue_scripts hook.
	 *
	 * Static CSS is shipped as assets/css/parcselect-local-seo.css; the brand
	 * color is injected at runtime as a CSS custom property so the stylesheet
	 * stays cacheable.
	 *
	 * @return void
	 */
	public static function enqueue_seo_styles() {
		wp_enqueue_style(
			'parcselect-local-seo',
			PARCSELECT_PLUGIN_URL . 'assets/css/parcselect-local-seo.css',
			array(),
			PARCSELECT_VERSION
		);
		$brand_color = esc_attr( self::BRAND_COLOR );
		wp_add_inline_style(
			'parcselect-local-seo',
			':root{--parcselect-seo-color:' . $brand_color . ';}'
		);
	}

	/**
	 * Legacy CSS builder — kept as a no-op shim. The full stylesheet ships as
	 * the static file assets/css/parcselect-local-seo.css enqueued by
	 * enqueue_seo_styles(); brand color is injected as a CSS custom property
	 * via wp_add_inline_style().
	 *
	 * @return string Always empty.
	 */
	private static function get_seo_css() {
		return '';
	}


	/**
	 * Render embedded CSS styles (legacy fallback — styles are now enqueued via wp_head).
	 *
	 * @return void
	 */
	public static function render_styles() {
		// Styles are enqueued via enqueue_seo_styles() hooked to wp_enqueue_scripts.
		// This method is kept as a no-op for backward compatibility.
	}

	// =========================================================================
	// Admin — Settings
	// =========================================================================

	/**
	 * Register the Local SEO toggle setting.
	 *
	 * @return void
	 */
	public static function register_setting() {
		register_setting(
			'parcselect_settings_group',
			self::OPTION_ENABLED,
			array(
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_text_field',
				'default'           => 'no',
			)
		);

		// v4.3.2 P0-1 fix: add_settings_field() lives in wp-admin/includes/template.php which is
		// only auto-loaded inside wp-admin context. WP-CLI / REST / cron hooks that fire admin_init
		// from outside wp-admin would crash with "Call to undefined function add_settings_field".
		if ( ! function_exists( 'add_settings_field' ) ) {
			require_once ABSPATH . 'wp-admin/includes/template.php';
		}

		add_settings_field(
			'parcselect_local_seo_toggle',
			__( 'Local SEO Pages', 'parcel-selector' ),
			array( __CLASS__, 'render_settings_field' ),
			'ps-settings',
			'default'
		);
	}

	/**
	 * Render the settings field checkbox.
	 *
	 * @return void
	 */
	public static function render_settings_field() {
		$enabled = get_option( self::OPTION_ENABLED, 'no' );
		?>
		<label for="ps-local-seo-toggle">
			<input
				type="checkbox"
				id="ps-local-seo-toggle"
				name="<?php echo esc_attr( self::OPTION_ENABLED ); ?>"
				value="yes"
				<?php checked( $enabled, 'yes' ); ?>
			/>
			<?php esc_html_e( 'Enable Local SEO pickup point pages', 'parcel-selector' ); ?>
		</label>
		<p class="description">
			<?php esc_html_e( 'Generates indexable pages listing pickup points by city. Helps your store rank for local pickup point searches.', 'parcel-selector' ); ?>
		</p>
		<?php
	}

	/**
	 * Handle the toggle save — invalidate rewrite flush flag when changed.
	 *
	 * Hooked via register_setting sanitize_callback is not enough for side effects,
	 * so we listen to option updates.
	 *
	 * @return void
	 */
	public static function on_option_update() {
		delete_option( self::OPTION_FLUSH );
	}

	// =========================================================================
	// Admin — Notice
	// =========================================================================

	/**
	 * Show an admin notice when Local SEO pages are active.
	 *
	 * @return void
	 */
	public static function admin_notice() {
		if ( 'yes' !== get_option( self::OPTION_ENABLED, 'no' ) ) {
			return;
		}

		// Only show on Parcel Selector admin pages and WooCommerce pages.
		$screen = get_current_screen();
		if ( ! $screen ) {
			return;
		}

		$show_on = array(
			'woocommerce_page_parcelselector',
			'toplevel_page_parcelselector',
		);

		if ( ! in_array( $screen->id, $show_on, true ) && 0 !== strpos( $screen->id, 'woocommerce' ) ) {
			return;
		}

		$country_code = self::get_store_country();
		$points       = self::get_points( $country_code );
		$cities       = self::group_by_city( $points );
		$total_points = count( $points );
		$total_cities = count( $cities );
		$base_slug    = self::get_base_slug();
		$page_url     = home_url( $base_slug . '/' );

		echo '<div class="notice notice-info is-dismissible">';
		echo '<p>';
		echo '<strong>' . esc_html__( 'ParcelSelector Local SEO', 'parcel-selector' ) . '</strong> — ';
		/* translators: 1: number of pickup points, 2: number of cities */
		$_ps_notice = sprintf( esc_html__( 'Local SEO pages are active — Google will index %1$d pickup points across %2$d cities.', 'parcel-selector' ), absint( $total_points ), absint( $total_cities ) );
		echo esc_html( $_ps_notice );
		echo ' <a href="' . esc_url( $page_url ) . '" target="_blank">' . esc_html__( 'View pages', 'parcel-selector' ) . '</a>';
		echo '</p>';
		echo '</div>';
	}
}

// Initialize the class.
Parcselect_Local_SEO::init();

// Listen for option update to trigger rewrite flush.
add_action( 'update_option_' . Parcselect_Local_SEO::OPTION_ENABLED, array( 'Parcselect_Local_SEO', 'on_option_update' ) );
add_action( 'add_option_' . Parcselect_Local_SEO::OPTION_ENABLED, array( 'Parcselect_Local_SEO', 'on_option_update' ) );
