<?php
/**
 * Frontend Robots Meta Output — Layered Decision Engine
 *
 * @package SEOvaultSEO
 */

namespace SEOvaultSEO\Frontend;

use SEOvaultSEO\Utils\RequestCache;

/**
 * Handles robots meta output through a layered decision engine.
 *
 * Singular noindex precedence:
 * 1. Per-post `_seovault_robots_noindex` meta when present (true → noindex, false → force index)
 * 2. Post-type noindex setting (`Options::is_post_type_noindex`) when per-post meta is absent
 * 3. Global / archive / protection rules from other layers
 *
 * Imports must not write a falsy per-post robots value for blank/default source
 * robots — that would create an explicit override and defeat post-type noindex.
 *
 * Decision layers (in evaluation order):
 * 1. Site visibility       — blog_public option
 * 2. Special pages         — search, 404
 * 3. Archive rules         — date, attachment, post-format, empty archives
 * 4. Per-object override   — singular post meta, then post-type defaults
 * 5. Protection rules      — password-protected, comment pagination, query exploitation
 * 6. Copyright directives  — max-snippet, max-image-preview, max-video-preview
 * 7. Final render          — normalize, dedup, resolve conflicts, sort, filter
 */
class Robots {

	/**
	 * Canonical directive order for deterministic output.
	 * Directives not listed here are sorted alphabetically after known ones.
	 *
	 * @var array
	 */
	private static $canonical_order = array(
		'noindex',
		'nofollow',
		'noarchive',
		'nosnippet',
		'noimageindex',
		'noodp',
		'notranslate',
		'unavailable_after',
		'max-snippet',
		'max-image-preview',
		'max-video-preview',
	);

	/**
	 * Valid preview-size values for max-image-preview directive.
	 *
	 * @var array
	 */
	private static $valid_preview_sizes = array( 'none', 'standard', 'large' );

	// ------------------------------------------------------------------
	// Public API
	// ------------------------------------------------------------------

	/**
	 * Output robots meta tag.
	 */
	public static function output() {
		$robots = self::get_robots_meta();

		if ( empty( $robots ) ) {
			return;
		}

		echo '<meta name="robots" content="' . esc_attr( $robots ) . '" />' . "\n";
	}

	/**
	 * Get robots meta content for current context.
	 *
	 * @return string
	 */
	public static function get_robots_meta() {
		return RequestCache::remember(
			'robots_meta',
			function () {
				$directives = self::build_directives();

				$directives = apply_filters( 'seovaultseo_robots_directives', $directives );

				$rendered = self::render_directives( $directives );

				return apply_filters( 'seovaultseo_robots_meta', $rendered );
			}
		);
	}

	// ------------------------------------------------------------------
	// Build pipeline — assembles raw directives from all layers
	// ------------------------------------------------------------------

	/**
	 * Build the raw directive array by evaluating all decision layers.
	 *
	 * @return array
	 */
	private static function build_directives() {
		$directives = array();

		// Layer 1: Site visibility
		$directives = self::merge_directives( $directives, self::get_site_visibility_directives() );

		// Layer 2: Special pages (search, 404)
		$directives = self::merge_directives( $directives, self::get_special_page_directives() );

		// Layer 3: Archive rules (date, attachment, post-format, empty archives)
		$directives = self::merge_directives( $directives, self::get_archive_directives() );

		// Layer 4: Per-object override
		$directives = self::merge_directives( $directives, self::get_per_object_directives() );

		// Layer 5: Protection rules (password-protected, comment pagination, query exploitation)
		$directives = self::merge_directives( $directives, self::get_protection_directives() );

		// Layer 6: Copyright directives (max-snippet, max-image-preview, max-video-preview)
		$directives = self::merge_directives( $directives, self::get_copyright_directives() );

		return $directives;
	}

	// ------------------------------------------------------------------
	// Decision layers
	// ------------------------------------------------------------------

	/**
	 * Layer 1: Site visibility — blog_public option.
	 *
	 * @return array
	 */
	private static function get_site_visibility_directives() {
		$directives = array();

		if ( '0' === get_option( 'blog_public' ) ) {
			$directives[] = 'noindex';
			$directives[] = 'nofollow';
		}

		return $directives;
	}

