<?php
/**
 * Google Search Console performance sync (Pro).
 *
 * @package Newsroom_AI_Assistant
 */

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

if ( ! defined( 'NEWSAI_GSC_OAUTH_SCOPE' ) ) {
	/** @var string GSC Search Analytics read scope. */
	define( 'NEWSAI_GSC_OAUTH_SCOPE', 'https://www.googleapis.com/auth/webmasters.readonly' );
}

if ( ! defined( 'NEWSAI_GSC_TOKEN_TRANSIENT' ) ) {
	/** @var string Transient key for GSC bearer token (separate from Indexing API). */
	define( 'NEWSAI_GSC_TOKEN_TRANSIENT', 'newsai_gsc_sa_access_token' );
}

if ( ! function_exists( 'newsai_gsc_table_name' ) ) {
	function newsai_gsc_table_name() {
		global $wpdb;
		return $wpdb->prefix . 'newsai_gsc_performance';
	}
}

if ( ! function_exists( 'newsai_gsc_create_table' ) ) {
	function newsai_gsc_create_table() {
		global $wpdb;

		$table   = newsai_gsc_table_name();
		$charset = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$table} (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			post_id BIGINT UNSIGNED NOT NULL,
			author_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			data_date DATE NOT NULL,
			search_type VARCHAR(16) NOT NULL DEFAULT 'web',
			clicks INT UNSIGNED NOT NULL DEFAULT 0,
			impressions INT UNSIGNED NOT NULL DEFAULT 0,
			ctr DECIMAL(6,4) NOT NULL DEFAULT 0,
			position DECIMAL(8,2) NOT NULL DEFAULT 0,
			fetched_at DATETIME NOT NULL,
			PRIMARY KEY  (id),
			UNIQUE KEY uq_post_date_type (post_id, data_date, search_type),
			KEY idx_author (author_id, data_date),
			KEY idx_date (data_date)
		) {$charset};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );

		if ( function_exists( 'newsai_gsc_create_site_tables' ) ) {
			newsai_gsc_create_site_tables();
		}
		if ( function_exists( 'newsai_gsc_create_query_table' ) ) {
			newsai_gsc_create_query_table();
		}
	}
}

if ( ! function_exists( 'newsai_gsc_site_daily_table_name' ) ) {
	function newsai_gsc_site_daily_table_name() {
		global $wpdb;
		return $wpdb->prefix . 'newsai_gsc_site_daily';
	}
}

if ( ! function_exists( 'newsai_gsc_device_table_name' ) ) {
	function newsai_gsc_device_table_name() {
		global $wpdb;
		return $wpdb->prefix . 'newsai_gsc_device';
	}
}

if ( ! function_exists( 'newsai_gsc_site_daily_table_exists' ) ) {
	function newsai_gsc_site_daily_table_exists() {
		global $wpdb;
		$table = newsai_gsc_site_daily_table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table;
	}
}

if ( ! function_exists( 'newsai_gsc_device_table_exists' ) ) {
	function newsai_gsc_device_table_exists() {
		global $wpdb;
		$table = newsai_gsc_device_table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table;
	}
}

if ( ! function_exists( 'newsai_gsc_create_site_tables' ) ) {
	/**
	 * Site-wide GSC aggregates (date + device) for Chief Insights charts.
	 */
	function newsai_gsc_create_site_tables() {
		global $wpdb;
		$charset = $wpdb->get_charset_collate();

		$daily = newsai_gsc_site_daily_table_name();
		$sql1  = "CREATE TABLE {$daily} (
			data_date DATE NOT NULL,
			search_type VARCHAR(16) NOT NULL DEFAULT 'web',
			clicks INT UNSIGNED NOT NULL DEFAULT 0,
			impressions INT UNSIGNED NOT NULL DEFAULT 0,
			fetched_at DATETIME NOT NULL,
			PRIMARY KEY  (data_date, search_type),
			KEY idx_type_date (search_type, data_date)
		) {$charset};";

		$device = newsai_gsc_device_table_name();
		$sql2   = "CREATE TABLE {$device} (
			data_date DATE NOT NULL,
			search_type VARCHAR(16) NOT NULL DEFAULT 'web',
			device VARCHAR(16) NOT NULL DEFAULT 'DESKTOP',
			clicks INT UNSIGNED NOT NULL DEFAULT 0,
			impressions INT UNSIGNED NOT NULL DEFAULT 0,
			fetched_at DATETIME NOT NULL,
			PRIMARY KEY  (data_date, search_type, device),
			KEY idx_device_window (search_type, data_date)
		) {$charset};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql1 );
		dbDelta( $sql2 );
	}
}

if ( ! function_exists( 'newsai_gsc_table_exists' ) ) {
	function newsai_gsc_table_exists() {
		global $wpdb;
		$table = newsai_gsc_table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table;
	}
}

if ( ! function_exists( 'newsai_gsc_row_count' ) ) {
	function newsai_gsc_row_count() {
		$cached = get_transient( 'newsai_gsc_row_count' );
		if ( false !== $cached ) {
			return (int) $cached;
		}
		if ( ! newsai_gsc_table_exists() ) {
			return 0;
		}
		global $wpdb;
		$table = newsai_gsc_table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
		set_transient( 'newsai_gsc_row_count', $count, 5 * MINUTE_IN_SECONDS );
		return $count;
	}
}

if ( ! function_exists( 'newsai_gsc_is_enabled' ) ) {
	function newsai_gsc_is_enabled() {
		return get_option( 'newsai_gsc_enabled', 'no' ) === 'yes';
	}
}

if ( ! function_exists( 'newsai_gsc_is_pro_active' ) ) {
	function newsai_gsc_is_pro_active() {
		return function_exists( 'newsai_is_pro' ) && newsai_is_pro();
	}
}

if ( ! function_exists( 'newsai_sanitize_sc_property' ) ) {
	/**
	 * Normalize Search Console property for API (URL-prefix or Domain).
	 *
	 * Accepts:
	 *   - sc-domain:example.com
	 *   - example.com  → sc-domain:example.com
	 *   - https://www.example.com/  (URL-prefix)
	 *
	 * @param string $value Raw input.
	 * @return string
	 */
	function newsai_sanitize_sc_property( $value ) {
		$value = trim( (string) wp_unslash( $value ) );
		if ( $value === '' ) {
			return '';
		}

		// Domain property (GSC API format).
		if ( preg_match( '#^sc-domain:(.+)$#i', $value, $m ) ) {
			$host = strtolower( trim( $m[1], " \t\n\r\0\x0B./" ) );
			if ( preg_match( '#^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$#i', $host ) ) {
				return 'sc-domain:' . $host;
			}
			return '';
		}

		// Bare hostname shown in GSC as "example.com" (Domain property).
		if ( preg_match( '#^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$#i', $value ) ) {
			return 'sc-domain:' . strtolower( $value );
		}

		// URL-prefix property — must be a full http(s) URL.
		if ( ! preg_match( '#^https?://#i', $value ) ) {
			$value = 'https://' . ltrim( $value, '/' );
		}
		$url = esc_url_raw( $value );
		if ( $url === '' ) {
			return '';
		}
		return trailingslashit( $url );
	}
}

if ( ! function_exists( 'newsai_gsc_get_property' ) ) {
	/**
	 * Search Console property identifier (must match GSC property exactly).
	 *
	 * @return string e.g. https://example.com/ or sc-domain:example.com
	 */
	function newsai_gsc_get_property() {
		$prop = trim( (string) get_option( 'newsai_indexing_sc_property', '' ) );
		if ( $prop !== '' && function_exists( 'newsai_sanitize_sc_property' ) ) {
			$normalized = newsai_sanitize_sc_property( $prop );
			if ( $normalized !== '' ) {
				$prop = $normalized;
			}
		}
		if ( $prop === '' ) {
			$prop = trailingslashit( home_url() );
		}
		return $prop;
	}
}

if ( ! function_exists( 'newsai_gsc_has_service_account' ) ) {
	function newsai_gsc_has_service_account() {
		$json = function_exists( 'newsai_secret_get' )
			? newsai_secret_get( 'indexing_service_json' )
			: (string) get_option( 'newsai_indexing_service_json', '' );
		return trim( $json ) !== '';
	}
}

if ( ! function_exists( 'newsai_gsc_get_access_token' ) ) {
	/**
	 * @param string $error Populated on failure.
	 * @return string
	 */
	function newsai_gsc_get_access_token( &$error = '' ) {
		if ( ! function_exists( 'newsai_get_service_account_access_token' ) ) {
			$error = 'Service account helper missing.';
			return '';
		}
		return newsai_get_service_account_access_token( $error, NEWSAI_GSC_OAUTH_SCOPE, NEWSAI_GSC_TOKEN_TRANSIENT );
	}
}

if ( ! function_exists( 'newsai_gsc_bump_cache_gen' ) ) {
	function newsai_gsc_bump_cache_gen() {
		update_option( 'newsai_gsc_cache_gen', (string) time(), false );
		if ( function_exists( 'newsai_pro_invalidate_author_performance' ) ) {
			newsai_pro_invalidate_author_performance();
		}
	}
}

