<?php
/**
 * The Singular Class
 *
 * @since      1.0.22
 * @package    RankMath
 * @subpackage RankMath\Paper
 * @author     Rank Math <support@rankmath.com>
 */

namespace RankMath\Paper;

use RankMath\Post;
use RankMath\Helper;
use RankMath\Helpers\Security;
use RankMath\Helpers\Str;
use RankMath\OpenGraph\OpenGraph;

defined( 'ABSPATH' ) || exit;

/**
 * Singular class.
 */
class Singular implements IPaper {

	/**
	 * Post object.
	 *
	 * @var WP_Post
	 */
	private $object;

	/**
	 * Retrieves the SEO title.
	 *
	 * @return string
	 */
	public function title() {
		return $this->get_post_title( $this->object );
	}

	/**
	 * Retrieves the SEO description.
	 *
	 * @return string
	 */
	public function description() {
		return $this->get_post_description( $this->object );
	}

	/**
	 * Retrieves the robots.
	 *
	 * @return string
	 */
	public function robots() {
		return $this->get_post_robots( $this->object );
	}

	/**
	 * Retrieves the advanced robots.
	 *
	 * @return array
	 */
	public function advanced_robots() {
		return $this->get_post_advanced_robots( $this->object );
	}

	/**
	 * Retrieves the canonical URL.
	 *
	 * @return array
	 */
	public function canonical() {
		$object_id          = Post::get_page_id();
		$canonical          = get_permalink( $object_id );
		$canonical_unpaged  = $canonical;
		$canonical_override = Post::get_meta( 'canonical_url', $object_id );

		/**
		 * Fix paginated pages canonical, but only if the page is truly paginated.
		 *
		 * @copyright Copyright (C) 2008-2019, Yoast BV
		 * The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
		 */
		if ( is_singular() && get_query_var( 'page' ) > 1 ) {
			$num_pages = ( substr_count( get_queried_object()->post_content, '<!--nextpage-->' ) + 1 );
			if ( $num_pages && get_query_var( 'page' ) <= $num_pages ) {
				global $wp_rewrite;
				$canonical = ! $wp_rewrite->using_permalinks() ? Security::add_query_arg_raw( 'page', get_query_var( 'page' ), $canonical ) :
					user_trailingslashit( trailingslashit( $canonical ) . get_query_var( 'page' ) );
			}
		}

		return [
			'canonical'          => $canonical,
			'canonical_unpaged'  => $canonical,
			'canonical_override' => $canonical_override,
		];
	}

	/**
	 * Retrieves meta keywords.
	 *
	 * @return string The focus keywords.
	 */
	public function keywords() {
		return Post::get_meta( 'focus_keyword', $this->object->ID );
	}

	/**
	 * Set post object.
	 *
	 * @param WP_Post $post Current post object.
	 */
	public function set_object( $post ) {
		$this->object = $post;
	}

	/**
	 * Get all SEO metadata for a post as a structured array.
	 *
	 * Uses existing Singular and Opengraph methods to ensure frontend fallback logic is applied consistently.
	 *
	 * @since 1.0.272
	 *
	 * @param int $post_id Post ID.
	 * @return array
	 */
	public function get_seo_meta( int $post_id ): array {
		$post = get_post( $post_id );
		if ( ! $post ) {
			return [ 'error' => esc_html__( 'Post not found.', 'seo-by-rank-math' ) ];
		}

		$this->set_object( $post );

		// Set post context so template variables and query-dependent functions resolve correctly.
		$prev_post       = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null;
		$GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
		setup_postdata( $post );

		rank_math()->variables->setup();

		$seo_title       = $this->title();
		$seo_description = $this->description();

		// Derive canonical from $post_id directly — canonical() depends on the active WP query
		// and may return get_permalink(0) outside a real frontend request.
		$canonical_override = Post::get_meta( 'canonical_url', $post_id );
		$canonical          = ! empty( $canonical_override ) ? $canonical_override : get_permalink( $post_id );

		// Use OpenGraph directly to avoid extra hooks registered by Facebook/Twitter constructors.
		$facebook          = new OpenGraph();
		$facebook->network = 'facebook';
		$facebook->prefix  = 'facebook';

		$twitter          = new OpenGraph();
		$twitter->network = 'twitter';
		$twitter->prefix  = Helper::get_post_meta( 'twitter_use_facebook', $post_id, true ) ? 'facebook' : 'twitter';

		$og_title       = $facebook->get_title( $post_id );
		$og_description = $facebook->get_description( $post_id );
		$tw_title       = $twitter->get_title( $post_id );
		$tw_description = $twitter->get_description( $post_id );

		$result = [
			'post_id'             => $post_id,
			'title'               => $seo_title,
			'description'         => $seo_description,
			'focus_keyword'       => $this->keywords(),
			'robots'              => array_values( $this->get_effective_robots() ),
			'canonical'           => (string) $canonical,
			'og_title'            => $og_title ? $og_title : $seo_title,
			'og_description'      => $og_description ? $og_description : $seo_description,
			'twitter_title'       => $tw_title ? $tw_title : $seo_title,
			'twitter_description' => $tw_description ? $tw_description : $seo_description,
			'seo_score'           => (int) Post::get_meta( 'seo_score', $post_id ),
		];

		wp_reset_postdata();
		$GLOBALS['post'] = $prev_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited

		return $result;
	}

