<?php
namespace AIOSEO\Plugin\Common\QueryArgs;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models;

/**
 * Class to control Crawl Cleanup.
 *
 * @since 4.5.8
 */
class CrawlCleanup {

	/**
	 * Construct method.
	 *
	 * @since 4.5.8
	 */
	public function __construct() {
		// Add action to clear crawl cleanup logs.
		add_action( 'aioseo_crawl_cleanup_clear_logs', [ $this, 'clearLogs' ] );

		if ( aioseo()->options->searchAppearance->advanced->blockArgs->optimizeUtmParameters ) {
			add_action( 'template_redirect', [ $this, 'maybeRedirectUtmParameters' ], 50 );
		}
	}

	/**
	 * Redirects the UTM parameters to with (#) equivalent.
	 *
	 * @since 4.8.0
	 *
	 * @return void
	 */
	public function maybeRedirectUtmParameters() {
		$requestUri = aioseo()->helpers->getRequestUrl();
		if ( empty( $requestUri ) ) {
			return;
		}

		$parsed = wp_parse_url( $requestUri );
		if ( empty( $parsed['query'] ) ) {
			return;
		}

		$args = [];
		wp_parse_str( $parsed['query'], $args );

		// Reset query to reconstruct without utm_ parameters.
		$parsed['query'] = '';

		// Initialize the fragment key if it's not set.
		if ( ! isset( $parsed['fragment'] ) ) {
			$parsed['fragment'] = '';
		}

		// Check if there are any utm_ parameters and redirect accordingly.
		$utmFound = false;
		foreach ( $args as $key => $value ) {
			$keyValue = $key . '=' . $value;
			if ( 0 === stripos( $key, 'utm_' ) ) {
				$utmFound = true;
				// Rebuild the URL with # instead of ?.
				$parsed['fragment'] .= ! empty( $parsed['fragment'] ) ? '&' . $keyValue : $keyValue;
			} else {
				$parsed['query'] .= ! empty( $parsed['query'] ) ? '&' . $keyValue : $keyValue;
			}
		}

		if ( $utmFound ) {
			aioseo()->helpers->redirect( aioseo()->helpers->buildUrl( $parsed ), 301, 'Optimize UTM parameters' );
		}
	}

