<?php
/**
 * Runs local checks (and optional AI enrichment) on a post.
 *
 * @package WPLens
 */

namespace WPLens\Scans;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

final class ScanRunner {

	/**
	 * Run a scan against a post. Returns the persisted scan ID.
	 *
	 * @param int  $post_id
	 * @param bool $with_ai Whether to enrich with AI provider call.
	 */
	public function scan_post( int $post_id, bool $with_ai = false ): array {
		$post = get_post( $post_id );
		if ( ! $post instanceof \WP_Post ) {
			return array(
				'ok'    => false,
				'error' => __( 'Post not found.', 'lens-ai-visibility-monitor' ),
			);
		}

		$checks = $this->run_local_checks( $post );

		$ai_text   = '';
		$ai_score  = null;
		$provider  = null;

		if ( $with_ai ) {
			$prov = \WPLens\AI\ProviderRegistry::default();
			if ( null !== $prov && $prov->is_configured() ) {
				$ai = $this->run_ai_enrichment( $prov, $post, $checks );
				if ( $ai['ok'] ) {
					$ai_text  = $ai['text'];
					$ai_score = $ai['score'];
					$provider = $prov->get_slug();
				} else {
					$checks['ai_error'] = $ai['error'];
				}
			} else {
				$checks['ai_error'] = __( 'No AI provider configured. Add an API key in Lens → Settings → AI Providers.', 'lens-ai-visibility-monitor' );
			}
		}

		$local_score = $this->compute_local_score( $checks );
		$final_score = null !== $ai_score ? (int) round( ( $local_score + $ai_score ) / 2 ) : $local_score;

		$findings = array(
			'local'    => $checks,
			'ai_text'  => $ai_text,
			'ai_score' => $ai_score,
		);

		$repo = new ScanRepository();
		$id   = $repo->insert(
			array(
				'post_id'     => $post_id,
				'scan_type'   => $with_ai && null !== $provider ? 'ai-enriched' : 'local',
				'ai_provider' => $provider,
				'score'       => $final_score,
				'findings'    => $findings,
			)
		);

		return array(
			'ok'       => true,
			'scan_id'  => $id,
			'score'    => $final_score,
			'findings' => $findings,
		);
	}