	/**
	 * Layer 2: Special pages — search, 404.
	 *
	 * @return array
	 */
	private static function get_special_page_directives() {
		$directives = array();

		// Search pages: noindex when option enabled.
		if ( is_search() && \SEOvaultSEO\Core\Options::is_search_noindex() ) {
			$directives[] = 'noindex';
		}

		// 404 pages: always noindex.
		if ( is_404() ) {
			$directives[] = 'noindex';
		}

		return $directives;
	}

	/**
	 * Layer 3: Archive rules — date, attachment, post-format, empty archives.
	 *
	 * @return array
	 */
	private static function get_archive_directives() {
		$directives = array();

		// Date archives: noindex when option enabled.
		if ( is_date() && \SEOvaultSEO\Core\Options::is_date_archives_noindex() ) {
			$directives[] = 'noindex';
		}

		// Attachment pages: noindex when option enabled.
		if ( is_attachment() && \SEOvaultSEO\Core\Options::is_attachment_noindex() ) {
			$directives[] = 'noindex';
		}

		// Post format archives: noindex when option enabled.
		if ( is_tax( 'post_format' ) && \SEOvaultSEO\Core\Options::is_post_format_noindex() ) {
			$directives[] = 'noindex';
		}

		// Empty archive protection: noindex archives with no posts.
		if ( \SEOvaultSEO\Core\Options::is_empty_archives_noindex() ) {
			global $wp_query;
			if ( is_archive() && isset( $wp_query ) && 0 === (int) $wp_query->post_count ) {
				$directives[] = 'noindex';
			}
		}

		return $directives;
	}

	/**
	 * Layer 4: Per-object override — singular post meta, then post-type defaults,
	 * plus term archive robots meta.
	 *
	 * When `_seovault_robots_noindex` is absent, post-type noindex applies.
	 * When present (including explicit false), per-post wins and post-type is skipped.
	 *
	 * @return array
	 */
	private static function get_per_object_directives() {
		$directives = array();

		if ( is_singular() ) {
			$post_id = get_queried_object_id();

			if ( $post_id ) {
				if ( \SEOvaultSEO\Core\PostMeta::has_noindex_meta( $post_id ) ) {
					if ( \SEOvaultSEO\Core\PostMeta::is_noindex( $post_id ) ) {
						$directives[] = 'noindex';
					}
				} elseif ( \SEOvaultSEO\Core\Options::is_post_type_noindex( get_post_type( $post_id ) ) ) {
					$directives[] = 'noindex';
				}

				if ( \SEOvaultSEO\Core\PostMeta::is_nofollow( $post_id ) ) {
					$directives[] = 'nofollow';
				}
			}
		} elseif ( is_category() || is_tag() || is_tax() ) {
			$term_id = get_queried_object_id();

			if ( $term_id ) {
				if ( \SEOvaultSEO\Core\TermMeta::has_noindex_meta( $term_id ) && \SEOvaultSEO\Core\TermMeta::is_noindex( $term_id ) ) {
					$directives[] = 'noindex';
				}

				if ( \SEOvaultSEO\Core\TermMeta::is_nofollow( $term_id ) ) {
					$directives[] = 'nofollow';
				}
			}
		}

		return $directives;
	}

	/**
	 * Layer 5: Protection rules — password-protected, comment pagination, query exploitation.
	 *
	 * @return array
	 */
	private static function get_protection_directives() {
		$directives = array();

		// Password-protected singular posts: noindex.
		if ( is_singular() && post_password_required( get_queried_object_id() ) ) {
			$directives[] = 'noindex';
		}

		// Comment pagination protection: noindex on comment pages > 1.
		if ( is_singular() && get_query_var( 'cpage' ) > 1 ) {
			$directives[] = 'noindex';
		}

		// Advanced query protection: noindex, nofollow on exploited requests.
		if ( \SEOvaultSEO\Core\Options::is_advanced_query_protection() && self::is_query_exploited() ) {
			$directives[] = 'noindex';
			$directives[] = 'nofollow';
		}

		return $directives;
	}

