<?php
/**
 * Defines ScoringRules page functionality
 *
 * @link       https://personalizewp.com/
 * @since      3.2.0 Merged into PWP
 * @since      1.1.0
 *
 * @package    PersonalizeWP
 * @subpackage PersonalizeWP/Admin
 */

namespace PersonalizeWP\Admin;

use PersonalizeWP\Admin\Admin;
use PersonalizeWP\Admin\Admin_Base_Page;
use PersonalizeWP\Model\Scoring_Rule;

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

/**
 * Defines ScoringRules page functionality
 */
class PageScoringRules extends Admin_Base_Page {

	/**
	 * Slug for Lead Scoring Rules admin page
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	const SLUG = 'personalizewp/scoringrules';

	/**
	 * URL for main Lead Score Rules admin page (listing)
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public static $index_url = 'admin.php?page=' . self::SLUG;

	/**
	 * URL for create scoring rule page
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public static $create_url = 'admin.php?page=' . self::SLUG . '&' . Admin::URL_ACTION . '=create';

	/**
	 * URL for edit scoring rule page
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public static $edit_url = 'admin.php?page=' . self::SLUG . '&' . Admin::URL_ACTION . '=edit&id=';

	/**
	 * URL for deleting a scoring rule
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public static $delete_url = 'admin.php?page=' . self::SLUG . '&' . Admin::URL_ACTION . '=delete&id=';

	/**
	 * Store the path for Segments templates
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	public $template_path;

	/**
	 * Used to store the current page from the URL, shared between process/route actions
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	protected $current_page;

	/**
	 * Used to store the currently processing form data, shared between process/route actions
	 *
	 * @since 1.1.0
	 *
	 * @var array
	 */
	protected $form_post = [];