if ( ! function_exists( 'newsai_gsc_data_available' ) ) {
	/**
	 * Whether GSC metrics can be shown in dashboards (sync on + table + Pro).
	 */
	function newsai_gsc_data_available() {
		return newsai_gsc_is_pro_active()
			&& newsai_gsc_is_enabled()
			&& newsai_gsc_table_exists();
	}
}

if ( ! function_exists( 'newsai_gsc_sanitize_search_type' ) ) {
	/**
	 * @param string $type web|discover|googleNews
	 * @return string
	 */
	function newsai_gsc_sanitize_search_type( $type ) {
		$allowed = [ 'web', 'discover', 'googleNews' ];
		return in_array( $type, $allowed, true ) ? $type : 'web';
	}
}

if ( ! function_exists( 'newsai_gsc_date_since' ) ) {
	/**
	 * Inclusive start date (Y-m-d) for a rolling window aligned with Author Performance.
	 *
	 * @param int $days Window length.
	 * @return string
	 */
	function newsai_gsc_date_since( $days ) {
		$days = max( 1, (int) $days );
		return gmdate( 'Y-m-d', strtotime( '-' . $days . ' days' ) );
	}
}

if ( ! function_exists( 'newsai_gsc_author_metrics' ) ) {
	/**
	 * Aggregate GSC clicks/impressions for an author in a window.
	 *
	 * @param int    $user_id     Author user ID.
	 * @param int    $days        Window in days.
	 * @param string $search_type web|discover|googleNews.
	 * @return array|null Metrics bundle or null when GSC is off / no rows.
	 */
	function newsai_gsc_author_metrics( $user_id, $days = 30, $search_type = 'discover' ) {
		if ( ! newsai_gsc_data_available() ) {
			return null;
		}
		global $wpdb;
		$user_id     = (int) $user_id;
		$days        = max( 1, (int) $days );
		$search_type = newsai_gsc_sanitize_search_type( $search_type );
		$since       = newsai_gsc_date_since( $days );
		$table       = newsai_gsc_table_name();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT SUM(clicks) AS clicks, SUM(impressions) AS impressions,
				        AVG(position) AS avg_position, COUNT(DISTINCT post_id) AS posts
				 FROM {$table}
				 WHERE author_id = %d AND data_date >= %s AND search_type = %s",
				$user_id,
				$since,
				$search_type
			)
		);
		if ( ! $row ) {
			return null;
		}
		$clicks      = (int) $row->clicks;
		$impressions = (int) $row->impressions;
		if ( $clicks < 1 && $impressions < 1 ) {
			return null;
		}
		$ctr = $impressions > 0 ? round( $clicks / $impressions, 4 ) : 0.0;

		return [
			'clicks'      => $clicks,
			'impressions' => $impressions,
			'ctr'         => $ctr,
			'position'    => round( (float) $row->avg_position, 1 ),
			'posts'       => (int) $row->posts,
			'window_days' => $days,
			'search_type' => $search_type,
			'last_sync'   => (string) get_option( 'newsai_gsc_last_sync', '' ),
		];
	}
}

if ( ! function_exists( 'newsai_gsc_post_traffic_map' ) ) {
	/**
	 * Per-post GSC clicks by search type for an author in a window.
	 *
	 * @param int   $user_id Author ID.
	 * @param int   $days    Window.
	 * @param int[] $post_ids Optional restrict to these posts.
	 * @return array<int, array{web:int, discover:int, googleNews:int, total:int}>
	 */
	function newsai_gsc_post_traffic_map( $user_id, $days = 30, $post_ids = [] ) {
		if ( ! newsai_gsc_data_available() ) {
			return [];
		}
		global $wpdb;
		$user_id = (int) $user_id;
		$days    = max( 1, (int) $days );
		$since   = newsai_gsc_date_since( $days );
		$table   = newsai_gsc_table_name();

		$sql = "SELECT post_id, search_type, SUM(clicks) AS clicks
		        FROM {$table}
		        WHERE author_id = %d AND data_date >= %s";
		$args = [ $user_id, $since ];

		$post_ids = array_values( array_filter( array_map( 'intval', (array) $post_ids ) ) );
		if ( ! empty( $post_ids ) ) {
			$placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) );
			$sql         .= " AND post_id IN ({$placeholders})";
			$args         = array_merge( $args, $post_ids );
		}
		$sql .= ' GROUP BY post_id, search_type';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
		$rows = $wpdb->get_results( $wpdb->prepare( $sql, $args ) );
		$map  = [];
		foreach ( (array) $rows as $r ) {
			$pid  = (int) $r->post_id;
			$type = newsai_gsc_sanitize_search_type( (string) $r->search_type );
			$n    = (int) $r->clicks;
			if ( ! isset( $map[ $pid ] ) ) {
				$map[ $pid ] = [
					'web'         => 0,
					'discover'    => 0,
					'googleNews'  => 0,
					'total'       => 0,
				];
			}
			$map[ $pid ][ $type ] = $n;
			$map[ $pid ]['total'] += $n;
		}
		return $map;
	}
}

if ( ! function_exists( 'newsai_gsc_author_top_post_ids' ) ) {
	/**
	 * Post IDs with the most GSC clicks in the window (all search types).
	 *
	 * @param int $user_id Author ID.
	 * @param int $days    Window.
	 * @param int $limit   Max posts.
	 * @return int[]
	 */
	function newsai_gsc_author_top_post_ids( $user_id, $days = 30, $limit = 5 ) {
		if ( ! newsai_gsc_data_available() ) {
			return [];
		}
		global $wpdb;
		$user_id = (int) $user_id;
		$days    = max( 1, (int) $days );
		$limit   = max( 1, (int) $limit );
		$since   = newsai_gsc_date_since( $days );
		$table   = newsai_gsc_table_name();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT post_id FROM {$table}
				 WHERE author_id = %d AND data_date >= %s
				 GROUP BY post_id
				 ORDER BY SUM(clicks) DESC
				 LIMIT %d",
				$user_id,
				$since,
				$limit
			)
		);
		return array_values( array_filter( array_map( 'intval', (array) $ids ) ) );
	}
}

// Wire Search Console into Author Performance bundles (Phase 2).
add_filter(
	'newsai_pro_author_discover_metrics',
	static function ( $metrics, $user_id, $days ) {
		unset( $metrics );
		return newsai_gsc_author_metrics( (int) $user_id, (int) $days, 'discover' );
	},
	10,
	3
);
add_filter(
	'newsai_pro_author_gnews_metrics',
	static function ( $metrics, $user_id, $days ) {
		unset( $metrics );
		return newsai_gsc_author_metrics( (int) $user_id, (int) $days, 'googleNews' );
	},
	10,
	3
);

if ( ! function_exists( 'newsai_gsc_normalize_page_url' ) ) {
	/**
	 * Strip tracking/AMP query variants before url_to_postid().
	 *
	 * @param string $url GSC page URL.
	 * @return string
	 */
	function newsai_gsc_normalize_page_url( $url ) {
		$url = trim( (string) $url );
		if ( $url === '' ) {
			return '';
		}
		$url = strtok( $url, '?' );
		$url = preg_replace( '#/(amp|embed)/?$#i', '', (string) $url );
		return untrailingslashit( (string) $url );
	}
}

if ( ! function_exists( 'newsai_gsc_url_lookup_candidates' ) ) {
	/**
	 * Permalink variants for url_to_postid (www/non-www, trailing slash).
	 *
	 * @param string $clean Normalized URL.
	 * @return string[]
	 */
	function newsai_gsc_url_lookup_candidates( $clean ) {
		$clean = trim( (string) $clean );
		if ( $clean === '' ) {
			return [];
		}
		$candidates = [
			$clean,
			trailingslashit( $clean ),
			untrailingslashit( $clean ),
		];
		$url_host  = (string) wp_parse_url( $clean, PHP_URL_HOST );
		$site_host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
		if ( $url_host !== '' && $site_host !== '' && strcasecmp( $url_host, $site_host ) !== 0 ) {
			$scheme = (string) wp_parse_url( $clean, PHP_URL_SCHEME );
			if ( $scheme === '' ) {
				$scheme = is_ssl() ? 'https' : 'http';
			}
			$path = (string) wp_parse_url( $clean, PHP_URL_PATH );
			$candidates[] = untrailingslashit( $scheme . '://' . $site_host . $path );
			$candidates[] = trailingslashit( $scheme . '://' . $site_host . $path );
			// Common www mismatch.
			if ( stripos( $url_host, 'www.' ) === 0 && stripos( $site_host, 'www.' ) !== 0 ) {
				$alt = substr( $url_host, 4 );
				$candidates[] = untrailingslashit( $scheme . '://' . $alt . $path );
			} elseif ( stripos( $url_host, 'www.' ) !== 0 && stripos( $site_host, 'www.' ) === 0 ) {
				$candidates[] = untrailingslashit( $scheme . '://www.' . $url_host . $path );
			}
		}
		return array_values( array_unique( array_filter( $candidates ) ) );
	}
}

