<?php
/**
 * Livada SEO — Google Search Console keywords import
 *
 * Lets the operator paste / upload a Search Console CSV export and stores
 * the per-URL top keywords (with impressions, clicks, position) as post meta
 * so the metabox can show "what is this page already ranking for?".
 *
 * Expected CSV columns (Google Search Console "Top queries" export, when
 * filtered by Pages → URL):
 *   - Top query / Top queries / Query
 *   - Clicks
 *   - Impressions
 *   - CTR
 *   - Position / Avg. position
 *
 * The plugin doesn't OAuth into GSC (heavy dependency); the user runs the
 * export manually from the GSC UI and uploads the CSV here. Cheap, lossless,
 * works for any site without API quota limits.
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_GSC_Import {

	/** Maximum keywords stored per URL. Top-N by clicks. */
	const KEYWORDS_PER_URL = 25;

	/**
	 * Post meta key holding the array of `[ query => { clicks, impressions,
	 * position } ]` entries for a post.
	 */
	const META_KEY = '_livada_seo_gsc_keywords';

	// ── Bootstrap ─────────────────────────────────────────────────────────────

	public static function init(): void {
		add_action( 'admin_post_livada_seo_gsc_import', [ self::class, 'handle_upload' ] );
	}

	// ── CSV upload handler ────────────────────────────────────────────────────

	public static function handle_upload(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'livada-seo' ) );
		}

		check_admin_referer( 'livada_seo_gsc_import' );

		if ( empty( $_FILES['gsc_csv']['tmp_name'] ) || ! is_uploaded_file( $_FILES['gsc_csv']['tmp_name'] ) ) { // phpcs:ignore
			self::redirect_back( 'missing' );
		}

		$tmp = $_FILES['gsc_csv']['tmp_name']; // phpcs:ignore
		$h   = fopen( $tmp, 'r' );
		if ( ! $h ) {
			self::redirect_back( 'invalid' );
		}

		// Detect delimiter from the first line.
		$first = fgets( $h );
		rewind( $h );

		$delim = ',';
		if ( false !== $first && substr_count( $first, ';' ) > substr_count( $first, ',' ) ) {
			$delim = ';';
		} elseif ( false !== $first && substr_count( $first, "\t" ) > substr_count( $first, ',' ) ) {
			$delim = "\t";
		}

		$header = fgetcsv( $h, 0, $delim );
		if ( ! is_array( $header ) ) {
			fclose( $h );
			self::redirect_back( 'invalid' );
		}

		$idx = self::map_header_columns( $header );
		if ( false === $idx['query'] || false === $idx['clicks'] || false === $idx['impressions'] ) {
			fclose( $h );
			self::redirect_back( 'columns' );
		}

		// Collect (URL or "global") → list of { query, clicks, impressions, position }.
		// Most "Top queries" exports don't carry the Page column, so when no page
		// column is found we store everything under a synthetic "*" key and also
		// best-effort attach to URLs we can guess from a "Page" / "URL" column.
		$rows           = [];
		$by_url         = [];
		$global_buffer  = [];
		$has_url_column = false !== $idx['url'];

		while ( ( $row = fgetcsv( $h, 0, $delim ) ) !== false ) {
			$query = trim( (string) ( $row[ $idx['query'] ] ?? '' ) );
			if ( '' === $query ) {
				continue;
			}

			$entry = [
				'query'       => $query,
				'clicks'      => (int)   ( $row[ $idx['clicks'] ]      ?? 0 ),
				'impressions' => (int)   ( $row[ $idx['impressions'] ] ?? 0 ),
				'position'    => false === $idx['position'] ? null : (float) ( $row[ $idx['position'] ] ?? 0 ),
			];

			if ( $has_url_column ) {
				$url = (string) ( $row[ $idx['url'] ] ?? '' );
				$url = self::normalise_url( $url );
				if ( '' !== $url ) {
					$by_url[ $url ][] = $entry;
					continue;
				}
			}

			$global_buffer[] = $entry;
		}
		fclose( $h );

		$updated = 0;
		$skipped = 0;

		// Match each URL to a post and persist the top-N keywords for that post.
		foreach ( $by_url as $url => $entries ) {
			$post_id = url_to_postid( $url );
			if ( $post_id <= 0 ) {
				$skipped++;
				continue;
			}

			$top = self::top_n_by_clicks( $entries, self::KEYWORDS_PER_URL );
			update_post_meta( $post_id, self::META_KEY, $top );
			$updated++;
		}

		// Anything in $global_buffer (no URL column or unmatchable URLs) is stored
		// in a single site-wide option for reference.
		if ( ! empty( $global_buffer ) ) {
			update_option(
				'livada_seo_gsc_global_keywords',
				self::top_n_by_clicks( $global_buffer, 200 ),
				false
			);
		}

		// Activity log.
		if ( class_exists( 'Livada_SEO_Activity_Log' ) ) {
			Livada_SEO_Activity_Log::log(
				'gsc_import',
				sprintf( 'GSC import: %d posts updated, %d URLs skipped', $updated, $skipped ),
				[ 'updated' => $updated, 'skipped' => $skipped, 'global_rows' => count( $global_buffer ) ]
			);
		}

		self::redirect_back( 'ok', $updated, $skipped );
	}

	/**
	 * Locates the column indexes for query / clicks / impressions / position /
	 * url in a GSC export header. Both English and French label variants are
	 * accepted because Google localises CSV headers based on the operator's
	 * Search Console UI language.
	 *
	 * @return array{query:int|false, clicks:int|false, impressions:int|false, position:int|false, url:int|false}
	 */
	private static function map_header_columns( array $header ): array {
		$norm = array_map( static function ( $c ) {
			return strtolower( trim( (string) $c ) );
		}, $header );

		$lookup = static function ( array $candidates ) use ( $norm ) {
			foreach ( $candidates as $c ) {
				$idx = array_search( $c, $norm, true );
				if ( false !== $idx ) {
					return $idx;
				}
			}
			return false;
		};

		return [
			'query'       => $lookup( [ 'query', 'top query', 'top queries', 'requête', 'requêtes principales' ] ),
			'clicks'      => $lookup( [ 'clicks', 'clics' ] ),
			'impressions' => $lookup( [ 'impressions' ] ),
			'position'    => $lookup( [ 'position', 'avg. position', 'average position', 'position moyenne' ] ),
			'url'         => $lookup( [ 'page', 'url', 'top page', 'pages principales' ] ),
		];
	}

	/**
	 * Sorts entries by clicks DESC then impressions DESC and keeps the top N.
	 *
	 * @param array<int, array{query:string,clicks:int,impressions:int,position:?float}> $entries
	 * @return array<int, array<string,mixed>>
	 */
	private static function top_n_by_clicks( array $entries, int $n ): array {
		usort( $entries, static function ( $a, $b ) {
			if ( $a['clicks'] === $b['clicks'] ) {
				return $b['impressions'] - $a['impressions'];
			}
			return $b['clicks'] - $a['clicks'];
		} );

		return array_slice( $entries, 0, max( 1, $n ) );
	}

	/**
	 * Strips trailing slash variability and lower-cases the host so URL → post
	 * matching is robust against http/https or www/no-www differences.
	 */
	private static function normalise_url( string $url ): string {
		$url = trim( $url );
		if ( '' === $url ) {
			return '';
		}
		// Some GSC exports use just the path (e.g. "/about/").
		if ( strpos( $url, '://' ) === false && strpos( $url, '/' ) === 0 ) {
			return rtrim( $url, '/' );
		}
		// Build an absolute URL on the current host so url_to_postid resolves it.
		$parts = wp_parse_url( $url );
		if ( ! is_array( $parts ) || empty( $parts['path'] ) ) {
			return '';
		}
		return rtrim( (string) $parts['path'], '/' );
	}

	/**
	 * Convenience: returns the keywords array stored on a post (top N).
	 *
	 * @return array<int, array<string,mixed>>
	 */
	public static function get_post_keywords( int $post_id ): array {
		$raw = get_post_meta( $post_id, self::META_KEY, true );
		return is_array( $raw ) ? $raw : [];
	}

	private static function redirect_back( string $status, int $updated = 0, int $skipped = 0 ): void {
		$url = add_query_arg(
			[
				'page'              => 'livada-seo-tools',
				'livada_gsc_status' => $status,
				'livada_gsc_updated' => $updated,
				'livada_gsc_skipped' => $skipped,
			],
			admin_url( 'admin.php' )
		);
		wp_safe_redirect( $url );
		exit;
	}
}
