<?php
/**
 * RSSHandler
 *
 * Adds related posts to RSS feed content.
 * Carries forward the label-rotation and used-post-deduplication logic
 * from v1.9.x, now as a clean namespaced class.
 *
 * @package Relatedly\RSS
 * @since   2.0.0
 */

namespace Relatedly\RSS;

use Relatedly\Core\Options;
use Relatedly\Core\QueryBuilder;
use Relatedly\Core\Debugger;
use Relatedly\Labels\LabelProvider;
use Relatedly\Labels\LabelPicker;
use Relatedly\Premium\FeatureGate;

defined( 'ABSPATH' ) || exit;

/**
 * Inserts related posts into RSS feed content.
 *
 * @since 2.0.0
 */
class RSSHandler {

	/**
	 * Plugin settings repository.
	 *
	 * @since 2.0.0
	 * @var Options
	 */
	private Options $options;

	/**
	 * Related posts query builder.
	 *
	 * @since 2.0.0
	 * @var QueryBuilder
	 */
	private QueryBuilder $query;

	/**
	 * Debug logger.
	 *
	 * @since 2.0.0
	 * @var Debugger
	 */
	private Debugger $debugger;

	/**
	 * Label provider.
	 *
	 * @since 2.0.0
	 * @var LabelProvider
	 */
	private LabelProvider $label_provider;

	/**
	 * Stateful per-post label rotation.
	 *
	 * @since 2.0.0
	 * @var LabelPicker
	 */
	private LabelPicker $label_picker;

	/**
	 * Prevents re-entrant calls.
	 *
	 * @since 2.0.0
	 * @var bool
	 */
	private static bool $processing = false;

	/**
	 * Post IDs already used inline.
	 *
	 * @since 2.0.0
	 * @var int[]
	 */
	private array $used_ids = [];

	/**
	 * Post ID currently being processed.
	 *
	 * @since 2.0.0
	 * @var int|null
	 */
	private ?int $current_post_id = null;

	/**
	 * Create the RSS handler.
	 *
	 * @since 2.0.0
	 * @param Options      $options  Plugin settings repository.
	 * @param QueryBuilder $query    Related posts query builder.
	 * @param Debugger     $debugger Debug logger.
	 */
	public function __construct( Options $options, QueryBuilder $query, Debugger $debugger ) {
		$this->options        = $options;
		$this->query          = $query;
		$this->debugger       = $debugger;
		$this->label_provider = new LabelProvider( $options );
		$this->label_picker   = new LabelPicker();
	}

	/**
	 * Register RSS feed hooks.
	 *
	 * @since 2.0.0
	 */
	public function init_hooks(): void {
		add_filter( 'the_content_feed', [ $this, 'filter_feed' ], 20 );
		add_filter( 'the_excerpt_rss', [ $this, 'filter_feed' ], 20 );

		// Reset per-post state when the feed moves to the next item.
		foreach ( [ 'rss2_item', 'rss_item', 'rdf_item', 'atom_entry' ] as $hook ) {
			add_action( $hook, [ $this, 'reset_post_state' ] );
		}
	}

	// -------------------------------------------------------------------------
	// Main filter
	// -------------------------------------------------------------------------

	/**
	 * Entry point for the_content_feed and the_excerpt_rss hooks.
	 *
	 * @since 2.0.0
	 * @param string $content Feed item content.
	 * @return string Filtered feed item content.
	 */
	public function filter_feed( string $content ): string {
		if ( ! is_feed() || self::$processing ) {
			return $content;
		}

		if ( ! $this->options->get( 'rss_enabled', true ) || ! $this->options->get( 'enabled', true ) ) {
			return $content;
		}

		$post = get_post();
		if ( ! $post ) {
			return $content;
		}

		$enabled_types = (array) $this->options->get( 'post_types', [ 'post' ] );
		if ( ! in_array( get_post_type( $post ), $enabled_types, true ) ) {
			return $content;
		}

		// Initialise label picker if this is a new post or picker is empty.
		if ( $post->ID !== $this->current_post_id || empty( $this->label_picker ) ) {
			$this->reset_post_state();
		}

		self::$processing = true;
		$content          = $this->insert_inline( $content, $post->ID );
		$content          = $this->append_end_content( $content, $post->ID );
		self::$processing = false;

		return $content;
	}

	/**
	 * Reset per-post state when the feed iterator moves to the next item.
	 *
	 * @since 2.0.0
	 */
	public function reset_post_state(): void {
		$post = get_post();
		if ( ! $post || $post->ID === $this->current_post_id ) {
			return;
		}

		$this->current_post_id = $post->ID;
		$this->used_ids        = [];
		$this->label_picker->seed( $this->label_provider->get_inline_labels() );

		$this->debugger->log( 'RSS: reset state for post', [ 'post_id' => $post->ID ] );
	}

	// -------------------------------------------------------------------------
	// Inline insertion
	// -------------------------------------------------------------------------

