<?php

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

require_once __DIR__ . '/class-content-miner-analysis-content.php';
require_once __DIR__ . '/class-content-miner-collision-recommendation.php';
require_once __DIR__ . '/class-content-miner-scoring-exclusions.php';

/**
 * Dependency-free analysis engine. It intentionally produces advisory signals,
 * not destructive actions.
 */
class Content_Miner_Analyzer {
	const PAIR_THRESHOLD = 0.40;
	const THIN_PAGE_REASON = 'This is a thin page. Traffic, conversions, backlinks, and business purpose are needed before deciding whether to expand or retire it.';
	const HIDDEN_CONTENT_REASON = 'This theme stores page content outside standard WordPress fields, so Content Miner cannot reliably judge how much is on this page. Open the page yourself before deciding whether it needs work.';

	/** @var string[] */
	private $stopwords = array(
		'a', 'about', 'after', 'all', 'also', 'an', 'and', 'are', 'as', 'at', 'be',
		'been', 'but', 'by', 'can', 'do', 'for', 'from', 'had', 'has', 'have', 'how',
		'i', 'if', 'in', 'into', 'is', 'it', 'its', 'more', 'not', 'of', 'on', 'or',
		'our', 'so', 'than', 'that', 'the', 'their', 'then', 'there', 'these', 'they',
		'this', 'to', 'up', 'was', 'we', 'what', 'when', 'where', 'which', 'who',
		'will', 'with', 'you', 'your',
	);

	/** Prepare one item for bounded full-library analysis. @return array<string,mixed> */
	public function prepare_for_analysis( array $item, array $locations = array() ) {
		return $this->prepare_item( $item, $locations );
	}

	/**
	 * Reconcile the stored-word-count recommendation with rendered verification
	 * without changing recommendations produced by any other signal.
	 *
	 * @param array<string,string> $recommendation Existing recommendation.
	 * @param array<string,mixed>  $verification Persisted rendered verification.
	 * @param string               $analysis_content_source Selected analysis body.
	 * @return array<string,string>
	 */
	public function qualify_recommendation( array $recommendation, array $verification, $analysis_content_source = 'stored_fallback' ) {
		$stored_words   = isset( $verification['stored_words'] ) ? (int) $verification['stored_words'] : 0;
		$rendered_words = isset( $verification['rendered_words'] ) ? (int) $verification['rendered_words'] : 0;
		$reason         = $recommendation['reason'] ?? '';
		$hidden_content = 'flagged' === ( $verification['status'] ?? '' )
			&& $rendered_words > $stored_words * 1.40;

		if ( 'rendered' === $analysis_content_source ) {
			return $recommendation;
		}

		if ( ! in_array( $reason, array( self::THIN_PAGE_REASON, self::HIDDEN_CONTENT_REASON ), true ) ) {
			return $recommendation;
		}

		if ( $hidden_content ) {
			return array(
				'action'     => 'Review',
				'confidence' => 'Low',
				'reason'     => self::HIDDEN_CONTENT_REASON,
			);
		}

		return array(
			'action'     => 'Review',
			'confidence' => 'Medium',
			'reason'     => self::THIN_PAGE_REASON,
		);
	}

	/** Build a deterministic, frequency-ranked lexical signature. @return string[] */
	public function build_lexical_signature( $title, $content, $limit = 150 ) {
		$normalized = $this->normalize_text( (string) $title . ' ' . (string) $content );
		if ( '' === $normalized ) {
			return array();
		}
		$tokens = preg_split( '/\s+/u', $normalized, -1, PREG_SPLIT_NO_EMPTY );
		$counts = array();
		foreach ( is_array( $tokens ) ? $tokens : array() as $token ) {
			if ( strlen( $token ) < 2 || in_array( $token, $this->stopwords, true ) ) {
				continue;
			}
			$counts[ $token ] = isset( $counts[ $token ] ) ? $counts[ $token ] + 1 : 1;
		}
		uksort(
			$counts,
			function ( $left, $right ) use ( $counts ) {
				if ( $counts[ $left ] === $counts[ $right ] ) {
					return strcmp( $left, $right );
				}
				return $counts[ $left ] > $counts[ $right ] ? -1 : 1;
			}
		);
		return array_slice( array_keys( $counts ), 0, max( 1, (int) $limit ) );
	}

