<?php

namespace WPDRMS\ASP\Statistics;

use wpdb;
use WPDRMS\ASP\Query\SearchQueryArgs;
use WPDRMS\PluginCore\Traits\SingletonTrait;
use WPDRMS\ASP\Query\SearchQuery as ASP_SearchQuery;
use WPDRMS\ASP\Statistics\Exporters\InteractionCSVExporter;
use WPDRMS\ASP\Statistics\Exporters\LegacyCSVExporter;
use WPDRMS\ASP\Statistics\Exporters\ResultsCSVExporter;
use WPDRMS\ASP\Statistics\Exporters\SearchCSVExporter;
use WPDRMS\ASP\Statistics\ORM\Interaction;
use WPDRMS\ASP\Statistics\ORM\Result;
use WPDRMS\ASP\Statistics\ORM\Search;
use WPDRMS\ASP\Statistics\ORM\StatisticsOptions;
use WPDRMS\ASP\Statistics\Queries\InteractionQuery;
use WPDRMS\ASP\Statistics\Queries\ResultQuery;
use WPDRMS\ASP\Statistics\Queries\SearchQuery;
use WPDRMS\Utils\MB;
use WPDRMS\Utils\Server;
use WPDRMS\Utils\Str;

/**
 * @phpstan-type Searches array<array{
 *     phrase?: string,
 *     page?: int,
 *     asp_id?: int,
 *     device_type?: string,
 *     lang?: string,
 *     found_results?: int,
 *     user_id?: int,
 *     cached?: 1|0,
 *     suggested?: 1|0,
 *     results?: mixed|array<array{id?: int, result_type?: string}>
 * }>
 */
class StatisticsService {
	use SingletonTrait;

	public StatisticsOptions $options;

	/**
	 * @var array<string, int>
	 */
	public static array $result_type_arr = array(
		'pagepost'        => 1,
		'term'            => 2,
		'user'            => 3,
		'blog'            => 4,
		'bp_group'        => 5,
		'bp_activity'     => 6,
		'comment'         => 7,
		'attachment'      => 8,
		'peepso_activity' => 9,
		'peepso_group'    => 10,
	);

	public static int $last_search_id = 0;

	/**
	 * @var SearchQuery
	 * @readonly
	 */
	public SearchQuery $search_query;

	public ResultQuery $result_query;
	public InteractionQuery $interaction_query;

	public LegacyCSVExporter $legacy_csv_exporter;
	public SearchCSVExporter $search_csv_exporter;
	public ResultsCSVExporter $results_csv_exporter;
	public InteractionCSVExporter $interaction_csv_exporter;

	private function __construct() {
		$this->search_query             = SearchQuery::instance();
		$this->result_query             = ResultQuery::instance();
		$this->legacy_csv_exporter      = LegacyCSVExporter::instance();
		$this->interaction_query        = InteractionQuery::instance();
		$this->search_csv_exporter      = SearchCSVExporter::instance();
		$this->interaction_csv_exporter = InteractionCSVExporter::instance();
	}

	public function loadHooks(): void {
		$this->options = StatisticsOptions::instance();
		if ( $this->options->status->value ) {
			add_action(
				'wp_footer',
				function () {
					?>
					<div id="asp-statistics" data-statistics-id="<?php echo esc_attr(self::$last_search_id); ?>" style="display:none;"></div>
					<?php
				}
			);
			add_action('asp/search/override/args', array( $this, 'searchOverrideArgsHook' ), 99, 4);
			add_action('asp/search/results', array( $this, 'registerSearchAndResultsHook' ), 99, 4);
			add_action('asp/ajax/search/custom_output', array( $this, 'registerCachedSearchesQueriesHook' ), 9999, 6);
			add_action(
				'asp/statistics/retention',
				function () {
					$this->enforceRetention(
						$this->options->data_retention_age->value,
						$this->options->data_retention_max_searches->value
					);
				}
			);
			if ( ! wp_next_scheduled( 'asp/statistics/retention' ) ) {
				wp_schedule_event( time() + 30, 'hourly', 'asp/statistics/retention' );
			}
		} else {
			if ( wp_next_scheduled( 'asp/statistics/retention' ) ) {
				wp_unschedule_event( wp_next_scheduled( 'asp/statistics/retention' ), 'asp/statistics/retention');
			}
			wp_clear_scheduled_hook('asp/statistics/retention');
		}
	}

	/**
	 *
	 * @uses apply_filters('asp/search/override/args', array $args, array $data);
	 *
	 * @param array $args
	 * @param array $data
	 * @return array
	 */
	public function searchOverrideArgsHook( array $args, array $data ) {
		if ( class_exists('SitePress') && $data['override_count'] === 0 ) {
			$args['record_statistics'] = false;
		}
		return $args;
	}