	/**
	 * Get the SEO title set in the post metabox.
	 *
	 * @param object|null $post Post object to retrieve the title for.
	 *
	 * @return string
	 */
	protected function get_post_title( $post = null ) {
		if ( ! is_object( $post ) ) {
			return Paper::get_from_options( '404_title', [], esc_html__( 'Page not found', 'seo-by-rank-math' ) );
		}

		$title = Post::get_meta( 'title', $post->ID );
		if ( '' !== $title ) {
			return $title;
		}

		$post_type = isset( $post->post_type ) ? $post->post_type : $post->query_var;
		return Paper::get_from_options( "pt_{$post_type}_title", $post, '%title% %sep% %sitename%' );
	}

	/**
	 * Retrieves the SEO description set in the post metabox.
	 *
	 * Retrieve in this order:
	 *     1. Custom meta description set for the post in SERP field
	 *     2. Excerpt
	 *     3. Description template set in the Titles & Meta
	 *     4. Paragraph with the focus keyword
	 *     5. The First paragraph of the content
	 *
	 * @param object|null $post Object to retrieve the description from.
	 *
	 * @return string The SEO description for the specified object, or queried object if not supplied.
	 */
	protected function get_post_description( $post = null ) {
		if ( ! is_object( $post ) ) {
			return '';
		}

		// 1. Custom meta description set for the post in SERP field.
		$description = Post::get_meta( 'description', $post->ID );
		if ( '' !== $description ) {
			return $description;
		}

		// 2. Excerpt
		if ( ! empty( $post->post_excerpt ) ) {
			return $post->post_excerpt;
		}

		// 3. Description template set in the Titles & Meta.
		$post_type = isset( $post->post_type ) ? $post->post_type : $post->query_var;

		return Str::truncate( Paper::get_from_options( "pt_{$post_type}_description", $post ), 160 );
	}

	/**
	 * Retrieves the robots set in the post metabox.
	 *
	 * @param object|null $post Object to retrieve the robots data from.
	 *
	 * @return string The robots for the specified object, or queried object if not supplied.
	 */
	protected function get_post_robots( $post = null ) {
		if ( ! is_object( $post ) ) {
			return [];
		}

		$post_type = $post->post_type;
		$robots    = Paper::robots_combine( Post::get_meta( 'robots', $post->ID ) );
		if ( empty( $robots ) && Helper::get_settings( "titles.pt_{$post_type}_custom_robots" ) ) {
			$robots = Paper::robots_combine( Helper::get_settings( "titles.pt_{$post_type}_robots" ), true );
		}

		// `noindex` these conditions.
		$noindex_private            = 'private' === $post->post_status;
		$no_index_subpages          = is_paged() && Helper::get_settings( 'titles.noindex_paginated_pages' );
		$noindex_password_protected = ! empty( $post->post_password ) && Helper::get_settings( 'titles.noindex_password_protected' );

		if ( $noindex_private || $noindex_password_protected || $no_index_subpages ) {
			$robots['index'] = 'noindex';
		}

		return $robots;
	}

	/**
	 * Retrieves the advanced robots set in the post metabox.
	 *
	 * @param object|null $post Object to retrieve the robots data from.
	 *
	 * @return string The robots for the specified object, or queried object if not supplied.
	 */
	protected function get_post_advanced_robots( $post = null ) {
		if ( ! is_object( $this->object ) ) {
			return [];
		}

		$post_type = $this->object->post_type;
		$robots    = Paper::advanced_robots_combine( Post::get_meta( 'advanced_robots', $this->object->ID ) );
		if ( ! is_array( $robots ) && Helper::get_settings( "titles.pt_{$post_type}_custom_robots" ) ) {
			$robots = Paper::advanced_robots_combine( Helper::get_settings( "titles.pt_{$post_type}_advanced_robots" ), true );
		}

		return $robots;
	}

	/**
	 * Resolve the effective robots directives for the current post.
	 *
	 * Applies the same fallback and validation logic as Paper::get_robots().
	 *
	 * @return array
	 */
	private function get_effective_robots(): array {
		$robots = $this->robots();

		if ( empty( $robots ) ) {
			$robots = Paper::robots_combine( Helper::get_settings( 'titles.robots_global' ) );
		}

		if ( empty( $robots ) || ! is_array( $robots ) ) {
			return [
				'index'  => 'index',
				'follow' => 'follow',
			];
		}

		if ( ! isset( $robots['index'] ) ) {
			$robots = [ 'index' => 'index' ] + $robots;
		}
		if ( ! isset( $robots['follow'] ) ) {
			$robots = [ 'follow' => 'follow' ] + $robots;
		}

		// Force noindex if the blog is not public.
		if ( 0 === absint( get_option( 'blog_public' ) ) ) {
			$robots['index']  = 'noindex';
			$robots['follow'] = 'nofollow';
		}

		$allowed = [
			'index'        => '',
			'follow'       => '',
			'noarchive'    => '',
			'noimageindex' => '',
			'nosnippet'    => '',
		];
		return array_unique( array_intersect_key( $robots, $allowed ) );
	}
}