	/** Compare two persisted signatures. @param string[] $left Left. @param string[] $right Right. @return float */
	public function signature_overlap( array $left, array $right ) {
		return $this->jaccard( $left, $right );
	}

	/** Compare two prepared items using the unchanged 0.6.1 lexical rules. @return array<string,mixed>|null */
	public function compare_prepared( array $left, array $right ) {
		$exact = ! empty( $left['normalized'] ) && $left['normalized'] === $right['normalized'];
		$score = $exact ? 1.0 : $this->item_similarity( $left, $right );
		if ( $score < self::PAIR_THRESHOLD ) {
			return null;
		}
		return $this->build_lexical_pair( $left, $right, $score, $exact );
	}

	/**
	 * Analyze a site inventory.
	 *
	 * @param array<int,array<string,mixed>> $items Content items.
	 * @param string[]                       $locations Business locations.
	 * @return array<string,mixed>
	 */
	public function analyze_inventory( array $items, array $locations = array() ) {
		$prepared           = array();
		$scored_prepared    = array();
		$has_scoring_context = false;

		foreach ( $items as $item ) {
			$has_scoring_context = $has_scoring_context || ( isset( $item['scoring'] ) && is_array( $item['scoring'] ) );
			$prepared_item = $this->prepare_item( $item, $locations );
			$prepared[] = $prepared_item;
			if ( self::item_is_in_scope( $item ) ) {
				$scored_prepared[] = $prepared_item;
			}
		}

		$overlap_pairs_total = 0;
		$pairs               = $this->find_pairs( $scored_prepared, $overlap_pairs_total );
		$pair_counts         = array();

		foreach ( $pairs as $pair ) {
			foreach ( array( $pair['left_id'], $pair['right_id'] ) as $id ) {
				$pair_counts[ $id ] = isset( $pair_counts[ $id ] ) ? $pair_counts[ $id ] + 1 : 1;
			}
		}

		$summary = array(
			'keep'                => 0,
			'refresh'             => 0,
			'localize'            => 0,
			'review'              => 0,
			'overlap_pairs'       => count( $pairs ),
			'overlap_pairs_total' => $overlap_pairs_total,
		);
		if ( $has_scoring_context ) {
			$summary['scored']            = count( $scored_prepared );
			$summary['excluded']          = count( $prepared ) - count( $scored_prepared );
			$summary['exclusion_reasons'] = array();
		}

		foreach ( $prepared as &$item ) {
			$item['overlap_count'] = isset( $pair_counts[ $item['id'] ] ) ? $pair_counts[ $item['id'] ] : 0;
			if ( ! self::item_is_in_scope( $item ) ) {
				$reason = isset( $item['scoring']['primary_reason'] ) ? sanitize_key( $item['scoring']['primary_reason'] ) : '';
				if ( $reason ) {
					$summary['exclusion_reasons'][ $reason ] = isset( $summary['exclusion_reasons'][ $reason ] ) ? $summary['exclusion_reasons'][ $reason ] + 1 : 1;
				}
				continue;
			}
			$key = strtolower( $item['recommendation']['action'] );
			if ( isset( $summary[ $key ] ) ) {
				++$summary[ $key ];
			}
		}
		unset( $item );

		return array(
			'generated_at' => gmdate( 'c' ),
			'item_count'   => count( $prepared ),
			'items'        => $prepared,
			'pairs'        => $pairs,
			'summary'      => $summary,
		);
	}

