<?php

namespace Yoast\WP\SEO\Premium\Integrations;

use wpdb;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Analytics\Domain\To_Be_Cleaned_Indexable_Bucket;
use Yoast\WP\SEO\Analytics\Domain\To_Be_Cleaned_Indexable_Count;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Cleanup_Repository;

/**
 * Adds cleanup hooks.
 */
class Cleanup_Integration implements Integration_Interface {

	/**
	 * The indexable cleanup repository.
	 *
	 * @var Indexable_Cleanup_Repository
	 */
	private $indexable_cleanup_repository;

	/**
	 * The constructor.
	 *
	 * @param Indexable_Cleanup_Repository $indexable_cleanup_repository The indexable cleanup repository.
	 */
	public function __construct( Indexable_Cleanup_Repository $indexable_cleanup_repository ) {
		$this->indexable_cleanup_repository = $indexable_cleanup_repository;
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array The array of conditionals.
	 */
	public static function get_conditionals() {
		return [];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_cleanup_tasks', [ $this, 'add_cleanup_tasks' ] );
		\add_action( 'wpseo_add_cleanup_counts_to_indexable_bucket', [ $this, 'add_cleanup_counts' ] );
	}

	/**
	 * Adds cleanup tasks for the cleanup integration.
	 *
	 * @param array $tasks Array of tasks to be added.
	 *
	 * @return array An associative array of tasks to be added to the cleanup integration.
	 */
	public function add_cleanup_tasks( $tasks ) {
		return \array_merge(
			$tasks,
			[
				'clean_orphaned_indexables_prominent_words' => function ( $limit ) {
					return $this->cleanup_orphaned_from_table( 'Prominent_Words', 'indexable_id', $limit );
				},
				'clean_old_prominent_word_entries' => function ( $limit ) {
					return $this->cleanup_old_prominent_words( $limit );
				},
				'clean_old_prominent_word_version_numbers' => function ( $limit ) {
					return $this->cleanup_old_prominent_word_version_numbers( $limit );
				},
			]
		);
	}

	/**
	 * Adds cleanup counts to the data bucket object.
	 *
	 * @param To_Be_Cleaned_Indexable_Bucket $to_be_cleaned_indexable_bucket The bucket with current indexable count data.
	 *
	 * @return void
	 */
	public function add_cleanup_counts( To_Be_Cleaned_Indexable_Bucket $to_be_cleaned_indexable_bucket ): void {
		$to_be_cleaned_indexable_bucket->add_to_be_cleaned_indexable_count( new To_Be_Cleaned_Indexable_Count( 'orphaned_indexables_prominent_words', $this->indexable_cleanup_repository->count_orphaned_from_table( 'Prominent_Words', 'indexable_id' ) ) );
		$to_be_cleaned_indexable_bucket->add_to_be_cleaned_indexable_count( new To_Be_Cleaned_Indexable_Count( 'orphaned_prominent_word_entries', $this->count_old_prominent_words() ) );
		$to_be_cleaned_indexable_bucket->add_to_be_cleaned_indexable_count( new To_Be_Cleaned_Indexable_Count( 'orphaned_prominent_word_version_numbers', $this->count_old_prominent_word_version_numbers() ) );
	}

	/**
	 * Cleans orphaned rows from a yoast table.
	 *
	 * @param string $table  The table to cleanup.
	 * @param string $column The table column the cleanup will rely on.
	 * @param int    $limit  The limit we'll apply to the queries.
	 *
	 * @return int The number of deleted rows.
	 */
	public function cleanup_orphaned_from_table( $table, $column, $limit ) {
		global $wpdb;

		$table           = Model::get_table_name( $table );
		$indexable_table = Model::get_table_name( 'Indexable' );

		// Warning: If this query is changed, make sure to update the query in cleanup_orphaned_from_table in Free as well.
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
		$query = $wpdb->prepare(
			"
			SELECT table_to_clean.{$column}
			FROM {$table} table_to_clean
			LEFT JOIN {$indexable_table} AS indexable_table
			ON table_to_clean.{$column} = indexable_table.id
			WHERE indexable_table.id IS NULL
			AND table_to_clean.{$column} IS NOT NULL
			LIMIT %d",
			$limit
		);
		// phpcs:enable

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
		$orphans = $wpdb->get_col( $query );

		if ( empty( $orphans ) ) {
			return 0;
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
		return \intval( $wpdb->query( "DELETE FROM $table WHERE {$column} IN( " . \implode( ',', $orphans ) . ' ) ' ) );
	}

	/**
	 * Cleans up old style prominent words from the database.
	 *
	 * @param int $limit The maximum amount of old prominent words to clean up in one go. Defaults to 1000.
	 *
	 * @return int The number of deleted rows.
	 */
	public function cleanup_old_prominent_words( $limit = 1000 ) {
		global $wpdb;

		$taxonomy_ids = $this->retrieve_prominent_word_taxonomies( $wpdb, $limit );

		if ( \count( $taxonomy_ids ) === 0 ) {
			return 0;
		}

		$nr_of_deleted_rows = $this->delete_prominent_word_taxonomies_and_terms( $wpdb, $taxonomy_ids );

		if ( $nr_of_deleted_rows === false ) {
			// Failed query.
			return 0;
		}

		return $nr_of_deleted_rows;
	}

	// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

	/**
	 * Count up old style prominent words from the database.
	 *
	 * @return int The number of old prominent word rows.
	 */
	public function count_old_prominent_words() {
		global $wpdb;

		$query = $wpdb->prepare(
			"SELECT count(term_taxonomy_id) FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s",
			[ 'yst_prominent_words' ]
		);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
		return $wpdb->get_col( $query )[0];
	}

	/**
	 * Retrieve a list of prominent word taxonomy IDs.
	 *
	 * @param wpdb $wpdb  The WordPress database object.
	 * @param int  $limit The maximum amount of prominent word taxonomies to retrieve.
	 *
	 * @return string[] A list of prominent word taxonomy IDs (of size 'limit').
	 */
	protected function retrieve_prominent_word_taxonomies( $wpdb, $limit ) {
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s LIMIT %d",
				[ 'yst_prominent_words', $limit ]
			)
		);
	}

