<?php

namespace Yoast\WP\SEO\Actions\Wincher;

use Exception;
use WP_Post;
use WPSEO_Meta;
use WPSEO_Utils;
use Yoast\WP\SEO\Config\Wincher_Client;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Class Wincher_Keyphrases_Action
 */
class Wincher_Keyphrases_Action {

	/**
	 * The Wincher keyphrase URL for bulk addition.
	 *
	 * @var string
	 */
	const KEYPHRASES_ADD_URL = 'https://api.wincher.com/beta/websites/%s/keywords/bulk';

	/**
	 * The Wincher tracked keyphrase retrieval URL.
	 *
	 * @var string
	 */
	const KEYPHRASES_URL = 'https://api.wincher.com/beta/yoast/%s';

	/**
	 * The Wincher delete tracked keyphrase URL.
	 *
	 * @var string
	 */
	const KEYPHRASE_DELETE_URL = 'https://api.wincher.com/beta/websites/%s/keywords/%s';

	/**
	 * The Wincher_Client instance.
	 *
	 * @var Wincher_Client
	 */
	protected $client;

	/**
	 * The Options_Helper instance.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * The Indexable_Repository instance.
	 *
	 * @var Indexable_Repository
	 */
	protected $indexable_repository;

	/**
	 * Wincher_Keyphrases_Action constructor.
	 *
	 * @param Wincher_Client       $client               The API client.
	 * @param Options_Helper       $options_helper       The options helper.
	 * @param Indexable_Repository $indexable_repository The indexables repository.
	 */
	public function __construct(
		Wincher_Client $client,
		Options_Helper $options_helper,
		Indexable_Repository $indexable_repository
	) {
		$this->client               = $client;
		$this->options_helper       = $options_helper;
		$this->indexable_repository = $indexable_repository;
	}

	/**
	 * Sends the tracking API request for one or more keyphrases.
	 *
	 * @param string|array $keyphrases One or more keyphrases that should be tracked.
	 * @param Object       $limits     The limits API call response data.
	 *
	 * @return Object The reponse object.
	 */
	public function track_keyphrases( $keyphrases, $limits ) {
		try {
			$endpoint = \sprintf(
				self::KEYPHRASES_ADD_URL,
				$this->options_helper->get( 'wincher_website_id' )
			);

			// Enforce arrrays to ensure a consistent way of preparing the request.
			if ( ! \is_array( $keyphrases ) ) {
				$keyphrases = [ $keyphrases ];
			}

			// Calculate if the user would exceed their limit.
			// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- To ensure JS code style, this can be ignored.
			if ( ! $limits->canTrack || $this->would_exceed_limits( $keyphrases, $limits ) ) {
				$response = [
					'limit'  => $limits->limit,
					'error'  => 'Account limit exceeded',
					'status' => 400,
				];

				return $this->to_result_object( $response );
			}

			$formatted_keyphrases = \array_values(
				\array_map(
					static function ( $keyphrase ) {
						return [
							'keyword' => $keyphrase,
							'groups'  => [],
						];
					},
					$keyphrases
				)
			);

			$results = $this->client->post( $endpoint, WPSEO_Utils::format_json_encode( $formatted_keyphrases ) );

			if ( ! \array_key_exists( 'data', $results ) ) {
				return $this->to_result_object( $results );
			}

			// The endpoint returns a lot of stuff that we don't want/need.
			$results['data'] = \array_map(
				static function( $keyphrase ) {
					return [
						'id'         => $keyphrase['id'],
						'keyword'    => $keyphrase['keyword'],
					];
				},
				$results['data']
			);

			$results['data'] = \array_combine(
				\array_column( $results['data'], 'keyword' ),
				\array_values( $results['data'] )
			);

			return $this->to_result_object( $results );
		} catch ( Exception $e ) {
			return (object) [
				'error'  => $e->getMessage(),
				'status' => $e->getCode(),
			];
		}
	}

	/**
	 * Sends an untrack request for the passed keyword ID.
	 *
	 * @param int $keyphrase_id The ID of the keyphrase to untrack.
	 *
	 * @return object The response object.
	 */
	public function untrack_keyphrase( $keyphrase_id ) {
		try {
			$endpoint = \sprintf(
				self::KEYPHRASE_DELETE_URL,
				$this->options_helper->get( 'wincher_website_id' ),
				$keyphrase_id
			);

			$this->client->delete( $endpoint );

			return (object) [
				'status' => 200,
			];
		} catch ( Exception $e ) {
			return (object) [
				'error'  => $e->getMessage(),
				'status' => $e->getCode(),
			];
		}
	}

