<?php
/**
 * GSC Sync.
 *
 * Pulls Search Analytics (by page) for the configured lookback window,
 * maps each page URL back to a local post, and upserts the metrics into
 * the cache table. Runs daily on cron and on demand from the settings UI.
 *
 * Data is requested with an end date 3 days behind "today" because Search
 * Console finalises data on a lag.
 *
 * @package Platform\Helm\Modules\GSC
 */

declare( strict_types = 1 );

namespace Platform\Helm\Modules\GSC;

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

final class GscSync {

	public const LAST_SYNC_OPTION = 'platform_helm_gsc_last_sync';

	private const PAGE_SIZE   = 25000;
	private const MAX_PAGES   = 8; // Safety bound: up to 200k rows per run.
	private const DATA_LAG    = 3;  // Days behind today for the end date.
	private const MIN_QUERY_IMPRESSIONS = 10; // Drop long-tail noise from the query layer.

	public function __construct(
		private GscClient $client,
		private GscStore $store,
		private GscQueryStore $query_store,
		private GscSnapshotStore $snapshots
	) {}

	/**
	 * Run a full Search Analytics sync.
	 *
	 * @return array{ok:bool,message:string,fetched:int,matched:int,skipped:int}
	 */
	public function run(): array {
		if ( ! GscCredentials::is_connected() ) {
			return $this->result( false, __( 'Not connected to Google Search Console.', 'platform-helm' ) );
		}
		$property = GscCredentials::property();
		if ( '' === $property ) {
			return $this->result( false, __( 'No Search Console property selected.', 'platform-helm' ) );
		}

		$end   = gmdate( 'Y-m-d', time() - ( self::DATA_LAG * DAY_IN_SECONDS ) );
		$start = gmdate( 'Y-m-d', time() - ( ( GscCredentials::range_days() + self::DATA_LAG ) * DAY_IN_SECONDS ) );

		$fetched = 0;
		$matched = 0;
		$skipped = 0;
		$buffer  = [];
		$tot_clicks = 0.0;
		$tot_impr   = 0.0;
		$pos_weight = 0.0;

		for ( $page = 0; $page < self::MAX_PAGES; $page++ ) {
			$resp = $this->client->query_search_analytics( $property, $start, $end, $page * self::PAGE_SIZE, self::PAGE_SIZE );
			if ( is_wp_error( $resp ) ) {
				// If we already collected some rows, persist them before reporting.
				if ( ! empty( $buffer ) ) {
					$this->store->upsert_metrics( $buffer );
				}
				return $this->result( false, $resp->get_error_message(), $fetched, $matched, $skipped );
			}
			$rows = $resp['rows'];
			if ( empty( $rows ) ) {
				break;
			}
			foreach ( $rows as $row ) {
				$fetched++;
				$tot_clicks += $row['clicks'];
				$tot_impr   += $row['impressions'];
				$pos_weight += $row['position'] * $row['impressions'];
				$url     = $row['keys'][0] ?? '';
				$post_id = $this->url_to_post( $url );
				if ( $post_id <= 0 ) {
					$skipped++;
					continue;
				}
				$buffer[] = [
					'post_id'     => $post_id,
					'url'         => $url,
					'clicks'      => (int) round( $row['clicks'] ),
					'impressions' => (int) round( $row['impressions'] ),
					'ctr'         => $row['ctr'],
					'position'    => $row['position'],
				];
				$matched++;
			}
			if ( count( $rows ) < self::PAGE_SIZE ) {
				break; // Last page.
			}
		}

		if ( ! empty( $buffer ) ) {
			$this->store->upsert_metrics( $buffer );
		}

		// Time-series snapshot: site-wide totals (post_id 0) + watch-listed posts.
		$avg_pos = $tot_impr > 0 ? ( $pos_weight / $tot_impr ) : 0.0;
		$ctr     = $tot_impr > 0 ? ( $tot_clicks / $tot_impr ) : 0.0;
		$this->snapshots->record( 0, (int) round( $tot_clicks ), (int) round( $tot_impr ), $ctr, $avg_pos );
		foreach ( $this->snapshots->watchlist() as $wid ) {
			$m = $this->store->get_for_post( $wid );
			if ( $m ) {
				$this->snapshots->record( $wid, (int) $m['clicks'], (int) $m['impressions'], (float) $m['ctr'], (float) $m['position'] );
			}
		}

		update_option( self::LAST_SYNC_OPTION, [
			'time'    => time(),
			'fetched' => $fetched,
			'matched' => $matched,
			'skipped' => $skipped,
			'start'   => $start,
			'end'     => $end,
		], false );

		return $this->result(
			true,
			sprintf(
				/* translators: 1: matched count, 2: fetched count, 3: skipped count */
				__( 'Synced %1$d posts from %2$d Search Console pages (%3$d non-post URLs skipped).', 'platform-helm' ),
				$matched,
				$fetched,
				$skipped
			),
			$fetched,
			$matched,
			$skipped
		);
	}

