<?php

namespace NewfoldLabs\WP\Module\Data\Listeners;

/**
 * Monitors Yoast events
 */
class Yoast extends Listener {

	/**
	 * Fields to skip when tracking site representation changes
	 *
	 * @var array
	 */
	private $site_representation_skip_fields = array( 'company_logo_id', 'person_logo_id', 'description' );

	/**
	 * Mapping between Yoast site representation option names and Hiive event tracking names
	 *
	 * @var array
	 */
	private $site_representation_map = array(
		'company_or_person'         => 'site_representation',
		'company_name'              => 'organization_name',
		'company_logo'              => 'organization_logo',
		'person_logo'               => 'logo',
		'company_or_person_user_id' => 'name',
		'website_name'              => 'website_name',
	);

	/**
	 * Mapping between Yoast social profiles option names and Hiive event tracking names
	 *
	 * @var array
	 */
	private $social_profiles_map = array(
		'facebook_site'     => 'facebook_profile',
		'twitter_site'      => 'twitter_profile',
		'other_social_urls' => 'other_profiles',
	);

	/**
	 * Register the hooks for the listener
	 *
	 * @return void
	 */
	public function register_hooks() {
		// First time configuration
		add_action( 'wpseo_ftc_post_update_site_representation', array( $this, 'site_representation_updated' ), 10, 3 );
		add_action( 'wpseo_ftc_post_update_social_profiles', array( $this, 'social_profiles_updated' ), 10, 3 );
		add_action( 'wpseo_ftc_post_update_enable_tracking', array( $this, 'tracking_updated' ), 10, 3 );

		// Upgrade database
		add_action( 'wpseo_run_upgrade', array( $this, 'database_upgrade' ) );

		// Updated options
		add_action( 'update_option_wpseo_titles', array( $this, 'option_updated' ), 10, 3 );
		add_action( 'update_option_wpseo_social', array( $this, 'option_updated' ), 10, 3 );
		add_action( 'update_option_wpseo', array( $this, 'option_updated' ), 10, 3 );
		add_action( 'update_option_wpseo_ms', array( $this, 'option_updated' ), 10, 3 );
	}

	/**
	 * The user just updated their site representation
	 *
	 * @param array $new_values The new values for the options related to the site representation
	 * @param array $old_values The old values for the options related to the site representation
	 * @param array $failures   The failures that occurred during the update
	 *
	 * @return void
	 */
	public function site_representation_updated( $new_values, $old_values, $failures ) {
		// All the options are unchanged, opt out
		if ( $new_values === $old_values ) {
			return;
		}

		$mapped_new_values = $this->map_params_names_to_hiive_names( $new_values, $this->site_representation_map, $this->site_representation_skip_fields );
		$mapped_old_values = $this->map_params_names_to_hiive_names( $old_values, $this->site_representation_map, $this->site_representation_skip_fields );
		$mapped_failures   = $this->map_failures_to_hiive_names( $failures, $this->site_representation_map, $this->site_representation_skip_fields );

		foreach ( $mapped_new_values as $key => $value ) {
			$this->maybe_push_site_representation_event( $key, $value, $mapped_old_values[ $key ], \in_array( $key, $mapped_failures ) );
		}
	}

	/**
	 * The user just updated their personal profiles
	 *
	 * @param array $new_values The new values for the options related to the site representation
	 * @param array $old_values The old values for the options related to the site representation
	 * @param array $failures   The failures that occurred during the update
	 *
	 * @return void
	 */
	public function social_profiles_updated( $new_values, $old_values, $failures ) {
		// Yoast stores only twitter username, and $new_values stores the pre-processed values
		if ( strpos( $new_values['twitter_site'], 'twitter.com/' ) !== false ) {
			$new_values['twitter_site'] = ( explode( 'twitter.com/', $new_values['twitter_site'] )[1] );
		}

		// All the options are unchanged, opt out
		if ( $new_values === $old_values ) {
			return;
		}

		// Remove multiple occurences of other_social_urls;
		$cleaned_failures = $this->clean_social_profiles_failures( $failures );

		$mapped_values     = $this->map_params_names_to_hiive_names( $new_values, $this->social_profiles_map );
		$mapped_old_values = $this->map_params_names_to_hiive_names( $old_values, $this->social_profiles_map );
		$mapped_failures   = $this->map_failures_to_hiive_names( $cleaned_failures, $this->social_profiles_map );

		foreach ( $mapped_values as $key => $value ) {
			// The option update failed
			if ( \in_array( $key, $mapped_failures ) ) {
				$this->push( "failed_$key", array( 'category' => 'ftc_personal_profiles' ) );
				return;
			}

			if ( $value !== $mapped_old_values[ $key ] ) {
				$this->maybe_push_social_profiles_event( $key, $value, $mapped_old_values[ $key ], \in_array( $key, $mapped_failures ) );
			}
		}
	}