	/**
	 * Insert inline related posts into feed content.
	 *
	 * @param string $content Feed item content.
	 * @param int    $post_id Source post ID.
	 * @return string Filtered feed item content.
	 */
	private function insert_inline( string $content, int $post_id ): string {
		$interval = (int) $this->options->get( 'rss_paragraph_interval', 10 );

		preg_match_all( '/<p[^>]*>.*?<\/p>/is', $content, $matches, PREG_OFFSET_CAPTURE );

		if ( empty( $matches[0] ) || count( $matches[0] ) < $interval ) {
			return $content;
		}

		$paragraphs = $matches[0];
		$count      = count( $paragraphs );
		$pool       = $this->query->get_related_posts( $post_id, 10, 'rss' );

		// Build insertion points (every N paragraphs), then process in reverse
		// so string offsets remain valid after each substr_replace.
		$points = [];
		for ( $i = $interval; $i < $count; $i += $interval ) {
			$points[] = $i;
		}

		foreach ( array_reverse( $points ) as $point ) {
			$available = array_values(
				array_filter(
					$pool,
					fn( $p ) => ! in_array( $p->ID, $this->used_ids, true )
				)
			);

			if ( empty( $available ) ) {
				break;
			}

			$related = $available[0];
			$html    = $this->render_inline_post( $related );

			if ( '' === $html ) {
				continue;
			}

			$insert_at        = $paragraphs[ $point ][1] + strlen( $paragraphs[ $point ][0] );
			$content          = substr_replace( $content, $html, $insert_at, 0 );
			$this->used_ids[] = $related->ID;

			$this->debugger->log(
				'RSS: inserted inline post',
				[
					'post_id'  => $related->ID,
					'position' => $point,
				]
			);
		}

		return $content;
	}

	// -------------------------------------------------------------------------
	// End-content append
	// -------------------------------------------------------------------------

	/**
	 * Append end-content related posts to feed content.
	 *
	 * @param string $content Feed item content.
	 * @param int    $post_id Source post ID.
	 * @return string Filtered feed item content.
	 */
	private function append_end_content( string $content, int $post_id ): string {
		if ( ! $this->options->get( 'rss_end_content_enabled', true ) ) {
			return $content;
		}

		$max  = min(
			(int) $this->options->get( 'rss_end_content_max_posts', 3 ),
			FeatureGate::max_rss_posts()
		);
		$pool = $this->query->get_related_posts( $post_id, $max + count( $this->used_ids ), 'rss_end' );

		$posts = array_values(
			array_filter(
				$pool,
				fn( $p ) => ! in_array( $p->ID, $this->used_ids, true )
			)
		);
		$posts = array_slice( $posts, 0, $max );

		if ( empty( $posts ) ) {
			return $content;
		}

		$html = '<p><strong>' . esc_html__( 'Related Posts:', 'relatedly' ) . '</strong></p>';

		foreach ( $posts as $post ) {
			$url   = $this->tracked_url( $post->ID, 'end' );
			$item  = sprintf(
				'<p>&#8226; <a href="%s">%s</a></p>',
				esc_url( $url ),
				esc_html( get_the_title( $post->ID ) )
			);
			$html .= apply_filters( 'relatedly_rss_end_content_format', $item, $post, $url );
		}

		return $content . $html;
	}

	// -------------------------------------------------------------------------
	// Rendering helpers
	// -------------------------------------------------------------------------

	/**
	 * Render one inline related post for RSS.
	 *
	 * @param \WP_Post $post Related post.
	 * @return string Rendered HTML.
	 */
	private function render_inline_post( \WP_Post $post ): string {
		$label = $this->label_picker->next();
		$url   = $this->tracked_url( $post->ID, 'inline' );

		$html = sprintf(
			'<p><strong>%s</strong> <a href="%s">%s</a></p>',
			esc_html( $label ),
			esc_url( $url ),
			esc_html( get_the_title( $post->ID ) )
		);

		return apply_filters( 'relatedly_rss_output_format', $html, $post, $label, $url );
	}

	/**
	 * Build a UTM-tagged permalink.
	 *
	 * @param int    $post_id     Destination post ID.
	 * @param string $context_key 'inline' | 'end' (maps to RSS UTM content options).
	 * @return string
	 * @since 2.0.0
	 */
	private function tracked_url( int $post_id, string $context_key ): string {
		$option_key = 'rss_utm_content_' . $context_key;
		$params     = [
			'utm_source'   => (string) $this->options->get( 'utm_source', 'rss' ),
			'utm_medium'   => (string) $this->options->get( 'utm_medium', 'related_posts' ),
			'utm_campaign' => (string) $this->options->get( 'utm_campaign', 'rss_related_posts' ),
			'utm_content'  => (string) $this->options->get( $option_key, $context_key ),
		];

		$params = apply_filters( 'relatedly_rss_utm_parameters', $params, $post_id );

		return add_query_arg( $params, get_permalink( $post_id ) );
	}
}