	/**
	 * Schedule clearing of the logs.
	 * This runs when the logs retention option is changed.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function scheduleClearingLogs() {
		aioseo()->actionScheduler->unschedule( 'aioseo_crawl_cleanup_clear_logs' );

		$optionLength = json_decode( aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention )->value;
		if (
			aioseo()->options->searchAppearance->advanced->blockArgs->enable &&
			'forever' !== $optionLength
		) {
			aioseo()->actionScheduler->scheduleRecurrent( 'aioseo_crawl_cleanup_clear_logs', 0, HOUR_IN_SECONDS );
		}
	}

	/**
	 * Clears the logs.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function clearLogs() {
		$optionLength = json_decode( aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention )->value;
		if ( 'forever' === $optionLength ) {
			return;
		}

		$date = gmdate( 'Y-m-d H:i:s', strtotime( '-1 ' . $optionLength ) );
		aioseo()->core->db
			->delete( 'aioseo_crawl_cleanup_logs' )
			->where( 'updated <', $date )
			->run();
	}

	/**
	 * Fetch Crawl Cleanup Logs.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function fetchLogs( $request ) {
		$filter            = $request->get_param( 'filter' );
		$body              = $request->get_json_params();
		$orderByUnblocked  = ! empty( $body['orderBy'] ) ? sanitize_text_field( $body['orderBy'] ) : 'logs.updated';
		$orderByBlocked    = ! empty( $body['orderBy'] ) ? sanitize_text_field( $body['orderBy'] ) : 'b.id';
		$orderDir          = ! empty( $body['orderDir'] ) && ! empty( $body['orderBy'] ) ? strtoupper( sanitize_text_field( $body['orderDir'] ) ) : 'DESC';
		$limit             = ! empty( $body['limit'] ) ? intval( $body['limit'] ) : aioseo()->settings->tablePagination['queryArgs'];
		$offset            = ! empty( $body['offset'] ) ? intval( $body['offset'] ) : 0;
		$searchTerm        = ! empty( $body['searchTerm'] ) ? sanitize_text_field( $body['searchTerm'] ) : null;
		$keyValueSeparator = Models\CrawlCleanupBlockedArg::getKeyValueSeparator();
		$dateFormat        = get_option( 'date_format' );
		$timeFormat        = get_option( 'time_format' );
		$dateTimeFormat    = $dateFormat . ' ' . $timeFormat;

		// Query to get Arg Logs (unblocked) and the total.
		$queryUnblocked = aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_logs as logs' )
			->select( ' logs.id,
						logs.slug,
						logs.param,
						logs.value,
						logs.hits,
						logs.updated' )
			->leftJoin( 'aioseo_crawl_cleanup_blocked_args as blocked',
				'blocked.param_value_hash = sha1(logs.param) OR
					blocked.param_value_hash = sha1(concat(logs.param, "' . $keyValueSeparator . '", logs.value))' )
			->limit( $limit, $offset );

		if ( ! empty( $searchTerm ) ) {
			// Apply escape to the search term.
			$searchTerm = esc_sql( aioseo()->core->db->db->esc_like( $searchTerm ) );
			$where = '
				(
					logs.slug LIKE \'%' . $searchTerm . '%\' OR
					logs.slug LIKE \'%' . str_replace( '%20', '-', $searchTerm ) . '%\' OR
					logs.slug LIKE \'%' . str_replace( '%20', '+', $searchTerm ) . '%\'
				)
			';

			$queryUnblocked->whereRaw( $where );
		}

		$queryUnblocked->where( 'blocked.id', null );
		$queryUnblocked->orderBy( "$orderByUnblocked $orderDir" );

		$rowsUnblocked = $queryUnblocked->run( false )->result();
		$totalUnblocked = $queryUnblocked->reset( [ 'limit' ] )->count();

		// Test logs (unblocked) to see if have some regex block.
		$regexMatches = [];
		foreach ( $rowsUnblocked as $unblocked ) {
			$blockedRegex = Models\CrawlCleanupBlockedArg::matchRegex( $unblocked->param, $unblocked->value );
			if ( $blockedRegex->exists() ) {
				$regexMatches[ $unblocked->id ] = $blockedRegex->regex;
			}
		}

		// Query to get Blocked Args and the total.
		$queryBlocked = aioseo()->core->db
			->select( ' b.id,
						b.param,
						b.value,
						b.regex,
						b.hits,
						b.updated' )
			->start( 'aioseo_crawl_cleanup_blocked_args as b' )
			->limit( $limit, $offset );

		if ( ! empty( $searchTerm ) ) {
			// Escape (esc_like) has already been applied.
			$searchTerms = [
				$searchTerm,
				str_replace( '%20', '-', $searchTerm ),
				str_replace( '%20', '+', $searchTerm )
			];

			$comparisons = [
				'b.param',
				'b.value',
				'b.regex',
				'CONCAT(b.param, \'' . $keyValueSeparator . '\', IF(b.value, b.value, \'*\'))'
			];

			$where = '';
			foreach ( $comparisons as $comparison ) {
				foreach ( $searchTerms as $s ) {
					if ( ! empty( $where ) ) {
						$where .= ' OR ';
					}

					$where .= aioseo()->core->db->db->prepare( " $comparison LIKE %s ", '%' . $s . '%' );
				}
			}

			$where = "( $where )";
			$queryBlocked->whereRaw( $where );
		}

		$queryBlocked->orderBy( "$orderByBlocked $orderDir" );

		$rowsBlocked = $queryBlocked->run( false )->result();
		$totalBlocked = $queryBlocked->reset( [ 'limit' ] )->count();

		switch ( $filter ) {
			case 'blocked':
				$total = $totalBlocked;
				$rows = $rowsBlocked;
				break;
			case 'unblocked':
				$total = $totalUnblocked;
				$rows = $rowsUnblocked;
				break;
			default:
				return new \WP_REST_Response( [
					'success' => false
				], 404 );
		}

		foreach ( $rows as $row ) {
			$row->updated = get_date_from_gmt( $row->updated, $dateTimeFormat );
		}

		return new \WP_REST_Response( [
			'success' => true,
			'rows'    => $rows,
			'regex'   => $regexMatches,
			'totals'  => [
				'total' => $total,
				'pages' => 0 === $total ? 1 : ceil( $total / $limit ),
				'page'  => 0 === $offset ? 1 : ( $offset / $limit ) + 1
			],
			'filters' => [
				[
					'slug'   => 'unblocked',
					'name'   => __( 'Unblocked', 'all-in-one-seo-pack' ),
					'count'  => $totalUnblocked,
					'active' => 'unblocked' === $filter
				],
				[
					'slug'   => 'blocked',
					'name'   => __( 'Blocked', 'all-in-one-seo-pack' ),
					'count'  => $totalBlocked,
					'active' => 'blocked' === $filter
				]
			]
		], 200 );
	}

	/**
	 * Set block Arg Query.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function blockArg( $request ) {
		$body      = $request->get_json_params();
		$return    = true;
		$listSaved = [];
		$exists    = [];
		$error     = 0;

		try {
			foreach ( $body as $block ) {
				if ( $block ) {
					$blocked = Models\CrawlCleanupBlockedArg::getByKeyValue( $block['param'], $block['value'] );
					if ( ! $blocked->exists() && ! empty( $block['regex'] ) ) {
						$blocked = Models\CrawlCleanupBlockedArg::getByRegex( $block['regex'] );
					}

					if ( $blocked->exists() ) {
						$exists[] = [
							'param' => $block['param'],
							'value' => $block['value']
						];

						$keyValue = sha1( Models\CrawlCleanupBlockedArg::getKeyValueString( $block['param'], $block['value'] ) );
						if ( ! in_array( $keyValue, $listSaved, true ) ) {
							$return = false;
							$error  = 1;
						}

						continue;
					}

					$blocked = new Models\CrawlCleanupBlockedArg();
					$blocked->set( $block );
					$blocked->save();

					$listSaved[] = $blocked->param_value_hash; // @phpstan-ignore-line
				}
			}
		} catch ( \Throwable $th ) {
			$return = false;
		}

		return new \WP_REST_Response( [
			'success' => $return,
			'error'   => $error,
			'exists'  => $exists
		], 200 );
	}

	/**
	 * Delete Blocked Arg.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteBlocked( $request ) {
		$body = $request->get_json_params();
		$return = true;

		try {
			foreach ( $body as $block ) {
				$blocked = new Models\CrawlCleanupBlockedArg( $block );
				if ( $blocked->exists() ) {
					$blocked->delete();
				}
			}
		} catch ( \Throwable $th ) {
			$return = false;
		}

		return new \WP_REST_Response( [
			'success' => $return
		], 200 );
	}

	/**
	 * Delete Log.
	 *
	 * @since 4.5.8
	 *
	 * @param  \WP_REST_Request  $request The REST Request.
	 * @return \WP_REST_Response          The response.
	 */
	public static function deleteLog( $request ) {
		$body = $request->get_json_params();
		$return = true;

		try {
			foreach ( $body as $block ) {
				$log = new Models\CrawlCleanupLog( $block );
				if ( $log->exists() ) {
					$log->delete();
				}
			}
		} catch ( \Throwable $th ) {
			$return = false;
		}

		return new \WP_REST_Response( [
			'success' => $return
		], 200 );
	}
}