if ( ! function_exists( 'newsai_gsc_resolve_post_by_slug' ) ) {
	/**
	 * @param string $clean Normalized URL.
	 * @return int Post ID or 0.
	 */
	function newsai_gsc_resolve_post_by_slug( $clean ) {
		$path = trim( (string) wp_parse_url( $clean, PHP_URL_PATH ), '/' );
		if ( $path === '' ) {
			return 0;
		}
		$slug = rawurldecode( (string) basename( str_replace( '\\', '/', $path ) ) );
		if ( $slug !== '' ) {
			$post = get_page_by_path( $slug, OBJECT, 'post' );
			if ( $post instanceof WP_Post ) {
				return (int) $post->ID;
			}
		}
		$post = get_page_by_path( rawurldecode( $path ), OBJECT, 'post' );
		return ( $post instanceof WP_Post ) ? (int) $post->ID : 0;
	}
}

if ( ! function_exists( 'newsai_gsc_resolve_post' ) ) {
	/**
	 * Map a GSC page URL to post_id + author_id.
	 *
	 * @param string $url Page URL from GSC.
	 * @return array{post_id:int,author_id:int}|null
	 */
	function newsai_gsc_resolve_post( $url ) {
		$clean = newsai_gsc_normalize_page_url( $url );
		if ( $clean === '' ) {
			return null;
		}

		$post_id = 0;
		foreach ( newsai_gsc_url_lookup_candidates( $clean ) as $candidate ) {
			$post_id = (int) url_to_postid( $candidate );
			if ( $post_id > 0 ) {
				break;
			}
		}
		if ( $post_id < 1 ) {
			$post_id = newsai_gsc_resolve_post_by_slug( $clean );
		}

		$post_id = (int) apply_filters( 'newsai_gsc_resolve_post_id', $post_id, $url, $clean );
		if ( $post_id < 1 ) {
			return null;
		}

		$post = get_post( $post_id );
		if ( ! $post || $post->post_type !== 'post' || $post->post_status !== 'publish' ) {
			return null;
		}

		return [
			'post_id'   => $post_id,
			'author_id' => (int) $post->post_author,
		];
	}
}

if ( ! function_exists( 'newsai_gsc_fetch_analytics_rows' ) ) {
	/**
	 * Query Search Analytics API (paginated).
	 *
	 * @param string        $token        Bearer token.
	 * @param string        $property_url GSC property.
	 * @param string        $start_date   Y-m-d.
	 * @param string        $end_date     Y-m-d.
	 * @param string        $type         web|discover|googleNews|news.
	 * @param array<string> $dimensions   GSC dimensions (page, date, device, …).
	 * @param int           $row_limit     Per request (max 25000).
	 * @param list<string>  $page_filters  Optional page URLs (OR filter).
	 * @return array<int,array<string,mixed>>|WP_Error
	 */
	function newsai_gsc_fetch_analytics_rows( $token, $property_url, $start_date, $end_date, $type = 'web', $dimensions = [ 'page', 'date' ], $row_limit = 2500, $timeout = 45, $page_filters = [] ) {
		$property_url = trim( (string) $property_url );
		$token        = trim( (string) $token );
		if ( $property_url === '' || $token === '' ) {
			return new WP_Error( 'gsc_args', 'Missing property or token.' );
		}

		$type    = sanitize_key( (string) $type );
		$allowed = [ 'web', 'discover', 'googlenews', 'news' ];
		if ( ! in_array( $type, $allowed, true ) ) {
			$type = 'web';
		}
		if ( $type === 'googlenews' ) {
			$type = 'googleNews';
		}

		$dims = [];
		foreach ( (array) $dimensions as $dim ) {
			$d = sanitize_key( (string) $dim );
			if ( in_array( $d, [ 'page', 'date', 'device', 'country', 'query' ], true ) ) {
				$dims[] = $d;
			}
		}
		if ( $dims === [] ) {
			$dims = [ 'page', 'date' ];
		}

		$row_limit = max( 1, min( 25000, (int) $row_limit ) );
		$timeout   = max( 8, min( 120, (int) $timeout ) );
		$endpoint  = 'https://www.googleapis.com/webmasters/v3/sites/'
			. rawurlencode( $property_url )
			. '/searchAnalytics/query';

		$all   = [];
		$start = 0;

		$filter_groups = [];
		if ( ! empty( $page_filters ) && in_array( 'page', $dims, true ) ) {
			$filters = [];
			foreach ( (array) $page_filters as $page_url ) {
				$page_url = trim( (string) $page_url );
				if ( $page_url === '' ) {
					continue;
				}
				$filters[] = [
					'dimension'  => 'page',
					'operator'   => 'equals',
					'expression' => $page_url,
				];
			}
			if ( $filters !== [] ) {
				$filter_groups[] = [ 'filters' => $filters ];
			}
		}

		do {
			$payload = [
				'startDate'  => $start_date,
				'endDate'    => $end_date,
				'dimensions' => $dims,
				'type'       => $type,
				'rowLimit'   => $row_limit,
				'startRow'   => $start,
			];
			if ( $filter_groups !== [] ) {
				$payload['dimensionFilterGroups'] = $filter_groups;
			}

			$response = wp_remote_post(
				$endpoint,
				[
					'timeout' => $timeout,
					'headers' => [
						'Authorization' => 'Bearer ' . $token,
						'Content-Type'  => 'application/json',
					],
					'body'    => wp_json_encode( $payload ),
				]
			);

			if ( is_wp_error( $response ) ) {
				return $response;
			}

			$code = (int) wp_remote_retrieve_response_code( $response );
			$body = json_decode( (string) wp_remote_retrieve_body( $response ), true );
			if ( $code < 200 || $code >= 300 ) {
				$msg = 'GSC API HTTP ' . $code;
				if ( is_array( $body ) && ! empty( $body['error']['message'] ) ) {
					$msg = sanitize_text_field( (string) $body['error']['message'] );
				}
				return new WP_Error( 'gsc_http', $msg );
			}

			$rows = isset( $body['rows'] ) && is_array( $body['rows'] ) ? $body['rows'] : [];
			$all  = array_merge( $all, $rows );
			$got  = count( $rows );
			$start += $got;
		} while ( $got >= $row_limit );

		return $all;
	}
}

if ( ! function_exists( 'newsai_gsc_fetch_rows' ) ) {
	/**
	 * @param string $token        Bearer token.
	 * @param string $property_url GSC property.
	 * @param string $start_date   Y-m-d.
	 * @param string $end_date     Y-m-d.
	 * @param string $type         web|discover|googleNews|news.
	 * @param int    $row_limit    Per request (max 25000).
	 * @return array<int,array<string,mixed>>|WP_Error
	 */
	function newsai_gsc_fetch_rows( $token, $property_url, $start_date, $end_date, $type = 'web', $row_limit = 2500, $timeout = 45, $page_filters = [] ) {
		return newsai_gsc_fetch_analytics_rows( $token, $property_url, $start_date, $end_date, $type, [ 'page', 'date' ], $row_limit, $timeout, $page_filters );
	}
}

if ( ! function_exists( 'newsai_gsc_get_max_post_data_date' ) ) {
	/**
	 * Latest data_date stored for post-level GSC rows.
	 *
	 * @return string Y-m-d or empty.
	 */
	function newsai_gsc_get_max_post_data_date() {
		global $wpdb;
		if ( ! newsai_gsc_table_exists() ) {
			return '';
		}
		$table = newsai_gsc_table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$max = $wpdb->get_var( "SELECT MAX(data_date) FROM {$table}" );
		if ( ! is_string( $max ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $max ) ) {
			return '';
		}
		return $max;
	}
}

if ( ! function_exists( 'newsai_gsc_compute_post_sync_window' ) ) {
	/**
	 * Incremental post-level window: from last stored date (minus overlap) through GSC-safe end.
	 *
	 * @param string $end_ymd Y-m-d (typically today - 3 days).
	 * @return array{start:string,end:string,mode:string,days:int}
	 */
	function newsai_gsc_compute_post_sync_window( $end_ymd ) {
		$end_ymd = (string) $end_ymd;
		$end_ts  = strtotime( $end_ymd . ' UTC' );
		if ( $end_ts === false ) {
			$end_ts = strtotime( '-3 days' );
			$end_ymd = gmdate( 'Y-m-d', $end_ts );
		}

		$max_date = newsai_gsc_get_max_post_data_date();
		if ( $max_date === '' ) {
			$start_ts = strtotime( '-10 days', $end_ts );
			$mode     = 'initial';
		} else {
			$start_ts = strtotime( $max_date . ' UTC -3 days' );
			$mode     = 'incremental';
			$cap_ts   = strtotime( '-14 days', $end_ts );
			if ( $start_ts < $cap_ts ) {
				$start_ts = $cap_ts;
				$mode     = 'catchup';
			}
		}

		if ( $start_ts > $end_ts ) {
			$start_ts = strtotime( '-3 days', $end_ts );
		}

		$start = gmdate( 'Y-m-d', $start_ts );
		$days  = (int) floor( ( $end_ts - $start_ts ) / DAY_IN_SECONDS ) + 1;

		return [
			'start' => $start,
			'end'   => $end_ymd,
			'mode'  => $mode,
			'days'  => max( 1, $days ),
		];
	}
}