	/**
	 * Sync the page×query layer (powers Refresh Intelligence + Cannibalization).
	 *
	 * @return array{ok:bool,rows:int,message?:string}
	 */
	public function run_queries(): array {
		if ( ! GscCredentials::is_connected() ) {
			return [ 'ok' => false, 'rows' => 0 ];
		}
		$property = GscCredentials::property();
		if ( '' === $property ) {
			return [ 'ok' => false, 'rows' => 0 ];
		}

		$end   = gmdate( 'Y-m-d', time() - ( self::DATA_LAG * DAY_IN_SECONDS ) );
		$start = gmdate( 'Y-m-d', time() - ( ( GscCredentials::range_days() + self::DATA_LAG ) * DAY_IN_SECONDS ) );

		$buffer = [];
		$stored = 0;

		for ( $page = 0; $page < self::MAX_PAGES; $page++ ) {
			$resp = $this->client->query_search_analytics( $property, $start, $end, $page * self::PAGE_SIZE, self::PAGE_SIZE, [ 'page', 'query' ] );
			if ( is_wp_error( $resp ) ) {
				if ( ! empty( $buffer ) ) {
					$stored += $this->query_store->upsert( $buffer );
				}
				return [ 'ok' => false, 'rows' => $stored, 'message' => $resp->get_error_message() ];
			}
			$rows = $resp['rows'];
			if ( empty( $rows ) ) {
				break;
			}
			foreach ( $rows as $row ) {
				if ( $row['impressions'] < self::MIN_QUERY_IMPRESSIONS ) {
					continue; // Skip long-tail noise.
				}
				$url   = $row['keys'][0] ?? '';
				$query = $row['keys'][1] ?? '';
				if ( '' === $query ) {
					continue;
				}
				$post_id = $this->url_to_post( $url );
				if ( $post_id <= 0 ) {
					continue;
				}
				$buffer[] = [
					'post_id'     => $post_id,
					'url'         => $url,
					'query'       => $query,
					'clicks'      => (int) round( $row['clicks'] ),
					'impressions' => (int) round( $row['impressions'] ),
					'ctr'         => $row['ctr'],
					'position'    => $row['position'],
				];
				if ( count( $buffer ) >= 1000 ) {
					$stored += $this->query_store->upsert( $buffer );
					$buffer = [];
				}
			}
			if ( count( $rows ) < self::PAGE_SIZE ) {
				break;
			}
		}

		if ( ! empty( $buffer ) ) {
			$stored += $this->query_store->upsert( $buffer );
		}
		return [ 'ok' => true, 'rows' => $stored ];
	}

	/**
	 * Resolve a Search Console page URL to a local post ID. Strips query
	 * and fragment first; returns 0 for anything that isn't a singular post.
	 */
	private function url_to_post( string $url ): int {
		if ( '' === $url ) {
			return 0;
		}
		$clean = strtok( $url, '#' );
		$clean = is_string( $clean ) ? strtok( $clean, '?' ) : $url;
		if ( ! is_string( $clean ) || '' === $clean ) {
			return 0;
		}
		return (int) url_to_postid( $clean );
	}

	/**
	 * @return array{time:int,fetched:int,matched:int,skipped:int,start:string,end:string}|null
	 */
	public static function last_sync(): ?array {
		$v = get_option( self::LAST_SYNC_OPTION, null );
		return is_array( $v ) ? $v : null;
	}

	/**
	 * @return array{ok:bool,message:string,fetched:int,matched:int,skipped:int}
	 */
	private function result( bool $ok, string $message, int $fetched = 0, int $matched = 0, int $skipped = 0 ): array {
		return [
			'ok'      => $ok,
			'message' => $message,
			'fetched' => $fetched,
			'matched' => $matched,
			'skipped' => $skipped,
		];
	}
}
