<?php
/**
 * PageSpeed Insights & CrUX client.
 *
 * Fetches Core Web Vitals (LCP, INP, CLS, FCP, TTFB) plus the Lighthouse
 * performance score from the PageSpeed Insights v5 API. CrUX (real field
 * data) is included automatically in the PSI response when the URL has
 * enough traffic; otherwise lab data is returned.
 *
 * Responses are cached for 6 hours per (URL, strategy) pair to stay well
 * within Google's free quota (25,000 queries/day) on busy sites.
 *
 * @package Antimanual\Seo\Google
 * @since 3.7.0
 */

namespace Antimanual\Seo\Google;

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

/**
 * PSI v5 + CrUX client.
 */
class PageSpeedClient {

	/** PSI v5 API endpoint. */
	private const PSI_ENDPOINT = 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed';

	/** Cache key prefix for PSI responses. */
	private const CACHE_PREFIX = 'atml_psi_';

	/** Cache lifetime for PSI responses (seconds). */
	private const CACHE_TTL = 6 * HOUR_IN_SECONDS;

	/**
	 * Run a PSI analysis.
	 *
	 * @param string $url      URL to analyze (must include scheme).
	 * @param string $strategy Either 'mobile' or 'desktop'.
	 * @param bool   $force    Skip the cache.
	 * @return array|\WP_Error Normalized result or error.
	 */
	public function analyze( string $url, string $strategy = 'mobile', bool $force = false ) {
		$url      = esc_url_raw( trim( $url ) );
		$strategy = in_array( $strategy, array( 'mobile', 'desktop' ), true ) ? $strategy : 'mobile';

		if ( '' === $url || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
			return new \WP_Error( 'invalid_url', __( 'A valid URL is required.', 'antimanual' ) );
		}

		$cache_key = self::CACHE_PREFIX . md5( $url . '|' . $strategy );
		if ( ! $force ) {
			$cached = get_transient( $cache_key );
			if ( is_array( $cached ) ) {
				$cached['cached'] = true;
				return $cached;
			}
		}

		$auth   = GoogleAuthService::instance();
		$query  = array(
			'url'      => $url,
			'strategy' => $strategy,
			// Performance is the only category we surface in Phase 1; the others
			// inflate response size and the existing SEO Agent already covers
			// SEO/accessibility/best-practices via its AI flow.
			'category' => 'performance',
		);
		$api_key = $auth->get_psi_key();
		if ( '' !== $api_key ) {
			$query['key'] = $api_key;
		}

		$response = wp_remote_get(
			add_query_arg( $query, self::PSI_ENDPOINT ),
			array(
				'timeout' => 90,
			)
		);

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

		$status = (int) wp_remote_retrieve_response_code( $response );
		$body   = wp_remote_retrieve_body( $response );
		$data   = json_decode( $body, true );

		if ( 200 !== $status ) {
			$msg = is_array( $data ) && isset( $data['error']['message'] )
				? (string) $data['error']['message']
				: sprintf(
					/* translators: %d: HTTP status code. */
					__( 'PageSpeed Insights returned HTTP %d.', 'antimanual' ),
					$status
				);

			return new \WP_Error( 'psi_error', sanitize_text_field( $msg ), array( 'status' => $status ) );
		}

		if ( ! is_array( $data ) ) {
			return new \WP_Error( 'psi_invalid', __( 'PageSpeed Insights returned an invalid response.', 'antimanual' ) );
		}

		$result = $this->normalize_response( $data, $url, $strategy );

		set_transient( $cache_key, $result, self::CACHE_TTL );

		return $result;
	}