if ( ! function_exists( 'newsai_gsc_should_run_full_site_chart_sync' ) ) {
	/**
	 * Full 93-day site chart refresh at most once per week.
	 */
	function newsai_gsc_should_run_full_site_chart_sync() {
		$last = (int) get_option( 'newsai_gsc_last_site_full_sync', 0 );
		return $last < 1 || ( time() - $last ) >= 7 * DAY_IN_SECONDS;
	}
}

if ( ! function_exists( 'newsai_gsc_compute_site_chart_window' ) ) {
	/**
	 * Site-wide chart sync: incremental 14-day slice daily, full 93-day weekly.
	 *
	 * @param string $end_ymd Y-m-d.
	 * @return array{start:string,end:string,mode:string}
	 */
	function newsai_gsc_compute_site_chart_window( $end_ymd ) {
		$end_ymd = (string) $end_ymd;
		$end_ts  = strtotime( $end_ymd . ' UTC' );
		if ( $end_ts === false ) {
			$end_ts  = strtotime( '-3 days' );
			$end_ymd = gmdate( 'Y-m-d', $end_ts );
		}
		if ( newsai_gsc_should_run_full_site_chart_sync() ) {
			return [
				'start' => gmdate( 'Y-m-d', strtotime( '-93 days', $end_ts ) ),
				'end'   => $end_ymd,
				'mode'  => 'full',
			];
		}
		return [
			'start' => gmdate( 'Y-m-d', strtotime( '-14 days', $end_ts ) ),
			'end'   => $end_ymd,
			'mode'  => 'incremental',
		];
	}
}

if ( ! function_exists( 'newsai_gsc_upsert_site_daily' ) ) {
	function newsai_gsc_upsert_site_daily( $date, $type, $clicks, $impressions ) {
		global $wpdb;
		if ( ! newsai_gsc_site_daily_table_exists() ) {
			newsai_gsc_create_site_tables();
		}
		$table = newsai_gsc_site_daily_table_name();
		$type  = newsai_gsc_sanitize_search_type( (string) $type );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$wpdb->query(
			$wpdb->prepare(
				"INSERT INTO {$table}
					(data_date, search_type, clicks, impressions, fetched_at)
				 VALUES (%s, %s, %d, %d, %s)
				 ON DUPLICATE KEY UPDATE
					clicks = VALUES(clicks),
					impressions = VALUES(impressions),
					fetched_at = VALUES(fetched_at)",
				$date,
				$type,
				(int) $clicks,
				(int) $impressions,
				current_time( 'mysql', true )
			)
		);
	}
}

if ( ! function_exists( 'newsai_gsc_upsert_device_row' ) ) {
	function newsai_gsc_upsert_device_row( $date, $type, $device, $clicks, $impressions ) {
		global $wpdb;
		if ( ! newsai_gsc_device_table_exists() ) {
			newsai_gsc_create_site_tables();
		}
		$table  = newsai_gsc_device_table_name();
		$type   = newsai_gsc_sanitize_search_type( (string) $type );
		$device = strtoupper( sanitize_key( (string) $device ) );
		if ( ! in_array( $device, [ 'DESKTOP', 'MOBILE', 'TABLET' ], true ) ) {
			return;
		}
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$wpdb->query(
			$wpdb->prepare(
				"INSERT INTO {$table}
					(data_date, search_type, device, clicks, impressions, fetched_at)
				 VALUES (%s, %s, %s, %d, %d, %s)
				 ON DUPLICATE KEY UPDATE
					clicks = VALUES(clicks),
					impressions = VALUES(impressions),
					fetched_at = VALUES(fetched_at)",
				$date,
				$type,
				$device,
				(int) $clicks,
				(int) $impressions,
				current_time( 'mysql', true )
			)
		);
	}
}

if ( ! function_exists( 'newsai_gsc_sync_site_metrics' ) ) {
	/**
	 * Pull site-wide daily + device rows from GSC (Chief Insights charts).
	 *
	 * @param string $token    Bearer token.
	 * @param string $property GSC property.
	 * @param string $start    Y-m-d.
	 * @param string $end      Y-m-d.
	 * @return array{site_rows:int,device_rows:int,errors:list<string>}
	 */
	function newsai_gsc_sync_site_metrics( $token, $property, $start, $end ) {
		$out = [
			'site_rows'   => 0,
			'device_rows' => 0,
			'errors'      => [],
		];
		if ( ! newsai_gsc_site_daily_table_exists() || ! newsai_gsc_device_table_exists() ) {
			newsai_gsc_create_site_tables();
		}

		$types = [ 'web', 'discover', 'googleNews' ];
		foreach ( $types as $type ) {
			$daily = newsai_gsc_fetch_analytics_rows( $token, $property, $start, $end, $type, [ 'date' ], 500, 45 );
			if ( is_wp_error( $daily ) ) {
				$out['errors'][] = $type . ' date: ' . $daily->get_error_message();
			} else {
				foreach ( (array) $daily as $row ) {
					$date = isset( $row['keys'][0] ) ? (string) $row['keys'][0] : '';
					if ( $date === '' ) {
						continue;
					}
					newsai_gsc_upsert_site_daily(
						$date,
						$type,
						(int) ( $row['clicks'] ?? 0 ),
						(int) ( $row['impressions'] ?? 0 )
					);
					++$out['site_rows'];
				}
			}

			$devices = newsai_gsc_fetch_analytics_rows( $token, $property, $start, $end, $type, [ 'device', 'date' ], 2500, 60 );
			if ( is_wp_error( $devices ) ) {
				$out['errors'][] = $type . ' device: ' . $devices->get_error_message();
			} else {
				foreach ( (array) $devices as $row ) {
					$device = isset( $row['keys'][0] ) ? (string) $row['keys'][0] : '';
					$date   = isset( $row['keys'][1] ) ? (string) $row['keys'][1] : '';
					if ( $device === '' || $date === '' ) {
						continue;
					}
					newsai_gsc_upsert_device_row(
						$date,
						$type,
						$device,
						(int) ( $row['clicks'] ?? 0 ),
						(int) ( $row['impressions'] ?? 0 )
					);
					++$out['device_rows'];
				}
			}
		}

		return $out;
	}
}

if ( ! function_exists( 'newsai_gsc_upsert_row' ) ) {
	function newsai_gsc_upsert_row( $post_id, $author_id, $date, $type, $clicks, $impressions, $ctr, $position ) {
		global $wpdb;
		$table = newsai_gsc_table_name();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$wpdb->query(
			$wpdb->prepare(
				"INSERT INTO {$table}
					(post_id, author_id, data_date, search_type, clicks, impressions, ctr, position, fetched_at)
				 VALUES (%d, %d, %s, %s, %d, %d, %f, %f, %s)
				 ON DUPLICATE KEY UPDATE
					author_id = VALUES(author_id),
					clicks = VALUES(clicks),
					impressions = VALUES(impressions),
					ctr = VALUES(ctr),
					position = VALUES(position),
					fetched_at = VALUES(fetched_at)",
				(int) $post_id,
				(int) $author_id,
				$date,
				$type,
				(int) $clicks,
				(int) $impressions,
				(float) $ctr,
				(float) $position,
				current_time( 'mysql', true )
			)
		);
	}
}

