<?php
/**
 * Defines Visitors REST API
 *
 * @link https://personalizewp.com/
 * @since 3.2.0
 *
 * @package PersonalizeWP
 * @subpackage PersonalizeWP/REST
 */

namespace PersonalizeWP\REST;

use PersonalizeWP\Traits\SingletonTrait;
use PersonalizeWP\Model\Activity;
use PersonalizeWP\Model\Contact;
use PersonalizeWP\Settings;

defined( 'ABSPATH' ) || exit; // Exit if accessed directly.

/**
 * Visitor Controller class
 */
class Visitor_Controller {

	use SingletonTrait;

	/**
	 * Endpoint namespace.
	 *
	 * @since 3.2.0
	 *
	 * @var string
	 */
	protected $namespace;

	/**
	 * Holds Instance of plugin object
	 *
	 * @var PersonalizeWP
	 */
	private $plugin;

	/**
	 * Initialize the class and set its properties.
	 *
	 * @param string $ns Namespace
	 */
	public function __construct( $ns ) {
		$this->namespace = $ns;
		$this->plugin    = \personalizewp();
	}

	/**
	 * Registers the routes for the visitor controller.
	 *
	 * @since 3.2.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			"{$this->namespace}v1",
			'/visitor-data',
			[
				'methods'             => \WP_REST_Server::CREATABLE, // Using POST to combat against possible caching of results
				'callback'            => [ $this, 'visitor_data_request' ],
				// Register our expected args to validate and sanitise.
				'args'                => $this->get_visitor_endpoint_args(),
				'permission_callback' => '__return_true', // nosemgrep: audit.php.wp.security.rest-route.permission-callback.return-true -- Reason: Public API
			]
		);

		register_rest_route(
			"{$this->namespace}v1",
			'/visitor-segments',
			[
				'methods'             => \WP_REST_Server::CREATABLE, // Using POST to combat against possible caching of results
				'callback'            => [ $this, 'visitor_segments_request' ],
				// Register our expected args to validate and sanitise.
				'args'                => $this->get_visitor_segments_endpoint_args(),
				'permission_callback' => '__return_true', // nosemgrep: audit.php.wp.security.rest-route.permission-callback.return-true -- Reason: Public API
			]
		);

		register_rest_route(
			"{$this->namespace}v1",
			'/visitor-activities',
			[
				'methods'             => \WP_REST_Server::CREATABLE, // Using POST to combat against possible caching of results
				'callback'            => [ $this, 'visitor_activities_request' ],
				// Register our expected args to validate and sanitise.
				'args'                => $this->get_visitor_activities_endpoint_args(),
				'permission_callback' => '__return_true', // nosemgrep: audit.php.wp.security.rest-route.permission-callback.return-true -- Reason: Public API
			]
		);
	}

	/**
	 * Retrieves an array of endpoint arguments for the Visitor Data endpoint.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.2.0
	 *
	 * @return array
	 */
	protected function get_visitor_endpoint_args(): array {

		$user_fields_pattern = '^(' .
			join(
				'|',
				// Built in user fields.
				array(
					'firstName',
					'lastName',
					'email',
					'country',
					'userRole',
					'userID',
					'leadScore',
				)
			) .
			// All custom user fields are prefixed with 'custom', and are run through `sanitize_key`.
			// See Settings::get_profile_custom_fields() for details. The first letter
			// (after 'custom') becomes uppercase due to the editor JS, so that is also allowed.
			'|custom[A-Za-z0-9_\-]+' .
			')$';

		$args = array(
			'uid'  => array(
				'type'              => 'string',
				'required'          => true,
				'validate_callback' => [ 'PersonalizeWP\API', 'validate_non_empty_string' ],
				'sanitize_callback' => [ 'PersonalizeWP\API', 'sanitize_arg' ],
			),
			'data' => array(
				'type'     => 'array',
				'required' => true,
				'items'    => array(
					'type'    => 'string',
					// Use pattern rather than enum (of all built-in and custom fields) as custom fields may be
					// referred to, but no longer be registered. enum checking would thus cause an error.
					'pattern' => $user_fields_pattern,
				),
				// Validation/Sanitizing automatic as type/enum/pattern set
			),
		);

		return $args;
	}

	/**
	 * Retrieves an array of endpoint arguments for the Visitor Segments endpoint.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.9.0
	 *
	 * @return array
	 */
	protected function get_visitor_segments_endpoint_args(): array {
		$args = array(
			'uid' => array(
				'type'              => 'string',
				'required'          => true,
				'validate_callback' => [ 'PersonalizeWP\API', 'validate_non_empty_string' ],
				'sanitize_callback' => [ 'PersonalizeWP\API', 'sanitize_arg' ],
			),
		);

		return $args;
	}

