<?php

namespace WPGraphQL\Utils;

/**
 * Class QueryLog
 *
 * @package WPGraphQL\Utils
 */
class QueryLog {

	/**
	 * Whether Query Logs are enabled
	 *
	 * @var bool
	 */
	protected $query_logs_enabled;

	/**
	 * The user role query logs should be limited to
	 *
	 * @var string
	 */
	protected $query_log_user_role;

	/**
	 * Initialize Query Logging
	 *
	 * @return void
	 */
	public function init() {

		// Check whether Query Logs have been enabled from the settings page
		$enabled                  = get_graphql_setting( 'query_logs_enabled', 'off' );
		$this->query_logs_enabled = 'on' === $enabled;

		$this->query_log_user_role = get_graphql_setting( 'query_log_user_role', 'manage_options' );

		if ( ! $this->query_logs_enabled ) {
			return;
		}

		add_action( 'init', [ $this, 'init_save_queries' ] );
		add_filter( 'graphql_request_results', [ $this, 'show_results' ], 10, 5 );
	}

	/**
	 * Tell WordPress to start saving queries.
	 *
	 * NOTE: This will affect all requests, not just GraphQL requests.
	 *
	 * @return void
	 */
	public function init_save_queries() {
		if ( is_graphql_http_request() && ! defined( 'SAVEQUERIES' ) ) {
			define( 'SAVEQUERIES', true );
		}
	}

	/**
	 * Determine if the requesting user can see logs
	 *
	 * @return bool
	 */
	public function user_can_see_logs() {
		$can_see = false;

		// If logs are disabled, user cannot see logs
		if ( ! $this->query_logs_enabled ) {
			$can_see = false;
		} elseif ( 'any' === $this->query_log_user_role ) {
			// If "any" is the selected role, anyone can see the logs
			$can_see = true;
		} else {
			// Get the current users roles
			$user = wp_get_current_user();

			// If the user doesn't have roles or the selected role isn't one the user has, the
			// user cannot see roles;
			if ( in_array( $this->query_log_user_role, $user->roles, true ) ) {
				$can_see = true;
			}
		}

		/**
		 * Filter whether the logs can be seen in the request results or not
		 *
		 * @param bool $can_see Whether the requester can see the logs or not
		 *
		 * @hookGroup debugging
		 * @since 0.2.0
		 */
		return apply_filters( 'graphql_user_can_see_query_logs', $can_see );
	}

	/**
	 * Filter the results of the GraphQL Response to include the Query Log
	 *
	 * @param mixed|array<string,mixed>|object $response
	 * @param \WPGraphQL\WPSchema              $schema The WPGraphQL Schema
	 * @param string                           $operation_name The operation name being executed
	 * @param string                           $request        The GraphQL Request being made
	 * @param array<string,mixed>              $variables      The variables sent with the request
	 *
	 * @return mixed|array<string,mixed>|object
	 */
	public function show_results( $response, $schema, $operation_name, $request, $variables ) {
		$query_log = $this->get_query_log();

		// If the user cannot see the logs, return the response as-is without the logs
		if ( ! $this->user_can_see_logs() ) {
			return $response;
		}

		if ( ! empty( $response ) ) {
			if ( is_array( $response ) ) {
				$response['extensions']['queryLog'] = $query_log;
			} elseif ( is_object( $response ) ) {
				// @phpstan-ignore-next-line
				$response->extensions['queryLog'] = $query_log;
			}
		}

		return $response;
	}

	/**
	 * Return the query log produced from the logs stored by WPDB.
	 *
	 * @return array<string,mixed>
	 */
	public function get_query_log() {
		global $wpdb;

		$save_queries_value = defined( 'SAVEQUERIES' ) && true === SAVEQUERIES ? 'true' : 'false';
		$default_message    = sprintf(
			// translators: %s is the value of the SAVEQUERIES constant
			__( 'Query Logging has been disabled. The \'SAVEQUERIES\' Constant is set to \'%s\' on your server.', 'wp-graphql' ),
			$save_queries_value
		);

		// Default message
		$trace = [ $default_message ];

		if ( ! empty( $wpdb->queries ) && is_array( $wpdb->queries ) ) {
			$queries = [];

			foreach ( $wpdb->queries as $index => $query ) {
				$queries[] = $this->normalize_query_log_entry( $query, (int) $index );
			}

			$times      = wp_list_pluck( $queries, 'time' );
			$total_time = array_sum( $times );
			$trace      = [
				'queryCount' => count( $queries ),
				'totalTime'  => $total_time,
				'queries'    => $queries,
			];
		}

		/**
		 * Filter the trace
		 *
		 * @param mixed[]                   $trace     The trace to return
		 * @param \WPGraphQL\Utils\QueryLog $instance  The QueryLog class instance
		 *
		 * @hookGroup debugging
		 * @since 0.2.0
		 */
		return apply_filters( 'graphql_tracing_response', $trace, $this );
	}

	/**
	 * Normalize one row from `$wpdb->queries` into the query log shape.
	 *
	 * WordPress core stores `[ sql, time, stack ]` (numeric keys). Database layers such as
	 * HyperDB may use associative keys (e.g. `query`, `elapsed`, `debug`). Defaults cover both;
	 * use the `graphql_query_log_entry` filter for other shapes.
	 *
	 * @since 2.12.0
	 *
	 * @param mixed $query Raw row from `$wpdb->queries`.
	 * @param int   $index Zero-based position in the saved query list.
	 *
	 * @return array{sql: string, time: float, stack: string}
	 */
	protected function normalize_query_log_entry( $query, $index ) {
		$defaults = [
			'sql'   => '',
			'time'  => 0.0,
			'stack' => '',
		];

		if ( is_array( $query ) ) {
			$defaults['sql']   = $query[0] ?? $query['query'] ?? '';
			$defaults['time']  = (float) ( $query[1] ?? $query['elapsed'] ?? 0 );
			$defaults['stack'] = $query[2] ?? $query['debug'] ?? '';
		}

		/**
		 * Filter a single saved query row after default normalization.
		 *
		 * @param array{sql: string, time: float, stack: string}  $parts     Normalized SQL, elapsed time, and stack/debug string.
		 * @param mixed                                           $query     Raw row from `$wpdb->queries`.
		 * @param int                                             $index     Index of this query in the log.
		 * @param \WPGraphQL\Utils\QueryLog                       $query_log The QueryLog instance.
		 *
		 * @hookGroup debugging
		 * @since 2.12.0
		 */
		$filtered = apply_filters( 'graphql_query_log_entry', $defaults, $query, $index, $this );

		if ( ! is_array( $filtered ) ) {
			$filtered = $defaults;
		}

		return [
			'sql'   => (string) ( $filtered['sql'] ?? '' ),
			'time'  => (float) ( $filtered['time'] ?? 0 ),
			'stack' => (string) ( $filtered['stack'] ?? '' ),
		];
	}
}