	/**
	 * Deletes the given list of taxonomies and their terms.
	 *
	 * @param wpdb     $wpdb         The WordPress database object.
	 * @param string[] $taxonomy_ids The IDs of the taxonomies to remove and their corresponding terms.
	 *
	 * @return bool|int `false` if the query failed, the amount of rows deleted otherwise.
	 */
	protected function delete_prominent_word_taxonomies_and_terms( $wpdb, $taxonomy_ids ) {
		return $wpdb->query(
			$wpdb->prepare(
				"DELETE t, tr, tt FROM {$wpdb->term_taxonomy} tt
				LEFT JOIN {$wpdb->terms} t ON tt.term_id = t.term_id 
				LEFT JOIN {$wpdb->term_relationships} tr ON tt.term_taxonomy_id = tr.term_taxonomy_id 
				WHERE tt.term_taxonomy_id IN ( " . \implode( ', ', \array_fill( 0, \count( $taxonomy_ids ), '%s' ) ) . ' )',
				$taxonomy_ids
			)
		);
	}

	/**
	 * Cleans up the old prominent word versions from the postmeta table in the database.
	 *
	 * @param int $limit The maximum number of prominent word version numbers to clean in one go.
	 *
	 * @return bool|int The number of cleaned up prominent word version numbers, or `false` if the query failed.
	 */
	protected function cleanup_old_prominent_word_version_numbers( $limit ) {
		global $wpdb;

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
		$query = $wpdb->prepare(
			"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s LIMIT %d",
			[ '_yst_prominent_words_version', $limit ]
		);
		// phpcs:enable

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
		return $wpdb->query( $query );
	}

	/**
	 * Counts up the old prominent word versions from the postmeta table in the database.
	 *
	 * @return bool|int The number of prominent word version numbers.
	 */
	protected function count_old_prominent_word_version_numbers() {
		global $wpdb;

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
		$query = $wpdb->prepare(
			"SELECT count(*) FROM {$wpdb->postmeta} WHERE meta_key = %s",
			[ '_yst_prominent_words_version' ]
		);
		// phpcs:enable

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
		return $wpdb->get_col( $query )[0];
	}

	// phpcs:enable
}