if ( ! function_exists( 'newsai_gsc_run_sync' ) ) {
	/**
	 * Pull GSC metrics into wp_newsai_gsc_performance.
	 *
	 * @return array{ok:bool,processed:int,skipped:int,message:string}
	 */
	function newsai_gsc_run_sync() {
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( 300 );
		}

		$out = [
			'ok'        => false,
			'processed' => 0,
			'skipped'   => 0,
			'message'   => '',
			'by_type'   => [
				'web'         => 0,
				'discover'    => 0,
				'googleNews'  => 0,
			],
		];

		if ( ! newsai_gsc_is_pro_active() ) {
			$out['message'] = __( 'Pro license required.', 'newsroom-ai-assistant' );
			return $out;
		}
		if ( ! newsai_gsc_is_enabled() ) {
			$out['message'] = __( 'GSC sync is disabled in settings.', 'newsroom-ai-assistant' );
			return $out;
		}
		if ( get_transient( 'newsai_gsc_backfill_lock' ) || ( function_exists( 'newsai_gsc_backfill_is_running' ) && newsai_gsc_backfill_is_running() ) ) {
			$out['message'] = __( 'Historical backfill is running. Try again when it finishes.', 'newsroom-ai-assistant' );
			return $out;
		}
		if ( ! newsai_gsc_table_exists() ) {
			newsai_gsc_create_table();
		} elseif ( function_exists( 'newsai_gsc_create_site_tables' ) && ( ! newsai_gsc_site_daily_table_exists() || ! newsai_gsc_device_table_exists() ) ) {
			newsai_gsc_create_site_tables();
		}
		if ( ! newsai_gsc_has_service_account() ) {
			$out['message'] = __( 'Service account JSON is not configured.', 'newsroom-ai-assistant' );
			return $out;
		}

		$property = newsai_gsc_get_property();
		if ( $property === '' ) {
			$out['message'] = __( 'Search Console property URL is empty.', 'newsroom-ai-assistant' );
			return $out;
		}

		$token_error = '';
		$token       = newsai_gsc_get_access_token( $token_error );
		if ( $token === '' ) {
			$out['message'] = $token_error !== '' ? $token_error : __( 'Could not obtain GSC access token.', 'newsroom-ai-assistant' );
			update_option( 'newsai_gsc_last_error', $out['message'], false );
			return $out;
		}

		// GSC data is typically 2–3 days behind.
		$end_ymd      = gmdate( 'Y-m-d', strtotime( '-3 days' ) );
		$post_window  = newsai_gsc_compute_post_sync_window( $end_ymd );
		$start        = $post_window['start'];
		$end          = $post_window['end'];
		$sync_mode    = (string) $post_window['mode'];
		$chart_window = newsai_gsc_compute_site_chart_window( $end_ymd );
		$chart_start  = $chart_window['start'];
		$chart_mode   = (string) $chart_window['mode'];

		$types        = [ 'web', 'discover', 'googleNews' ];
		$processed    = 0;
		$skipped      = 0;
		$skipped_urls = [];
		$errors       = [];

		foreach ( $types as $type ) {
			$rows = newsai_gsc_fetch_rows( $token, $property, $start, $end, $type, 2500, 60 );
			if ( is_wp_error( $rows ) ) {
				$errors[] = $type . ': ' . $rows->get_error_message();
				continue;
			}
			foreach ( $rows as $row ) {
				$url  = isset( $row['keys'][0] ) ? (string) $row['keys'][0] : '';
				$date = isset( $row['keys'][1] ) ? (string) $row['keys'][1] : '';
				if ( $url === '' || $date === '' ) {
					++$skipped;
					continue;
				}

				$resolved = newsai_gsc_resolve_post( $url );
				if ( ! $resolved ) {
					++$skipped;
					$norm = newsai_gsc_normalize_page_url( $url );
					if ( $norm !== '' ) {
						$skipped_urls[ $norm ] = true;
					}
					continue;
				}

				newsai_gsc_upsert_row(
					$resolved['post_id'],
					$resolved['author_id'],
					$date,
					$type,
					(int) ( $row['clicks'] ?? 0 ),
					(int) ( $row['impressions'] ?? 0 ),
					(float) ( $row['ctr'] ?? 0 ),
					(float) ( $row['position'] ?? 0 )
				);
				++$processed;
				if ( isset( $out['by_type'][ $type ] ) ) {
					++$out['by_type'][ $type ];
				}
			}
		}

		$site_sync   = function_exists( 'newsai_gsc_sync_site_metrics' )
			? newsai_gsc_sync_site_metrics( $token, $property, $chart_start, $end )
			: [ 'site_rows' => 0, 'device_rows' => 0, 'errors' => [] ];
		if ( $chart_mode === 'full' ) {
			update_option( 'newsai_gsc_last_site_full_sync', time(), false );
		}
		if ( ! empty( $site_sync['errors'] ) ) {
			$errors = array_merge( $errors, $site_sync['errors'] );
		}

		$query_sync = function_exists( 'newsai_gsc_sync_query_metrics' )
			? newsai_gsc_sync_query_metrics( $token, $property, $start, $end )
			: [ 'rows' => 0, 'mode' => '', 'errors' => [] ];
		if ( ! empty( $query_sync['errors'] ) ) {
			$errors = array_merge( $errors, $query_sync['errors'] );
		}
		update_option( 'newsai_gsc_last_query_rows', (int) ( $query_sync['rows'] ?? 0 ), false );
		update_option( 'newsai_gsc_last_query_sync_mode', (string) ( $query_sync['mode'] ?? '' ), false );
		if ( (int) ( $query_sync['rows'] ?? 0 ) > 0 && function_exists( 'newsai_seasonal_radar_compute' ) && function_exists( 'newsai_seasonal_radar_enabled' ) && newsai_seasonal_radar_enabled() ) {
			newsai_seasonal_radar_compute();
		}

		$has_site_data  = (int) ( $site_sync['site_rows'] ?? 0 ) > 0 || (int) ( $site_sync['device_rows'] ?? 0 ) > 0;
		$has_query_data = (int) ( $query_sync['rows'] ?? 0 ) > 0;
		if ( ! empty( $errors ) && $processed === 0 && ! $has_site_data && ! $has_query_data ) {
			$out['message'] = implode( ' ', $errors );
			update_option( 'newsai_gsc_last_error', $out['message'], false );
			return $out;
		}

		update_option( 'newsai_gsc_last_sync', current_time( 'mysql', true ), false );
		update_option( 'newsai_gsc_last_count', $processed, false );
		update_option( 'newsai_gsc_last_by_type', $out['by_type'], false );
		update_option( 'newsai_gsc_last_site_rows', (int) ( $site_sync['site_rows'] ?? 0 ), false );
		update_option( 'newsai_gsc_last_device_rows', (int) ( $site_sync['device_rows'] ?? 0 ), false );
		update_option( 'newsai_gsc_last_sync_window', $start . '..' . $end, false );
		update_option( 'newsai_gsc_last_sync_mode', $sync_mode, false );
		delete_option( 'newsai_gsc_last_error' );
		delete_transient( 'newsai_gsc_row_count' );
		newsai_gsc_bump_cache_gen();
		if ( function_exists( 'newsai_gsc_prune_all_gsc_data' ) ) {
			newsai_gsc_prune_all_gsc_data();
		}

		$out['ok']             = $processed > 0 || $has_site_data || $has_query_data;
		$out['processed']      = $processed;
		$out['skipped']        = $skipped;
		$out['skipped_unique'] = count( $skipped_urls );
		$site_note = '';
		if ( (int) ( $site_sync['site_rows'] ?? 0 ) > 0 || (int) ( $site_sync['device_rows'] ?? 0 ) > 0 ) {
			$site_note = ' ' . sprintf(
				/* translators: 1: site daily rows, 2: device rows */
				__( 'Site charts: %1$d daily · %2$d device rows.', 'newsroom-ai-assistant' ),
				(int) ( $site_sync['site_rows'] ?? 0 ),
				(int) ( $site_sync['device_rows'] ?? 0 )
			);
		}
		if ( (int) ( $query_sync['rows'] ?? 0 ) > 0 ) {
			$site_note .= ' ' . sprintf(
				/* translators: 1: query rows, 2: sync mode */
				__( 'Search queries: %1$d rows (%2$s).', 'newsroom-ai-assistant' ),
				(int) ( $query_sync['rows'] ?? 0 ),
				(string) ( $query_sync['mode'] ?? '' )
			);
		}

		$out['message'] = $processed > 0
			? sprintf(
				/* translators: 1: total rows, 2: web rows, 3: discover rows, 4: google news rows, 5: skipped row count, 6: unique unmatched URLs, 7: start date, 8: end date, 9: sync mode */
				__( 'Synced %1$d rows (%7$s–%8$s, %9$s) — Web %2$d · Discover %3$d · Google News %4$d. Skipped %5$d (≈%6$d URLs: homepage, categories, tags, or permalinks with no post match).', 'newsroom-ai-assistant' ),
				$processed,
				(int) $out['by_type']['web'],
				(int) $out['by_type']['discover'],
				(int) $out['by_type']['googleNews'],
				$skipped,
				(int) $out['skipped_unique'],
				$start,
				$end,
				$sync_mode
			) . $site_note
			: (
				$has_site_data
					? __( 'Post-level sync had no matches, but site-wide GSC chart data was updated.', 'newsroom-ai-assistant' ) . $site_note
					: (
						$has_query_data
							? __( 'Post-level sync had no matches, but search query data was stored for Seasonal radar.', 'newsroom-ai-assistant' ) . $site_note
							: __( 'Sync completed but no post rows were stored. Check property URL and that articles have traffic in GSC.', 'newsroom-ai-assistant' )
					)
			);

		if ( ! empty( $errors ) ) {
			$out['message'] .= ' ' . implode( ' ', $errors );
		}

		return $out;
	}
}

if ( ! function_exists( 'newsai_gsc_site_charts_has_data' ) ) {
	/**
	 * Whether site-wide GSC rows exist for Chief trend / device charts.
	 */
	function newsai_gsc_site_charts_has_data() {
		if ( ! function_exists( 'newsai_gsc_site_daily_table_exists' ) || ! newsai_gsc_site_daily_table_exists() ) {
			return false;
		}
		global $wpdb;
		$table = newsai_gsc_site_daily_table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ) > 0;
	}
}

if ( ! function_exists( 'newsai_gsc_charts_sync_notice_active' ) ) {
	/**
	 * Show admin reminder until first site-wide GSC chart sync (2.2.2+).
	 */
	function newsai_gsc_charts_sync_notice_active() {
		if ( ! is_admin() || ! function_exists( 'newsai_gsc_data_available' ) || ! newsai_gsc_data_available() ) {
			return false;
		}
		if ( ! current_user_can( 'edit_others_posts' ) ) {
			return false;
		}
		if ( newsai_gsc_site_charts_has_data() ) {
			return false;
		}
		$user_id = (int) get_current_user_id();
		if ( $user_id > 0 && get_user_meta( $user_id, 'newsai_dismiss_gsc_charts_notice', true ) === '1' ) {
			return false;
		}
		return true;
	}
}

if ( ! function_exists( 'newsai_gsc_charts_sync_settings_url' ) ) {
	function newsai_gsc_charts_sync_settings_url() {
		return admin_url( 'admin.php?page=newsai-settings&tab=pro&newsai_gsc_focus=1' );
	}
}

