<?php
/**
 * SEO module REST controller.
 *
 * @package MonitorCat\Modules\Seo
 */

declare(strict_types=1);

namespace MonitorCat\Modules\Seo;

defined( 'ABSPATH' ) || exit;

use MonitorCat\Modules\Seo\Analyzer\AiCrawlerAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\CanonicalAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\GeoReadinessAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\HeadingAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\ImageAltAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\LinkAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\LlmsTxtAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\MetaAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\OpenGraphAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\ReadabilityAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\RobotsAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\SchemaAnalyzer;
use MonitorCat\Modules\Seo\Analyzer\SitemapAnalyzer;
use WP_Error;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * Handles REST API endpoints for the SEO module.
 */
final class RestController extends WP_REST_Controller {

	/**
	 * REST API namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'monitor-cat/v1';

	/**
	 * Resource base.
	 *
	 * @var string
	 */
	protected $rest_base = 'seo';

	/**
	 * Cache duration for per-post scan results (24 hours).
	 *
	 * @var int
	 */
	private const SCAN_CACHE_DURATION = DAY_IN_SECONDS;

	/**
	 * Cache duration for overview results (12 hours).
	 *
	 * @var int
	 */
	private const OVERVIEW_CACHE_DURATION = 12 * HOUR_IN_SECONDS;

	/**
	 * Maximum number of posts to include in overview.
	 *
	 * @var int
	 */
	private const OVERVIEW_POST_LIMIT = 10;

