<?php
/**
 * News Sitemap Generator
 *
 * Generates and serves a Google News sitemap at /news-sitemap.xml.
 * Only includes posts published within the last 2 days (Google's requirement).
 * Cache TTL is 5 minutes to keep the feed fresh for breaking news.
 * Optionally pings Google on publish.
 *
 * @package TWTAEO_Connector
 */

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

class TWTAEO_News_Sitemap {

	const OPTION_SETTINGS = 'twtaeo_news_settings';
	const TRANSIENT_CACHE = 'twtaeo_news_sitemap_xml';
	const TRANSIENT_FLUSH = 'twtaeo_news_sitemap_flush';
	const CACHE_TTL       = 300; // 5 minutes — news must stay fresh.
	const SITEMAP_SLUG    = 'news-sitemap.xml';
	const QUERY_VAR       = 'twtaeo_news_sitemap';
	const NONCE_SETTINGS  = 'twtaeo_news_save';
	const NONCE_FLUSH     = 'twtaeo_news_flush';

	// ── Hook registration ─────────────────────────────────────────────────────

	public static function register_hooks() {
		self::add_rewrite_rule();
		add_filter( 'query_vars',        array( __CLASS__, 'add_query_var' ) );
		add_action( 'template_redirect', array( __CLASS__, 'maybe_serve' ), 1 );
		add_action( 'save_post',         array( __CLASS__, 'flush_cache' ) );
		add_action( 'delete_post',       array( __CLASS__, 'flush_cache' ) );
		add_action( 'admin_init',        array( __CLASS__, 'maybe_flush_rewrite_rules' ) );

		// Ping Google on publish when the setting is enabled.
		add_action( 'publish_post', array( __CLASS__, 'maybe_ping_google' ), 10, 2 );
	}

	// ── Rewrite rule ──────────────────────────────────────────────────────────

	public static function add_rewrite_rule() {
		add_rewrite_rule(
			'^' . preg_quote( self::SITEMAP_SLUG, '/' ) . '$',
			'index.php?' . self::QUERY_VAR . '=1',
			'top'
		);
	}

	public static function add_query_var( array $vars ) {
		$vars[] = self::QUERY_VAR;
		return $vars;
	}

	// ── Serving ───────────────────────────────────────────────────────────────