	/**
	 * Constructor
	 *
	 * @since 1.1.0
	 */
	protected function __construct() {
		parent::__construct();

		$this->page_title    = __( 'Lead Scoring', 'personalizewp' );
		$this->template_path = plugin_dir_path( __DIR__ ) . 'admin/partials/scoring-rules/';

		$this->current_action = isset( $_REQUEST[ Admin::URL_ACTION ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ Admin::URL_ACTION ] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: Form verified within each process
		$this->current_page   = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: Form verified within each process

		add_action( 'admin_init', [ $this, 'process' ] );
		add_filter( 'set-screen-option', [ $this, 'set_screen_options' ], 10, 3 );
	}

	/**
	 * Display the Dashboard screen options.
	 *
	 * @since 1.1.0
	 */
	public function dashboard_screen_options() {
		$screen = get_current_screen();

		$is_screen = is_object( $screen ) && str_ends_with( $screen->id, self::SLUG );
		if ( ! $is_screen ) {
			return;
		}
		// Re-trigger core WP, as admins' call runs way too early IMHO.
		set_screen_options();

		$args = array(
			'label'   => __( 'Number of Scoring rules per page:', 'personalizewp' ),
			'default' => 20,
			'option'  => 'edit_pwp_scoring_rules_per_page',
		);
		add_screen_option( 'per_page', $args );
	}

	/**
	 * Store the screen options.
	 *
	 * @since 1.1.0
	 *
	 * @param mixed  $screen_option The value to save instead of the option value.
	 *                              Default false (to skip saving the current option).
	 * @param string $option        The option name.
	 * @param int    $value         The option value.
	 */
	public function set_screen_options( $screen_option, $option, $value ) {
		if ( 'edit_pwp_scoring_rules_per_page' === $option ) {
			return $value;
		}
		return $screen_option;
	}

	/**
	 * Process the current Lead Score request, typically POST/GET, followed by a redirect.
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function process() {
		// Double check for user caps before any possible processing of data.
		if ( ! current_user_can( Admin::CAP ) ) {
			return;
		}

		if ( self::SLUG !== $this->current_page ) {
			return;
		}

		$score_rule_id = (int) isset( $_REQUEST['id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['id'] ) ) : false;

		if ( ! empty( $_POST['pwp_form'] ) ) {
			// POST specific actions
			switch ( $this->current_action ) {
				case 'create':
					check_admin_referer( 'create-scoring-rule' ); // Immediately dies if failing.

					$this->form_post = filter_input( INPUT_POST, 'pwp_form', \FILTER_CALLBACK, [ 'options' => 'sanitize_text_field' ] );
					$this->create_action( $this->form_post );
					break;

				case 'edit':
					if ( empty( $score_rule_id ) ) {
						wp_safe_redirect( admin_url( self::$index_url ) );
						die();
					}
					check_admin_referer( 'edit-scoring-rule-' . $score_rule_id ); // Immediately dies on failing.

					$this->form_post = filter_input( INPUT_POST, 'pwp_form', \FILTER_CALLBACK, [ 'options' => 'sanitize_text_field' ] );
					$this->edit_action( $score_rule_id, $this->form_post );
					break;
			}
		}

		// GET Specific actions
		switch ( $this->current_action ) {
			case 'delete':
				if ( empty( $score_rule_id ) ) {
					wp_safe_redirect( admin_url( self::$index_url ) );
					die();
				}
				check_admin_referer( 'delete-scoring-rule-' . $score_rule_id ); // Immediately dies on failing.

				$this->delete_action( $score_rule_id );
				break;
		}
	}

	/**
	 * Route to the correct screen
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	public function route() {
		// Double check for user caps before any possible processing of data.
		if ( ! current_user_can( Admin::CAP ) ) {
			return;
		}

		if ( self::SLUG !== $this->current_page ) {
			return;
		}

		$score_rule_id = (int) isset( $_REQUEST['id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['id'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Not used to change data

		switch ( $this->current_action ) {
			case 'create':
				$this->create_screen( $this->form_post );
				break;

			case 'edit':
				if ( empty( $score_rule_id ) ) {
					wp_safe_redirect( admin_url( self::$index_url ) );
					die();
				}

				$this->edit_screen( $score_rule_id, $this->form_post );
				break;

			case 'index':
			default:
				$this->index_screen();
				break;
		}
	}

	/**
	 * Displays Lead Scores listing screen
	 *
	 * @since 1.1.0
	 *
	 * @return void
	 */
	protected function index_screen() {

		$status_message = '';
		// Trigger messaging from actions.
		if ( isset( $_REQUEST['created'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this is only displaying a message with no dynamic values.
			$status_message = __( 'Scoring rule created', 'personalizewp' );
		}
		if ( isset( $_REQUEST['updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this is only displaying a message with no dynamic values.
			$status_message = __( 'Scoring rule updated', 'personalizewp' );
		}
		if ( isset( $_REQUEST['deleted'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this is only displaying a message with no dynamic values.
			$status_message = __( 'Scoring rule deleted', 'personalizewp' );
		}
		if ( $status_message ) {
			wp_admin_notice(
				'<span class="dashicons dashicons-yes-alt"></span>' . $status_message,
				array(
					'type'        => 'success',
					'id'          => 'message',
					'dismissible' => true,
				)
			);
		}

		$this->display_header();
		require_once $this->template_path . 'index.php';
		$this->display_footer();
	}

	/**
	 * Display Lead Score Rule creation screen
	 *
	 * @since 1.1.0
	 *
	 * @param array $data Form data
	 *
	 * @return void
	 */
	protected function create_screen( $data = [] ) {

		// Mock an empty Scoring Rule to start creation, or fill with existing form data
		if ( ! empty( $data ) ) {
			unset( $data['ID'] );
		}
		$score_rule = new Scoring_Rule( $data );

		$this->display_header();
		require_once $this->template_path . 'create.php';
		$this->display_footer();
	}

	/**
	 * Display Lead Score Rule view/edit screen
	 *
	 * @since 1.1.0
	 *
	 * @param int   $score_rule_id Scoring Rule ID
	 * @param array $data          Form data
	 *
	 * @return void
	 */
	protected function edit_screen( $score_rule_id, $data = [] ) {

		$score_rule = Scoring_Rule::find( $score_rule_id );

		if ( ! $score_rule ) {
			$this->showError( __( 'You attempted to edit an item that does not exist. Perhaps it was deleted?', 'personalizewp' ) );
		}

		// Re-populate with changes if set
		if ( ! empty( $data ) ) {
			$data['ID'] = $score_rule_id;
			$score_rule = new Scoring_Rule( $data );
		}

		$this->display_header();
		require_once $this->template_path . 'edit.php';
		$this->display_footer();
	}

	/**
	 * Create Lead Score rule
	 *
	 * @since 1.1.0
	 *
	 * @param array $data Form data
	 *
	 * @return void
	 */
	protected function create_action( $data = [] ) {

		$valid_data = $this->validate_sanitize( $data );
		if ( false !== $valid_data ) {
			$score_rule = new Scoring_Rule( $valid_data );
			$score_rule->save();

			wp_safe_redirect( add_query_arg( array( 'created' => 1 ), admin_url( self::$index_url ) ) );
			exit;
		}
	}

	/**
	 * Save Scoring Rule changes
	 *
	 * @since 1.1.0
	 *
	 * @param int   $score_rule_id Scoring Rule ID
	 * @param array $data          Form data
	 *
	 * @return void
	 */
	protected function edit_action( $score_rule_id, $data = [] ) {

		$score_rule = Scoring_Rule::find( $score_rule_id );

		if ( ! $score_rule ) {
			// Replace with wp_die();
			$this->showError( __( 'You attempted to edit an item that does not exist. Perhaps it was deleted?', 'personalizewp' ) );
		}

		$valid_data = $this->validate_sanitize( $data, $score_rule );
		if ( false !== $valid_data ) {
			$score_rule->update( $valid_data );

			wp_safe_redirect( add_query_arg( array( 'updated' => 1 ), admin_url( self::$index_url ) ) );
			exit;
		}
	}

	/**
	 * Delete Scoring Rule
	 *
	 * @since 1.1.0
	 *
	 * @param int $score_rule_id Scoring Rule ID
	 *
	 * @return void
	 */
	protected function delete_action( $score_rule_id ) {

		// Deleting the Segment automatically deletes the Contacts relationships
		$result = Scoring_Rule::delete( $score_rule_id );
		if ( ! $result ) {
			// Nothing to do
			wp_safe_redirect( admin_url( self::$index_url ) );
			exit;
		}

		wp_safe_redirect( add_query_arg( array( 'deleted' => 1 ), admin_url( self::$index_url ) ) );
		exit;
	}

	/**
	 * Validate submitted form data for Scoring rules
	 *
	 * Sets validation errors on missing data, and sets up the 'conditions' for storing to the db.
	 *
	 * @todo Share this with Segment validate_sanitize() as shares so much code?
	 *
	 * @since 1.1.0
	 *
	 * @param  array              $data       Form data
	 * @param  Scoring_Rule|false $score_rule Existing Scoring_Rule if editing
	 *
	 * @uses addValidationError()
	 *
	 * @return array|false  Return sanitized data if valid, false otherwise
	 */
	protected function validate_sanitize( $data, $score_rule = false ) {

		$invalid    = false;
		$valid_data = [];

		if ( empty( $data ) ) {
			$this->addValidationError( 'form', __( 'Form is not valid', 'personalizewp' ) );
			return false;
		}

		if ( $score_rule instanceof Scoring_Rule &&
			( empty( $data['id'] ) || $score_rule->ID !== (int) $data['id'] )
			) {
			$this->addValidationError( 'form', __( 'ID is required to update a scoring rule', 'personalizewp' ) );
			$invalid = true;
		} elseif ( $score_rule instanceof Scoring_Rule && $score_rule->ID === (int) $data['id'] ) {
			$valid_data['id'] = $score_rule->ID;
		}

		if ( empty( $data['name'] ) ) {
			$this->addValidationError( 'name', __( 'Please enter a name', 'personalizewp' ) );
			$invalid = true;
		} else {
			$valid_data['name'] = $data['name'];
		}

		if ( empty( $data['type'] ) ) {
			$this->addValidationError( 'type', __( 'Please select a type', 'personalizewp' ) );
			$invalid = true;
			// Immediately return as type is so fundamental.
			return false;
		} else {
			$valid_data['type'] = $data['type'];
		}

		// This also checks for a zero value. We allow positive and negative.
		if ( empty( $data['lead_score'] ) ) {
			$this->addValidationError( 'lead_score', __( 'Please set a numeric score value', 'personalizewp' ) );
			$invalid = true;
			// Immediately return as score is so fundamental.
			return false;
		} else {
			$valid_data['lead_score'] = $data['lead_score'];
		}

		$valid_operators        = [ 'ANY', 'ALL' ];
		$is_valid_operator      = ( ! empty( $data['operator'] ) && in_array( $data['operator'], $valid_operators, true ) );
		$valid_data['operator'] = $is_valid_operator ? $data['operator'] : Scoring_Rule::$default_operator;

		// Clear out/setup conditions to store
		$conditions = '';

		// Validate/sanitize all Conditions
		switch ( $data['type'] ) {
			case 'product':
				if ( ! empty( $data['product'] ) ) {
					// Sanitize/Validate product/categories.
					$conditions      = [];
					$data['product'] = array_map( 'strtolower', $data['product'] );
					foreach ( $data['product'] as $i => $row ) {
						if ( empty( $row ) ) {
							$this->addValidationError( 'conditions', __( 'A valid product or product category needs to be selected', 'personalizewp' ) );
							$invalid = true;
						}
						if ( ! str_contains( $row, ':' ) ) {
							continue;
						}
						// Split into "[plugin]:[type]:[id]" e.g. "woocommerce:product:245";
						$rowdata = array_map( 'sanitize_text_field', explode( ':', $row, 3 ) );
						if ( 3 !== count( $rowdata ) ) {
							continue;
						}
						$conditions[ $rowdata[0] ][ $rowdata[1] ][] = (int) $rowdata[2];
					}
				}
				break;

			case 'form':
				// Sanitize/Validate forms
				if ( ! empty( $data['form'] ) ) {
					$conditions = [];
					foreach ( $data['form'] as $i => $row ) {
						$valid_form = false;
						$form_type  = '';
						$form_id    = 0;
						if ( str_contains( $row, ':' ) ) {
							list( $form_type, $form_id ) = explode( ':', $row );
							$form_id                     = (int) $form_id;
							$form_type                   = strtolower( $form_type );
						}

						// Check each supported Form plugin. Each have their own API.
						switch ( $form_type ) {
							case 'gravityforms':
								// Validate that the GF form exists.
								if ( class_exists( '\GFAPI' ) && false !== \GFAPI::get_form( $form_id ) ) {
									$conditions[ $form_id ] = $form_type;
									$valid_form             = true;
								}
								break;
							case 'ninjaforms':
								// Validate that the NF form exists.
								if ( class_exists( '\Ninja_Forms' ) && false !== \Ninja_Forms()->form( $form_id )->get() ) {
									$conditions[ $form_id ] = $form_type;
									$valid_form             = true;
								}
								break;
							case 'wsform':
								// Validate that the WSF form exists.
								if ( class_exists( '\WS_Form' ) && false !== wsf_form_get_label_by_id( $form_id ) ) {
									$conditions[ $form_id ] = $form_type;
									$valid_form             = true;
								}
								break;
						}

						if ( ! $valid_form ) {
							$this->addValidationError( 'conditions', __( 'A valid form needs to be selected', 'personalizewp' ) );
							$invalid = true;
						}
					}
				}
				break;

			case 'page':
				// Clear out empty values.
				$data['page']['id']  = isset( $data['page']['id'] ) ? array_filter( $data['page']['id'] ) : [];
				$data['page']['url'] = isset( $data['page']['url'] ) ? array_filter( $data['page']['url'] ) : [];
				if ( empty( $data['page']['id'] ) && empty( $data['page']['url'] ) ) {
					$this->addValidationError( 'conditions', __( 'A valid page needs to be selected, or a URL needs entering', 'personalizewp' ) );
					$invalid = true;
				} else {
					// Sanitize/Validate Pages
					$conditions = [];
					// ID and/or URL, we prioritise the ID
					foreach ( $data['page']['id'] as $entry ) {
						$id = (int) $entry;
						if ( empty( $id ) ) {
							$this->addValidationError( 'conditions', __( 'A valid page needs to be selected, or a URL needs entering', 'personalizewp' ) );
							$invalid = true;
						} else {
							$conditions[] = $id;
						}
					}
					foreach ( $data['page']['url'] as $i => $row ) {
						$is_regex = ! empty( $data['page']['regex'][ $i ] );
						// Only sanitise non-regex URLs, otherwise regex destroyed.
						if ( ! $is_regex ) {
							$url = sanitize_url( $url );
						}
						if ( empty( $url ) ) {
							$this->addValidationError( 'conditions', __( 'A valid page needs to be selected, or a URL needs entering', 'personalizewp' ) );
							$invalid = true;
						} else {
							// Mark as either straight URL or raw regex.
							$conditions[ $url ] = $is_regex ? 'regex' : 'url';
						}
					}
				}
				break;

		}
		// Encode, but only if required.
		if ( is_array( $conditions ) ) {
			$valid_data['conditions'] = wp_json_encode( $conditions );
		} else {
			$valid_data['conditions'] = $conditions;
		}

		return ! $invalid ? $valid_data : false;
	}

	/**
	 * Returns array of Scoring Rule types
	 *
	 * @since 1.1.0
	 *
	 * @return array
	 */
	public static function get_scoring_rule_types() {

		$types = [
			'product' => __( 'Purchased Product', 'personalizewp' ),
			'form'    => __( 'Submitted Form', 'personalizewp' ),
			'page'    => __( 'Visited Page', 'personalizewp' ),
		];

		/**
		 * Filters the Scoring rule types
		 *
		 * @since 1.1.0
		 *
		 * @param array $types Scoring rule types
		 */
		$types = apply_filters( 'personalizewp_scoring_rule_types', $types );

		// Sort Scoring Rule types alphabetically, maintaining the keys.
		asort( $types );

		return $types;
	}

	/**
	 * Converts Scoring rule type into translated string.
	 *
	 * @since 1.1.0
	 *
	 * @param string $type Scoring rule type
	 *
	 * @return string
	 */
	public static function translate_type( $type ) {

		$types = self::get_scoring_rule_types();
		if ( array_key_exists( $type, $types ) ) {
			$label = $types[ $type ];
		} else {
			$label = __( 'Unknown', 'personalizewp' );
		}

		/**
		 * Filters the Scoring rule type label
		 *
		 * @since 1.1.0
		 *
		 * @param string $label  Processed label
		 * @param string $type   Segment type
		 */
		return apply_filters( 'personalizewp_scoring_rule_type_label', $label, $type );
	}

	/**
	 * Converts type string into translated string.
	 *
	 * @since 1.1.0
	 *
	 * @param mixed  $conditions Segment Conditions
	 * @param string $type       Type of Segment
	 *
	 * @return string
	 */
	public static function translate_conditions( $conditions, $type ) {

		$label = '';
		switch ( $type ) {
			case 'form':
				if ( empty( $conditions ) ) {
					$label = __( 'Submitted any form', 'personalizewp' );
				} else {
					$titles = [];
					$forms  = json_decode( $conditions, true );
					foreach ( $forms as $form => $opts ) {
						$titles[] = Utils::get_form_title( (int) $form, $opts );
					}
					if ( ! empty( $titles ) ) {
						$label = sprintf(
							/* translators: 1: Plural list of forms. */
							_n( 'Submitted \'%s\'', 'Submitted \'%s\'', count( $titles ), 'personalizewp' ),
							implode( "', '", array_filter( $titles ) )
						);
					}
				}
				break;

			case 'product':
				if ( empty( $conditions ) ) {
					$label = __( 'Purchased Product...', 'personalizewp' );
				} else {
					$titles   = [];
					$products = json_decode( $conditions, true );
					foreach ( $products as $_plugin => $_args ) {
						foreach ( $_args as $_type => $_ids ) {
							foreach ( $_ids as $id ) {
								$titles[] = Utils::get_product_title( $id, $_type, $_plugin );
							}
						}
					}
					if ( ! empty( $titles ) ) {
						$label = sprintf(
							/* translators: 1: List of products/categories. */
							__( 'Purchased %s', 'personalizewp' ),
							implode( ', ', $titles )
						);
					}
				}
				break;

			case 'page':
				if ( empty( $conditions ) ) {
					$label = __( 'Visited any page', 'personalizewp' );
				} else {
					$titles = [];
					$pages  = json_decode( $conditions, true );
					foreach ( $pages as $index => $page ) {
						if ( is_numeric( $page ) ) {
							$titles[] = sprintf(
								/* translators: 1: Page title */
								__( 'Page: \'%s\'', 'personalizewp' ),
								get_the_title( (int) $page )
							);
						} elseif ( is_numeric( $index ) && ! is_numeric( $page ) ) {
							$titles[] = sprintf(
								/* translators: 1: URL */
								__( 'URL: \'%s\'', 'personalizewp' ),
								$page
							);
						} elseif ( ! is_numeric( $index ) && ! is_numeric( $page ) ) {
							$titles[] = sprintf(
								/* translators: 1: URL */
								__( 'URL: \'%s\'', 'personalizewp' ),
								$index
							);
						}
					}
					if ( ! empty( $titles ) ) {
						$label = sprintf(
							/* translators: 1: List of condition entries. */
							__( 'Visited %s', 'personalizewp' ),
							implode( ', ', $titles )
						);
					}
				}
				break;
		}

		if ( empty( $label ) ) {
			$label = __( 'Unknown', 'personalizewp' );
		}

		/**
		 * Filters the Scoring rule conditions label
		 *
		 * @since 1.1.0
		 *
		 * @param string $label       Processed label
		 * @param string $conditions  Segment conditions
		 * @param string $type        Segment type
		 */
		return apply_filters( 'personalizewp_scoring_rule_conditions_label', $label, $conditions, $type );
	}
}