	/**
	 * Register REST API routes.
	 */
	public function register(): void {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/scan',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'handle_scan' ),
				'permission_callback' => array( $this, 'check_permission' ),
				'args'                => array(
					'post_id' => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
						'validate_callback' => function ( $value ) {
							return is_numeric( $value ) && (int) $value >= 0;
						},
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/results',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'handle_results' ),
				'permission_callback' => array( $this, 'check_permission' ),
				'args'                => array(
					'post_id' => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
						'validate_callback' => function ( $value ) {
							return is_numeric( $value ) && (int) $value > 0;
						},
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/overview',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'handle_overview' ),
				'permission_callback' => array( $this, 'check_permission' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/posts',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'handle_posts' ),
				'permission_callback' => array( $this, 'check_permission' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/site-health',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'handle_site_health' ),
				'permission_callback' => array( $this, 'check_permission' ),
			)
		);
	}

	/**
	 * Check if the current user has permission to access SEO endpoints.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return bool True if the user has manage_options capability.
	 */
	public function check_permission( WP_REST_Request $request ): bool {
		return current_user_can( 'manage_options' );
	}

	/**
	 * Handle the SEO scan request.
	 *
	 * Instantiates all analyzers, runs each, aggregates results,
	 * and caches in a transient for 24 hours.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_REST_Response
	 */
	public function handle_scan( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$post_id = (int) $request->get_param( 'post_id' );

		// Resolve homepage (id=0) to the actual front page.
		if ( 0 === $post_id ) {
			$post_id = $this->resolve_homepage_id();
		}

		$post = get_post( $post_id );

		if ( ! $post ) {
			return new WP_Error(
				'monitorcat_post_not_found',
				__( 'Post not found.', 'monitor-cat' ),
				array( 'status' => 404 )
			);
		}

		$results = $this->run_page_scan( $post_id );

		// Build meta description for SERP preview.
		$meta_desc = '';
		if ( ! empty( $post->post_excerpt ) ) {
			$meta_desc = $post->post_excerpt;
		} else {
			$content   = get_post_field( 'post_content', $post_id );
			$content   = is_string( $content ) ? $content : '';
			$meta_desc = wp_strip_all_tags( $content );
		}
		$meta_desc = trim( (string) preg_replace( '/\s+/', ' ', $meta_desc ) );
		if ( mb_strlen( $meta_desc ) > 160 ) {
			$meta_desc = mb_substr( $meta_desc, 0, 157 ) . '...';
		}

		$data = array(
			'post_id'          => $post_id,
			'post_title'       => get_the_title( $post_id ),
			'post_url'         => get_permalink( $post_id ),
			'meta_description' => $meta_desc,
			'overall_score'    => $results['overall_score'],
			'overall_status'   => $results['overall_status'],
			'timestamp'        => gmdate( 'c' ),
			'analyzers'        => $results['analyzers'],
			'protections'      => $this->get_active_protections(),
		);

		set_transient( 'monitorcat_seo_' . $post_id, $data, self::SCAN_CACHE_DURATION );

		return new WP_REST_Response(
			array(
				'success' => true,
				'data'    => $data,
			),
			200
		);
	}

	/**
	 * Handle the SEO results request.
	 *
	 * Returns cached transient or 404 if no results available.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_REST_Response
	 */
	public function handle_results( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$post_id = (int) $request->get_param( 'post_id' );
		$cached  = get_transient( 'monitorcat_seo_' . $post_id );

		if ( false === $cached ) {
			return new WP_Error(
				'monitorcat_no_results',
				__( 'No cached SEO results found. Run a scan first.', 'monitor-cat' ),
				array( 'status' => 404 )
			);
		}

		return new WP_REST_Response(
			array(
				'success' => true,
				'data'    => $cached,
			),
			200
		);
	}

	/**
	 * Handle the SEO overview request.
	 *
	 * Scans recent published posts, aggregates scores,
	 * and caches in a transient for 12 hours.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_REST_Response
	 */
	public function handle_overview( WP_REST_Request $request ): WP_REST_Response {
		$cached = get_transient( 'monitorcat_seo_overview' );

		if ( false !== $cached ) {
			return new WP_REST_Response(
				array(
					'success' => true,
					'data'    => $cached,
				),
				200
			);
		}

		$posts = get_posts(
			array(
				'post_type'              => array( 'post', 'page' ),
				'post_status'            => 'publish',
				'posts_per_page'         => self::OVERVIEW_POST_LIMIT,
				'orderby'                => 'date',
				'order'                  => 'DESC',
				'no_found_rows'          => true,
				'update_post_term_cache' => false,
			)
		);

		if ( empty( $posts ) ) {
			$data = array(
				'score'     => null,
				'status'    => 'ready',
				'posts'     => array(),
				'timestamp' => gmdate( 'c' ),
			);

			return new WP_REST_Response(
				array(
					'success' => true,
					'data'    => $data,
				),
				200
			);
		}

		$post_results = array();
		$total_score  = 0;

		foreach ( $posts as $post ) {
			$results      = $this->run_scan( $post->ID );
			$total_score += $results['overall_score'];

			$post_results[] = array(
				'post_id'        => $post->ID,
				'post_title'     => get_the_title( $post->ID ),
				'overall_score'  => $results['overall_score'],
				'overall_status' => $results['overall_status'],
			);

			// Cache individual post results too.
			$post_data = array(
				'post_id'        => $post->ID,
				'post_title'     => get_the_title( $post->ID ),
				'overall_score'  => $results['overall_score'],
				'overall_status' => $results['overall_status'],
				'timestamp'      => gmdate( 'c' ),
				'analyzers'      => $results['analyzers'],
			);

			set_transient( 'monitorcat_seo_' . $post->ID, $post_data, self::SCAN_CACHE_DURATION );
		}

		$avg_score      = (int) round( $total_score / count( $posts ) );
		$overall_status = $this->determine_status( $avg_score );

		$data = array(
			'score'     => $avg_score,
			'status'    => $overall_status,
			'posts'     => $post_results,
			'timestamp' => gmdate( 'c' ),
		);

		set_transient( 'monitorcat_seo_overview', $data, self::OVERVIEW_CACHE_DURATION );

		return new WP_REST_Response(
			array(
				'success' => true,
				'data'    => $data,
			),
			200
		);
	}

	/**
	 * Handle the SEO posts list request.
	 *
	 * Returns a list of published posts and pages for the scan dropdown.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_REST_Response
	 */
	public function handle_posts( WP_REST_Request $request ): WP_REST_Response {
		$posts = get_posts(
			array(
				'post_type'              => array( 'post', 'page' ),
				'post_status'            => 'publish',
				'posts_per_page'         => 50,
				'orderby'                => 'title',
				'order'                  => 'ASC',
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			)
		);

		$items = array(
			array(
				'id'    => 0,
				'title' => __( 'Homepage', 'monitor-cat' ),
				'type'  => 'homepage',
				'date'  => '',
			),
		);

		foreach ( $posts as $post ) {
			$items[] = array(
				'id'    => $post->ID,
				'title' => get_the_title( $post->ID ),
				'type'  => $post->post_type,
				'date'  => $post->post_date,
			);
		}

		return new WP_REST_Response( $items, 200 );
	}

	/**
	 * Handle site-wide health check.
	 *
	 * Runs only site-wide analyzers (sitemap, robots, AI crawlers, llms.txt)
	 * that don't depend on a specific post. Cached for 12 hours.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_REST_Response
	 */
	public function handle_site_health( WP_REST_Request $request ): WP_REST_Response {
		$cached = get_transient( 'monitorcat_seo_site_health' );

		if ( false !== $cached ) {
			return new WP_REST_Response(
				array(
					'success' => true,
					'data'    => $cached,
				),
				200
			);
		}

		$analyzers = $this->get_site_analyzers();
		$results   = array();
		$total     = 0;

		foreach ( $analyzers as $key => $analyzer ) {
			$result          = $analyzer->analyze( 0 );
			$results[ $key ] = $result;
			$total          += $result['score'];
		}

		$count          = count( $analyzers );
		$overall_score  = $count > 0 ? (int) round( $total / $count ) : 0;
		$overall_status = $this->determine_status( $overall_score );

		$data = array(
			'overall_score'  => $overall_score,
			'overall_status' => $overall_status,
			'timestamp'      => gmdate( 'c' ),
			'analyzers'      => $results,
			'protections'    => $this->get_active_protections(),
		);

		set_transient( 'monitorcat_seo_site_health', $data, self::OVERVIEW_CACHE_DURATION );

		return new WP_REST_Response(
			array(
				'success' => true,
				'data'    => $data,
			),
			200
		);
	}

	/**
	 * Run page-specific analyzers only.
	 *
	 * @param int $post_id The post ID to scan.
	 * @return array{overall_score: int, overall_status: string, analyzers: array<string, array{score: int, status: string, checks: array<int, array{label: string, status: string, message: string, value: string|null}>}>}
	 */
	private function run_page_scan( int $post_id ): array {
		$analyzers = $this->get_page_analyzers();
		$results   = array();
		$total     = 0;

		foreach ( $analyzers as $key => $analyzer ) {
			$result          = $analyzer->analyze( $post_id );
			$results[ $key ] = $result;
			$total          += $result['score'];
		}

		$count          = count( $analyzers );
		$overall_score  = $count > 0 ? (int) round( $total / $count ) : 0;
		$overall_status = $this->determine_status( $overall_score );

		return array(
			'overall_score'  => $overall_score,
			'overall_status' => $overall_status,
			'analyzers'      => $results,
		);
	}

	/**
	 * Run all analyzers on a post and aggregate results.
	 *
	 * @param int $post_id The post ID to scan.
	 * @return array{overall_score: int, overall_status: string, analyzers: array<string, array{score: int, status: string, checks: array<int, array{label: string, status: string, message: string, value: string|null}>}>}
	 */
	private function run_scan( int $post_id ): array {
		$analyzers = $this->get_all_analyzers();
		$results   = array();
		$total     = 0;

		foreach ( $analyzers as $key => $analyzer ) {
			$result          = $analyzer->analyze( $post_id );
			$results[ $key ] = $result;
			$total          += $result['score'];
		}

		$count          = count( $analyzers );
		$overall_score  = $count > 0 ? (int) round( $total / $count ) : 0;
		$overall_status = $this->determine_status( $overall_score );

		return array(
			'overall_score'  => $overall_score,
			'overall_status' => $overall_status,
			'analyzers'      => $results,
		);
	}

	/**
	 * Get site-wide analyzer instances (no post dependency).
	 *
	 * @return array<string, SitemapAnalyzer|RobotsAnalyzer|AiCrawlerAnalyzer|LlmsTxtAnalyzer>
	 */
	private function get_site_analyzers(): array {
		return array(
			'sitemap'    => new SitemapAnalyzer(),
			'robots'     => new RobotsAnalyzer(),
			'ai_crawler' => new AiCrawlerAnalyzer(),
			'llms_txt'   => new LlmsTxtAnalyzer(),
		);
	}

	/**
	 * Get page-specific analyzer instances.
	 *
	 * @return array<string, MetaAnalyzer|OpenGraphAnalyzer|HeadingAnalyzer|ImageAltAnalyzer|SchemaAnalyzer|LinkAnalyzer|CanonicalAnalyzer|ReadabilityAnalyzer|GeoReadinessAnalyzer>
	 */
	private function get_page_analyzers(): array {
		return array(
			'meta'          => new MetaAnalyzer(),
			'open_graph'    => new OpenGraphAnalyzer(),
			'headings'      => new HeadingAnalyzer(),
			'images'        => new ImageAltAnalyzer(),
			'schema'        => new SchemaAnalyzer(),
			'links'         => new LinkAnalyzer(),
			'canonical'     => new CanonicalAnalyzer(),
			'readability'   => new ReadabilityAnalyzer(),
			'geo_readiness' => new GeoReadinessAnalyzer(),
		);
	}

	/**
	 * Get all analyzer instances (site-wide + page-specific).
	 *
	 * @return array<string, MetaAnalyzer|OpenGraphAnalyzer|HeadingAnalyzer|ImageAltAnalyzer|SchemaAnalyzer|LinkAnalyzer|SitemapAnalyzer|RobotsAnalyzer|CanonicalAnalyzer|ReadabilityAnalyzer|GeoReadinessAnalyzer|AiCrawlerAnalyzer|LlmsTxtAnalyzer>
	 */
	private function get_all_analyzers(): array {
		return array_merge( $this->get_page_analyzers(), $this->get_site_analyzers() );
	}

	/**
	 * Resolve homepage to an actual post ID.
	 *
	 * If a static front page is set, returns that ID.
	 * Otherwise, returns the most recent published post.
	 *
	 * @return int The resolved post ID.
	 */
	private function resolve_homepage_id(): int {
		// Check for static front page.
		if ( 'page' === get_option( 'show_on_front' ) ) {
			$front_page_id = (int) get_option( 'page_on_front' );
			if ( $front_page_id > 0 ) {
				return $front_page_id;
			}
		}

		// Fallback: most recent published post.
		$latest = get_posts(
			array(
				'post_type'              => array( 'post', 'page' ),
				'post_status'            => 'publish',
				'posts_per_page'         => 1,
				'orderby'                => 'date',
				'order'                  => 'DESC',
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			)
		);

		if ( ! empty( $latest ) ) {
			return $latest[0]->ID;
		}

		return 1;
	}

	/**
	 * Get the list of active SEO protections based on settings.
	 *
	 * @return array<int, array{key: string, label: string, icon: string, color: string}>
	 */
	private function get_active_protections(): array {
		$mc_settings  = get_option( 'monitorcat_settings', array() );
		$mc_settings  = is_array( $mc_settings ) ? $mc_settings : array();
		$seo_settings = $mc_settings['seo'] ?? array();
		$seo_settings = is_array( $seo_settings ) ? $seo_settings : array();

		$has_third_party = defined( 'WPSEO_VERSION' ) || defined( 'AIOSEO_VERSION' ) || defined( 'RANK_MATH_VERSION' );
		$protections     = array();

		if ( $seo_settings['auto_schema'] ?? true ) {
			$protections[] = array(
				'key'   => 'schema',
				'label' => __( 'JSON-LD Schema (WebSite, Organization, Article, Breadcrumbs, FAQ)', 'monitor-cat' ),
				'icon'  => 'fa-code',
				'color' => 'cyan',
			);
		}

		if ( ! $has_third_party && ( $seo_settings['auto_meta_tags'] ?? true ) ) {
			$protections[] = array(
				'key'   => 'meta',
				'label' => __( 'Meta descriptions & Open Graph tags on every page', 'monitor-cat' ),
				'icon'  => 'fa-tags',
				'color' => 'lavender',
			);
			$protections[] = array(
				'key'   => 'canonical',
				'label' => __( 'Canonical URLs to prevent duplicate content', 'monitor-cat' ),
				'icon'  => 'fa-link',
				'color' => 'primary',
			);
			$protections[] = array(
				'key'   => 'twitter',
				'label' => __( 'Twitter Card markup for social sharing', 'monitor-cat' ),
				'icon'  => 'fa-card-heart',
				'color' => 'coral',
			);
		}

		if ( $seo_settings['auto_llms_txt'] ?? true ) {
			$protections[] = array(
				'key'   => 'llms',
				'label' => __( 'llms.txt & llms-full.txt for AI engine discovery', 'monitor-cat' ),
				'icon'  => 'fa-file-lines',
				'color' => 'mint',
			);
		}

		return $protections;
	}

	/**
	 * Determine overall status from score.
	 *
	 * @param int $score The calculated score.
	 * @return string 'pass', 'warning', or 'fail'.
	 */
	private function determine_status( int $score ): string {
		if ( $score >= 80 ) {
			return 'pass';
		}

		if ( $score >= 50 ) {
			return 'warning';
		}

		return 'fail';
	}
}