	/**
	 * Run all local, non-AI checks. Returns an associative array of result rows.
	 */
	private function run_local_checks( \WP_Post $post ): array {
		$content = (string) $post->post_content;
		$title   = (string) $post->post_title;
		$url     = (string) get_permalink( $post );

		$out = array();

		$out['title_present'] = $this->result(
			'' !== trim( $title ) && mb_strlen( $title ) <= 70,
			__( 'Page title is present and 70 characters or fewer.', 'lens-ai-visibility-monitor' ),
			__( 'Title is missing or longer than 70 characters.', 'lens-ai-visibility-monitor' )
		);

		$meta_desc = $this->meta_description( $post );
		$out['meta_description'] = $this->result(
			'' !== $meta_desc && mb_strlen( $meta_desc ) <= 160,
			__( 'Meta description present and 160 characters or fewer.', 'lens-ai-visibility-monitor' ),
			__( 'Meta description missing or too long.', 'lens-ai-visibility-monitor' )
		);

		$h1_count = preg_match_all( '#<h1\b#i', $content );
		$out['h1_uniqueness'] = $this->result(
			$h1_count <= 1,
			__( 'Single H1 (or H1 set via theme).', 'lens-ai-visibility-monitor' ),
			__( 'Multiple H1 tags detected in content.', 'lens-ai-visibility-monitor' )
		);

		$h2_count = preg_match_all( '#<h2\b#i', $content );
		$h3_count = preg_match_all( '#<h3\b#i', $content );
		$out['heading_hierarchy'] = $this->result(
			$h2_count > 0,
			sprintf( /* translators: %1$d H2 count, %2$d H3 count */ __( '%1$d H2 and %2$d H3 headings found.', 'lens-ai-visibility-monitor' ), (int) $h2_count, (int) $h3_count ),
			__( 'No H2 headings found — break long content into sections.', 'lens-ai-visibility-monitor' )
		);

		$has_faq = false !== stripos( $content, 'FAQPage' ) || false !== stripos( $content, 'Question' );
		$out['faq_schema'] = $this->result(
			$has_faq,
			__( 'FAQ markup detected.', 'lens-ai-visibility-monitor' ),
			__( 'No FAQ schema detected. Adding a Q&A block can improve AI citability.', 'lens-ai-visibility-monitor' )
		);

		$has_howto = false !== stripos( $content, 'HowTo' );
		$out['howto_schema'] = $this->result(
			$has_howto,
			__( 'HowTo markup detected.', 'lens-ai-visibility-monitor' ),
			__( 'No HowTo schema — only relevant for step-by-step content.', 'lens-ai-visibility-monitor' ),
			'info'
		);

		$has_article = false !== stripos( $content, 'Article' ) || false !== stripos( $content, 'BlogPosting' );
		$out['article_schema'] = $this->result(
			$has_article,
			__( 'Article schema markup detected.', 'lens-ai-visibility-monitor' ),
			__( 'No Article schema detected.', 'lens-ai-visibility-monitor' )
		);

		// Image alt coverage.
		preg_match_all( '#<img\b[^>]*>#i', $content, $imgs );
		$total = count( $imgs[0] );
		$with_alt = 0;
		foreach ( $imgs[0] as $img ) {
			if ( preg_match( '#\balt\s*=\s*"[^"]+"#i', $img ) || preg_match( "#\balt\s*=\s*'[^']+'#i", $img ) ) {
				$with_alt++;
			}
		}
		$pct = $total > 0 ? (int) round( ( $with_alt / $total ) * 100 ) : 100;
		$out['image_alt_coverage'] = $this->result(
			$pct >= 90,
			sprintf( /* translators: %d alt coverage percent */ __( '%d%% of images have alt text.', 'lens-ai-visibility-monitor' ), $pct ),
			sprintf( /* translators: %d alt coverage percent */ __( 'Only %d%% of images have alt text.', 'lens-ai-visibility-monitor' ), $pct )
		);

		$out['llms_txt_present'] = $this->result(
			$this->llms_txt_present(),
			__( 'llms.txt found at site root.', 'lens-ai-visibility-monitor' ),
			__( 'No llms.txt at site root. Consider publishing one (the Website LLMs.txt plugin can help).', 'lens-ai-visibility-monitor' )
		);

		// Internal link density.
		$home = (string) wp_parse_url( home_url(), PHP_URL_HOST );
		preg_match_all( '#<a\b[^>]*href=["\']([^"\']+)["\']#i', $content, $links );
		$internal = 0;
		foreach ( $links[1] as $href ) {
			$h = wp_parse_url( $href, PHP_URL_HOST );
			if ( '' === (string) $h || strtolower( (string) $h ) === strtolower( $home ) ) {
				$internal++;
			}
		}
		$out['internal_links'] = $this->result(
			$internal >= 2,
			sprintf( /* translators: %d internal link count */ __( '%d internal links found.', 'lens-ai-visibility-monitor' ), $internal ),
			__( 'Fewer than 2 internal links — add some to help bots discover related content.', 'lens-ai-visibility-monitor' )
		);

		// Answer-shaped first paragraph (heuristic).
		$first_p = '';
		if ( preg_match( '#<p\b[^>]*>(.*?)</p>#is', $content, $m ) ) {
			$first_p = wp_strip_all_tags( $m[1] );
		}
		$ans_shaped = '' !== $first_p && (
			preg_match( '#\b(is|are|means|refers to)\b#i', $first_p )
			|| substr( trim( $first_p ), -1 ) === '?'
		);
		$out['answer_shaped_intro'] = $this->result(
			$ans_shaped,
			__( 'Opening paragraph reads like an answer.', 'lens-ai-visibility-monitor' ),
			__( 'Opening paragraph is not answer-shaped. Start with a definition or direct answer.', 'lens-ai-visibility-monitor' ),
			'info'
		);

		return $out;
	}