	/**
	 * Gets the keyphrase data for the passed keyphrases.
	 * Retrieves all available data if no keyphrases are provided.
	 *
	 * @param array|null  $used_keyphrases The currently used keyphrases. Optional.
	 * @param string|null $permalink       The current permalink. Optional.
	 *
	 * @return object The keyphrase chart data.
	 */
	public function get_tracked_keyphrases( $used_keyphrases = null, $permalink = null ) {
		try {
			if ( $used_keyphrases === null ) {
				$used_keyphrases = $this->collect_all_keyphrases();
			}

			// If we still have no keyphrases the API will return an error, so
			// don't even bother sending a request.
			if ( empty( $used_keyphrases ) ) {
				return $this->to_result_object(
					[
						'data'   => [],
						'status' => 200,
					]
				);
			}

			$endpoint = \sprintf(
				self::KEYPHRASES_URL,
				$this->options_helper->get( 'wincher_website_id' )
			);

			$results = $this->client->post(
				$endpoint,
				WPSEO_Utils::format_json_encode(
					[
						'keywords' => $used_keyphrases,
						'url'      => $permalink,
					]
				),
				[
					'timeout' => 60,
				]
			);

			if ( ! \array_key_exists( 'data', $results ) ) {
				return $this->to_result_object( $results );
			}

			$results['data'] = $this->filter_results_by_used_keyphrases( $results['data'], $used_keyphrases );

			// Extract the positional data and assign it to the keyphrase.
			$results['data'] = \array_combine(
				\array_column( $results['data'], 'keyword' ),
				\array_values( $results['data'] )
			);

			return $this->to_result_object( $results );
		} catch ( Exception $e ) {
			return (object) [
				'error'  => $e->getMessage(),
				'status' => $e->getCode(),
			];
		}
	}

	/**
	 * Collects the keyphrases associated with the post.
	 *
	 * @param WP_Post $post The post object.
	 *
	 * @return array The keyphrases.
	 */
	public function collect_keyphrases_from_post( $post ) {
		$keyphrases        = [];
		$primary_keyphrase = $this->indexable_repository
			->query()
			->select( 'primary_focus_keyword' )
			->where( 'object_id', $post->ID )
			->find_one();

		if ( $primary_keyphrase ) {
			$keyphrases[] = $primary_keyphrase->primary_focus_keyword;
		}

		if ( \YoastSEO()->helpers->product->is_premium() ) {
			$additional_keywords = \json_decode( WPSEO_Meta::get_value( 'focuskeywords', $post->ID ), true );

			$keyphrases = \array_merge( $keyphrases, $additional_keywords );
		}

		return $keyphrases;
	}

	/**
	 * Collects all keyphrases known to Yoast.
	 *
	 * @return array
	 */
	protected function collect_all_keyphrases() {
		global $wpdb;

		// Collect primary keyphrases first.
		$keyphrases = \array_column(
			$this->indexable_repository
				->query()
				->select( 'primary_focus_keyword' )
				->where_not_null( 'primary_focus_keyword' )
				->where( 'object_type', 'post' )
				->where_not_equal( 'post_status', 'trash' )
				->distinct()
				->find_array(),
			'primary_focus_keyword'
		);

		if ( \YoastSEO()->helpers->product->is_premium() ) {
			// Collect all related keyphrases.
			$meta_key = WPSEO_Meta::$meta_prefix . 'focuskeywords';

			$query = "
				SELECT meta_value
				FROM $wpdb->postmeta
				JOIN $wpdb->posts ON {$wpdb->posts}.id = {$wpdb->postmeta}.post_id
				WHERE meta_key = '$meta_key' AND post_status != 'trash'
			";

			// phpcs:ignore -- ignoring since it's complaining about not using prepare when it's perfectly safe here.
			$results = $wpdb->get_results( $query );

			if ( $results ) {
				foreach ( $results as $row ) {
					$additional_keywords = \json_decode( $row->meta_value, true );
					if ( $additional_keywords !== null ) {
						$additional_keywords = \array_column( $additional_keywords, 'keyword' );
						$keyphrases          = \array_merge( $keyphrases, $additional_keywords );
					}
				}
			}
		}

		// Filter out empty entries.
		return \array_filter( $keyphrases );
	}

	/**
	 * Filters the results based on the passed keyphrases.
	 *
	 * @param array $results         The results to filter.
	 * @param array $used_keyphrases The used keyphrases.
	 *
	 * @return array The filtered results.
	 */
	protected function filter_results_by_used_keyphrases( $results, $used_keyphrases ) {
		return \array_filter(
			$results,
			static function( $result ) use ( $used_keyphrases ) {
				return \in_array( $result['keyword'], \array_map( 'strtolower', $used_keyphrases ), true );
			}
		);
	}

	/**
	 * Determines whether the amount of keyphrases would mean the user exceeds their account limits.
	 *
	 * @param string|array $keyphrases The keyphrases to be added.
	 * @param object       $limits     The current account limits.
	 *
	 * @return bool Whether the limit is exceeded.
	 */
	protected function would_exceed_limits( $keyphrases, $limits ) {
		if ( ! \is_array( $keyphrases ) ) {
			$keyphrases = [ $keyphrases ];
		}

		if ( \is_null( $limits->limit ) ) {
			return false;
		}

		return ( \count( $keyphrases ) + $limits->usage ) > $limits->limit;
	}

	/**
	 * Converts the passed dataset to an object.
	 *
	 * @param array $result The result dataset to convert to an object.
	 *
	 * @return object The result object.
	 */
	protected function to_result_object( $result ) {
		if ( \array_key_exists( 'data', $result ) ) {
			$result['results'] = $result['data'];

			unset( $result['data'] );
		}

		if ( \array_key_exists( 'message', $result ) ) {
			$result['error'] = $result['message'];

			unset( $result['message'] );
		}

		return (object) $result;
	}
}