if ( ! function_exists( 'newsai_gsc_charts_sync_admin_notice' ) ) {
	function newsai_gsc_charts_sync_admin_notice() {
		if ( ! newsai_gsc_charts_sync_notice_active() ) {
			return;
		}
		$dismiss_url = wp_nonce_url(
			add_query_arg( 'newsai_dismiss_gsc_charts', '1' ),
			'newsai_dismiss_gsc_charts'
		);
		$settings_url = newsai_gsc_charts_sync_settings_url();
		?>
		<div class="notice notice-warning is-dismissible newsai-gsc-charts-notice">
			<p>
				<strong><?php esc_html_e( 'Newsroom OS:', 'newsroom-ai-assistant' ); ?></strong>
				<?php esc_html_e( 'Chief Insights GSC charts need a one-time Search Console sync. Run Sync now under Settings → PRO → Google Search Console.', 'newsroom-ai-assistant' ); ?>
			</p>
			<p>
				<a class="button button-primary" href="<?php echo esc_url( $settings_url ); ?>"><?php esc_html_e( 'Open GSC settings', 'newsroom-ai-assistant' ); ?></a>
				<a class="button button-secondary" href="<?php echo esc_url( $dismiss_url ); ?>"><?php esc_html_e( 'Dismiss for me', 'newsroom-ai-assistant' ); ?></a>
			</p>
		</div>
		<?php
	}
}
add_action( 'admin_notices', 'newsai_gsc_charts_sync_admin_notice' );

add_action(
	'admin_init',
	static function () {
		if ( ! isset( $_GET['newsai_dismiss_gsc_charts'] ) || $_GET['newsai_dismiss_gsc_charts'] !== '1' ) {
			return;
		}
		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'newsai_dismiss_gsc_charts' ) ) {
			return;
		}
		if ( ! current_user_can( 'edit_others_posts' ) ) {
			return;
		}
		update_user_meta( (int) get_current_user_id(), 'newsai_dismiss_gsc_charts_notice', '1' );
		wp_safe_redirect( remove_query_arg( [ 'newsai_dismiss_gsc_charts', '_wpnonce' ] ) );
		exit;
	}
);

if ( ! function_exists( 'newsai_gsc_test_connection' ) ) {
	/**
	 * Verify token + property with a minimal Search Analytics query.
	 *
	 * @return array{ok:bool,message:string}
	 */
	function newsai_gsc_test_connection() {
		if ( ! newsai_gsc_is_pro_active() ) {
			return [
				'ok'      => false,
				'message' => __( 'Pro license required.', 'newsroom-ai-assistant' ),
			];
		}
		if ( ! newsai_gsc_has_service_account() ) {
			return [
				'ok'      => false,
				'message' => __( 'Service account JSON is not configured.', 'newsroom-ai-assistant' ),
			];
		}

		$property = newsai_gsc_get_property();
		$err      = '';
		$token    = newsai_gsc_get_access_token( $err );
		if ( $token === '' ) {
			return [
				'ok'      => false,
				'message' => $err !== '' ? $err : __( 'Token error.', 'newsroom-ai-assistant' ),
			];
		}

		$end   = gmdate( 'Y-m-d', strtotime( '-3 days' ) );
		$start = gmdate( 'Y-m-d', strtotime( '-5 days' ) );
		$rows  = newsai_gsc_fetch_rows( $token, $property, $start, $end, 'web', 1, 20 );
		if ( is_wp_error( $rows ) ) {
			return [
				'ok'      => false,
				'message' => $rows->get_error_message(),
			];
		}

		$hint = '';
		if ( function_exists( 'newsai_gsc_table_breakdown' ) ) {
			$breakdown = newsai_gsc_table_breakdown();
			if ( ! empty( $breakdown ) ) {
				$parts = [];
				foreach ( $breakdown as $b ) {
					$parts[] = $b['label'] . ' ' . number_format_i18n( (int) $b['clicks'] ) . ' clicks';
				}
				$hint = ' ' . implode( ' · ', $parts );
			}
		}

		return [
			'ok'      => true,
			'message' => __( 'GSC API OK (web query).', 'newsroom-ai-assistant' ) . $hint,
		];
	}
}

if ( ! function_exists( 'newsai_gsc_table_breakdown' ) ) {
	/**
	 * Stored row counts + clicks per search_type (all authors).
	 *
	 * @return array<int, array{type:string,label:string,rows:int,clicks:int,impressions:int}>
	 */
	function newsai_gsc_table_breakdown() {
		if ( ! newsai_gsc_table_exists() ) {
			return [];
		}
		global $wpdb;
		$table = newsai_gsc_table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$rows = $wpdb->get_results(
			"SELECT search_type, COUNT(*) AS row_cnt, SUM(clicks) AS clicks, SUM(impressions) AS impressions
			 FROM {$table}
			 GROUP BY search_type",
			ARRAY_A
		);
		$labels = [
			'web'        => __( 'Web', 'newsroom-ai-assistant' ),
			'discover'   => __( 'Discover', 'newsroom-ai-assistant' ),
			'googleNews' => __( 'Google News', 'newsroom-ai-assistant' ),
		];
		$out = [];
		foreach ( (array) $rows as $r ) {
			$type = newsai_gsc_sanitize_search_type( (string) ( $r['search_type'] ?? 'web' ) );
			$out[] = [
				'type'        => $type,
				'label'       => $labels[ $type ] ?? $type,
				'rows'        => (int) ( $r['row_cnt'] ?? 0 ),
				'clicks'      => (int) ( $r['clicks'] ?? 0 ),
				'impressions' => (int) ( $r['impressions'] ?? 0 ),
			];
		}
		return $out;
	}
}

if ( ! function_exists( 'newsai_gsc_author_channel_metrics' ) ) {
	/**
	 * All GSC channels for one author (zeros when no rows).
	 *
	 * @param int $user_id Author ID.
	 * @param int $days    Window.
	 * @return array{discover:array,googleNews:array,web:array}
	 */
	function newsai_gsc_author_channel_metrics( $user_id, $days = 30 ) {
		$empty = [
			'clicks'      => 0,
			'impressions' => 0,
			'ctr'         => 0.0,
			'position'    => 0.0,
			'posts'       => 0,
			'window_days' => max( 1, (int) $days ),
			'last_sync'   => (string) get_option( 'newsai_gsc_last_sync', '' ),
		];
		$out = [
			'discover'   => $empty,
			'googleNews' => $empty,
			'web'        => $empty,
		];
		if ( ! newsai_gsc_data_available() ) {
			return $out;
		}
		foreach ( [ 'discover', 'googleNews', 'web' ] as $type ) {
			$m = newsai_gsc_author_metrics( $user_id, $days, $type );
			if ( is_array( $m ) ) {
				$out[ $type ] = $m;
			}
		}
		return $out;
	}
}

if ( ! function_exists( 'newsai_gsc_insights_period_bounds' ) ) {
	/**
	 * Current and previous GSC windows for Chief Insights trend arrows.
	 *
	 * @param int $days Window length (7–90).
	 * @return array{cur_start:string,cur_end:string,prev_start:string,prev_end:string}
	 */
	function newsai_gsc_insights_period_bounds( $days ) {
		$days = max( 7, min( 90, (int) $days ) );
		$now  = time();
		return [
			'cur_start'   => gmdate( 'Y-m-d', $now - ( $days * DAY_IN_SECONDS ) ),
			'cur_end'     => gmdate( 'Y-m-d', $now ),
			'prev_start'  => gmdate( 'Y-m-d', $now - ( 2 * $days * DAY_IN_SECONDS ) ),
			'prev_end'    => gmdate( 'Y-m-d', $now - ( $days * DAY_IN_SECONDS ) - DAY_IN_SECONDS ),
		];
	}
}

