<?php
/**
 * File Path: includes/admin/class-mbs-dashboard-feed-fetcher.php
 *
 * Dashboard Feed Fetcher
 *
 * Owns MBS news feed and Brand Monitor feed fetching plus their AJAX handlers.
 * Extracted from MBS_Admin_Dashboard (REF-008). Constructor wires both
 * wp_ajax_ hooks so the class is self-contained.
 *
 * @package MyBookShowroom
 * @since   1.1.0
 */

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

class MBS_Dashboard_Feed_Fetcher {

	/**
	 * Constructor. Registers both AJAX hooks.
	 */
	public function __construct() {
		add_action( 'wp_ajax_mbs_refresh_news_feed',     array( $this, 'ajax_refresh_news_feed' ) );
		add_action( 'wp_ajax_mbs_refresh_brand_monitor', array( $this, 'ajax_refresh_brand_monitor' ) );
	}

	// -------------------------------------------------------------------------
	// AJAX handlers
	// -------------------------------------------------------------------------

	/**
	 * AJAX handler -- refresh the MBS news feed.
	 *
	 * Action:  wp_ajax_mbs_refresh_news_feed (admin-only; no _nopriv variant).
	 * Returns: wp_send_json_success( html_string ) or wp_send_json_error().
	 *
	 * @return void
	 * @since 1.1.0
	 */
	public function ajax_refresh_news_feed(): void {
		check_ajax_referer( 'mbs_refresh_news_feed' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( -1, '', array( 'response' => 403 ) );
		}

		delete_transient( 'mbs_news_feed_cache' );
		$items = $this->fetch_news_feed();

		if ( empty( $items ) ) {
			wp_send_json_success(
				'<li class="mbs-news-empty">' .
				esc_html__( 'No news items available.', 'my-book-showroom' ) .
				'</li>'
			);
		}

		$html = '';
		foreach ( $items as $item ) {
			$html .= '<li class="mbs-news-item">'
				. '<a href="' . esc_url( $item['url'] ) . '" target="_blank" rel="noopener">'
				. esc_html( $item['title'] )
				. '</a>'
				. '<span class="mbs-news-date">' . esc_html( $item['date'] ) . '</span>'
				. '</li>';
		}

		wp_send_json_success( $html );
	}

	/**
	 * AJAX handler -- refresh Brand Monitor feed (PRO only).
	 *
	 * Action:  wp_ajax_mbs_refresh_brand_monitor (admin-only; no _nopriv variant).
	 * Returns: wp_send_json_success( html_string ) or wp_send_json_error().
	 *
	 * @return void
	 * @since 1.1.0
	 */
	public function ajax_refresh_brand_monitor(): void {
		check_ajax_referer( 'mbs_refresh_brand_monitor' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( -1, '', array( 'response' => 403 ) );
		}
		if ( ! mbs_is_pro() ) {
			wp_send_json_error( 'PRO license required', 403 );
		}
		// Clear per-URL transients so fetch_brand_feed() refreshes every feed.
		// RF-012: read feed URLs from DB table (MBS_Brand_Feed_URL_DAO).
		$all_urls = MBS_Brand_Feed_URL_DAO::get_all();
		if ( empty( $all_urls ) ) {
			$legacy = get_option( 'mbs_setting_brand_monitor_url', '' );
			if ( ! empty( $legacy ) ) {
				$all_urls = array( $legacy );
			}
		}
		foreach ( $all_urls as $url ) {
			delete_transient( 'mbs_brand_feed_' . md5( $url ) );
		}

		$grouped = $this->fetch_brand_feed();

		if ( empty( $grouped ) ) {
			wp_send_json_success(
				'<p class="mbs-brand-empty">' .
				esc_html__( 'No mentions found.', 'my-book-showroom' ) .
				'</p>'
			);
			return;
		}

		$html = '';
		foreach ( $grouped as $domain => $items ) {
			$html .= '<div class="mbs-brand-feed-group">';
			$html .= '<h4 class="mbs-brand-feed-domain">' . esc_html( $domain ) . '</h4>';
			$html .= '<ul class="mbs-brand-item-list">';
			foreach ( $items as $item ) {
				$html .= '<li class="mbs-brand-item">'
					. '<a href="' . esc_url( $item['url'] ) . '" target="_blank" rel="noopener">'
					. esc_html( $item['title'] )
					. '</a>'
					. '<span class="mbs-brand-date">' . esc_html( $item['date'] ) . '</span>'
					. '</li>';
			}
			$html .= '</ul></div>';
		}
		wp_send_json_success( $html );
	}

