<?php
/**
 * The llms.txt module.
 *
 * @package    RankMath
 * @subpackage RankMath\LLMS
 */

namespace RankMath\LLMS;

use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Arr;
use RankMath\Sitemap\Sitemap;
use RankMath\Sitemap\Router;
use RankMath\Helpers\Url;
use WP_Query;

defined( 'ABSPATH' ) || exit;

/**
 * LLMS_Txt class.
 */
class LLMS_Txt {
	use Hooker;

	/**
	 * Class constructor.
	 *
	 * Registers hooks and filters for the llms.txt module.
	 */
	public function __construct() {
		$this->action( 'init', 'add_rewrite_rule' );
		$this->action( 'template_redirect', 'maybe_serve_llms_txt' );
		$this->filter( 'rank_math/settings/general', 'add_settings' );
		$this->action( 'wp_loaded', 'remove_canonical_redirect' );
	}

	/**
	 * Remove the canonical redirect for the llms.txt file.
	 *
	 * @hook wp_loaded
	 * @return void
	 */
	public function remove_canonical_redirect() {
		if ( strpos( Url::get_current_url(), '/llms.txt' ) !== false ) {
			remove_filter( 'template_redirect', 'redirect_canonical' );
		}
	}

	/**
	 * Add the llms.txt settings tab to the General settings panel.
	 *
	 * @hook rank_math/settings/general
	 *
	 * @param array $tabs Option panel tabs.
	 * @return array Modified tabs array with llms.txt tab added.
	 */
	public function add_settings( $tabs ) {
		Arr::insert(
			$tabs,
			[
				'llms' => [
					'icon'    => 'rm-icon rm-icon-bot',
					'title'   => esc_html__( 'Edit llms.txt', 'rank-math' ),
					'desc'    => esc_html__( 'Configure your llms.txt file for custom crawling/indexing rules.', 'rank-math' ),
					'file'    => __DIR__ . '/options.php',
					'classes' => 'rank-math-advanced-option',
					'json'    => [
						'llmsUrl' => esc_url( home_url( '/llms.txt' ) ),
					],
				],
			],
			5
		);
		return $tabs;
	}

	/**
	 * Add the rewrite rule and query var for llms.txt.
	 *
	 * @hook init
	 * @return void
	 */
	public function add_rewrite_rule() {
		add_rewrite_rule( '^llms\.txt$', 'index.php?llms_txt=1', 'top' );
		add_rewrite_tag( '%llms_txt%', '1' );
	}

	/**
	 * Serve the llms.txt file if the endpoint is hit.
	 *
	 * @hook template_redirect
	 * @return void
	 */
	public function maybe_serve_llms_txt() {
		if ( intval( get_query_var( 'llms_txt' ) ) !== 1 ) {
			return;
		}

		if ( substr( Url::get_current_url(), -1 ) === '/' ) {
			wp_safe_redirect( home_url( '/llms.txt' ) );
			exit;
		}

		$this->output();
		exit;
	}

	/**
	 * Output the llms.txt file content in Markdown format.
	 *
	 * @action rank_math/llms_txt/before_output Fires before llms.txt output is sent.
	 * @action rank_math/llms_txt/after_output Fires after llms.txt output is sent.
	 * @filter rank_math/llms_txt/posts_query_args Filter WP_Query args for posts.
	 * @filter rank_math/llms_txt/posts Filter post IDs to include.
	 * @filter rank_math/llms_txt/terms Filter term IDs to include.
	 * @filter rank_math/llms_txt/extra_content Filter extra content output.
	 *
	 * @return void
	 */
	public function output() {
		if ( headers_sent() ) {
			return;
		}

		header( 'Content-Type: text/plain; charset=utf-8' );
		header( 'X-Robots-Tag: noindex, nofollow', true );
		/**
		 * Fires before the llms.txt output is sent to the browser.
		 *
		 * @since 1.0.250
		 */
		$this->do_action( 'llms_txt/before_output' );

		$limit = absint( Helper::get_settings( 'general.llms_limit', 100 ) );
		$this->add_header_content();
		$this->add_post_types_data( $limit );
		$this->add_taxonomies_data( $limit );
		$this->add_extra_content();

		/**
		 * Fires after the llms.txt output is sent to the browser.
		 *
		 * @since 1.0.250
		 */
		$this->do_action( 'llms_txt/after_output' );
	}

	/**
	 * Adds header content to the llms.txt output.
	 *
	 * @return void
	 */
	private function add_header_content() {
		$site_title  = Helper::get_settings( 'titles.knowledgegraph_name', get_bloginfo( 'name' ) );
		$site_desc   = Helper::get_settings( 'titles.organization_description', get_bloginfo( 'description' ) );
		$site_title .= $site_desc ? ': ' . $site_desc : '';

		$this->output_line( 'Generated by Rank Math SEO, this is an llms.txt file designed to help LLMs better understand and index this website.' );
		$this->output_line( '' );
		$this->output_line( '# ' . esc_html( $site_title ) );

		if ( ! Helper::is_module_active( 'sitemap' ) ) {
			return;
		}

		$this->output_line( '' );
		$sitemap_url = Router::get_base_url( Sitemap::get_sitemap_index_slug() . '.xml' );
		$this->output_line( '## Sitemaps' );
		$this->output_line( '[XML Sitemap](' . esc_url( $sitemap_url ) . '): Includes all crawlable and indexable pages.' );
		// Add an extra blank line after the header content.
		$this->output_line( '' );
	}

