<?php

namespace SerpIQ\Api\Controllers;

use SerpIQ\Gsc\OAuth;
use SerpIQ\Helpers\Options;

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

final class Dashboard_Controller {
	private static ?Dashboard_Controller $instance = null;

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

	public function register_routes(): void {
		register_rest_route(
			'serpiq/v1',
			'/dashboard/overview',
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'overview' ),
				'permission_callback' => array( \SerpIQ\Api\Rest_Permission::class, 'admin_rest_request' ),
				'args'                => array(
					'start'           => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => array( $this, 'validate_date_arg' ) ),
					'end'             => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => array( $this, 'validate_date_arg' ) ),
					'country'         => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ),
					'device'          => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ),
					'topQueriesLimit' => array( 'required' => false, 'type' => 'integer', 'default' => 3, 'sanitize_callback' => 'absint' ),
					'topPagesLimit'   => array( 'required' => false, 'type' => 'integer', 'default' => 3, 'sanitize_callback' => 'absint' ),
				),
			)
		);
	}

	public function validate_date_arg( mixed $value, \WP_REST_Request $request, string $param ): bool {
		unset( $request, $param );
		$v = is_scalar( $value ) ? (string) $value : '';
		$v = trim( $v );
		if ( '' === $v ) {
			return true;
		}
		return (bool) preg_match( '/^\d{4}-\d{2}-\d{2}$/', $v );
	}

	public function overview( \WP_REST_Request $request ): \WP_REST_Response {
		global $wpdb;

		$start = $this->sanitize_date( (string) $request->get_param( 'start' ) );
		$end = $this->sanitize_date( (string) $request->get_param( 'end' ) );
		$top_limit = absint( $request->get_param( 'topQueriesLimit' ) );
		if ( $top_limit <= 0 ) {
			$top_limit = 3;
		} elseif ( $top_limit > 500 ) {
			$top_limit = 500;
		}

		$top_pages_limit = absint( $request->get_param( 'topPagesLimit' ) );
		if ( $top_pages_limit <= 0 ) {
			$top_pages_limit = 3;
		} elseif ( $top_pages_limit > 500 ) {
			$top_pages_limit = 500;
		}
		if ( ! $start || ! $end ) {
			$end = gmdate( 'Y-m-d', strtotime( '-1 day' ) );
			$start = gmdate( 'Y-m-d', strtotime( '-28 day' ) );
		}

		$country = sanitize_text_field( (string) $request->get_param( 'country' ) );
		$device = sanitize_text_field( (string) $request->get_param( 'device' ) );

		// If country/device filters are applied, prefer a synthetic API query (date-only)
		// to avoid undercounting caused by granular datasets (page/query) being truncated.
		if ( '' !== $country || '' !== $device ) {
			$api = $this->overview_from_api( $start, $end, $country, $device );
			if ( is_array( $api ) ) {
				return new \WP_REST_Response(
					array(
						'start'     => $start,
						'end'       => $end,
						'totals'    => $api['totals'],
						'series'    => $api['series'],
						'topQueries'=> array(),
						'topPages'  => array(),
						'devices'   => array(),
						'source'    => 'gsc_api',
					),
					200
				);
			}
		}

		// Base WHERE for pages/queries/devices.
		$table_queries = $wpdb->prefix . 'serpiq_queries';
		$table_pages   = $wpdb->prefix . 'serpiq_pages';
		$where_qp = 'date BETWEEN %s AND %s';
		$params_qp = array( $start, $end );

		if ( '' !== $country ) {
			$where_qp .= ' AND country = %s';
			$params_qp[] = $country;
		}
		if ( '' !== $device ) {
			$where_qp .= ' AND device = %s';
			$params_qp[] = $device;
		}

		// Prefer serpiq_totals (daily totals by date only) when no country/device filters are applied.
		$table_totals = $wpdb->prefix . 'serpiq_totals';
		$where = 'date BETWEEN %s AND %s';
		$params = array( $start, $end );

		// If filters are applied, or totals table is empty, fall back to pages table.
		$use_totals = ( '' === $country && '' === $device );
		if ( $use_totals ) {
			$has_totals = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE date BETWEEN %s AND %s', $table_totals, $start, $end ) );
			if ( $has_totals <= 0 ) {
				$use_totals = false;
			}
		}

		if ( '' !== $country ) {
			$where .= ' AND country = %s';
			$params[] = $country;
		}
		if ( '' !== $device ) {
			$where .= ' AND device = %s';
			$params[] = $device;
		}

		$table = $use_totals ? $table_totals : $table_pages;

		$sql_totals = "
			SELECT
				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 {$where}
		";

		$totals = $wpdb->get_row( $wpdb->prepare( $sql_totals, array_merge( array( $table ), $params ) ), ARRAY_A );

		$sql_series = "
			SELECT
				date,
				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 {$where}
			GROUP BY date
			ORDER BY date ASC
		";

		$series = $wpdb->get_results( $wpdb->prepare( $sql_series, array_merge( array( $table ), $params ) ), ARRAY_A );

		// Top queries (by clicks, configurable limit; defaults to 3).
		$sql_top_queries = "
			SELECT
				query,
				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 {$where_qp}
			GROUP BY query
			ORDER BY clicks DESC
			LIMIT %d
		";
		$params_top = array_merge( array( $table_queries ), $params_qp, array( $top_limit ) );
		$top_queries = $wpdb->get_results( $wpdb->prepare( $sql_top_queries, $params_top ), ARRAY_A );

		// Top pages (by clicks, configurable limit; defaults to 3).
		$sql_top_pages = "
			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
			FROM %i
			WHERE {$where_qp}
			GROUP BY page_url
			ORDER BY clicks DESC
			LIMIT %d
		";
		$params_top_pages = array_merge( array( $table_pages ), $params_qp, array( $top_pages_limit ) );
		$top_pages = $wpdb->get_results( $wpdb->prepare( $sql_top_pages, $params_top_pages ), ARRAY_A );

		// Device distribution (from pages).
		$sql_devices = "
			SELECT
				COALESCE(device,'(not set)') AS device,
				COALESCE(SUM(clicks),0) AS clicks
			FROM %i
			WHERE {$where_qp}
			GROUP BY device
			ORDER BY clicks DESC
		";
		$device_rows = $wpdb->get_results( $wpdb->prepare( $sql_devices, array_merge( array( $table_pages ), $params_qp ) ), ARRAY_A );
		$devices = array();
		if ( is_array( $device_rows ) ) {
			$total_clicks = 0;
			foreach ( $device_rows as $row ) {
				$total_clicks += (int) ( $row['clicks'] ?? 0 );
			}
			foreach ( $device_rows as $row ) {
				$clicks = (int) ( $row['clicks'] ?? 0 );
				$percentage = $total_clicks > 0 ? ( ( $clicks / $total_clicks ) * 100 ) : 0;
				$devices[] = array(
					'device'     => (string) ( $row['device'] ?? '' ),
					'clicks'     => $clicks,
					'percentage' => $percentage,
				);
			}
		}

		// Location distribution (country): traffic by country + share + distinct keyword count.
		// "Traffic" is implemented as total clicks aggregated by country.
		// "Keywords" counts distinct queries present in the same country/date range.
		$sql_locations = "
			SELECT
				COALESCE(country,'(not set)') AS country,
				COALESCE(SUM(clicks),0) AS clicks
			FROM %i
			WHERE {$where_qp}
			GROUP BY country
			ORDER BY clicks DESC
		";
		$location_rows = $wpdb->get_results( $wpdb->prepare( $sql_locations, array_merge( array( $table_pages ), $params_qp ) ), ARRAY_A );

		$locations = array();
		if ( is_array( $location_rows ) && count( $location_rows ) > 0 ) {
			$total_clicks_locations = 0;
			foreach ( $location_rows as $row ) {
				$total_clicks_locations += (int) ( $row['clicks'] ?? 0 );
			}

			$sql_keywords_by_country = "
				SELECT
					COALESCE(country,'(not set)') AS country,
					COUNT(DISTINCT query) AS keywords
				FROM %i
				WHERE {$where_qp}
				GROUP BY country
			";
			$keyword_rows = $wpdb->get_results( $wpdb->prepare( $sql_keywords_by_country, array_merge( array( $table_queries ), $params_qp ) ), ARRAY_A );
			$keyword_map = array();
			if ( is_array( $keyword_rows ) ) {
				foreach ( $keyword_rows as $kr ) {
					$key = (string) ( $kr['country'] ?? '' );
					$keyword_map[ $key ] = (int) ( $kr['keywords'] ?? 0 );
				}
			}

			foreach ( $location_rows as $row ) {
				$country_key = (string) ( $row['country'] ?? '' );
				$clicks = (int) ( $row['clicks'] ?? 0 );
				$percentage = $total_clicks_locations > 0 ? ( ( $clicks / $total_clicks_locations ) * 100 ) : 0;
				$locations[] = array(
					'country'   => $country_key,
					'clicks'    => $clicks,
					'percentage'=> $percentage,
					'keywords'  => (int) ( $keyword_map[ $country_key ] ?? 0 ),
				);
			}
		}

		return new \WP_REST_Response(
			array(
				'start'  => $start,
				'end'    => $end,
				'totals' => $totals ? $totals : array(),
				'series' => is_array( $series ) ? $series : array(),
				'topQueries' => is_array( $top_queries ) ? $top_queries : array(),
				'topPages'   => is_array( $top_pages ) ? $top_pages : array(),
				'devices'    => $devices,
				'locations'  => $locations,
				'source'     => $use_totals ? 'db_totals' : 'db_pages',
			),
			200
		);
	}

	/**
	 * @return array{totals:array<string,mixed>,series:array<int,array<string,mixed>>}|null
	 */
	private function overview_from_api( string $start, string $end, string $country, string $device ): ?array {
		$cache_key = 'serpiq_dash_api_' . md5( $start . '|' . $end . '|' . $country . '|' . $device );
		$cached = get_transient( $cache_key );
		if ( is_array( $cached ) && isset( $cached['totals'], $cached['series'] ) ) {
			return $cached;
		}

		$property = (string) get_option( Options::google_property_key(), '' );
		if ( '' === $property ) {
			return null;
		}

		$client = OAuth::instance()->client();
		if ( is_wp_error( $client ) ) {
			return null;
		}

		$service = null;
		if ( class_exists( '\Google_Service_SearchConsole' ) ) {
			$service = new \Google_Service_SearchConsole( $client );
		} elseif ( class_exists( '\Google_Service_Webmasters' ) ) {
			$service = new \Google_Service_Webmasters( $client );
		} else {
			return null;
		}

		$request = class_exists( '\Google_Service_SearchConsole_SearchAnalyticsQueryRequest' )
			? new \Google_Service_SearchConsole_SearchAnalyticsQueryRequest()
			: new \Google_Service_Webmasters_SearchAnalyticsQueryRequest();

		$request->setStartDate( $start );
		$request->setEndDate( $end );
		$request->setDimensions( array( 'date' ) );
		$request->setRowLimit( 25000 );

		if ( method_exists( $request, 'setSearchType' ) ) {
			$request->setSearchType( 'web' );
		}
		if ( method_exists( $request, 'setAggregationType' ) ) {
			$request->setAggregationType( 'auto' );
		}
		if ( method_exists( $request, 'setDataState' ) ) {
			$recent_threshold = gmdate( 'Y-m-d', strtotime( '-3 day' ) );
			$data_state = ( $end >= $recent_threshold ) ? 'all' : 'final';
			$request->setDataState( $data_state );
		}

		$filters = array();
		if ( '' !== $country ) {
			$filters[] = array(
				'dimension'   => 'country',
				'operator'    => 'equals',
				'expression'  => $country,
			);
		}
		if ( '' !== $device ) {
			$filters[] = array(
				'dimension'   => 'device',
				'operator'    => 'equals',
				'expression'  => $device,
			);
		}
		if ( ! empty( $filters ) && method_exists( $request, 'setDimensionFilterGroups' ) ) {
			// Build proper filter objects (Google client library expects typed models).
			$filter_objs = array();
			$filter_class = class_exists( '\Google_Service_SearchConsole_ApiDimensionFilter' )
				? '\Google_Service_SearchConsole_ApiDimensionFilter'
				: ( class_exists( '\Google_Service_Webmasters_ApiDimensionFilter' ) ? '\Google_Service_Webmasters_ApiDimensionFilter' : '' );

			$group_class = class_exists( '\Google_Service_SearchConsole_ApiDimensionFilterGroup' )
				? '\Google_Service_SearchConsole_ApiDimensionFilterGroup'
				: ( class_exists( '\Google_Service_Webmasters_ApiDimensionFilterGroup' ) ? '\Google_Service_Webmasters_ApiDimensionFilterGroup' : '' );

			if ( '' !== $filter_class && '' !== $group_class ) {
				foreach ( $filters as $f ) {
					$obj = new $filter_class();
					if ( method_exists( $obj, 'setDimension' ) ) {
						$obj->setDimension( (string) $f['dimension'] );
					}
					if ( method_exists( $obj, 'setOperator' ) ) {
						$obj->setOperator( (string) $f['operator'] );
					}
					if ( method_exists( $obj, 'setExpression' ) ) {
						$obj->setExpression( (string) $f['expression'] );
					}
					$filter_objs[] = $obj;
				}

				$group = new $group_class();
				if ( method_exists( $group, 'setGroupType' ) ) {
					$group->setGroupType( 'and' );
				}
				if ( method_exists( $group, 'setFilters' ) ) {
					$group->setFilters( $filter_objs );
				}
				$request->setDimensionFilterGroups( array( $group ) );
			}
		}

		try {
			$response = $service->searchanalytics->query( $property, $request );
		} catch ( \Throwable $e ) {
			return null;
		}

		$rows = array();
		if ( is_object( $response ) && method_exists( $response, 'getRows' ) ) {
			$rows = (array) $response->getRows();
		} elseif ( is_object( $response ) && isset( $response->rows ) ) {
			$rows = (array) $response->rows;
		}
		if ( empty( $rows ) ) {
			return null;
		}

		$series = array();
		$total_clicks = 0.0;
		$total_impr = 0.0;
		$pos_num = 0.0; // impressions-weighted

		foreach ( $rows as $row ) {
			$keys = method_exists( $row, 'getKeys' ) ? (array) $row->getKeys() : ( isset( $row->keys ) ? (array) $row->keys : array() );
			$date = (string) ( $keys[0] ?? '' );
			$clicks = (float) ( method_exists( $row, 'getClicks' ) ? $row->getClicks() : ( $row->clicks ?? 0 ) );
			$impr = (float) ( method_exists( $row, 'getImpressions' ) ? $row->getImpressions() : ( $row->impressions ?? 0 ) );
			$pos = (float) ( method_exists( $row, 'getPosition' ) ? $row->getPosition() : ( $row->position ?? 0 ) );

			$total_clicks += $clicks;
			$total_impr += $impr;
			$pos_num += $pos * $impr;

			$series[] = array(
				'date'        => $date,
				'clicks'      => $clicks,
				'impressions' => $impr,
				'ctr'         => $impr > 0 ? ( $clicks / $impr ) : 0,
				'position'    => $pos,
			);
		}

		$totals = array(
			'clicks'      => $total_clicks,
			'impressions' => $total_impr,
			'ctr'         => $total_impr > 0 ? ( $total_clicks / $total_impr ) : 0,
			'position'    => $total_impr > 0 ? ( $pos_num / $total_impr ) : 0,
		);

		$result = array(
			'totals' => $totals,
			'series' => $series,
		);
		set_transient( $cache_key, $result, 30 * MINUTE_IN_SECONDS );

		return $result;
	}

	private function sanitize_date( string $date ): ?string {
		if ( '' === $date ) {
			return null;
		}
		return preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ? $date : null;
	}
}