	/**
	 * Reduce the very large PSI response to the fields we actually display.
	 *
	 * @param array  $data     Raw PSI response.
	 * @param string $url      Analyzed URL.
	 * @param string $strategy 'mobile' or 'desktop'.
	 * @return array Normalized payload.
	 */
	private function normalize_response( array $data, string $url, string $strategy ): array {
		$lighthouse = $data['lighthouseResult'] ?? array();
		$audits     = is_array( $lighthouse['audits'] ?? null ) ? $lighthouse['audits'] : array();
		$categories = is_array( $lighthouse['categories'] ?? null ) ? $lighthouse['categories'] : array();

		$performance_score = isset( $categories['performance']['score'] )
			? (int) round( ( (float) $categories['performance']['score'] ) * 100 )
			: null;

		$lab = array(
			'lcp'  => $this->extract_audit_value( $audits, 'largest-contentful-paint', 'numericValue' ),
			'inp'  => $this->extract_audit_value( $audits, 'interaction-to-next-paint', 'numericValue' ),
			'cls'  => $this->extract_audit_value( $audits, 'cumulative-layout-shift', 'numericValue' ),
			'fcp'  => $this->extract_audit_value( $audits, 'first-contentful-paint', 'numericValue' ),
			'ttfb' => $this->extract_audit_value( $audits, 'server-response-time', 'numericValue' ),
			'tbt'  => $this->extract_audit_value( $audits, 'total-blocking-time', 'numericValue' ),
		);

		$loading_experience    = $this->normalize_field_data( $data['loadingExperience'] ?? null );
		$origin_loading_experience = $this->normalize_field_data( $data['originLoadingExperience'] ?? null );

		// Top 3 opportunities (sorted by potential time saved, descending).
		$opportunities = array();
		foreach ( $audits as $id => $audit ) {
			if ( ! is_array( $audit ) ) {
				continue;
			}
			if ( 'numeric' !== ( $audit['scoreDisplayMode'] ?? '' ) && 'metricSavings' !== ( $audit['scoreDisplayMode'] ?? '' ) ) {
				// Skip non-opportunity audits (binary, manual, informative).
				continue;
			}
			$saved = isset( $audit['details']['overallSavingsMs'] ) ? (float) $audit['details']['overallSavingsMs'] : 0.0;
			if ( $saved <= 0 ) {
				continue;
			}
			$opportunities[] = array(
				'id'          => sanitize_key( (string) $id ),
				'title'       => isset( $audit['title'] ) ? sanitize_text_field( (string) $audit['title'] ) : '',
				'description' => isset( $audit['description'] ) ? wp_kses_post( (string) $audit['description'] ) : '',
				'savings_ms'  => (int) round( $saved ),
			);
		}
		usort(
			$opportunities,
			static fn( $a, $b ) => $b['savings_ms'] <=> $a['savings_ms']
		);
		$opportunities = array_slice( $opportunities, 0, 5 );

		return array(
			'url'                    => $url,
			'strategy'               => $strategy,
			'fetched_at'             => gmdate( 'c' ),
			'cached'                 => false,
			'performance_score'      => $performance_score,
			'lab'                    => $lab,
			'field'                  => $loading_experience,
			'origin_field'           => $origin_loading_experience,
			'opportunities'          => $opportunities,
			'lighthouse_version'     => isset( $lighthouse['lighthouseVersion'] ) ? sanitize_text_field( (string) $lighthouse['lighthouseVersion'] ) : '',
		);
	}

	/**
	 * Pull a single audit metric (e.g. numericValue) from the audits collection.
	 *
	 * @param array  $audits Audits indexed by ID.
	 * @param string $id     Audit ID (e.g. 'largest-contentful-paint').
	 * @param string $field  Field within the audit (typically 'numericValue').
	 * @return float|null
	 */
	private function extract_audit_value( array $audits, string $id, string $field ) {
		if ( ! isset( $audits[ $id ][ $field ] ) ) {
			return null;
		}
		$value = $audits[ $id ][ $field ];
		return is_numeric( $value ) ? (float) $value : null;
	}

	/**
	 * Normalize a CrUX field-data payload (loadingExperience / originLoadingExperience).
	 *
	 * @param mixed $payload Raw field data block from PSI.
	 * @return array|null Normalized payload, or null if no data.
	 */
	private function normalize_field_data( $payload ): ?array {
		if ( ! is_array( $payload ) || empty( $payload['metrics'] ) || ! is_array( $payload['metrics'] ) ) {
			return null;
		}

		$metric_map = array(
			'LARGEST_CONTENTFUL_PAINT_MS' => 'lcp',
			'INTERACTION_TO_NEXT_PAINT'   => 'inp',
			'CUMULATIVE_LAYOUT_SHIFT_SCORE' => 'cls',
			'FIRST_CONTENTFUL_PAINT_MS'   => 'fcp',
			'EXPERIMENTAL_TIME_TO_FIRST_BYTE' => 'ttfb',
		);

		$metrics = array();
		foreach ( $metric_map as $crux_key => $alias ) {
			$entry = $payload['metrics'][ $crux_key ] ?? null;
			if ( ! is_array( $entry ) ) {
				continue;
			}
			$metrics[ $alias ] = array(
				'percentile' => isset( $entry['percentile'] ) ? (float) $entry['percentile'] : null,
				'category'   => isset( $entry['category'] ) ? sanitize_key( (string) $entry['category'] ) : '',
			);
		}

		return array(
			'overall_category' => isset( $payload['overall_category'] ) ? sanitize_key( (string) $payload['overall_category'] ) : '',
			'metrics'          => $metrics,
		);
	}
}