	/**
	 * Check an editorial idea against the inventory.
	 *
	 * @param string                          $idea Idea or brief.
	 * @param string                          $location Optional target location.
	 * @param array<int,array<string,mixed>>  $items Inventory.
	 * @return array<string,mixed>
	 */
	public function analyze_idea( $idea, $location, array $items ) {
		$matches = array();

		foreach ( $items as $item ) {
			if ( ! self::item_is_in_scope( $item ) ) {
				continue;
			}
			$score = $this->idea_similarity( $idea, $item );
			if ( $score >= 0.18 ) {
				$matches[] = array(
					'id'       => (int) $item['id'],
					'title'    => $item['title'],
					'edit_url' => isset( $item['edit_url'] ) ? $item['edit_url'] : '',
					'url'      => isset( $item['url'] ) ? $item['url'] : '',
					'score'    => $score,
				);
			}
		}

		usort(
			$matches,
			function ( $a, $b ) {
				return $b['score'] <=> $a['score'];
			}
		);
		$matches = array_slice( $matches, 0, 5 );
		$top     = isset( $matches[0] ) ? $matches[0] : null;

		if ( $top && $top['score'] >= 0.72 ) {
			$decision = array(
				'action'     => 'Update',
				'confidence' => 'High',
				'reason'     => 'This idea closely matches an existing piece. Strengthening that page is safer than creating another competing URL.',
			);
		} elseif ( '' !== trim( $location ) && $top && $top['score'] >= 0.34 ) {
			$decision = array(
				'action'     => 'Localize',
				'confidence' => $top['score'] >= 0.55 ? 'High' : 'Medium',
				'reason'     => 'An existing general piece can anchor a location-specific version. The localized page must contain materially useful local information.',
			);
		} elseif ( $top && $top['score'] >= 0.48 ) {
			$decision = array(
				'action'     => 'Differentiate',
				'confidence' => 'Medium',
				'reason'     => 'Related content already exists. A new page needs a distinct audience, intent, or outcome before publication.',
			);
		} else {
			$decision = array(
				'action'     => 'Create',
				'confidence' => $top ? 'Medium' : 'High',
				'reason'     => 'No strong collision was found in the current WordPress inventory.',
			);
		}

		return array(
			'idea'     => $idea,
			'location' => $location,
			'decision' => $decision,
			'matches'  => $matches,
		);
	}


	/** Legacy or malformed items fail toward inclusion. */
	private static function item_is_in_scope( array $item ) {
		return Content_Miner_Scoring_Exclusions::is_in_scope( $item );
	}

	/**
	 * Normalize and classify an item.
	 *
	 * @param array<string,mixed> $item Item.
	 * @param string[]            $locations Locations.
	 * @return array<string,mixed>
	 */
	private function prepare_item( array $item, array $locations ) {
		$analysis_content = Content_Miner_Analysis_Content::from_item( $item );
		$plain      = $this->normalize_text( $analysis_content['body'] );
		$word_count = $this->count_words( $plain );
		$age_days   = $this->age_in_days( isset( $item['modified_at'] ) ? $item['modified_at'] : '' );
		$item       = array_merge(
			$item,
			array(
				'normalized'  => $plain,
				'title_tokens'=> $this->tokenize( $analysis_content['title'] ),
				'body_tokens' => $this->tokenize( $plain ),
				'word_count'  => $word_count,
				'age_days'    => $age_days,
			)
		);

		$recommendation = $this->recommend_item( $item, $locations );
		$item['recommendation'] = $this->qualify_recommendation(
			$recommendation,
			isset( $item['verification'] ) && is_array( $item['verification'] ) ? $item['verification'] : array(),
			isset( $item['analysis_content_source'] ) ? $item['analysis_content_source'] : 'stored_fallback'
		);
		$has_search    = ! empty( $item['search_console'] );
		$has_analytics = ! empty( $item['analytics'] );
		$item['data_confidence'] = $has_search && $has_analytics ? 'Full evidence' : ( $has_search ? 'Search verified' : ( $has_analytics ? 'Engagement verified' : 'Content only' ) );

		return $item;
	}

