<?php
/**
 * Livada SEO — AI Visibility Score
 *
 * Computes a single 0–100 "AI Visibility Readiness" score that quantifies how
 * well the site is prepared to be discovered, understood and cited by AI
 * engines (ChatGPT, Perplexity, Gemini, Claude, Copilot, Google AI Overviews).
 *
 * Design constraint: ZERO per-client external API cost. The score is derived
 * entirely from on-site signals the plugin already produces — the same signals
 * AthenaHQ/Profound charge $99–295/month to track. This is the concrete number
 * behind the "And by AI." promise.
 *
 * The result is cached in a 6-hour transient; the admin view can force a
 * refresh. Pure read-only computation — no frontend hooks, no extra SQL on the
 * public site.
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_AI_Visibility {

	/** Transient cache key for the computed score. */
	private const CACHE_KEY = 'livada_seo_ai_visibility';

	/** Cache lifetime (6 hours). */
	private const CACHE_TTL = 21600;

	/** Number of recent posts sampled for content-signal checks. */
	private const SAMPLE_SIZE = 12;

	// ── Public API ──────────────────────────────────────────────────────────────

	/**
	 * Returns the AI Visibility report, using the cached value when available.
	 *
	 * @param bool $force Bypass the cache and recompute.
	 * @return array{
	 *   score:int, grade:string, headline:string,
	 *   checks:array<int,array{key:string,label:string,status:string,points:int,max:int,detail:string,fix:string}>,
	 *   generated:string
	 * }
	 */
	public static function get_report( bool $force = false ): array {
		if ( ! $force ) {
			$cached = get_transient( self::CACHE_KEY );
			if ( is_array( $cached ) && isset( $cached['score'] ) ) {
				return $cached;
			}
		}

		$report = self::compute();
		set_transient( self::CACHE_KEY, $report, self::CACHE_TTL );

		return $report;
	}

	/** Clears the cached score (call after settings saves). */
	public static function flush(): void {
		delete_transient( self::CACHE_KEY );
	}

	// ── Computation ───────────────────────────────────────────────────────────

	/**
	 * Builds the full report from on-site signals.
	 *
	 * @return array<string,mixed>
	 */
	private static function compute(): array {
		$checks = [
			self::check_llms_txt(),
			self::check_knowledge_graph(),
			self::check_business_schema(),
			self::check_expertise(),
			self::check_eat_signals(),
			self::check_faq_coverage(),
			self::check_question_headings(),
			self::check_meta_descriptions(),
			self::check_sitemap(),
			self::check_max_snippet(),
			self::check_wikidata(),
		];

		$earned = 0;
		$total  = 0;
		foreach ( $checks as $c ) {
			$earned += $c['points'];
			$total  += $c['max'];
		}

		$score = $total > 0 ? (int) round( ( $earned / $total ) * 100 ) : 0;

		return [
			'score'     => $score,
			'grade'     => self::grade( $score ),
			'headline'  => self::headline( $score ),
			'checks'    => $checks,
			'generated' => gmdate( 'c' ),
		];
	}

	// ── Individual checks ───────────────────────────────────────────────────────

	/**
	 * llms.txt — the single strongest AI-discoverability signal and the one
	 * RankMath/Yoast do not provide. Worth the most points.
	 */
	private static function check_llms_txt(): array {
		$enabled = (bool) Livada_SEO_Loader::get_setting( 'aeo_llms_txt', true )
			&& (bool) Livada_SEO_Loader::get_setting( 'aeo_enabled', true );

		if ( $enabled ) {
			return self::result( 'llms_txt', __( 'llms.txt for AI crawlers', 'livada-seo' ), 'green', 20, 20,
				__( 'Your site serves /llms.txt — a curated map for AI engines. Most SEO plugins do not offer this.', 'livada-seo' ),
				''
			);
		}

		return self::result( 'llms_txt', __( 'llms.txt for AI crawlers', 'livada-seo' ), 'red', 0, 20,
			__( 'llms.txt is disabled. AI crawlers have no curated entry point to your key pages.', 'livada-seo' ),
			__( 'Enable llms.txt in General settings (AEO section).', 'livada-seo' )
		);
	}

	/** Knowledge Graph / Organization entity on the front page. */
	private static function check_knowledge_graph(): array {
		$kg   = (bool) Livada_SEO_Loader::get_setting( 'aeo_knowledge_graph', true );
		$name = trim( (string) Livada_SEO_Loader::get_setting( 'schema_name', '' ) );

		if ( $kg && '' !== $name ) {
			return self::result( 'knowledge_graph', __( 'Knowledge Graph entity', 'livada-seo' ), 'green', 12, 12,
				__( 'An Organization/Business entity is published so AI engines can identify who you are.', 'livada-seo' ),
				''
			);
		}

		$detail = '' === $name
			? __( 'No business name set — AI engines cannot anchor an entity for your site.', 'livada-seo' )
			: __( 'Knowledge Graph output is disabled.', 'livada-seo' );

		return self::result( 'knowledge_graph', __( 'Knowledge Graph entity', 'livada-seo' ), 'red', 0, 12,
			$detail,
			__( 'Set your Business Name in Schema settings and enable the Knowledge Graph entity.', 'livada-seo' )
		);
	}

	/** LocalBusiness schema completeness (address, geo, phone). */
	private static function check_business_schema(): array {
		$fields = [
			'schema_address' => Livada_SEO_Loader::get_setting( 'schema_address', '' ),
			'schema_city'    => Livada_SEO_Loader::get_setting( 'schema_city', '' ),
			'schema_phone'   => Livada_SEO_Loader::get_setting( 'schema_phone', '' ),
			'schema_geo_lat' => Livada_SEO_Loader::get_setting( 'schema_geo_lat', '' ),
			'schema_geo_lng' => Livada_SEO_Loader::get_setting( 'schema_geo_lng', '' ),
		];

		$filled = 0;
		foreach ( $fields as $v ) {
			if ( '' !== trim( (string) $v ) ) {
				$filled++;
			}
		}
		$total = count( $fields );

		if ( $filled === $total ) {
			return self::result( 'business_schema', __( 'Complete business schema', 'livada-seo' ), 'green', 12, 12,
				__( 'Address, city, phone and GPS coordinates are all set — maximum local + AI trust.', 'livada-seo' ),
				''
			);
		}
		if ( $filled >= 3 ) {
			return self::result( 'business_schema', __( 'Complete business schema', 'livada-seo' ), 'orange', 7, 12,
				sprintf( __( '%1$d of %2$d business fields filled. Add the rest for full trust signals.', 'livada-seo' ), $filled, $total ),
				__( 'Fill the missing address / phone / GPS fields in Schema settings.', 'livada-seo' )
			);
		}

		return self::result( 'business_schema', __( 'Complete business schema', 'livada-seo' ), 'red', 0, 12,
			sprintf( __( 'Only %1$d of %2$d business fields filled.', 'livada-seo' ), $filled, $total ),
			__( 'Complete address, city, phone and GPS in Schema settings.', 'livada-seo' )
		);
	}

	/** knowsAbout / areas of expertise — tells AI what you are an authority on. */
	private static function check_expertise(): array {
		$topics = Livada_SEO_Loader::get_setting( 'aeo_org_knows_about', [] );
		$count  = is_array( $topics ) ? count( array_filter( array_map( 'trim', array_map( 'strval', $topics ) ), 'strlen' ) ) : 0;

		if ( $count >= 5 ) {
			return self::result( 'expertise', __( 'Declared areas of expertise', 'livada-seo' ), 'green', 10, 10,
				sprintf( __( '%d expertise topics declared — AI engines know what to cite you for.', 'livada-seo' ), $count ),
				''
			);
		}
		if ( $count >= 1 ) {
			return self::result( 'expertise', __( 'Declared areas of expertise', 'livada-seo' ), 'orange', 5, 10,
				sprintf( __( '%d expertise topic(s) declared. Aim for 5+ specific topics.', 'livada-seo' ), $count ),
				__( 'Add more areas of expertise in General settings (AEO section).', 'livada-seo' )
			);
		}

		return self::result( 'expertise', __( 'Declared areas of expertise', 'livada-seo' ), 'red', 0, 10,
			__( 'No areas of expertise declared. AI engines cannot tell what you specialise in.', 'livada-seo' ),
			__( 'List 5+ topics you want to be cited for in General settings (AEO section).', 'livada-seo' )
		);
	}

	/** E-E-A-T signals: organization description + founder. */
	private static function check_eat_signals(): array {
		$desc    = trim( (string) Livada_SEO_Loader::get_setting( 'aeo_org_description', '' ) );
		$founder = trim( (string) Livada_SEO_Loader::get_setting( 'aeo_org_founder', '' ) );

		$score = ( '' !== $desc ? 1 : 0 ) + ( '' !== $founder ? 1 : 0 );

		if ( 2 === $score ) {
			return self::result( 'eat', __( 'E-E-A-T signals', 'livada-seo' ), 'green', 8, 8,
				__( 'Organization description and founder are set — strong experience/authority signals.', 'livada-seo' ),
				''
			);
		}
		if ( 1 === $score ) {
			return self::result( 'eat', __( 'E-E-A-T signals', 'livada-seo' ), 'orange', 4, 8,
				__( 'Partial E-E-A-T data. Add both an organization description and a founder name.', 'livada-seo' ),
				__( 'Complete the description and founder fields in General settings.', 'livada-seo' )
			);
		}

		return self::result( 'eat', __( 'E-E-A-T signals', 'livada-seo' ), 'red', 0, 8,
			__( 'No organization description or founder set.', 'livada-seo' ),
			__( 'Add an organization description and founder name in General settings.', 'livada-seo' )
		);
	}

	/** FAQ schema coverage — Q&A is the format AI engines extract most readily. */
	private static function check_faq_coverage(): array {
		$count = (int) self::count_meta_flag( '_livada_seo_faq_schema' );

		if ( $count >= 3 ) {
			return self::result( 'faq', __( 'FAQ structured data', 'livada-seo' ), 'green', 10, 10,
				sprintf( __( '%d page(s) expose FAQ schema — directly answerable by AI.', 'livada-seo' ), $count ),
				''
			);
		}
		if ( $count >= 1 ) {
			return self::result( 'faq', __( 'FAQ structured data', 'livada-seo' ), 'orange', 5, 10,
				sprintf( __( '%d page(s) with FAQ schema. Add FAQs to more key pages.', 'livada-seo' ), $count ),
				__( 'Enable FAQ schema on more pages via the SEO metabox.', 'livada-seo' )
			);
		}

		return self::result( 'faq', __( 'FAQ structured data', 'livada-seo' ), 'red', 0, 10,
			__( 'No pages expose FAQ schema. AI engines love extractable Q&A.', 'livada-seo' ),
			__( 'Add an FAQ block and enable FAQ schema on your money pages.', 'livada-seo' )
		);
	}

	/** Question-format headings across a sample of recent content. */
	private static function check_question_headings(): array {
		$sample = self::sample_post_ids();

		if ( empty( $sample ) ) {
			return self::result( 'question_headings', __( 'Question-format headings', 'livada-seo' ), 'orange', 4, 8,
				__( 'Not enough published content to evaluate.', 'livada-seo' ),
				__( 'Publish content using question-format H2/H3 headings.', 'livada-seo' )
			);
		}

		$with_questions = 0;
		foreach ( $sample as $pid ) {
			$content = apply_filters( 'the_content', (string) get_post_field( 'post_content', $pid ) );
			if ( preg_match_all( '/<h[23][^>]*>(.*?)<\/h[23]>/is', $content, $m ) ) {
				foreach ( $m[1] as $h ) {
					if ( '?' === substr( trim( wp_strip_all_tags( $h ) ), -1 ) ) {
						$with_questions++;
						break;
					}
				}
			}
		}

		$ratio = $with_questions / count( $sample );

		if ( $ratio >= 0.5 ) {
			return self::result( 'question_headings', __( 'Question-format headings', 'livada-seo' ), 'green', 8, 8,
				sprintf( __( '%1$d of %2$d sampled pages use question headings — AI can surface them as answers.', 'livada-seo' ), $with_questions, count( $sample ) ),
				''
			);
		}
		if ( $with_questions > 0 ) {
			return self::result( 'question_headings', __( 'Question-format headings', 'livada-seo' ), 'orange', 4, 8,
				sprintf( __( 'Only %1$d of %2$d sampled pages use question headings.', 'livada-seo' ), $with_questions, count( $sample ) ),
				__( 'Phrase more H2/H3 headings as the questions visitors actually ask.', 'livada-seo' )
			);
		}

		return self::result( 'question_headings', __( 'Question-format headings', 'livada-seo' ), 'red', 0, 8,
			__( 'No question-format headings found in sampled content.', 'livada-seo' ),
			__( 'Add H2/H3 headings phrased as visitor questions (ending with "?").', 'livada-seo' )
		);
	}

	/** Meta description coverage across the sample. */
	private static function check_meta_descriptions(): array {
		$sample = self::sample_post_ids();

		if ( empty( $sample ) ) {
			return self::result( 'meta_desc', __( 'Meta description coverage', 'livada-seo' ), 'orange', 3, 6,
				__( 'Not enough content to evaluate.', 'livada-seo' ),
				__( 'Write concise meta descriptions for your key pages.', 'livada-seo' )
			);
		}

		$with_desc = 0;
		foreach ( $sample as $pid ) {
			$d = (string) get_post_meta( $pid, '_livada_seo_description', true );
			if ( '' === trim( $d ) ) {
				$d = (string) get_the_excerpt( $pid );
			}
			if ( '' !== trim( $d ) ) {
				$with_desc++;
			}
		}

		$ratio = $with_desc / count( $sample );

		if ( $ratio >= 0.8 ) {
			return self::result( 'meta_desc', __( 'Meta description coverage', 'livada-seo' ), 'green', 6, 6,
				sprintf( __( '%1$d of %2$d sampled pages have a summary AI can quote.', 'livada-seo' ), $with_desc, count( $sample ) ),
				''
			);
		}
		if ( $ratio >= 0.4 ) {
			return self::result( 'meta_desc', __( 'Meta description coverage', 'livada-seo' ), 'orange', 3, 6,
				sprintf( __( '%1$d of %2$d sampled pages have a usable summary.', 'livada-seo' ), $with_desc, count( $sample ) ),
				__( 'Add meta descriptions to the remaining pages.', 'livada-seo' )
			);
		}

		return self::result( 'meta_desc', __( 'Meta description coverage', 'livada-seo' ), 'red', 0, 6,
			__( 'Most sampled pages lack a concise summary for AI to quote.', 'livada-seo' ),
			__( 'Write meta descriptions for your important pages.', 'livada-seo' )
		);
	}

	/** XML sitemap present — the discovery backbone. */
	private static function check_sitemap(): array {
		$enabled = self::module_enabled( 'sitemap' );

		if ( $enabled ) {
			return self::result( 'sitemap', __( 'XML sitemap', 'livada-seo' ), 'green', 6, 6,
				__( 'An XML sitemap exposes every page to crawlers, including AI bots.', 'livada-seo' ),
				''
			);
		}

		return self::result( 'sitemap', __( 'XML sitemap', 'livada-seo' ), 'red', 0, 6,
			__( 'The XML sitemap module is disabled.', 'livada-seo' ),
			__( 'Enable the Sitemap module in General settings.', 'livada-seo' )
		);
	}

	/** max-snippet directive — lets AI engines quote full passages. */
	private static function check_max_snippet(): array {
		$on = (bool) Livada_SEO_Loader::get_setting( 'aeo_max_snippet', true )
			&& (bool) Livada_SEO_Loader::get_setting( 'aeo_enabled', true );

		if ( $on ) {
			return self::result( 'max_snippet', __( 'Snippet directives', 'livada-seo' ), 'green', 8, 8,
				__( 'max-snippet directives allow AI engines to quote full passages from your pages.', 'livada-seo' ),
				''
			);
		}

		return self::result( 'max_snippet', __( 'Snippet directives', 'livada-seo' ), 'red', 0, 8,
			__( 'Snippet directives are off — AI engines may truncate quotes from your content.', 'livada-seo' ),
			__( 'Enable max-snippet directives in General settings (AEO section).', 'livada-seo' )
		);
	}

	/**
	 * Wikidata entity link — the first off-site identity signal. When the
	 * site declares its Wikidata Q-id, the schema sameAs array is augmented
	 * with the canonical entity URL, anchoring the business in the same
	 * structured knowledge base that ChatGPT, Perplexity, Gemini and Google
	 * AI Overviews rely on to identify and cite real-world entities.
	 *
	 * Zero external HTTP call — purely setting-driven, matching the plugin's
	 * "no per-client API cost" design constraint.
	 */
	private static function check_wikidata(): array {
		$raw = trim( (string) Livada_SEO_Loader::get_setting( 'aeo_wikidata_id', '' ) );

		if ( '' !== $raw && preg_match( '/^Q\d+$/', $raw ) ) {
			$url    = 'https://www.wikidata.org/wiki/' . $raw;
			$detail = sprintf(
				/* translators: 1: Wikidata Q-id, 2: Wikidata entity URL */
				__( 'Your Wikidata entity %1$s is declared and auto-linked in your schema sameAs. AI engines can now anchor your business to a verified structured-data record. View on Wikidata: %2$s', 'livada-seo' ),
				$raw,
				$url
			);
			return self::result( 'wikidata', __( 'Wikidata entity (off-site identity)', 'livada-seo' ), 'green', 8, 8,
				$detail,
				''
			);
		}

		if ( '' !== $raw ) {
			return self::result( 'wikidata', __( 'Wikidata entity (off-site identity)', 'livada-seo' ), 'orange', 0, 8,
				__( 'A Wikidata identifier is set but does not match the expected format (Q followed by digits, e.g. Q140007358).', 'livada-seo' ),
				__( 'Fix the Wikidata entity ID in General settings (AEO section). Copy the Q-id from the URL of your Wikidata page.', 'livada-seo' )
			);
		}

		return self::result( 'wikidata', __( 'Wikidata entity (off-site identity)', 'livada-seo' ), 'red', 0, 8,
			__( 'No Wikidata entity declared. AI engines have no off-site structured record to identify your business.', 'livada-seo' ),
			__( 'Create a Wikidata page for your business, then paste its Q-id (e.g. Q140007358) in General settings (AEO section). See the Livada SEO guide for details.', 'livada-seo' )
		);
	}

	// ── Helpers ───────────────────────────────────────────────────────────────

	/**
	 * Returns up to SAMPLE_SIZE published post/page IDs (pages first, then posts),
	 * cached per-request via a static.
	 *
	 * @return array<int,int>
	 */
	private static function sample_post_ids(): array {
		static $ids = null;
		if ( null !== $ids ) {
			return $ids;
		}

		$ids = get_posts( [
			'post_type'     => [ 'page', 'post' ],
			'post_status'   => 'publish',
			'numberposts'   => self::SAMPLE_SIZE,
			'orderby'       => 'modified',
			'order'         => 'DESC',
			'no_found_rows' => true,
			'fields'        => 'ids',
		] );

		$ids = array_map( 'intval', (array) $ids );
		return $ids;
	}

	/**
	 * Counts published posts/pages carrying a truthy boolean meta flag.
	 *
	 * @param string $meta_key
	 * @return int
	 */
	private static function count_meta_flag( string $meta_key ): int {
		$ids = get_posts( [
			'post_type'     => [ 'page', 'post' ],
			'post_status'   => 'publish',
			'numberposts'   => 200,
			'no_found_rows' => true,
			'fields'        => 'ids',
			'meta_query'    => [
				[
					'key'     => $meta_key,
					'value'   => [ '1', 'true', 'yes', 'on' ],
					'compare' => 'IN',
				],
			],
		] );

		return is_array( $ids ) ? count( $ids ) : 0;
	}

	/** Whether a feature module is enabled in settings. */
	private static function module_enabled( string $key ): bool {
		$modules = Livada_SEO_Loader::get_setting( 'modules', [] );
		return is_array( $modules ) && in_array( $key, $modules, true );
	}

	/**
	 * Builds a single check result row.
	 *
	 * @return array<string,mixed>
	 */
	private static function result( string $key, string $label, string $status, int $points, int $max, string $detail, string $fix ): array {
		return [
			'key'    => $key,
			'label'  => $label,
			'status' => $status,
			'points' => $points,
			'max'    => $max,
			'detail' => $detail,
			'fix'    => $fix,
		];
	}

	/** Maps a numeric score to a letter grade. */
	private static function grade( int $score ): string {
		if ( $score >= 90 ) { return 'A'; }
		if ( $score >= 75 ) { return 'B'; }
		if ( $score >= 55 ) { return 'C'; }
		if ( $score >= 35 ) { return 'D'; }
		return 'E';
	}

	/** Returns the human headline shown under the gauge. */
	private static function headline( int $score ): string {
		if ( $score >= 90 ) {
			return __( 'Excellent — your site is fully prepared to be found and cited by AI.', 'livada-seo' );
		}
		if ( $score >= 75 ) {
			return __( 'Strong — AI engines can find and quote you. A few gaps remain.', 'livada-seo' );
		}
		if ( $score >= 55 ) {
			return __( 'Fair — the foundations are there but AI visibility is leaking points.', 'livada-seo' );
		}
		if ( $score >= 35 ) {
			return __( 'Weak — AI engines struggle to understand and cite your site.', 'livada-seo' );
		}
		return __( 'Critical — your site is nearly invisible to AI engines. Start with the fixes below.', 'livada-seo' );
	}
}