	// -------------------------------------------------------------------------
	// Feed fetchers
	// -------------------------------------------------------------------------

	/**
	 * Fetch and parse the MBS news RSS feed. Cached for 6 hours.
	 *
	 * @return array<int,array{title:string,url:string,date:string}>
	 * @since 1.1.0
	 */
	public function fetch_news_feed(): array {
		$cached = get_transient( 'mbs_news_feed_cache' );
		if ( is_array( $cached ) ) {
			return $cached;
		}

		$feed_url = apply_filters( 'mbs_news_feed_url', 'https://mybookshowroom.com/feed/' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix
		// SA-H3: Validate the (possibly filtered) URL before fetching — prevents SSRF injection via filter.
		if ( ! wp_http_validate_url( $feed_url ) ) {
			return array();
		}
		$response = wp_safe_remote_get( esc_url_raw( $feed_url ), array( 'timeout' => 8, 'reject_unsafe_urls' => true ) );

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

		$body = wp_remote_retrieve_body( $response );
		if ( empty( $body ) ) {
			return array();
		}

		// SA-M2: LIBXML_NONET | LIBXML_NOENT prevent XXE injection.
		$xml = simplexml_load_string( $body, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NONET | LIBXML_NOENT );
		if ( false === $xml || ! isset( $xml->channel->item ) ) {
			return array();
		}

		$items = array();
		$limit = 5;
		$count = 0;

		foreach ( $xml->channel->item as $item ) {
			if ( $count >= $limit ) {
				break;
			}
			$items[] = array(
				'title' => (string) $item->title,
				'url'   => (string) $item->link,
				'date'  => date_i18n(
					get_option( 'date_format' ),
					strtotime( (string) $item->pubDate )
				),
			);
			$count++;
		}

		set_transient( 'mbs_news_feed_cache', $items, 6 * HOUR_IN_SECONDS );
		return $items;
	}

	/**
	 * Fetch and parse the Brand Monitor Google Alerts RSS feed. Cached 12 hours.
	 *
	 * @return array<int,array{title:string,url:string,date:string}>
	 * @since 1.1.0
	 */
	/**
	 * Fetch all configured Brand Monitor feeds, store in mbs_brand_feed_cache (max 2
	 * rows per feed_url — FIFO), and return results grouped by domain.
	 *
	 * Each feed URL is rate-guarded by a 1-hour per-URL transient. Callers that need
	 * an immediate refresh (e.g. the "Refresh All" AJAX handler) must delete the
	 * per-URL transients before calling this method.
	 *
	 * @return array<string, array<int, array{title:string,url:string,date:string}>>
	 *         Grouped by feed domain. Empty array if no feeds configured.
	 * @since 1.1.0
	 */
	public function fetch_brand_feed(): array {
		// Resolve all configured feed URLs (multi-URL or legacy single).
		// RF-012: read feed URLs from DB table (MBS_Brand_Feed_URL_DAO).
		$feed_urls = MBS_Brand_Feed_URL_DAO::get_all();
		if ( empty( $feed_urls ) ) {
			$legacy = get_option( 'mbs_setting_brand_monitor_url', '' );
			if ( ! empty( $legacy ) ) {
				$feed_urls = array( $legacy );
			}
		}
		$feed_urls = array_values( array_filter( array_map( 'esc_url_raw', $feed_urls ) ) );

		if ( empty( $feed_urls ) ) {
			return array();
		}

		$date_fmt = get_option( 'date_format' );

		foreach ( $feed_urls as $feed_url ) {
			// Per-URL 1-hour rate guard — skip if recently fetched.
			$transient_key = 'mbs_brand_feed_' . md5( $feed_url );
			if ( get_transient( $transient_key ) ) {
				continue;
			}

			// SA-H3: Use wp_safe_remote_get (single DNS resolution, rejects RFC-1918,
			// loopback, and link-local ranges) to eliminate the TOCTOU rebinding window.
			if ( ! wp_http_validate_url( $feed_url ) ) {
				continue; // Invalid URL structure.
			}

			$feed_response = wp_safe_remote_get(
				$feed_url,
				array(
					'timeout'            => 10,
					'reject_unsafe_urls' => true,
				)
			);

			if ( is_wp_error( $feed_response ) || 200 !== wp_remote_retrieve_response_code( $feed_response ) ) {
				set_transient( $transient_key, 'error', 15 * MINUTE_IN_SECONDS );
				continue;
			}

			$feed_body = wp_remote_retrieve_body( $feed_response );
			if ( empty( $feed_body ) ) {
				set_transient( $transient_key, 'error', 15 * MINUTE_IN_SECONDS );
				continue;
			}

			// SA-M2: LIBXML_NONET | LIBXML_NOENT prevent XXE injection.
			libxml_use_internal_errors( true );
			$feed_xml = simplexml_load_string( $feed_body, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NONET | LIBXML_NOENT );
			libxml_clear_errors();

			if ( false === $feed_xml ) {
				set_transient( $transient_key, 'error', 15 * MINUTE_IN_SECONDS );
				continue;
			}

			$fetched_at   = current_time( 'mysql', true );
			$inserted     = 0;

			// Parse RSS 2.0 (<channel><item>) and Atom (<entry>) feed formats.
			$parsed_items = array();
			if ( isset( $feed_xml->channel->item ) ) {
				foreach ( $feed_xml->channel->item as $rss_item ) {
					$parsed_items[] = array(
						'title' => wp_strip_all_tags( (string) ( $rss_item->title ?? '' ) ),
						'url'   => esc_url_raw( (string) ( $rss_item->link ?? '' ) ),
					);
				}
			} else {
				// Atom: root element is <feed>, entries are <entry>.
				foreach ( $feed_xml->entry as $atom_entry ) {
					$entry_link = '';
					foreach ( $atom_entry->link as $lnk ) {
						$rel = (string) ( $lnk['rel'] ?? 'alternate' );
						if ( 'alternate' === $rel || '' === $rel ) {
							$entry_link = esc_url_raw( (string) ( $lnk['href'] ?? '' ) );
							break;
						}
					}
					$parsed_items[] = array(
						'title' => wp_strip_all_tags( (string) ( $atom_entry->title ?? '' ) ),
						'url'   => $entry_link,
					);
				}
			}

			$max_feed_items = MBS_Brand_Feed_Cache_DAO::RESULTS_PER_FEED;
			foreach ( array_slice( $parsed_items, 0, $max_feed_items ) as $parsed_item ) {
				if ( empty( $parsed_item['title'] ) || empty( $parsed_item['url'] ) ) {
					continue;
				}
				MBS_Brand_Feed_Cache_DAO::insert_result( $feed_url, $parsed_item['title'], $parsed_item['url'], $fetched_at );
				$inserted++;
			}

			// FIFO: enforce per-feed cap via DAO (RESULTS_PER_FEED = 5).
			if ( $inserted > 0 ) {
				MBS_Brand_Feed_Cache_DAO::prune_fifo( $feed_url );
			}

			set_transient( $transient_key, true, HOUR_IN_SECONDS );
		}

		// Read all stored rows for the current feed URLs, return grouped by domain.
		$rows = MBS_Brand_Feed_Cache_DAO::get_all();
		if ( empty( $rows ) ) {
			return array();
		}

		$grouped = array();
		foreach ( $rows as $row ) {
			$domain = (string) ( wp_parse_url( $row->feed_url, PHP_URL_HOST ) ?: $row->feed_url );
			if ( ! isset( $grouped[ $domain ] ) ) {
				$grouped[ $domain ] = array();
			}
			if ( count( $grouped[ $domain ] ) < MBS_Brand_Feed_Cache_DAO::RESULTS_PER_FEED ) {
				$grouped[ $domain ][] = array(
					'title' => $row->result_title,
					'url'   => $row->result_url,
					'date'  => date_i18n( $date_fmt, strtotime( $row->fetched_at ) ),
				);
			}
		}
		return $grouped;
	}
}