	/**
	 * Make a conservative recommendation using only locally available signals.
	 *
	 * @param array<string,mixed> $item Item.
	 * @param string[]            $locations Locations.
	 * @return array<string,string>
	 */
	private function recommend_item( array $item, array $locations ) {
		$search = isset( $item['search_console'] ) && is_array( $item['search_console'] ) ? $item['search_console'] : array();
		$analytics = isset( $item['analytics'] ) && is_array( $item['analytics'] ) ? $item['analytics'] : array();
		if ( 'publish' !== $item['status'] ) {
			return array(
				'action'     => 'Review',
				'confidence' => 'High',
				'reason'     => 'This content is not currently published, so it should be reviewed before it influences the live editorial plan.',
			);
		}

		if ( ! empty( $item['extraction_warnings'] ) ) {
			return array(
				'action'     => 'Review',
				'confidence' => 'Low',
				'reason'     => 'Stored builder data was found but could not be fully read, so Content Miner cannot reliably judge how much is on this page. Open the page yourself before deciding whether it needs work.',
			);
		}

		if ( ! empty( $search['impressions'] ) && (float) ( $search['ctr'] ?? 0 ) < 0.01 && (int) $search['impressions'] >= 250 ) {
			return array(
				'action'     => 'Refresh',
				'confidence' => 'High',
				'reason'     => 'Search Console shows meaningful visibility but a click-through rate below 1% in the latest 28-day period. Review the query intent, title, description, and page promise.',
			);
		}

		if ( (int) ( $analytics['sessions'] ?? 0 ) >= 50 && (float) ( $analytics['engagement_rate'] ?? 0 ) < 0.35 ) {
			return array(
				'action'     => 'Refresh',
				'confidence' => 'High',
				'reason'     => 'Google Analytics shows meaningful landing-page traffic but an engagement rate below 35% in the latest 28-day period. Review intent alignment, page experience, calls to action, and content usefulness.',
			);
		}

		if ( $item['word_count'] < 250 ) {
			return array(
				'action'     => 'Review',
				'confidence' => 'Medium',
				'reason'     => self::THIN_PAGE_REASON,
			);
		}

		if ( $item['age_days'] > 540 ) {
			return array(
				'action'     => 'Refresh',
				'confidence' => 'Medium',
				'reason'     => 'This page has not been modified in more than 18 months. Check facts, search intent, links, and performance before changing it.',
			);
		}

		if ( $this->is_localization_candidate( $item, $locations ) ) {
			return array(
				'action'     => 'Localize',
				'confidence' => 'Low',
				'reason'     => 'This substantial evergreen piece does not mention a configured market. Validate local demand and add genuinely local facts before creating a separate URL.',
			);
		}

		$keep_reason = 'No structural concern was found from WordPress data alone. Performance integrations will make this recommendation stronger.';
		if ( $search && $analytics ) {
			$keep_reason = 'No structural concern was found, and the page has current search and engagement evidence. Backlink and business-purpose data can strengthen the decision further.';
		} elseif ( $search ) {
			$keep_reason = 'No structural concern was found, and the page has current Search Console evidence. Conversion and backlink data can strengthen the decision further.';
		} elseif ( $analytics ) {
			$keep_reason = 'No structural concern was found, and the page has current engagement evidence. Search visibility and backlink data can strengthen the decision further.';
		}

		return array(
			'action'     => 'Keep',
			'confidence' => $search || $analytics ? 'Medium' : 'Low',
			'reason'     => $keep_reason,
		);
	}