	/**
	 * Layer 6: Copyright directives — max-snippet, max-image-preview, max-video-preview.
	 *
	 * @return array
	 */
	private static function get_copyright_directives() {
		$directives = array();

		$max_snippet = \SEOvaultSEO\Core\Options::get_robots_max_snippet();
		if ( $max_snippet >= -1 ) {
			$directives[] = 'max-snippet:' . (int) $max_snippet;
		}

		$max_image    = self::normalize_preview_value( \SEOvaultSEO\Core\Options::get_robots_max_image_preview() );
		$directives[] = 'max-image-preview:' . $max_image;

		$max_video = \SEOvaultSEO\Core\Options::get_robots_max_video_preview();
		if ( $max_video >= -1 ) {
			$directives[] = 'max-video-preview:' . (int) $max_video;
		}

		return $directives;
	}

	/**
	 * Detect query exploitation attempts.
	 * Checks for suspicious query variables with invalid values when no real object is found.
	 *
	 * @return bool
	 */
	private static function is_query_exploited() {
		if ( is_search() || is_front_page() ) {
			return false;
		}

		if ( get_queried_object_id() ) {
			return false;
		}

		global $wp_query;
		if ( ! isset( $wp_query->query ) || empty( $wp_query->query ) ) {
			return false;
		}

		$numeric_vars = array( 'page_id', 'p', 'attachment_id', 'year', 'monthnum', 'day', 'w', 'm', 'hour', 'minute', 'second' );
		foreach ( $numeric_vars as $var ) {
			if ( isset( $wp_query->query[ $var ] ) ) {
				$value = $wp_query->query[ $var ];
				if ( ! is_numeric( $value ) || '0' === (string) $value ) {
					return true;
				}
			}
		}

		$array_vars = array( 'cat', 'author' );
		foreach ( $array_vars as $var ) {
			if ( isset( $wp_query->query[ $var ] ) ) {
				$value = $wp_query->query[ $var ];
				if ( is_array( $value ) ) {
					foreach ( $value as $v ) {
						if ( ! is_numeric( $v ) || '0' === (string) $v ) {
							return true;
						}
					}
				} elseif ( ! is_numeric( $value ) || '0' === (string) $value ) {
					return true;
				}
			}
		}

		return false;
	}

	// ------------------------------------------------------------------
	// Render pipeline — normalize, dedup, resolve conflicts, sort
	// ------------------------------------------------------------------

	/**
	 * Render the directive array into a comma-separated string.
	 * Returns 'index, follow' when no directives are present.
	 *
	 * @param array $directives Raw directive array.
	 * @return string
	 */
	private static function render_directives( $directives ) {
		$directives = self::sanitize_directives( $directives );

		if ( empty( $directives ) ) {
			return 'index, follow';
		}

		// If only copyright directives are present (no behavioral directives),
		// prepend implicit index, follow.
		$has_behavioral = false;
		foreach ( $directives as $d ) {
			if ( 0 !== strpos( $d, 'max-' ) ) {
				$has_behavioral = true;
				break;
			}
		}

		if ( ! $has_behavioral ) {
			array_unshift( $directives, 'follow' );
			array_unshift( $directives, 'index' );
		}

		return implode( ', ', $directives );
	}

	// ------------------------------------------------------------------
	// Sanitization / normalization — public for unit testing
	// ------------------------------------------------------------------

	/**
	 * Full sanitization pipeline: normalize → dedup → resolve conflicts → sort.
	 *
	 * @param array $directives Raw directives.
	 * @return array
	 */
	public static function sanitize_directives( $directives ) {
		$directives = self::normalize_directives( $directives );
		$directives = self::deduplicate_directives( $directives );
		$directives = self::resolve_conflicts( $directives );
		$directives = self::sort_directives( $directives );

		return $directives;
	}

	/**
	 * Normalize directive values: lowercase, trim, remove empties.
	 *
	 * @param array $directives Raw directives.
	 * @return array
	 */
	public static function normalize_directives( $directives ) {
		$normalized = array();

		foreach ( (array) $directives as $directive ) {
			$directive = strtolower( trim( $directive ) );

			if ( '' !== $directive ) {
				$normalized[] = $directive;
			}
		}

		return $normalized;
	}