	public static function maybe_serve() {
		if ( get_query_var( self::QUERY_VAR ) ) {
			self::serve();
			return;
		}
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET[ self::QUERY_VAR ] ) ) {
			self::serve();
			return;
		}
		$path = trim( wp_parse_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) ), PHP_URL_PATH ), '/' );
		if ( $path === self::SITEMAP_SLUG ) {
			self::serve();
		}
	}

	public static function serve() {
		$xml = get_transient( self::TRANSIENT_CACHE );

		if ( false === $xml ) {
			$xml = self::generate();
			set_transient( self::TRANSIENT_CACHE, $xml, self::CACHE_TTL );
		}

		status_header( 200 );
		header( 'Content-Type: application/xml; charset=UTF-8' );
		header( 'X-Robots-Tag: noindex' );
		// XML sitemap document served as application/xml. Every dynamic value was
		// escaped during assembly in generate() (esc_url() on <loc>, esc_html() on all
		// text nodes); the rest is a static literal. There is no whole-document XML
		// escaper, and esc_html() here would double-encode the already-safe markup.
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-escaped XML document; see note above.
		echo $xml;
		exit;
	}

	// ── Generation ────────────────────────────────────────────────────────────

	public static function generate() {
		$settings   = self::get_settings();
		$post_types = ! empty( $settings['post_types'] ) ? (array) $settings['post_types'] : array( 'post' );

		$args = array(
			'post_type'              => $post_types,
			'post_status'            => 'publish',
			'posts_per_page'         => 1000,
			'date_query'             => array(
				array(
					'after'     => '2 days ago',
					'inclusive' => true,
				),
			),
			'orderby'                => 'date',
			'order'                  => 'DESC',
			'no_found_rows'          => true,
			'update_post_term_cache' => false,
			'update_post_meta_cache' => false,
		);

		$posts = get_posts( $args );

		// Opt-in: only include posts explicitly marked as news articles.
		$posts = array_filter( $posts, function( $post ) {
			return TWTAEO_News_Meta::is_included( $post->ID );
		} );

		return self::build_xml( array_values( $posts ) );
	}

	private static function build_xml( array $posts ) {
		$settings = self::get_settings();
		$pub_name = ! empty( $settings['publication_name'] )
			? $settings['publication_name']
			: get_bloginfo( 'name' );
		$language = self::get_language_code();

		$out  = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
		$out .= '<!-- Generated by TWT AEO Ultimate ' . TWTAEO_VERSION . ' -->' . "\n";
		$out .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' . "\n";
		$out .= '        xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">' . "\n";

		foreach ( $posts as $post ) {
			$keywords = TWTAEO_News_Meta::get_keywords( $post->ID );
			$pub_date = get_post_time( 'c', true, $post->ID );

			$out .= "\t<url>\n";
			$out .= "\t\t<loc>" . esc_url( get_permalink( $post->ID ) ) . "</loc>\n";
			$out .= "\t\t<news:news>\n";
			$out .= "\t\t\t<news:publication>\n";
			$out .= "\t\t\t\t<news:name>" . esc_html( $pub_name ) . "</news:name>\n";
			$out .= "\t\t\t\t<news:language>" . esc_html( $language ) . "</news:language>\n";
			$out .= "\t\t\t</news:publication>\n";
			$out .= "\t\t\t<news:publication_date>" . esc_html( $pub_date ) . "</news:publication_date>\n";
			$out .= "\t\t\t<news:title>" . esc_html( html_entity_decode( get_the_title( $post->ID ), ENT_QUOTES, 'UTF-8' ) ) . "</news:title>\n";
			if ( ! empty( $keywords ) ) {
				$out .= "\t\t\t<news:keywords>" . esc_html( implode( ', ', $keywords ) ) . "</news:keywords>\n";
			}
			$out .= "\t\t</news:news>\n";
			$out .= "\t</url>\n";
		}

		$out .= '</urlset>';
		return $out;
	}

	// ── Google Ping ───────────────────────────────────────────────────────────

	public static function maybe_ping_google( $post_id, WP_Post $post ) {
		$settings = self::get_settings();
		if ( empty( $settings['ping_google'] ) ) {
			return;
		}

		$post_types = ! empty( $settings['post_types'] ) ? (array) $settings['post_types'] : array( 'post' );
		if ( ! in_array( $post->post_type, $post_types, true ) ) {
			return;
		}

		if ( ! TWTAEO_News_Meta::is_included( $post_id ) ) {
			return;
		}

		$sitemap_url = self::get_url();
		wp_remote_get(
			add_query_arg( 'sitemap', rawurlencode( $sitemap_url ), 'https://www.google.com/ping' ),
			array( 'timeout' => 5, 'blocking' => false )
		);

		self::flush_cache();
	}

	// ── Cache ─────────────────────────────────────────────────────────────────

	public static function flush_cache() {
		delete_transient( self::TRANSIENT_CACHE );
	}

	public static function schedule_rewrite_flush() {
		set_transient( self::TRANSIENT_FLUSH, 1, MINUTE_IN_SECONDS * 5 );
	}

	public static function maybe_flush_rewrite_rules() {
		$rules   = get_option( 'rewrite_rules', array() );
		$pattern = '^' . preg_quote( self::SITEMAP_SLUG, '/' ) . '$';

		if ( ! isset( $rules[ $pattern ] ) ) {
			flush_rewrite_rules( false );
			return;
		}

		if ( get_transient( self::TRANSIENT_FLUSH ) ) {
			delete_transient( self::TRANSIENT_FLUSH );
			flush_rewrite_rules( false );
		}
	}

	// ── URL helpers ───────────────────────────────────────────────────────────

	public static function get_url() {
		return home_url( '/' . self::SITEMAP_SLUG );
	}

	public static function get_fallback_url() {
		return add_query_arg( self::QUERY_VAR, '1', home_url( '/' ) );
	}

	// ── Settings ──────────────────────────────────────────────────────────────

	public static function get_settings() {
		$saved = get_option( self::OPTION_SETTINGS, array() );
		return array_merge( self::get_default_settings(), $saved );
	}

	public static function save_settings( array $raw ) {
		$post_types = isset( $raw['post_types'] ) ? array_map( 'sanitize_key', (array) $raw['post_types'] ) : array( 'post' );
		$all_types  = array_keys( get_post_types( array( 'public' => true ), 'names' ) );
		$post_types = array_values( array_intersect( $post_types, $all_types ) );

		$settings = array(
			'post_types'       => ! empty( $post_types ) ? $post_types : array( 'post' ),
			'publication_name' => sanitize_text_field( $raw['publication_name'] ?? '' ),
			'ping_google'      => ! empty( $raw['ping_google'] ) ? '1' : '',
			'schema_enabled'   => ! empty( $raw['schema_enabled'] ) ? '1' : '',
		);

		update_option( self::OPTION_SETTINGS, $settings );
		self::flush_cache();
		self::schedule_rewrite_flush();

		return $settings;
	}

	private static function get_default_settings() {
		return array(
			'post_types'       => array( 'post' ),
			'publication_name' => '',
			'ping_google'      => '',
			'schema_enabled'   => '1',
		);
	}

	// ── Helpers ───────────────────────────────────────────────────────────────

	private static function get_language_code() {
		$locale = get_locale();
		// ISO 639-1: take the first 2 characters of the locale.
		return strtolower( substr( $locale, 0, 2 ) ) ?: 'en';
	}

	public static function get_recent_posts( $limit = 50 ) {
		$settings   = self::get_settings();
		$post_types = ! empty( $settings['post_types'] ) ? (array) $settings['post_types'] : array( 'post' );

		$args = array(
			'post_type'              => $post_types,
			'post_status'            => 'publish',
			'posts_per_page'         => $limit,
			'date_query'             => array(
				array( 'after' => '2 days ago', 'inclusive' => true ),
			),
			'orderby'                => 'date',
			'order'                  => 'DESC',
			'no_found_rows'          => true,
			'update_post_term_cache' => false,
		);

		$posts = get_posts( $args );
		return array_filter( $posts, function( $p ) {
			return TWTAEO_News_Meta::is_included( $p->ID );
		} );
	}
}