	/**
	 * Find potentially competing pairs.
	 *
	 * @param array<int,array<string,mixed>> $items Prepared items.
	 * @param int                            $total Total matching pairs before the display limit.
	 * @return array<int,array<string,mixed>>
	 */
	private function find_pairs( array $items, &$total = 0 ) {
		$pairs = array();
		$count = count( $items );

		for ( $left = 0; $left < $count; ++$left ) {
			for ( $right = $left + 1; $right < $count; ++$right ) {
				$pair = $this->compare_prepared( $items[ $left ], $items[ $right ] );
				if ( null === $pair ) {
					continue;
				}
				$pairs[] = $pair;
			}
		}

		usort(
			$pairs,
			function ( $a, $b ) {
				return $b['score'] <=> $a['score'];
			}
		);

		$total = count( $pairs );
		return array_slice( $pairs, 0, 100 );
	}

	/** Build the legacy lexical pair shape. @return array<string,mixed> */
	private function build_lexical_pair( array $left, array $right, $score, $exact ) {
		$left_search     = isset( $left['search_console'] ) && is_array( $left['search_console'] ) ? $left['search_console'] : array();
		$right_search    = isset( $right['search_console'] ) && is_array( $right['search_console'] ) ? $right['search_console'] : array();
		$left_analytics  = isset( $left['analytics'] ) && is_array( $left['analytics'] ) ? $left['analytics'] : array();
		$right_analytics = isset( $right['analytics'] ) && is_array( $right['analytics'] ) ? $right['analytics'] : array();
		$performance_reason = '';
		if ( $left_search && $right_search ) {
			$left_clicks  = (int) ( $left_search['clicks'] ?? 0 );
			$right_clicks = (int) ( $right_search['clicks'] ?? 0 );
			if ( $left_clicks !== $right_clicks ) {
				$primary_title      = $left_clicks > $right_clicks ? $left['title'] : $right['title'];
				$performance_reason = ' Search Console currently favors “' . $primary_title . '” by clicks; review it as the likely primary URL.';
			}
		} elseif ( $left_analytics && $right_analytics ) {
			$left_sessions  = (int) ( $left_analytics['sessions'] ?? 0 );
			$right_sessions = (int) ( $right_analytics['sessions'] ?? 0 );
			if ( $left_sessions !== $right_sessions ) {
				$primary_title      = $left_sessions > $right_sessions ? $left['title'] : $right['title'];
				$performance_reason = ' Google Analytics currently favors “' . $primary_title . '” by landing-page sessions; review it as the likely primary URL.';
			}
		}
		$pair = array(
			'left_id'     => $left['id'],
			'left_title'  => $left['title'],
			'left_url'    => $left['edit_url'],
			'right_id'    => $right['id'],
			'right_title' => $right['title'],
			'right_url'   => $right['edit_url'],
			'score'       => $score,
			'evidence'    => $exact ? 'exact copy' : 'lexical overlap ' . round( $score * 100 ) . '%',
			'exact'       => (bool) $exact,
			'reason_suffix' => $performance_reason,
		);
		return Content_Miner_Collision_Recommendation::resolve( $pair );
	}

	/**
	 * @param array<string,mixed> $left Left item.
	 * @param array<string,mixed> $right Right item.
	 * @return float
	 */
	private function item_similarity( array $left, array $right ) {
		$title = $this->jaccard( $left['title_tokens'], $right['title_tokens'] );
		$body  = $this->jaccard( $left['body_tokens'], $right['body_tokens'] );

		return round( ( 0.42 * $title ) + ( 0.58 * $body ), 4 );
	}

	/**
	 * @param string              $idea Idea text.
	 * @param array<string,mixed> $item Inventory item.
	 * @return float
	 */
	private function idea_similarity( $idea, array $item ) {
		$idea_tokens  = $this->tokenize( $idea );
		$title_tokens = $this->tokenize( isset( $item['analysis_title'] ) ? $item['analysis_title'] : $item['title'] );
		$body_tokens  = $this->tokenize( isset( $item['content'] ) ? $item['content'] : '' );

		if ( empty( $idea_tokens ) ) {
			return 0.0;
		}

		$title_overlap = $this->coverage( $idea_tokens, $title_tokens );
		$body_overlap  = $this->coverage( $idea_tokens, $body_tokens );

		return round( ( 0.68 * $title_overlap ) + ( 0.32 * $body_overlap ), 4 );
	}