	/**
	 * @uses apply_filters('asp/search/results', $results, SearchQueryArgs $args, SearchQuery $query);
	 *
	 * @param array           $results
	 * @param SearchQueryArgs $args
	 * @param SearchQuery     $query
	 * @return array
	 */
	public function registerSearchAndResultsHook( array $results, SearchQueryArgs $args, ASP_SearchQuery $query ) {
		if ( !$args->record_statistics ) {
			return $results;
		}

		/**
		 * Exclude requests from admin preview
		 */
		$referer = Server::getCleanReferrerPath();
		if ( str_starts_with($referer ?? '', Server::getCleanUrlPath( get_admin_url() ) ?? 'wp-admin/admin.php' ) ) {
			return $results;
		}

		/**
		 * Excluded keywords
		 */
		if ( $args->s !== '' ) {
			foreach ( $this->options->exclude_phrases_partial->value as $excluded_phrase ) {
				if ( str_contains($args->s, $excluded_phrase) ) {
					return $results;
				}
			}

			if ( in_array($args->s, $this->options->exclude_phrases_whole->value, true) ) {
				return $results;
			}
		}

		$search            = new Search();
		$search->phrase    = MB::substr(sanitize_text_field($args->s), 0, $this->options->max_phrase_length->value);
		$search->type      = $args->_ajax_search ? 1 : 0;
		$search->user_id   = get_current_user_id();
		$search->page      = $args->_ajax_search ? ( $args->_call_num === 0 ? 1 : $args->_call_num + 1 ) : $args->page;
		$search->asp_id    = $args->_id > 0 ? $args->_id : null;
		$search->suggested = $args->is_suggestion_query ? 1 : 0;

		if ( !$this->options->record_suggested_searches->value && $search->suggested !== 0 ) {
			return $results;
		}

		if ( is_multisite() ) {
			$search->blog_id = get_current_blog_id();
		}
		switch ( $args->device_type ) {
			case 'tablet':
				$search->device_type = 2;
				break;
			case 'mobile':
				$search->device_type = 3;
				break;
			default:
				$search->device_type = 1;
		}
		if ( $args->_wpml_lang !== '' ) {
			$search->lang = $args->_wpml_lang;
		} elseif ( $args->_polylang_lang !== '' ) {
			$search->lang = $args->_polylang_lang;
		}
		$search->found_results = $query->found_posts;
		$search->referer       = $referer;
		$search                = $search->save();  // Save and return updated model

		if ( !$search ) {
			return $results;
		}

		self::$last_search_id = $search->id;

		if ( $this->options->record_results->value ) {
			$results_arr = array();
			$count       = 0;
			$max_count   = $this->options->record_results_max_count->value;
			foreach ( $results as $result ) {
				$r            = new Result();
				$r->search_id = $search->id;
				/**
				 * $result->ID can be a string, intval() is not redundant
				 */
				$r->result_id = intval( $result->id ?? $result->ID ?? 0 );
				if ( $r->result_id === 0 ) {
					continue;
				}
				$r->result_type = isset($result->content_type, self::$result_type_arr[ $result->content_type ]) ?
					self::$result_type_arr[ $result->content_type ] : 1;
				$results_arr[]  = $r;
				++$count;
				if ( $count >= $max_count ) {
					break;
				}
			}
			if ( !empty($results_arr) ) {
				Result::bulkInsert($results_arr);
			}
		}

		return $results;
	}

	public function registerCachedSearchesQueriesHook( string $output, int $id, string $s, array $options, int $call_num, $autop ): string {
		$data = json_decode($output, true);
		if ( empty($data) || !isset($data['results']) || !is_array($data['results']) ) {
			return $output;
		}
		$search_data = array(
			'phrase'        => $s,
			'page'          => $call_num + 1,
			'asp_id'        => $id,
			'device_type'   => $options['device'] ?? '',
			'lang'          => $data['lang'] ?? '',
			'found_results' => $data['full_results_count'] ?? '',
			'suggested'     => $data['suggested'] ? 1 : 0,
			'user_id'       => get_current_user_id(),
		);
		if ( isset($data['results']['groups']) && is_array($data['results']['groups']) ) {
			$search_data['results'] = array();
			foreach ( $data['results']['groups'] as $group ) {
				if ( isset($group['items']) && is_array($group['items']) ) {
					foreach ( $group['items'] as $result ) {
						$result_id    = $result['id'] ?? $result['result_id'] ?? 0;
						$content_type = $result['content_type'] ?? $result['result_type'] ?? 'pagepost';

						$exists = false;
						foreach ( $search_data['results'] as $existing_result ) {
							if ( $existing_result['id'] === $result_id ) {
								$exists = true;
								break;
							}
						}

						if ( !$exists ) {
							$search_data['results'][] = array(
								'id'          => $result_id,
								'result_type' => $content_type,
							);
						}
					}
				}
			}
		} else {
			$search_data['results'] = array_filter(
				array_map(
					function ( $result ) {
						return array(
							'id'          => $result['id'] ?? $result['result_id'] ?? 0,
							'result_type' => $result['content_type'] ?? $result['result_type'] ?? 'pagepost',
						);
					},
					$data['results']
				),
				function ( $result ) {
					return $result['id'] > 0;
				}
			);
		}

		$this->registerCachedSearchesQueries(array( $search_data ));

		return $output;
	}


