<?php

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

final class ClaimPrism_Scanner {
	const TABLE_VERSION = '1';

	public static function supported_post_types() {
		$types = array( 'product', 'page', 'post' );
		return apply_filters( 'claimprism_post_types', $types );
	}

	public static function table_name() {
		global $wpdb;
		return $wpdb->prefix . 'claimprism_findings';
	}

	public static function create_table() {
		global $wpdb;

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		$table   = self::table_name();
		$charset = $wpdb->get_charset_collate();
		$sql     = "CREATE TABLE {$table} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			content_id bigint(20) unsigned NOT NULL,
			content_type varchar(32) NOT NULL,
			content_title text NOT NULL,
			rule_id varchar(80) NOT NULL,
			rule_label varchar(160) NOT NULL,
			severity varchar(16) NOT NULL,
			matched_phrase varchar(255) NOT NULL,
			context text NOT NULL,
			source_url text NOT NULL,
			fingerprint char(64) NOT NULL,
			detected_at datetime NOT NULL,
			PRIMARY KEY  (id),
			UNIQUE KEY fingerprint (fingerprint),
			KEY content_id (content_id),
			KEY severity (severity)
		) {$charset};";

		dbDelta( $sql );
		update_option( 'claimprism_table_version', self::TABLE_VERSION, false );
	}

	public function scan_site( $limit = 100 ) {
		$query = new WP_Query(
			array(
				'post_type'              => self::supported_post_types(),
				'post_status'            => array( 'publish', 'draft', 'private' ),
				'posts_per_page'         => max( 1, min( 1000, (int) $limit ) ),
				'orderby'                => 'modified',
				'order'                  => 'DESC',
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			)
		);

		$total_findings = 0;
		foreach ( $query->posts as $post ) {
			$total_findings += count( $this->scan_post( $post ) );
		}

		update_option( 'claimprism_last_scan', current_time( 'mysql' ), false );

		return array(
			'scanned'  => count( $query->posts ),
			'findings' => $total_findings,
		);
	}

	public function scan_post( $post ) {
		if ( is_numeric( $post ) ) {
			$post = get_post( (int) $post );
		}

		if ( ! $post instanceof WP_Post ) {
			return array();
		}

		$text = apply_filters( 'claimprism_post_text', $this->collect_post_text( $post ), $post );

		$findings = $this->scan_text( $text, (int) $post->ID, $post->post_type, get_the_title( $post ) );
		$this->replace_findings_for_post( (int) $post->ID, $findings );
		do_action( 'claimprism_post_scanned', (int) $post->ID, $findings, $post );

		return $findings;
	}

	private function collect_post_text( WP_Post $post ) {
		$parts = array(
			'[Title] ' . $post->post_title,
			'[Excerpt] ' . $post->post_excerpt,
			'[Content] ' . $post->post_content,
		);

		$seo_keys = array(
			'_yoast_wpseo_title'       => 'SEO title',
			'_yoast_wpseo_metadesc'    => 'SEO description',
			'rank_math_title'           => 'SEO title',
			'rank_math_description'     => 'SEO description',
			'_aioseo_title'             => 'SEO title',
			'_aioseo_description'       => 'SEO description',
		);
		foreach ( $seo_keys as $meta_key => $label ) {
			$value = get_post_meta( $post->ID, $meta_key, true );
			if ( is_string( $value ) && '' !== trim( $value ) ) {
				$parts[] = '[' . $label . '] ' . $value;
			}
		}

		$image_ids = array();
		$thumbnail = get_post_thumbnail_id( $post->ID );
		if ( $thumbnail ) {
			$image_ids[] = (int) $thumbnail;
		}

		if ( 'product' === $post->post_type ) {
			$gallery = (string) get_post_meta( $post->ID, '_product_image_gallery', true );
			foreach ( array_filter( array_map( 'absint', explode( ',', $gallery ) ) ) as $image_id ) {
				$image_ids[] = $image_id;
			}
		}

		foreach ( array_unique( $image_ids ) as $image_id ) {
			$alt = get_post_meta( $image_id, '_wp_attachment_image_alt', true );
			if ( is_string( $alt ) && '' !== trim( $alt ) ) {
				$parts[] = '[Image alt] ' . $alt;
			}
		}

		return implode( "\n\n", array_filter( $parts ) );
	}

	public function scan_text( $text, $content_id = 0, $content_type = 'text', $content_title = '' ) {
		$plain = $this->normalise_text( $text );
		if ( '' === trim( $plain ) ) {
			return array();
		}

		$findings = array();
		foreach ( ClaimPrism_Rules::all() as $rule ) {
			if ( ! empty( $rule['phrases'] ) ) {
				foreach ( $rule['phrases'] as $phrase ) {
					$positions = $this->find_all_positions( $plain, $phrase );
					foreach ( $positions as $position ) {
						$findings[] = $this->build_finding( $rule, $plain, $phrase, $position, $content_id, $content_type, $content_title );
					}
				}
			}

			if ( ! empty( $rule['patterns'] ) ) {
				foreach ( $rule['patterns'] as $pattern ) {
					if ( preg_match_all( $pattern, $plain, $matches, PREG_OFFSET_CAPTURE ) ) {
						foreach ( $matches[0] as $match ) {
							$findings[] = $this->build_finding( $rule, $plain, $match[0], $this->byte_to_character_offset( $plain, $match[1] ), $content_id, $content_type, $content_title );
						}
					}
				}
			}
		}

		$findings = $this->deduplicate_findings( $findings );

		return apply_filters( 'claimprism_findings', $findings, $plain, $content_id, $content_type, $content_title );
	}

	private function normalise_text( $text ) {
		$text = html_entity_decode( wp_strip_all_tags( (string) $text ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$text = preg_replace( '/\s+/u', ' ', $text );
		return trim( (string) $text );
	}

	private function find_all_positions( $haystack, $needle ) {
		$positions = array();
		$offset    = 0;
		$lower_haystack = $this->lower( $haystack );
		$lower_needle   = $this->lower( $needle );
		$needle_length  = $this->length( $lower_needle );

		if ( 0 === $needle_length ) {
			return $positions;
		}

		while ( false !== ( $position = $this->strpos( $lower_haystack, $lower_needle, $offset ) ) ) {
			if ( $this->has_word_boundaries( $lower_haystack, $position, $needle_length ) ) {
				$positions[] = $position;
			}
			$offset = $position + $needle_length;
		}

		return $positions;
	}

	private function has_word_boundaries( $text, $position, $length ) {
		$before = $position > 0 ? $this->substr( $text, $position - 1, 1 ) : '';
		$after  = $this->substr( $text, $position + $length, 1 );
		$word   = '/[\p{L}\p{N}_]/u';

		return ( '' === $before || ! preg_match( $word, $before ) ) && ( '' === $after || ! preg_match( $word, $after ) );
	}

	private function build_finding( $rule, $plain, $phrase, $position, $content_id, $content_type, $content_title ) {
		$context_start = max( 0, $position - 90 );
		$context       = $this->substr( $plain, $context_start, $this->length( $phrase ) + 180 );
		$severity      = $this->adjust_severity( $rule['severity'], $rule['id'], $context );
		$fingerprint   = hash( 'sha256', implode( '|', array( $content_id, $rule['id'], $this->lower( $phrase ), $context ) ) );

		return array(
			'content_id'     => (int) $content_id,
			'content_type'   => sanitize_key( $content_type ),
			'content_title'  => sanitize_text_field( $content_title ),
			'rule_id'        => $rule['id'],
			'rule_label'     => $rule['label'],
			'severity'       => $severity,
			'matched_phrase' => sanitize_text_field( $phrase ),
			'context'        => $context,
			'description'    => $rule['description'],
			'source_url'     => esc_url_raw( $rule['source'] ),
			'fingerprint'    => $fingerprint,
			'claim_key'      => self::claim_key( $rule['id'], $phrase ),
		);
	}

	public static function claim_key( $rule_id, $phrase ) {
		$normalised_phrase = function_exists( 'mb_strtolower' ) ? mb_strtolower( trim( (string) $phrase ), 'UTF-8' ) : strtolower( trim( (string) $phrase ) );
		return hash( 'sha256', sanitize_key( $rule_id ) . '|' . $normalised_phrase );
	}

	private function adjust_severity( $severity, $rule_id, $context ) {
		if ( 'generic_environmental_claim' !== $rule_id ) {
			return $severity;
		}

		$lower_context = $this->lower( $context );
		foreach ( ClaimPrism_Rules::recognised_performance_markers() as $marker ) {
			if ( false !== $this->strpos( $lower_context, $this->lower( $marker ), 0 ) ) {
				return 'medium';
			}
		}

		return $severity;
	}

	private function deduplicate_findings( $findings ) {
		$unique = array();
		foreach ( $findings as $finding ) {
			$unique[ $finding['fingerprint'] ] = $finding;
		}
		return array_values( $unique );
	}

	private function replace_findings_for_post( $post_id, $findings ) {
		global $wpdb;
		$table = self::table_name();

		$this->delete_findings_for_post( $post_id );
		foreach ( $findings as $finding ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Findings are stored in ClaimPrism's dedicated table and have no WordPress CRUD API.
			$wpdb->insert(
				$table,
				array(
					'content_id'     => $finding['content_id'],
					'content_type'   => $finding['content_type'],
					'content_title'  => $finding['content_title'],
					'rule_id'        => $finding['rule_id'],
					'rule_label'     => $finding['rule_label'],
					'severity'       => $finding['severity'],
					'matched_phrase' => $finding['matched_phrase'],
					'context'        => $finding['context'],
					'source_url'     => $finding['source_url'],
					'fingerprint'    => $finding['fingerprint'],
					'detected_at'    => current_time( 'mysql' ),
				),
				array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
			);
		}
	}

	public function delete_findings_for_post( $post_id ) {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Findings are stored in ClaimPrism's dedicated table; deletion must take effect immediately.
		$wpdb->delete( self::table_name(), array( 'content_id' => (int) $post_id ), array( '%d' ) );
	}

	public function get_findings( $limit = 100 ) {
		return $this->get_findings_page( $limit, 0 );
	}

	public function get_findings_page( $limit = 100, $offset = 0 ) {
		global $wpdb;
		$table = self::table_name();
		$limit = max( 1, min( 500, (int) $limit ) );
		$offset = max( 0, (int) $offset );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal table identifier cannot be parameterized; live review data must not be stale-cached.
		$rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} ORDER BY FIELD(severity, 'critical', 'high', 'medium', 'low'), detected_at DESC, id ASC LIMIT %d OFFSET %d", $limit, $offset ), ARRAY_A );
		return $this->add_claim_keys( $rows );
	}

	public function get_findings_for_post( $post_id ) {
		global $wpdb;
		$table = self::table_name();
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal table identifier cannot be parameterized; editor findings need current data.
		$rows  = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$table} WHERE content_id = %d ORDER BY FIELD(severity, 'critical', 'high', 'medium', 'low'), detected_at DESC",
				(int) $post_id
			),
			ARRAY_A
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		return $this->add_claim_keys( $rows );
	}

	private function add_claim_keys( $rows ) {
		foreach ( $rows as &$row ) {
			$row['claim_key'] = self::claim_key( $row['rule_id'], $row['matched_phrase'] );
		}
		unset( $row );

		return $rows;
	}

	public function get_summary() {
		global $wpdb;
		$table = self::table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal table identifier cannot be parameterized; dashboard summary reflects current findings.
		$rows  = $wpdb->get_results( "SELECT severity, COUNT(*) AS total FROM {$table} GROUP BY severity", ARRAY_A );
		$summary = array( 'critical' => 0, 'high' => 0, 'medium' => 0, 'low' => 0 );
		foreach ( $rows as $row ) {
			if ( isset( $summary[ $row['severity'] ] ) ) {
				$summary[ $row['severity'] ] = (int) $row['total'];
			}
		}
		return $summary;
	}

	public function get_affected_content_count() {
		global $wpdb;
		$table = self::table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal table identifier cannot be parameterized; dashboard count reflects current findings.
		return (int) $wpdb->get_var( "SELECT COUNT(DISTINCT content_id) FROM {$table}" );
	}

	/**
	 * Turn finding counts into a triage indicator. This intentionally measures
	 * review workload, not legal compliance.
	 */
	public static function calculate_review_priority( $summary ) {
		$summary = wp_parse_args(
			(array) $summary,
			array(
				'critical' => 0,
				'high'     => 0,
				'medium'   => 0,
				'low'      => 0,
			)
		);

		$score = ( 20 * max( 0, (int) $summary['critical'] ) )
			+ ( 8 * max( 0, (int) $summary['high'] ) )
			+ ( 3 * max( 0, (int) $summary['medium'] ) )
			+ max( 0, (int) $summary['low'] );

		return min( 100, $score );
	}

	private function lower( $text ) {
		return function_exists( 'mb_strtolower' ) ? mb_strtolower( $text, 'UTF-8' ) : strtolower( $text );
	}

	private function length( $text ) {
		return function_exists( 'mb_strlen' ) ? mb_strlen( $text, 'UTF-8' ) : strlen( $text );
	}

	private function substr( $text, $start, $length = null ) {
		if ( function_exists( 'mb_substr' ) ) {
			return null === $length ? mb_substr( $text, $start, null, 'UTF-8' ) : mb_substr( $text, $start, $length, 'UTF-8' );
		}
		return null === $length ? substr( $text, $start ) : substr( $text, $start, $length );
	}

	private function strpos( $haystack, $needle, $offset = 0 ) {
		return function_exists( 'mb_strpos' ) ? mb_strpos( $haystack, $needle, $offset, 'UTF-8' ) : strpos( $haystack, $needle, $offset );
	}

	private function byte_to_character_offset( $text, $byte_offset ) {
		if ( function_exists( 'mb_strlen' ) ) {
			return mb_strlen( substr( $text, 0, $byte_offset ), 'UTF-8' );
		}
		return $byte_offset;
	}
}