	/**
	 * @param array<string,mixed> $item Item.
	 * @param string[]            $locations Locations.
	 * @return bool
	 */
	private function is_localization_candidate( array $item, array $locations ) {
		if ( empty( $locations ) || $item['word_count'] < 700 ) {
			return false;
		}

		$haystack = $this->normalize_text( ( isset( $item['analysis_title'] ) ? $item['analysis_title'] : $item['title'] ) . ' ' . $item['content'] );
		foreach ( $locations as $location ) {
			if ( '' !== $location && false !== strpos( $haystack, $this->normalize_text( $location ) ) ) {
				return false;
			}
		}

		$evergreen_markers = array( 'guide', 'cost', 'choose', 'checklist', 'service', 'tips', 'best', 'planning' );
		foreach ( $evergreen_markers as $marker ) {
			if ( false !== strpos( $haystack, $marker ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param string $date GMT date.
	 * @return int
	 */
	private function age_in_days( $date ) {
		$timestamp = strtotime( $date . ' UTC' );
		if ( ! $timestamp ) {
			return 0;
		}

		return max( 0, (int) floor( ( time() - $timestamp ) / DAY_IN_SECONDS ) );
	}

	/**
	 * @param string $text Input text.
	 * @return string
	 */
	private function normalize_text( $text ) {
		$text = function_exists( 'wp_strip_all_tags' ) ? wp_strip_all_tags( (string) $text ) : preg_replace( '/<[^>]*>/', '', (string) $text );
		$text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$text = function_exists( 'mb_strtolower' ) ? mb_strtolower( $text, 'UTF-8' ) : strtolower( $text );
		$text = preg_replace( '/[^\p{L}\p{N}\s-]+/u', ' ', $text );
		$text = preg_replace( '/\s+/u', ' ', $text );

		return trim( (string) $text );
	}

	/**
	 * @param string $text Input text.
	 * @param bool   $remove_stopwords Remove common words.
	 * @return string[]
	 */
	private function tokenize( $text, $remove_stopwords = true ) {
		$normalized = $this->normalize_text( $text );
		if ( '' === $normalized ) {
			return array();
		}

		$tokens = preg_split( '/\s+/u', $normalized );
		$tokens = array_filter(
			$tokens,
			function ( $token ) use ( $remove_stopwords ) {
				if ( strlen( $token ) < 2 ) {
					return false;
				}

				return ! $remove_stopwords || ! in_array( $token, $this->stopwords, true );
			}
		);

		return array_values( array_unique( $tokens ) );
	}

	/**
	 * Count all words, including repetitions, for editorial length signals.
	 *
	 * @param string $text Input text.
	 * @return int
	 */
	private function count_words( $text ) {
		$normalized = $this->normalize_text( $text );
		if ( '' === $normalized ) {
			return 0;
		}

		$tokens = preg_split( '/\s+/u', $normalized, -1, PREG_SPLIT_NO_EMPTY );

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

	/**
	 * @param string[] $left Left tokens.
	 * @param string[] $right Right tokens.
	 * @return float
	 */
	private function jaccard( array $left, array $right ) {
		if ( empty( $left ) || empty( $right ) ) {
			return 0.0;
		}

		$intersection = array_intersect( $left, $right );
		$union        = array_unique( array_merge( $left, $right ) );

		return count( $intersection ) / count( $union );
	}

	/**
	 * Measure how much of the query appears in a document.
	 *
	 * @param string[] $query Query tokens.
	 * @param string[] $document Document tokens.
	 * @return float
	 */
	private function coverage( array $query, array $document ) {
		if ( empty( $query ) || empty( $document ) ) {
			return 0.0;
		}

		return count( array_intersect( $query, $document ) ) / count( array_unique( $query ) );
	}
}