	/**
	 * @param Searches $searches
	 * @return void
	 */
	public function registerCachedSearchesQueries( array $searches ): void {
		if ( empty($searches) || !$this->options->status->value ) {
			return;
		}

		/**
		 * Exclude requests from admin preview
		 */
		$referer = Server::getCleanReferrerPath();
		if ( str_starts_with($referer ?? '', Server::getCleanUrlPath( get_admin_url() ) ?? 'wp-admin/admin.php' ) ) {
			return;
		}

		$search_models     = array();
		$results_by_search = array();

		foreach ( $searches as $search_data ) {
			$phrase = Str::anyToString($search_data['phrase'] ?? '');
			/**
			 * Excluded keywords
			 */
			if ( $phrase !== '' ) {
				foreach ( $this->options->exclude_phrases_partial->value as $excluded_phrase ) {
					if ( str_contains($phrase, $excluded_phrase) ) {
						continue 2;
					}
				}

				if ( in_array($phrase, $this->options->exclude_phrases_whole->value, true) ) {
					continue;
				}
			}

			$search            = new Search();
			$search->phrase    = MB::substr($phrase, 0, $this->options->max_phrase_length->value);
			$search->type      = 1;
			$search->user_id   = isset($search_data['user_id']) ? intval($search_data['user_id']) : get_current_user_id();
			$search->page      = isset($search_data['page']) ? intval($search_data['page']) : 1;
			$search->asp_id    = isset($search_data['asp_id']) && $search_data['asp_id'] > 0 ? intval($search_data['asp_id']) : null;
			$search->cached    = 1;
			$search->suggested = isset($search_data['suggested']) ? intval($search_data['suggested']) : 0;

			if ( !$this->options->record_suggested_searches->value && $search->suggested !== 0 ) {
				continue;
			}

			if ( is_multisite() ) {
				$search->blog_id = get_current_blog_id();
			}

			if ( isset($search_data['device_type']) ) {
				if ( is_numeric($search_data['device_type']) ) {
					$search->device_type = intval($search_data['device_type']);
					$search->device_type = max(1, min(3, $search->device_type));
				} else {
					switch ( $search_data['device_type'] ) {
						case 'tablet':
							$search->device_type = 2;
							break;
						case 'mobile':
							$search->device_type = 3;
							break;
						default:
							$search->device_type = 1;
					}
				}
			} else {
				$search->device_type = 1;
			}

			if ( isset($search_data['lang']) && $search_data['lang'] !== '' ) {
				$search->lang = $search_data['lang'];
			}

			$search->found_results = isset($search_data['found_results']) ? intval($search_data['found_results']) : 0;
			$search->referer       = $referer;

			$search = $search->save();

			if ( $search && isset($search_data['results']) && is_array($search_data['results']) ) {
				$search_models[]                  = $search;
				$results_by_search[ $search->id ] = $search_data['results'];
			}
		}

		if ( $this->options->record_results->value && !empty($search_models) ) {
			$results_arr = array();
			$max_count   = $this->options->record_results_max_count->value;

			foreach ( $search_models as $search ) {
				$count   = 0;
				$results = $results_by_search[ $search->id ] ?? array();

				foreach ( $results as $result ) {
					$r            = new Result();
					$r->search_id = $search->id;
					$r->result_id = intval($result['id'] ?? $result['result_id'] ?? 0);

					if ( $r->result_id === 0 ) {
						continue;
					}

					$content_type   = $result['content_type'] ?? $result['result_type'] ?? 'pagepost';
					$r->result_type = self::$result_type_arr[ $content_type ] ?? 1;

					$results_arr[] = $r;
					++$count;

					if ( $count >= $max_count ) {
						break;
					}
				}
			}

			if ( !empty($results_arr) ) {
				Result::bulkInsert($results_arr);
			}
		}
	}