	/**
	 * @param array<string, array<string, mixed>> $checks
	 */
	private function compute_local_score( array $checks ): int {
		$weights = array(
			'title_present'       => 8,
			'meta_description'    => 10,
			'h1_uniqueness'       => 6,
			'heading_hierarchy'   => 8,
			'faq_schema'          => 12,
			'howto_schema'        => 2,
			'article_schema'      => 10,
			'image_alt_coverage'  => 10,
			'llms_txt_present'    => 8,
			'internal_links'      => 8,
			'answer_shaped_intro' => 8,
		);
		$max  = array_sum( $weights );
		$earned = 0;
		foreach ( $weights as $key => $w ) {
			if ( isset( $checks[ $key ]['pass'] ) && $checks[ $key ]['pass'] ) {
				$earned += $w;
			}
		}
		return (int) round( ( $earned / max( 1, $max ) ) * 100 );
	}

	private function run_ai_enrichment( \WPLens\AI\Provider $prov, \WP_Post $post, array $checks ): array {
		$body = wp_strip_all_tags( $post->post_content );
		$body = mb_substr( $body, 0, 4000 );

		$system = 'You are an Answer Engine Optimisation reviewer. Given a web page, you produce: (1) a numeric AEO citability score from 0 to 100, (2) three concrete, prioritised fixes the author can make to increase the chance an AI search engine will cite this page. Be specific and reference the actual content. Output the score on the first line as "Score: NN" then the three fixes as a numbered list.';

		$user = "Title: {$post->post_title}\nURL: " . get_permalink( $post ) . "\n\nContent:\n{$body}";

		$res = $prov->generate( $system, $user, array( 'max_tokens' => 700, 'temperature' => 0.2 ) );
		if ( ! $res->ok ) {
			return array(
				'ok'    => false,
				'error' => $res->error_message ?? __( 'AI call failed.', 'lens-ai-visibility-monitor' ),
			);
		}

		$score = null;
		if ( preg_match( '#Score\s*:\s*(\d{1,3})#i', $res->text, $m ) ) {
			$score = max( 0, min( 100, (int) $m[1] ) );
		}

		return array(
			'ok'    => true,
			'text'  => $res->text,
			'score' => $score,
		);
	}

	private function meta_description( \WP_Post $post ): string {
		// Yoast / Rank Math / AIOSEO common keys, then fall back to excerpt.
		$candidates = array(
			'_yoast_wpseo_metadesc',
			'rank_math_description',
			'_aioseo_description',
			'_genesis_description',
		);
		foreach ( $candidates as $k ) {
			$v = get_post_meta( $post->ID, $k, true );
			if ( is_string( $v ) && '' !== trim( $v ) ) {
				return $v;
			}
		}
		return (string) $post->post_excerpt;
	}

	private function llms_txt_present(): bool {
		$key   = 'wplens_llms_txt_present';
		$cached = get_transient( $key );
		if ( false !== $cached ) {
			return (bool) $cached;
		}

		$url      = trailingslashit( home_url() ) . 'llms.txt';
		$response = wp_remote_head( $url, array( 'timeout' => 5, 'redirection' => 1 ) );
		$ok       = ! is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response );

		set_transient( $key, $ok ? 1 : 0, 6 * HOUR_IN_SECONDS );
		return $ok;
	}

	private function result( bool $pass, string $pass_msg, string $fail_msg, string $severity = 'normal' ): array {
		return array(
			'pass'     => $pass,
			'message'  => $pass ? $pass_msg : $fail_msg,
			'severity' => $severity,
		);
	}
}