	/**
	 * Remove duplicate directives, preserving first occurrence order.
	 *
	 * @param array $directives Normalized directives.
	 * @return array
	 */
	public static function deduplicate_directives( $directives ) {
		return array_values( array_unique( $directives ) );
	}

	/**
	 * Resolve conflicting directives.
	 * - noindex overrides index
	 * - nofollow overrides follow
	 *
	 * @param array $directives Deduplicated directives.
	 * @return array
	 */
	public static function resolve_conflicts( $directives ) {
		if ( in_array( 'noindex', $directives, true ) ) {
			$directives = array_values( array_diff( $directives, array( 'index' ) ) );
		}

		if ( in_array( 'nofollow', $directives, true ) ) {
			$directives = array_values( array_diff( $directives, array( 'follow' ) ) );
		}

		return $directives;
	}

	/**
	 * Sort directives into canonical order.
	 * Unknown directives are sorted alphabetically after known ones.
	 *
	 * @param array $directives Directives to sort.
	 * @return array
	 */
	public static function sort_directives( $directives ) {
		$order = array_flip( self::$canonical_order );

		usort(
			$directives,
			function ( $a, $b ) use ( $order ) {
				$a_pos = self::get_directive_sort_position( $a, $order );
				$b_pos = self::get_directive_sort_position( $b, $order );

				if ( $a_pos === $b_pos ) {
					return strcmp( $a, $b );
				}

				return $a_pos - $b_pos;
			}
		);

		return $directives;
	}

	/**
	 * Normalize a max-image-preview value.
	 * Falls back to 'large' for invalid values.
	 *
	 * @param string $value Raw value.
	 * @return string 'none', 'standard', or 'large'
	 */
	public static function normalize_preview_value( $value ) {
		$value = strtolower( trim( $value ) );

		if ( in_array( $value, self::$valid_preview_sizes, true ) ) {
			return $value;
		}

		return 'large';
	}

	// ------------------------------------------------------------------
	// Public formatting helpers — for unit testing
	// ------------------------------------------------------------------

	/**
	 * Format a max-snippet directive from a raw value.
	 *
	 * @param mixed $value Raw value (-1 = unlimited, 0 = none, positive = limit).
	 * @return string
	 */
	public static function format_max_snippet_directive( $value ) {
		$value = (int) $value;
		if ( $value < -1 ) {
			$value = -1;
		}
		return 'max-snippet:' . $value;
	}

	/**
	 * Format a max-image-preview directive from a raw value.
	 *
	 * @param mixed $value Raw value ('none', 'standard', 'large').
	 * @return string
	 */
	public static function format_max_image_preview_directive( $value ) {
		return 'max-image-preview:' . self::normalize_preview_value( $value );
	}

	/**
	 * Format a max-video-preview directive from a raw value.
	 *
	 * @param mixed $value Raw value (-1 = unlimited, 0 = none, positive = seconds).
	 * @return string
	 */
	public static function format_max_video_preview_directive( $value ) {
		$value = (int) $value;
		if ( $value < -1 ) {
			$value = -1;
		}
		return 'max-video-preview:' . $value;
	}

	/**
	 * Get copyright directives for current options.
	 *
	 * @return array
	 */
	public static function get_copyright_directives_public() {
		return self::get_copyright_directives();
	}

	// ------------------------------------------------------------------
	// Internal helpers
	// ------------------------------------------------------------------

	/**
	 * Get the sort position for a directive, handling prefixed directives.
	 *
	 * @param string $directive Directive name.
	 * @param array  $order     Flipped canonical order array.
	 * @return int
	 */
	private static function get_directive_sort_position( $directive, $order ) {
		if ( isset( $order[ $directive ] ) ) {
			return $order[ $directive ];
		}

		$base = strstr( $directive, ':', true );
		if ( false !== $base && isset( $order[ $base ] ) ) {
			return $order[ $base ];
		}

		return 999;
	}

	/**
	 * Merge two directive arrays.
	 *
	 * @param array $existing Existing directives.
	 * @param array $new      New directives to append.
	 * @return array
	 */
	private static function merge_directives( $existing, $new ) {
		return array_merge( $existing, $new );
	}
}