if ( ! function_exists( 'newsai_gsc_insights_top_posts_with_trends' ) ) {
	/**
	 * Top posts for one GSC channel with clicks/impressions and period-over-period trend.
	 *
	 * @param int    $days            Window (7–90).
	 * @param string $search_type   web|discover|googleNews.
	 * @param int    $limit           Max rows.
	 * @param int    $min_impressions Minimum impressions in current window (0 = any traffic).
	 * @return list<array{post_id:int,title:string,clicks:int,impressions:int,prev_clicks:int,prev_impressions:int,delta_clicks:int,trend:string}>
	 */
	function newsai_gsc_insights_top_posts_with_trends( $days = 30, $search_type = 'discover', $limit = 8, $min_impressions = 1 ) {
		if ( ! newsai_gsc_data_available() ) {
			return [];
		}
		global $wpdb;

		$days          = max( 7, min( 90, (int) $days ) );
		$limit         = max( 1, min( 15, (int) $limit ) );
		$min_impressions = max( 0, (int) $min_impressions );
		$search_type   = newsai_gsc_sanitize_search_type( $search_type );
		$bounds        = newsai_gsc_insights_period_bounds( $days );
		$table         = newsai_gsc_table_name();

		$sql = "
			SELECT p.ID AS post_id, p.post_title AS title,
				SUM(CASE WHEN g.data_date >= %s AND g.data_date <= %s THEN g.clicks ELSE 0 END) AS clicks,
				SUM(CASE WHEN g.data_date >= %s AND g.data_date <= %s THEN g.impressions ELSE 0 END) AS impressions,
				SUM(CASE WHEN g.data_date >= %s AND g.data_date <= %s THEN g.clicks ELSE 0 END) AS prev_clicks,
				SUM(CASE WHEN g.data_date >= %s AND g.data_date <= %s THEN g.impressions ELSE 0 END) AS prev_impressions
			FROM {$table} g
			INNER JOIN {$wpdb->posts} p ON p.ID = g.post_id
			WHERE g.search_type = %s
				AND g.data_date >= %s AND g.data_date <= %s
				AND p.post_status = 'publish' AND p.post_type = 'post'
			GROUP BY p.ID, p.post_title
			HAVING impressions >= %d OR clicks > 0
			ORDER BY clicks DESC, impressions DESC
			LIMIT %d";

		$rows = $wpdb->get_results(
			$wpdb->prepare(
				$sql,
				$bounds['cur_start'],
				$bounds['cur_end'],
				$bounds['cur_start'],
				$bounds['cur_end'],
				$bounds['prev_start'],
				$bounds['prev_end'],
				$bounds['prev_start'],
				$bounds['prev_end'],
				$search_type,
				$bounds['prev_start'],
				$bounds['cur_end'],
				$min_impressions,
				$limit
			),
			ARRAY_A
		);

		$out = [];
		foreach ( (array) $rows as $r ) {
			$clicks     = (int) ( $r['clicks'] ?? 0 );
			$prev       = (int) ( $r['prev_clicks'] ?? 0 );
			$delta      = $clicks - $prev;
			$out[]      = [
				'post_id'           => (int) ( $r['post_id'] ?? 0 ),
				'title'             => (string) ( $r['title'] ?? '' ),
				'clicks'            => $clicks,
				'impressions'       => (int) ( $r['impressions'] ?? 0 ),
				'prev_clicks'       => $prev,
				'prev_impressions'  => (int) ( $r['prev_impressions'] ?? 0 ),
				'delta_clicks'      => $delta,
				'trend'             => newsai_gsc_insights_trend_key( $delta, $prev, $clicks ),
			];
		}
		return $out;
	}
}

if ( ! function_exists( 'newsai_gsc_insights_trend_key' ) ) {
	/**
	 * @param int $delta       Current minus previous clicks.
	 * @param int $prev_clicks Previous window clicks.
	 * @param int $cur_clicks  Current window clicks.
	 * @return string up|down|flat|new
	 */
	function newsai_gsc_insights_trend_key( $delta, $prev_clicks, $cur_clicks ) {
		if ( $prev_clicks <= 0 && $cur_clicks > 0 ) {
			return 'new';
		}
		if ( $delta > 0 ) {
			return 'up';
		}
		if ( $delta < 0 ) {
			return 'down';
		}
		return 'flat';
	}
}

// ── Daily cron (04:00 site time) ─────────────────────────────────────────────

if ( ! function_exists( 'newsai_gsc_should_schedule_cron' ) ) {
	/**
	 * Whether the daily GSC sync cron should be registered.
	 */
	function newsai_gsc_should_schedule_cron() {
		return newsai_gsc_is_pro_active()
			&& newsai_gsc_is_enabled()
			&& newsai_gsc_has_service_account();
	}
}

if ( ! function_exists( 'newsai_gsc_next_cron_timestamp' ) ) {
	/**
	 * Next 04:00 in the site timezone.
	 *
	 * @return int Unix timestamp.
	 */
	function newsai_gsc_next_cron_timestamp() {
		try {
			$tz   = wp_timezone();
			$now  = new DateTime( 'now', $tz );
			$next = new DateTime( 'today 04:00:00', $tz );
			if ( $next <= $now ) {
				$next->modify( '+1 day' );
			}
			return $next->getTimestamp();
		} catch ( Exception $e ) {
			return time() + HOUR_IN_SECONDS;
		}
	}
}

if ( ! function_exists( 'newsai_gsc_unschedule_cron' ) ) {
	function newsai_gsc_unschedule_cron() {
		wp_clear_scheduled_hook( 'newsai_gsc_daily_sync' );
	}
}

if ( ! function_exists( 'newsai_gsc_schedule_cron' ) ) {
	/**
	 * (Re)register daily cron when Pro + enabled + service account exist.
	 */
	function newsai_gsc_schedule_cron() {
		newsai_gsc_unschedule_cron();
		if ( ! newsai_gsc_should_schedule_cron() ) {
			return;
		}
		wp_schedule_event( newsai_gsc_next_cron_timestamp(), 'daily', 'newsai_gsc_daily_sync' );
	}
}

if ( ! function_exists( 'newsai_gsc_maybe_schedule_cron' ) ) {
	function newsai_gsc_maybe_schedule_cron() {
		if ( newsai_gsc_should_schedule_cron() ) {
			if ( ! wp_next_scheduled( 'newsai_gsc_daily_sync' ) ) {
				newsai_gsc_schedule_cron();
			}
		} else {
			newsai_gsc_unschedule_cron();
		}
	}
}
add_action( 'admin_init', 'newsai_gsc_maybe_schedule_cron' );

if ( ! function_exists( 'newsai_gsc_cron_run_sync' ) ) {
	/**
	 * WP-Cron callback: pull GSC metrics (skipped when lock held or not eligible).
	 */
	function newsai_gsc_cron_run_sync() {
		if ( ! newsai_gsc_should_schedule_cron() ) {
			newsai_gsc_unschedule_cron();
			return;
		}
		if ( get_transient( 'newsai_gsc_sync_lock' ) || get_transient( 'newsai_gsc_backfill_lock' ) ) {
			return;
		}
		if ( function_exists( 'newsai_gsc_backfill_is_running' ) && newsai_gsc_backfill_is_running() ) {
			return;
		}
		set_transient( 'newsai_gsc_sync_lock', '1', 20 * MINUTE_IN_SECONDS );
		newsai_gsc_run_sync();
		delete_transient( 'newsai_gsc_sync_lock' );
	}
}
add_action( 'newsai_gsc_daily_sync', 'newsai_gsc_cron_run_sync' );

// ── Posts list column (chief / edit_others_posts) ───────────────────────────

if ( ! function_exists( 'newsai_gsc_posts_list_window_days' ) ) {
	/**
	 * Rolling window for the Posts → All Posts GSC column.
	 *
	 * @return int
	 */
	function newsai_gsc_posts_list_window_days() {
		$days = 30;
		return max( 7, min( 90, (int) apply_filters( 'newsai_gsc_posts_list_days', $days ) ) );
	}
}

if ( ! function_exists( 'newsai_gsc_posts_list_can_view' ) ) {
	/**
	 * Chiefs only — desk-wide traffic, not per-writer list columns.
	 */
	function newsai_gsc_posts_list_can_view() {
		return newsai_gsc_data_available()
			&& current_user_can( 'edit_others_posts' );
	}
}

if ( ! function_exists( 'newsai_gsc_posts_list_is_posts_screen' ) ) {
	function newsai_gsc_posts_list_is_posts_screen() {
		if ( ! is_admin() ) {
			return false;
		}
		global $pagenow;
		if ( $pagenow !== 'edit.php' ) {
			return false;
		}
		$pt = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : 'post';
		return $pt === 'post';
	}
}

if ( ! function_exists( 'newsai_gsc_post_traffic_store' ) ) {
	/**
	 * Request-level cache for posts list column (shared prefetch + read).
	 *
	 * @return array<string, array>
	 */
	function &newsai_gsc_post_traffic_store() {
		static $store = [];
		return $store;
	}
}

if ( ! function_exists( 'newsai_gsc_prefetch_post_traffic' ) ) {
	/**
	 * Batch-load GSC clicks for many posts (one query per list page).
	 *
	 * @param int[] $post_ids Post IDs.
	 * @param int   $days     Window.
	 */
	function newsai_gsc_prefetch_post_traffic( $post_ids, $days = 30 ) {
		$store = &newsai_gsc_post_traffic_store();

		$post_ids = array_values( array_unique( array_filter( array_map( 'intval', (array) $post_ids ) ) ) );
		if ( empty( $post_ids ) || ! newsai_gsc_data_available() ) {
			return;
		}

		$days   = max( 1, (int) $days );
		$since  = newsai_gsc_date_since( $days );
		$needed = [];
		foreach ( $post_ids as $pid ) {
			$key = $pid . ':' . $days;
			if ( ! array_key_exists( $key, $store ) ) {
				$needed[] = $pid;
			}
		}
		if ( empty( $needed ) ) {
			return;
		}

		global $wpdb;
		$table         = newsai_gsc_table_name();
		$placeholders  = implode( ',', array_fill( 0, count( $needed ), '%d' ) );
		$sql           = "SELECT post_id, search_type, SUM(clicks) AS clicks, SUM(impressions) AS impressions
			FROM {$table}
			WHERE post_id IN ({$placeholders}) AND data_date >= %s
			GROUP BY post_id, search_type";
		$args          = array_merge( $needed, [ $since ] );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
		$rows = $wpdb->get_results( $wpdb->prepare( $sql, $args ) );

		foreach ( $needed as $pid ) {
			$store[ $pid . ':' . $days ] = [
				'discover'    => 0,
				'googleNews'  => 0,
				'web'         => 0,
				'total'       => 0,
				'impressions' => 0,
			];
		}

		foreach ( (array) $rows as $r ) {
			$pid  = (int) $r->post_id;
			$key  = $pid . ':' . $days;
			$type = newsai_gsc_sanitize_search_type( (string) $r->search_type );
			if ( ! isset( $store[ $key ] ) ) {
				continue;
			}
			$clicks = (int) $r->clicks;
			$impr   = (int) $r->impressions;
			$store[ $key ][ $type ]       = $clicks;
			$store[ $key ]['total']      += $clicks;
			$store[ $key ]['impressions'] += $impr;
		}
	}
}

