<?php

namespace SerpIQ\Content;

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

/**
 * Content Refresh: backend scoring model (Milestone 1).
 *
 * This service computes, for each page URL that has GSC data and a matching
 * WordPress post, a set of metrics and a Refresh Priority Score (0–100).
 * It does NOT expose any REST routes; controllers can call it later.
 */
final class Content_Refresh {

	/** @var Content_Refresh|null */
	private static ?Content_Refresh $instance = null;

	public static function instance(): Content_Refresh {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	private function __construct() {}

	/**
	 * Compute Content Refresh candidates for the given period.
	 *
	 * @param string $end_date   End date (Y-m-d). If empty, defaults to yesterday (UTC).
	 * @param int    $period_days Number of days for current period (28 or 90).
	 * @param array  $args        Optional filters:
	 *                            - min_age_days (int)
	 *                            - min_freshness_gap_days (int)
	 *                            - min_impressions_current (int)
	 *                            - min_clicks_total (int)
	 *                            - post_type (string|array)
	 * @return array{
	 *   overview: array<string,mixed>,
	 *   items: array<int,array<string,mixed>>
	 * }
	 */
	public function compute( string $end_date = '', int $period_days = 28, array $args = array() ): array {
		global $wpdb;

		if ( $period_days !== 90 ) {
			$period_days = 28;
		}

		if ( '' === $end_date ) {
			$end_date = gmdate( 'Y-m-d', strtotime( '-1 day' ) );
		}

		$end_ts = strtotime( $end_date . ' 00:00:00 UTC' );
		if ( false === $end_ts ) {
			$end_ts  = strtotime( '-1 day' );
			$end_date = gmdate( 'Y-m-d', $end_ts );
		}

		$current_start_ts = $end_ts - ( $period_days - 1 ) * DAY_IN_SECONDS;
		$current_start    = gmdate( 'Y-m-d', $current_start_ts );

		$prev_end_ts   = $current_start_ts - DAY_IN_SECONDS;
		$prev_start_ts = $prev_end_ts - ( $period_days - 1 ) * DAY_IN_SECONDS;
		$prev_end      = gmdate( 'Y-m-d', $prev_end_ts );
		$prev_start    = gmdate( 'Y-m-d', $prev_start_ts );

		// Thresholds with sensible defaults, can be overridden via $args.
		$min_age_days            = isset( $args['min_age_days'] ) ? (int) $args['min_age_days'] : 90;
		$min_freshness_gap_days  = isset( $args['min_freshness_gap_days'] ) ? (int) $args['min_freshness_gap_days'] : 60;
		$min_impr_current        = isset( $args['min_impressions_current'] ) ? (int) $args['min_impressions_current'] : ( ( 90 === $period_days ) ? 300 : 100 );
		$min_clicks_total        = isset( $args['min_clicks_total'] ) ? (int) $args['min_clicks_total'] : 10;

		// Aggregate GSC page data for both periods (current and previous).
		$table_pages = $wpdb->prefix . 'serpiq_pages';

		$sql_current = "
			SELECT
				page_url,
				COALESCE(SUM(clicks),0) AS clicks,
				COALESCE(SUM(impressions),0) AS impressions,
				CASE WHEN COALESCE(SUM(impressions),0) > 0
					THEN (COALESCE(SUM(clicks),0) / COALESCE(SUM(impressions),0))
					ELSE 0 END AS ctr,
				CASE WHEN COALESCE(SUM(impressions),0) > 0
					THEN (COALESCE(SUM(position * impressions),0) / COALESCE(SUM(impressions),0))
					ELSE 0 END AS position
			FROM %i
			WHERE date BETWEEN %s AND %s
			GROUP BY page_url
		";
		$current_rows = $wpdb->get_results(
			$wpdb->prepare( $sql_current, array_merge( array( $table_pages ), array( $current_start, $end_date ) ) ),
			ARRAY_A
		);

		$sql_prev = "
			SELECT
				page_url,
				COALESCE(SUM(clicks),0) AS clicks,
				COALESCE(SUM(impressions),0) AS impressions,
				CASE WHEN COALESCE(SUM(impressions),0) > 0
					THEN (COALESCE(SUM(clicks),0) / COALESCE(SUM(impressions),0))
					ELSE 0 END AS ctr,
				CASE WHEN COALESCE(SUM(impressions),0) > 0
					THEN (COALESCE(SUM(position * impressions),0) / COALESCE(SUM(impressions),0))
					ELSE 0 END AS position
			FROM %i
			WHERE date BETWEEN %s AND %s
			GROUP BY page_url
		";
		$prev_rows = $wpdb->get_results(
			$wpdb->prepare( $sql_prev, array_merge( array( $table_pages ), array( $prev_start, $prev_end ) ) ),
			ARRAY_A
		);

		$current_map = array();
		foreach ( (array) $current_rows as $row ) {
			if ( empty( $row['page_url'] ) ) {
				continue;
			}
			$current_map[ (string) $row['page_url'] ] = $row;
		}

		$prev_map = array();
		foreach ( (array) $prev_rows as $row ) {
			if ( empty( $row['page_url'] ) ) {
				continue;
			}
			$prev_map[ (string) $row['page_url'] ] = $row;
		}

		$all_urls = array_unique( array_merge( array_keys( $current_map ), array_keys( $prev_map ) ) );

		$candidates           = array();
		$total_clicks_involved = 0;
		$urgent_count         = 0;
		$recommended_count    = 0;
		$opportunity_count    = 0;

		foreach ( $all_urls as $url ) {
			$curr = $current_map[ $url ] ?? array();
			$prev = $prev_map[ $url ] ?? array();

			$c_clicks = (int) ( $curr['clicks'] ?? 0 );
			$c_impr   = (int) ( $curr['impressions'] ?? 0 );
			$c_ctr    = (float) ( $curr['ctr'] ?? 0 );
			$c_pos    = (float) ( $curr['position'] ?? 0 );

			$p_clicks = (int) ( $prev['clicks'] ?? 0 );
			$p_impr   = (int) ( $prev['impressions'] ?? 0 );
			$p_ctr    = (float) ( $prev['ctr'] ?? 0 );
			$p_pos    = (float) ( $prev['position'] ?? 0 );

			// Raw deltas.
			$delta_clicks = $c_clicks - $p_clicks;
			$delta_impr   = $c_impr - $p_impr;
			$delta_ctr    = $c_ctr - $p_ctr;
			$delta_pos    = $c_pos - $p_pos;

			$delta_clicks_pct = ( $p_clicks > 0 ) ? ( $delta_clicks / $p_clicks ) : 0.0;
			$delta_impr_pct   = ( $p_impr > 0 ) ? ( $delta_impr / $p_impr ) : 0.0;

			// Skip pages with very low visibility / signals in the current period.
			if ( $c_impr < $min_impr_current ) {
				continue;
			}
			if ( ( $c_clicks + $p_clicks ) < $min_clicks_total ) {
				continue;
			}

			// Resolve to WP post and compute age/freshness.
			$wp = $this->get_wp_data_for_url( $url );
			if ( null === $wp['post'] ) {
				continue;
			}

			$post = $wp['post'];
			if ( 'publish' !== $post->post_status ) {
				continue;
			}

			$age_days       = $wp['age_days'];
			$freshness_days = $wp['freshness_days'];

			if ( $age_days < $min_age_days ) {
				continue;
			}
			if ( $freshness_days < $min_freshness_gap_days ) {
				continue;
			}

			$position_for_expected_ctr = $c_pos > 0 ? $c_pos : $p_pos;
			$ctr_expected              = $this->expected_ctr_for_position( $position_for_expected_ctr );
			$ctr_gap                   = ( $ctr_expected > 0 && $c_ctr < $ctr_expected )
				? ( ( $ctr_expected - $c_ctr ) / $ctr_expected )
				: 0.0;

			$score = $this->compute_priority_score(
				$age_days,
				$freshness_days,
				$c_clicks,
				$c_impr,
				$delta_clicks_pct,
				$delta_impr_pct,
				$delta_pos,
				$ctr_gap
			);

			if ( $score <= 0 ) {
				continue;
			}

			// Classify priority and reason.
			$priority = $this->classify_priority_level( $score, $delta_clicks_pct, $delta_impr_pct, $delta_pos, $freshness_days );
			if ( 'none' === $priority ) {
				continue;
			}

			$reason = $this->classify_reason(
				$age_days,
				$freshness_days,
				$c_clicks,
				$c_impr,
				$c_pos,
				$delta_clicks_pct,
				$delta_impr_pct,
				$delta_pos,
				$ctr_gap,
				$p_clicks
			);

			if ( 'urgent' === $priority ) {
				$urgent_count++;
			} elseif ( 'recommended' === $priority ) {
				$recommended_count++;
			} elseif ( 'opportunity' === $priority ) {
				$opportunity_count++;
			}

			$total_clicks_involved += $c_clicks;

			$candidates[] = array(
				'page_url'           => $url,
				'post_id'            => $post->ID,
				'post_type'          => $post->post_type,
				'post_status'        => $post->post_status,
				'post_title'         => $post->post_title,
				'post_date'          => $post->post_date_gmt ?: $post->post_date,
				'post_modified'      => $post->post_modified_gmt ?: $post->post_modified,
				'age_days'           => $age_days,
				'freshness_days'     => $freshness_days,
				'clicks_current'     => $c_clicks,
				'clicks_previous'    => $p_clicks,
				'impressions_current'  => $c_impr,
				'impressions_previous' => $p_impr,
				'ctr_current'        => $c_ctr,
				'ctr_previous'       => $p_ctr,
				'position_current'   => $c_pos,
				'position_previous'  => $p_pos,
				'delta_clicks'       => $delta_clicks,
				'delta_clicks_pct'   => $delta_clicks_pct,
				'delta_impressions'  => $delta_impr,
				'delta_impressions_pct' => $delta_impr_pct,
				'delta_ctr'          => $delta_ctr,
				'delta_position'     => $delta_pos,
				'ctr_expected'       => $ctr_expected,
				'ctr_gap'            => $ctr_gap,
				'priority_score'     => $score,
				'priority_level'     => $priority,
				'refresh_reason'     => $reason['code'],
				'refresh_reason_label' => $reason['label'],
			);
		}

		// Sort candidates by priority score descending.
		usort(
			$candidates,
			static function ( $a, $b ) {
				$sa = (float) ( $a['priority_score'] ?? 0 );
				$sb = (float) ( $b['priority_score'] ?? 0 );
				return $sb <=> $sa;
			}
		);

		$overview = array(
			'periodDays'          => $period_days,
			'currentRange'        => array(
				'start' => $current_start,
				'end'   => $end_date,
			),
			'previousRange'       => array(
				'start' => $prev_start,
				'end'   => $prev_end,
			),
			'totalCandidates'     => count( $candidates ),
			'urgentCount'         => $urgent_count,
			'recommendedCount'    => $recommended_count,
			'opportunityCount'    => $opportunity_count,
			'clicksInvolved'      => $total_clicks_involved,
			'minAgeDays'          => $min_age_days,
			'minFreshnessGapDays' => $min_freshness_gap_days,
			'minImpressionsCurrent' => $min_impr_current,
			'minClicksTotal'      => $min_clicks_total,
		);

		return array(
			'overview' => $overview,
			'items'    => $candidates,
		);
	}

	/**
	 * Resolve a page URL to a WordPress post and compute age/freshness.
	 *
	 * @return array{
	 *   post: ?\WP_Post,
	 *   age_days: int,
	 *   freshness_days: int
	 * }
	 */
	private function get_wp_data_for_url( string $url ): array {
		$url = trim( $url );
		if ( '' === $url ) {
			return array(
				'post'           => null,
				'age_days'       => 0,
				'freshness_days' => 0,
			);
		}

		if ( ! function_exists( 'url_to_postid' ) ) {
			return array(
				'post'           => null,
				'age_days'       => 0,
				'freshness_days' => 0,
			);
		}

		$normalized = trailingslashit( $url );
		$post_id    = url_to_postid( $normalized );
		if ( $post_id <= 0 ) {
			return array(
				'post'           => null,
				'age_days'       => 0,
				'freshness_days' => 0,
			);
		}

		$post = get_post( $post_id );
		if ( ! $post || ! ( $post instanceof \WP_Post ) ) {
			return array(
				'post'           => null,
				'age_days'       => 0,
				'freshness_days' => 0,
			);
		}

		// Age and freshness in days, based on UTC dates.
		$today      = gmdate( 'Y-m-d' );
		$today_ts   = strtotime( $today . ' 00:00:00 UTC' );
		$published  = $post->post_date_gmt ?: $post->post_date;
		$modified   = $post->post_modified_gmt ?: $post->post_modified;

		$published_date = $published ? gmdate( 'Y-m-d', strtotime( $published ) ) : $today;
		$modified_date  = $modified ? gmdate( 'Y-m-d', strtotime( $modified ) ) : $published_date;

		$pub_ts = strtotime( $published_date . ' 00:00:00 UTC' );
		$mod_ts = strtotime( $modified_date . ' 00:00:00 UTC' );

		$age_days       = ( $pub_ts && $today_ts )
			? max( 0, (int) floor( ( $today_ts - $pub_ts ) / DAY_IN_SECONDS ) )
			: 0;
		$freshness_days = ( $mod_ts && $today_ts )
			? max( 0, (int) floor( ( $today_ts - $mod_ts ) / DAY_IN_SECONDS ) )
			: 0;

		return array(
			'post'           => $post,
			'age_days'       => $age_days,
			'freshness_days' => $freshness_days,
		);
	}

	/**
	 * Expected CTR baseline by position bucket (0–1).
	 */
	private function expected_ctr_for_position( float $position ): float {
		if ( $position <= 0 ) {
			return 0.0;
		}
		if ( $position <= 3 ) {
			return 0.25;
		}
		if ( $position <= 6 ) {
			return 0.15;
		}
		if ( $position <= 10 ) {
			return 0.10;
		}
		if ( $position <= 20 ) {
			return 0.05;
		}
		return 0.02;
	}

	/**
	 * Compute Refresh Priority Score (0–100).
	 */
	private function compute_priority_score(
		int $age_days,
		int $freshness_days,
		int $clicks_current,
		int $impressions_current,
		float $delta_clicks_pct,
		float $delta_impr_pct,
		float $delta_pos,
		float $ctr_gap
	): float {
		// Age: older content gets higher age_score up to 365 days.
		$age_score = min( 1.0, $age_days / 365.0 );

		// Freshness: more days since last update up to 180 days.
		$freshness_score = min( 1.0, $freshness_days / 180.0 );

		// Value: based on current clicks and impressions.
		$value_raw   = 0.5 * log( 1 + max( 0, $clicks_current ) ) + 0.5 * log( 1 + max( 0, $impressions_current ) );
		$value_denom = log( 1 + 5000 ); // 5000 as a reference high-volume page.
		$value_score = $value_denom > 0 ? min( 1.0, $value_raw / $value_denom ) : 0.0;

		// Trend: capture negative changes in clicks, impressions, and position.
		$trend_clicks = 0.0;
		if ( $delta_clicks_pct < 0 ) {
			$trend_clicks = min( 1.0, abs( $delta_clicks_pct ) / 0.5 ); // 50% loss → 1
		}
		$trend_impr = 0.0;
		if ( $delta_impr_pct < 0 ) {
			$trend_impr = min( 1.0, abs( $delta_impr_pct ) / 0.5 );
		}
		$trend_pos = 0.0;
		if ( $delta_pos > 0 ) {
			$trend_pos = min( 1.0, $delta_pos / 3.0 ); // +3 positions worse → 1
		}
		$trend_score = 0.5 * $trend_clicks + 0.3 * $trend_impr + 0.2 * $trend_pos;

		// CTR opportunity: gap vs expected CTR baselines.
		$ctr_score = max( 0.0, min( 1.0, $ctr_gap ) );

		$score = 100.0 * (
			0.20 * $age_score +
			0.25 * $freshness_score +
			0.25 * $value_score +
			0.20 * $trend_score +
			0.10 * $ctr_score
		);

		return round( max( 0.0, min( 100.0, $score ) ), 1 );
	}

	/**
	 * Classify overall priority level based on score and trend.
	 *
	 * @return string One of: urgent, recommended, opportunity, none.
	 */
	private function classify_priority_level(
		float $score,
		float $delta_clicks_pct,
		float $delta_impr_pct,
		float $delta_pos,
		int $freshness_days
	): string {
		if ( $score < 30.0 ) {
			return 'none';
		}

		$is_strong_decline = ( $delta_clicks_pct <= -0.25 ) || ( $delta_impr_pct <= -0.25 ) || ( $delta_pos >= 2.0 );

		if ( $score >= 75.0 && $is_strong_decline && $freshness_days >= 120 ) {
			return 'urgent';
		}

		if ( $score >= 50.0 ) {
			return 'recommended';
		}

		return 'opportunity';
	}

	/**
	 * Classify main reason for refresh.
	 *
	 * @return array{code:string,label:string}
	 */
	private function classify_reason(
		int $age_days,
		int $freshness_days,
		int $clicks_current,
		int $impressions_current,
		float $position_current,
		float $delta_clicks_pct,
		float $delta_impr_pct,
		float $delta_pos,
		float $ctr_gap,
		int $clicks_previous
	): array {
		$impressions_high = $impressions_current >= 1000;
		$strong_decline   = ( $delta_clicks_pct <= -0.25 ) || ( $delta_impr_pct <= -0.25 );
		$moderate_decline = ( $delta_clicks_pct <= -0.10 ) || ( $delta_impr_pct <= -0.10 );
		$ctr_low_enough   = $ctr_gap >= 0.4; // current CTR < 60% of expected.

		// 1) Declining and stale.
		if ( $strong_decline && $freshness_days >= 90 ) {
			return array(
				'code'  => 'declining_and_stale',
				'label' => 'Declining and not updated',
			);
		}

		// 2) Low CTR for its current position (position <= 10).
		if ( $position_current > 0 && $position_current <= 10 && $ctr_low_enough && $impressions_current >= 300 ) {
			return array(
				'code'  => 'low_ctr_for_position',
				'label' => 'Low CTR for its position',
			);
		}

		// 3) High visibility, mid ranking (positions 8–20).
		if ( $impressions_high && $position_current >= 8 && $position_current <= 20 ) {
			return array(
				'code'  => 'high_visibility_mid_ranking',
				'label' => 'High visibility, mid ranking',
			);
		}

		// 4) Old but still strategic: very old, long not updated, with impressions.
		if ( $age_days >= 365 && $freshness_days >= 180 && $impressions_current >= 500 && $delta_clicks_pct > -0.10 ) {
			return array(
				'code'  => 'old_but_strategic',
				'label' => 'Old but still strategic',
			);
		}

		// 5) Historically strong, now slowing down.
		if ( $clicks_previous >= 50 && $moderate_decline && $freshness_days >= 180 ) {
			return array(
				'code'  => 'historically_strong_needs_refresh',
				'label' => 'Historically strong, now slowing down',
			);
		}

		// 6) Stale but still alive: not updated for long, but with some traffic.
		if ( $freshness_days >= 180 && ( $clicks_current > 0 || $impressions_current > 0 ) ) {
			return array(
				'code'  => 'stale_but_alive',
				'label' => 'Not updated but still alive',
			);
		}

		return array(
			'code'  => 'refresh_opportunity',
			'label' => 'Refresh opportunity',
		);
	}
}