	/**
	 * The user updated their tracking preferences
	 *
	 * @param string $new_value The new value for the option related to tracking
	 * @param string $old_value The old value for the option related to tracking
	 * @param bool   $failed    Whether the option update failed
	 *
	 * @return void
	 */
	public function tracking_updated( $new_value, $old_value, $failed ) {
		// Option unchanged, opt out
		if ( $new_value === $old_value ) {
			return;
		}

		$failed ? $this->push( 'failed_usage_tracking', array( 'category' => 'ftc_tracking' ) ) : $this->push( 'changed_usage_tracking', array( 'category' => 'ftc_tracking' ) );
	}

	/**
	 * A method used to (maybe) push a site representation-related event to the queue.
	 *
	 * @param string $key       The option key
	 * @param string $value     The new option value
	 * @param string $old_value The old option value
	 * @param bool   $failure   Whether the option update failed
	 *
	 * @return void
	 */
	private function maybe_push_site_representation_event( $key, $value, $old_value, $failure ) {
		$category = 'ftc_site_representation';

		// The option update failed
		if ( $failure ) {
			$this->push( "failed_$key", array( 'category' => $category ) );
			return;
		}

		// The option value changed
		if ( $value !== $old_value ) {
			// The option was set for the first time

			// name is a special case, because it represents the company_or_person_user_id which is initialised to false, and the first time the user saves the site representation step
			// is set either to 0 if the site represents an organisation, or to an integer > 0 if the site represents a person
			if ( 'name' === $key ) {
				if ( false === $old_value && 0 === $value ) {
					return;
				}
			}

			// Again, name is a special case, because if its old value was 0 and a value different that 0 is being received, it means that the user
			// switched from organisation to person, and then the person id is being set.
			// Once the name is assigned an integer > 0, it can never go back to 0, even if the user switches back to organisation
			// ( it "caches" the last user id that was set)
			if ( ( $this->is_param_empty( $old_value ) ) || ( 'name' === $key && 0 === $old_value ) ) {
				$this->push( "set_$key", array( 'category' => $category ) );
				return;
			}

			// The option was updated
			$data = array(
				'category' => $category,
				'data'     => array(
					'label_key' => $key,
					'new_value' => $value,
				),
			);

			$this->push(
				"changed_$key",
				$data
			);
		}
	}

	/**
	 * A method used to (maybe) push a social profile-related event to the queue.
	 *
	 * @param string $key       The option key
	 * @param string $value     The new option value
	 * @param string $old_value The old option value
	 * @param bool   $failure   Whether the option update failed
	 *
	 * @return void
	 */
	private function maybe_push_social_profiles_event( $key, $value, $old_value, $failure ) {
		$category = 'ftc_personal_profiles';

		// The option update failed
		if ( $failure ) {
			$this->push( "failed_$key", array( 'category' => $category ) );
			return;
		}

		// The option value changed
		if ( $value !== $old_value ) {
			if ( 'other_profiles' === $key ) {
				$this->push_other_social_profiles( $key, $value, $old_value, $category );
				return;
			}

			// The option was set for the first time
			if ( $this->is_param_empty( $old_value ) ) {
				$this->push( "set_$key", array( 'category' => $category ) );
				return;
			}

			// The option was updated
			$this->push( "changed_$key", array( 'category' => $category ) );
		}
	}


	/**
	 * A method used to (maybe) push the other_profiles-related event to the queue.
	 *
	 * @param string $key       The option key (other_profiles)
	 * @param array  $new_value The array of new social profiles
	 * @param array  $old_value The array of old social profiles
	 * @param string $category  The category of the event
	 *
	 * @return void
	 */
	private function push_other_social_profiles( $key, $new_value, $old_value, $category ) {
		// The option was set for the first time
		if ( $this->is_param_empty( $old_value ) ) {
			$this->push( "set_$key", array( 'category' => $category ) );
			return;
		}

		$changed_profiles = \array_map(
			function ( $value ) {
				return $this->get_base_url( \wp_unslash( $value ) );
			},
			$new_value
		);

		// The option was updated
		$data = array(
			'category' => $category,
			'data'     => array(
				'label_key' => $key,
				'new_value' => $changed_profiles,
			),
		);

		$this->push( 'changed_other_profiles', $data );
	}