	public function createTables(): void {
		Search::createTable();
		Result::createTable();
		Interaction::createTable();
	}

	public function dropTables(): void {
		Search::dropTable();
		Result::dropTable();
		Interaction::dropTable();
	}


	/**
	 * Add an interaction
	 *
	 * @param int    $search_id
	 * @param int    $result_id
	 * @param string $result_type
	 * @return ?Interaction The saved Interaction model
	 */
	public function addInteraction( int $search_id, int $result_id, string $result_type ): ?Interaction {
		if ( $result_id < 1 ) {
			return null;
		}

		$type    = self::$result_type_arr[ $result_type ] ?? 1;
		$results = Result::findBy(
			array(
				'search_id'   => $search_id,
				'result_id'   => $result_id,
				'result_type' => $type,
			),
			1
		);
		if ( empty($results) ) {
			return null;
		}
		$result                 = $results[0];
		$interaction            = new Interaction();
		$interaction->result_id = $result->id;
		$interaction->date      = current_time('mysql'); // @phpstan-ignore-line

		return $interaction->save();
	}

	public function deleteSearch( int $id ): bool {
		global $wpdb;
		$search = Search::find($id);
		if ( $search === null ) {
			return false;
		}
		$wpdb->query(
			$wpdb->prepare(
				'DELETE FROM ' . Interaction::getTableName() . ' WHERE result_id IN (
					SELECT id FROM ' . Result::getTableName() . ' WHERE search_id = %d
				)',
				$search->id
			)
		);
		$wpdb->query(
			$wpdb->prepare(
				'DELETE FROM ' . Result::getTableName() . ' WHERE search_id = %d',
				$search->id
			)
		);
		$search->delete();

		return true;
	}

	public function enforceRetention( string $max_age = '1 year', int $max_searches = 1000000 ): void {
		/**
		 * @var wpdb $wpdb;
		 */
		global $wpdb;

		$allowed_max_age = array( '1 year', '2 year', '1 month', '3 month', '6 month', '1 week', '2 week' );
		if ( !in_array($max_age, $allowed_max_age, true ) ) {
			$max_age = '1 year';
		}

		// phpcs:disable
		$wpdb->query(
			'DELETE FROM ' . Interaction::getTableName() . ' WHERE result_id IN (
					SELECT id FROM ' . Result::getTableName() . ' WHERE search_id IN (
                         SELECT id FROM ' . Search::getTableName() . ' WHERE date < DATE_SUB(NOW(), INTERVAL ' . $max_age . ')
                     )
			)'
		);

		// phpcs:disable
		$wpdb->query(
			'DELETE FROM ' . Result::getTableName() . ' WHERE search_id IN (
            SELECT id FROM ' . Search::getTableName() . ' WHERE date < DATE_SUB(NOW(), INTERVAL ' . $max_age . ')
        )'
		);



		$wpdb->query(
			'DELETE FROM ' . Search::getTableName() . ' WHERE date < DATE_SUB(NOW(), INTERVAL ' . $max_age . ')'
		);

		$count = $wpdb->get_var('SELECT COUNT(*) FROM ' . Search::getTableName());
		if ($count > $max_searches) {
			$excess = $count - $max_searches;

			// Use JOIN for interactions deletion
			$wpdb->query(
				$wpdb->prepare(
					'DELETE i FROM ' . Interaction::getTableName() . ' i
				 JOIN (
                   SELECT * FROM ' . Result::getTableName() . '
                 ) r ON i.result_id = r.id
                 JOIN (
                   SELECT id as sid FROM ' . Search::getTableName() . ' ORDER BY date ASC LIMIT %d
                 ) s ON r.search_id = s.sid',
					$excess
				)
			);

			// Use JOIN for results deletion to avoid subquery LIMIT limitation
			$wpdb->query(
				$wpdb->prepare(
					'DELETE r FROM ' . Result::getTableName() . ' r
                 JOIN (
                   SELECT id FROM ' . Search::getTableName() . ' ORDER BY date ASC LIMIT %d
                 ) s ON r.search_id = s.id',
					$excess
				)
			);

			// Direct DELETE for searches (no subquery issue)
			$wpdb->query(
				$wpdb->prepare(
					'DELETE FROM ' . Search::getTableName() . ' ORDER BY date ASC LIMIT %d',
					$excess
				)
			);
		}
		// phpcs:enable
	}

	/**
	 * Deletes all data from the tables with an auto-increment reset
	 *
	 * @return void
	 */
	public function reset(): void {
		Search::truncateTable();
		Result::truncateTable();
		Interaction::truncateTable();
	}
}