	/**
	 * Adds post type data to the llms.txt output.
	 *
	 * @param int $limit The maximum number of posts to include per post type.
	 * @return void
	 */
	private function add_post_types_data( $limit ) {
		$post_types = Helper::get_settings( 'general.llms_post_types', [] );
		if ( empty( $post_types ) ) {
			return;
		}

		foreach ( $post_types as $post_type ) {
			$args = [
				'post_type'      => $post_type,
				'post_status'    => 'publish',
				'posts_per_page' => $limit,
				'no_found_rows'  => true,
			];

			/**
			 * Filter the WP_Query arguments used to fetch posts for llms.txt.
			 *
			 * @since 1.0.250
			 * @param array $args The WP_Query arguments.
			 * @return array Modified WP_Query arguments.
			 */
			$args  = $this->do_filter( 'llms_txt/posts_query_args', $args );
			$query = new \WP_Query( $args );

			/**
			 * Filter the list of post IDs to be included in llms.txt for a post type.
			 *
			 * @since 1.0.250
			 * @param array $posts List of posts.
			 * @param array $args     The WP_Query arguments.
			 * @return array Modified list of post IDs.
			 */
			$posts = $this->do_filter( 'llms_txt/posts', $query->posts, $args );
			if ( empty( $posts ) ) {
				continue;
			}

			$label = get_post_type_object( $post_type )->labels->name;
			$this->output_line( '## ' . esc_html( $label ) );

			foreach ( $posts as $object ) {
				if ( ! Helper::is_post_indexable( $object ) ) {
					continue;
				}

				$title = get_the_title( $object );
				$link  = get_permalink( $object );
				$desc  = wp_strip_all_tags( Helper::replace_vars( '%excerpt%', $object ) );

				$this->output_line(
					$desc
						? '- [' . esc_html( $title ) . '](' . esc_url( $link ) . '): ' . esc_html( $desc )
						: '- [' . esc_html( $title ) . '](' . esc_url( $link ) . ')'
				);
			}
			$this->output_line( '' );
		}
	}

	/**
	 * Adds taxonomy data to the llms.txt output.
	 *
	 * @param int $limit The maximum number of terms to include per taxonomy.
	 * @return void
	 */
	private function add_taxonomies_data( $limit ) {
		$taxonomies = Helper::get_settings( 'general.llms_taxonomies', [] );
		if ( empty( $taxonomies ) ) {
			return;
		}

		foreach ( $taxonomies as $taxonomy ) {
			$tax_obj = get_taxonomy( $taxonomy );
			if ( ! $tax_obj ) {
				continue;
			}

			$terms = get_terms(
				[
					'taxonomy'   => $taxonomy,
					'hide_empty' => true,
					'number'     => $limit,
				]
			);

			/**
			 * Filter the list of terms to be included in llms.txt for a taxonomy.
			 *
			 * @since 1.0.250
			 * @param array  $terms    List of terms.
			 * @param string $taxonomy Taxonomy name.
			 * @return array Modified list of term IDs.
			 */
			$terms = $this->do_filter( 'llms_txt/terms', $terms, $taxonomy );
			if ( empty( $terms ) ) {
				continue;
			}

			$label = $tax_obj->labels->name;
			$this->output_line( '## ' . esc_html( $label ) );
			foreach ( $terms as $term ) {
				if ( $term && Helper::is_term_indexable( $term ) ) {
					$name = $term->name;
					$link = get_term_link( $term );
					$this->output_line( '- [' . esc_html( $name ) . '](' . esc_url( $link ) . ')' );
				}
			}
			$this->output_line( '' );
		}
	}

	/**
	 * Adds extra content to the end of the llms.txt output.
	 *
	 * @return void
	 */
	private function add_extra_content() {
		/**
		 * Filter the extra content output at the end of llms.txt.
		 *
		 * @since 1.0.250
		 * @param string $extra The extra content string.
		 * @return string Modified extra content.
		 */
		$extra = $this->do_filter( 'llms_txt/extra_content', Helper::get_settings( 'general.llms_extra_content', '' ) );
		if ( ! empty( $extra ) ) {
			$this->output_line( esc_html( str_replace( "\n", "\n", $extra ) ) );
		}
	}

	/**
	 * Outputs a line with a newline character (\n).
	 *
	 * @param string $content The content to output.
	 * @return void
	 */
	private function output_line( $content ) {
		echo esc_html( $content ) . "\n";
	}
}