	/**
	 * Maps the param names to the names used for Hiive events tracking.
	 *
	 * @param array $params      The params to map.
	 * @param array $map         The map to use.
	 * @param array $skip_fields The fields to skip.
	 *
	 * @return array The mapped params.
	 */
	private function map_params_names_to_hiive_names( $params, $map, $skip_fields = array() ) {
		$mapped_params = array();

		foreach ( $params as $param_name => $param_value ) {
			if ( in_array( $param_name, $skip_fields, true ) ) {
				continue;
			}

			$new_name                   = $map[ $param_name ];
			$mapped_params[ $new_name ] = $param_value;
		}

		return $mapped_params;
	}

	/**
	 * Maps the names of the params which failed the update to the names used for Hiive events tracking.
	 *
	 * @param array $failures    The params names to map.
	 * @param array $map         The map to use.
	 * @param array $skip_fields The fields to skip.
	 *
	 * @return array The mapped params names.
	 */
	private function map_failures_to_hiive_names( $failures, $map, $skip_fields = array() ) {
		$mapped_failures = array();

		foreach ( $failures as $failed_field_name ) {
			if ( in_array( $failed_field_name, $skip_fields, true ) ) {
				continue;
			}

			$mapped_failures[] = $map[ $failed_field_name ];
		}

		return $mapped_failures;
	}

	/**
	 * Checks whether a param is empty.
	 *
	 * @param mixed $param The param to check.
	 *
	 * @return bool Whether the param is empty.
	 */
	private function is_param_empty( $param ) {
		if ( is_array( $param ) ) {
			return ( count( $param ) === 0 );
		}

		return ( strlen( $param ) === 0 );
	}

	/**
	 * Gets the base url of a given url.
	 *
	 * @param string $url The url.
	 *
	 * @return string The base url.
	 */
	private function get_base_url( $url ) {
		$parts = \parse_url( $url );

		return $parts['scheme'] . '://' . $parts['host'];
	}

	/**
	 * Removes multiple occurences of other_social_urls from the failures array
	 *
	 * @param array $failures The failures array
	 *
	 * @return array The cleaned failures array
	 */
	private function clean_social_profiles_failures( $failures ) {
		$cleaned_failures             = array();
		$other_social_profiles_failed = false;

		foreach ( $failures as $failure ) {
			if ( strpos( $failure, 'other_social_urls' ) === 0 ) {
				$other_social_profiles_failed = true;
				continue;
			}
			$cleaned_failures[] = $failure;
		}

		if ( $other_social_profiles_failed ) {
			$cleaned_failures[] = 'other_social_urls';
		}

		return $cleaned_failures;
	}

	/**
	 * Yoast SEO database upgrade.
	 *
	 * @param string|null $previous_version The previous version of Yoast SEO.
	 *
	 * @return void
	 */
	public function database_upgrade( $previous_version ) {

		if ( ! $previous_version ) {
			$previous_version = '';
		}

		$data = array(
			'category' => 'yoast_event',
			'data'     => array(
				'previous_version' => $previous_version,
				'new_version'      => WPSEO_VERSION,
			),
		);

		$this->push(
			'database_upgrade',
			$data
		);
	}

	/**
	 * Yoast SEO option updated.
	 *
	 * @param mixed  $old_value The old value.
	 * @param mixed  $new_value The new value.
	 * @param string $option    The option name.
	 *
	 * @return void
	 */
	public function option_updated( $old_value, $new_value, $option ) {

		$modified_values = array();

		if ( is_array( $old_value ) && is_array( $new_value ) ) {
			foreach ( $new_value as $key => $value ) {

				if ( ! array_key_exists( $key, $old_value ) ) {
					$modified_values[ $key ] = $value;
					continue;
				}
				$old_val     = $old_value[ $key ];
				$has_changed = ( is_array( $value ) || is_object( $value ) )
					? json_encode( $value ) !== json_encode( $old_val )
					: $value !== $old_val;

				if ( $has_changed ) {
					$modified_values[ $key ] = $value;
				}
			}
		} elseif ( $old_value !== $new_value ) {
			$modified_values = $new_value;
		}

		if ( ! empty( $modified_values ) ) {
			$data = array(
				'category' => 'yoast_event',
				'data'     => $modified_values,
			);
			$this->push(
				$option,
				$data
			);
		}
	}
}