	/**
	 * Retrieves an array of endpoint arguments for the Visitor Activities endpoint.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.9.0
	 *
	 * @return array
	 */
	protected function get_visitor_activities_endpoint_args(): array {
		$args = array(
			'uid'     => array(
				'type'              => 'string',
				'required'          => true,
				'validate_callback' => [ 'PersonalizeWP\API', 'validate_non_empty_string' ],
				'sanitize_callback' => [ 'PersonalizeWP\API', 'sanitize_arg' ],
			),
			'filters' => array(
				'type'              => 'object',
				'required'          => false,
				'validate_callback' => function ( $value ) {
					$allowed_filters_keys = [
						'id',
						'activity_type',
						'url',
						'object_id',
						'object_type',
					];
					foreach ( $value as $key => $val ) {
						if ( ! in_array( $key, $allowed_filters_keys, true ) ) {
							return false;
						}
					}
					if ( isset( $value['id'] ) && intval( $value['id'] ) <= 0 ) {
						return false;
					}
					if ( isset( $value['object_id'] ) && intval( $value['object_id'] ) <= 0 ) {
						return false;
					}

					return is_array( $value );
				},
				'sanitize_callback' => function ( $value ) {
					if ( isset( $value['id'] ) ) {
						$value['id'] = absint( $value['id'] );
					}
					if ( isset( $value['activity_type'] ) ) {
						$value['activity_type'] = sanitize_key( $value['activity_type'] );
					}
					if ( isset( $value['url'] ) ) {
						$value['url'] = sanitize_url( $value['url'] );
					}
					if ( isset( $value['object_id'] ) ) {
						$value['object_id'] = absint( $value['object_id'] );
					}
					if ( isset( $value['object_type'] ) ) {
						$value['object_type'] = sanitize_key( $value['object_type'] );
					}

					return $value;
				},
			),
		);

		return $args;
	}

	/**
	 * Handles visitor profile data request. Returns specific meta data for a specified visitor.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.2.0
	 *
	 * @param \WP_REST_Request $request api request
	 *
	 * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response
	 */
	public function visitor_data_request( \WP_REST_Request $request ) {

		$params = $request->get_params();

		$return  = array();
		$contact = Contact::find_by_uid( $params['uid'] );
		if ( $contact ) {
			foreach ( $params['data'] as $data ) {
				$key = $data;

				// Translate the camelCase $data format to snake_case meta key format
				$data = strtolower( preg_replace( '/(?<!^)[A-Z]+/', '_$0', $data ) );

				switch ( $data ) {
					case 'first_name':
					case 'last_name':
					case 'email':
					case 'lead_score':
						$return[ $key ] = $contact->$data;
						break;

					case 'country':
						$return[ $key ] = $contact->get_metadata( 'address_country', true );
						break;

					case 'user_role':
					case 'user_id':
						// Add prefix to name of data.
						$return[ $key ] = $contact->get_metadata( "wp_{$data}", true );
						break;

					// Any custom registered visitor fields.
					default:
						// Validate custom field request against known fields. Otherwise anything *could* be requested.
						// This also ensures that previously registered fields will no longer embed data once unregistered.
						if ( ! isset( $known_custom_fields ) ) {
							// Lazy load during first request.
							$known_custom_fields = Settings::instance()->get_profile_custom_fields();
						}
						if ( isset( $known_custom_fields[ $data ] ) ) {
							$return[ $key ] = $contact->get_metadata( $data, true );
						} else {
							// Hardcoded return empty.
							$return[ $key ] = '';
						}
						break;
				}
			}
			// Sort alphabetically.
			ksort( $return );
		}

		$response = array(
			'success' => ! empty( $contact ),
			'data'    => $return,
		);

		return rest_ensure_response( $response );
	}

	/**
	 * Handles visitor segments request. Returns all segments for a specified visitor.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.9.0
	 *
	 * @param \WP_REST_Request $request API request
	 *
	 * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response
	 */
	public function visitor_segments_request( \WP_REST_Request $request ) {

		$params  = $request->get_params();
		$return  = array();
		$contact = Contact::find_by_uid( $params['uid'] );
		if ( $contact ) {
			$segments = $contact->segments;
			foreach ( $segments as $segment ) {
				if ( '1' === $segment->is_active ) {
					$return[] = array(
						'id'      => $segment->ID,
						'name'    => $segment->name,
						'type'    => $segment->type,
						'created' => $segment->created,
					);
				}
			}
		}

		$response = array(
			'success' => ! empty( $return ),
			'data'    => $return,
		);

		return rest_ensure_response( $response );
	}

	/**
	 * Handles visitor activities request. Returns all activities for a specified visitor.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.9.0
	 *
	 * @param \WP_REST_Request $request api request
	 *
	 * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response
	 */
	public function visitor_activities_request( \WP_REST_Request $request ) {

		$params = $request->get_params();

		$return = array();

		$contact = Contact::find_by_uid( $params['uid'] );
		if ( $contact ) {
			$activities = Activity::find_by_contact_id( $contact->ID );
			if ( $activities ) {
				if ( ! empty( $params['filters'] ) ) {
					$activities = array_filter(
						$activities,
						function ( $activity ) use ( $params ) {
							foreach ( $params['filters'] as $filter_key => $filter_val ) {
								if ( 'id' === $filter_key ) {
									$filter_key = 'ID';
								}
								if ( $activity[ $filter_key ] !== $filter_val ) {
									return false;
								}
							}
							return true;
						}
					);
				}

				foreach ( $activities as $activity ) {
					$return[] = [
						'id'            => $activity['ID'],
						'activity_type' => $activity['activity_type'],
						'url'           => $activity['url'],
						'object_id'     => $activity['object_id'] ? $activity['object_id'] : '',
						'object_type'   => $activity['object_type'],
						'lead_score'    => $activity['lead_score'],
						'created'       => $activity['created'],
					];
				}
			}
		}

		$response = [
			'success' => ! empty( $return ),
			'data'    => $return,
		];
		return rest_ensure_response( $response );
	}
}