if ( ! function_exists( 'newsai_gsc_get_post_traffic_cached' ) ) {
	/**
	 * @param int $post_id Post ID.
	 * @param int $days    Window (must match prefetch).
	 * @return array{discover:int,googleNews:int,web:int,total:int,impressions:int}|null
	 */
	function newsai_gsc_get_post_traffic_cached( $post_id, $days = 30 ) {
		$store = &newsai_gsc_post_traffic_store();

		$post_id = (int) $post_id;
		$days    = max( 1, (int) $days );
		$key     = $post_id . ':' . $days;

		if ( ! array_key_exists( $key, $store ) ) {
			newsai_gsc_prefetch_post_traffic( [ $post_id ], $days );
		}

		return isset( $store[ $key ] ) ? $store[ $key ] : null;
	}
}

if ( ! function_exists( 'newsai_gsc_render_posts_list_column' ) ) {
	/**
	 * @param array $traffic From newsai_gsc_get_post_traffic_cached().
	 * @param int   $days    Window label.
	 */
	function newsai_gsc_render_posts_list_column( $traffic, $days ) {
		if ( empty( $traffic ) || (int) ( $traffic['total'] ?? 0 ) < 1 ) {
			echo '<span class="newsai-gsc-col-empty" style="color:#9ca3af;">—</span>';
			return;
		}

		$disc = (int) ( $traffic['discover'] ?? 0 );
		$gn   = (int) ( $traffic['googleNews'] ?? 0 );
		$web  = (int) ( $traffic['web'] ?? 0 );
		$impr = (int) ( $traffic['impressions'] ?? 0 );

		$tip = sprintf(
			/* translators: 1: window days, 2: discover clicks, 3: google news clicks, 4: web clicks, 5: impressions */
			__( 'Last %1$d days — Discover: %2$s clicks · Google News: %3$s · Web: %4$s · Impressions: %5$s', 'newsroom-ai-assistant' ),
			(int) $days,
			number_format_i18n( $disc ),
			number_format_i18n( $gn ),
			number_format_i18n( $web ),
			number_format_i18n( $impr )
		);

		echo '<span class="newsai-gsc-col" title="' . esc_attr( $tip ) . '" style="font-size:12px; line-height:1.45; color:#047857;">';
		echo '<strong>' . esc_html( number_format_i18n( (int) $traffic['total'] ) ) . '</strong>';
		$parts = [];
		if ( $disc > 0 ) {
			$parts[] = '✨' . number_format_i18n( $disc );
		}
		if ( $gn > 0 ) {
			$parts[] = '📰' . number_format_i18n( $gn );
		}
		if ( $web > 0 ) {
			$parts[] = '🔍' . number_format_i18n( $web );
		}
		if ( ! empty( $parts ) ) {
			echo '<br><span style="font-size:10px;color:#6b7280;">' . esc_html( implode( ' · ', $parts ) ) . '</span>';
		}
		echo '</span>';
	}
}

if ( ! function_exists( 'newsai_gsc_posts_list_prefetch_the_posts' ) ) {
	/**
	 * @param WP_Post[]     $posts Query posts.
	 * @param WP_Query|null $query Query.
	 * @return WP_Post[]
	 */
	function newsai_gsc_posts_list_prefetch_the_posts( $posts, $query ) {
		if ( ! newsai_gsc_posts_list_can_view() || ! newsai_gsc_posts_list_is_posts_screen() ) {
			return $posts;
		}
		if ( ! $query instanceof WP_Query || ! $query->is_main_query() ) {
			return $posts;
		}
		$ids = [];
		foreach ( (array) $posts as $p ) {
			if ( $p instanceof WP_Post ) {
				$ids[] = (int) $p->ID;
			}
		}
		if ( ! empty( $ids ) ) {
			newsai_gsc_prefetch_post_traffic( $ids, newsai_gsc_posts_list_window_days() );
		}
		return $posts;
	}
}
add_filter( 'the_posts', 'newsai_gsc_posts_list_prefetch_the_posts', 10, 2 );

if ( ! function_exists( 'newsai_gsc_posts_list_add_column' ) ) {
	/**
	 * @param string[] $columns List table columns.
	 * @return string[]
	 */
	function newsai_gsc_posts_list_add_column( $columns ) {
		if ( ! newsai_gsc_posts_list_can_view() ) {
			return $columns;
		}
		$days  = newsai_gsc_posts_list_window_days();
		$label = sprintf(
			/* translators: %d: rolling window in days */
			__( 'GSC (%dd)', 'newsroom-ai-assistant' ),
			$days
		);
		$out   = [];
		$added = false;
		foreach ( $columns as $key => $title ) {
			$out[ $key ] = $title;
			if ( $key === 'title' ) {
				$out['newsai_gsc'] = $label;
				$added           = true;
			}
		}
		if ( ! $added ) {
			$out['newsai_gsc'] = $label;
		}
		return $out;
	}
}
add_filter( 'manage_posts_columns', 'newsai_gsc_posts_list_add_column' );

if ( ! function_exists( 'newsai_gsc_posts_list_render_column' ) ) {
	/**
	 * @param string $column  Column key.
	 * @param int    $post_id Post ID.
	 */
	function newsai_gsc_posts_list_render_column( $column, $post_id ) {
		if ( $column !== 'newsai_gsc' || ! newsai_gsc_posts_list_can_view() ) {
			return;
		}
		$days    = newsai_gsc_posts_list_window_days();
		$traffic = newsai_gsc_get_post_traffic_cached( (int) $post_id, $days );
		newsai_gsc_render_posts_list_column( is_array( $traffic ) ? $traffic : [], $days );
	}
}
add_action( 'manage_posts_custom_column', 'newsai_gsc_posts_list_render_column', 10, 2 );

if ( ! function_exists( 'newsai_gsc_posts_list_admin_styles' ) ) {
	function newsai_gsc_posts_list_admin_styles() {
		if ( ! newsai_gsc_posts_list_can_view() || ! newsai_gsc_posts_list_is_posts_screen() ) {
			return;
		}
		echo '<style>.column-newsai_gsc { width: 96px; }</style>';
	}
}
add_action( 'admin_head-edit.php', 'newsai_gsc_posts_list_admin_styles' );

// ── AJAX (Settings) ─────────────────────────────────────────────────────────

if ( ! function_exists( 'newsai_gsc_ajax_test' ) ) {
	function newsai_gsc_ajax_test() {
		check_ajax_referer( 'newsai_ajax_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
		}
		$result = newsai_gsc_test_connection();
		if ( empty( $result['ok'] ) ) {
			wp_send_json_error( [ 'message' => (string) $result['message'] ] );
		}
		wp_send_json_success( [ 'message' => (string) $result['message'] ] );
	}
}
add_action( 'wp_ajax_newsai_gsc_test', 'newsai_gsc_ajax_test' );

if ( ! function_exists( 'newsai_gsc_ajax_sync_now' ) ) {
	function newsai_gsc_ajax_sync_now() {
		check_ajax_referer( 'newsai_ajax_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
		}
		if ( ! newsai_gsc_is_pro_active() ) {
			wp_send_json_error( [ 'message' => __( 'Pro license required.', 'newsroom-ai-assistant' ) ], 402 );
		}

		if ( function_exists( 'ignore_user_abort' ) ) {
			ignore_user_abort( true );
		}
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( 300 );
		}

		ob_start();
		$result = newsai_gsc_run_sync();
		ob_end_clean();
		if ( empty( $result['ok'] ) ) {
			wp_send_json_error(
				[
					'message'   => (string) $result['message'],
					'processed' => (int) $result['processed'],
					'skipped'   => (int) $result['skipped'],
					'rows'      => newsai_gsc_row_count(),
				]
			);
		}

		wp_send_json_success(
			[
				'message'   => (string) $result['message'],
				'processed' => (int) $result['processed'],
				'skipped'   => (int) $result['skipped'],
				'rows'      => newsai_gsc_row_count(),
				'last_sync' => (string) get_option( 'newsai_gsc_last_sync', '' ),
			]
		);
	}
}
add_action( 'wp_ajax_newsai_gsc_sync_now', 'newsai_gsc_ajax_sync_now' );

if ( file_exists( __DIR__ . '/pro-gsc-backfill.php' ) ) {
	require_once __DIR__ . '/pro-gsc-backfill.php';
}
if ( file_exists( __DIR__ . '/pro-gsc-query-sync.php' ) ) {
	require_once __DIR__ . '/pro-gsc-query-sync.php';
}
