<?php
/**
 * Admin screen for managing Rule.
 *
 * @link       https://personalizewp.com
 * @since      1.0.0
 *
 * @package    PersonalizeWP
 * @subpackage PersonalizeWP/Admin
 */

namespace PersonalizeWP\Admin;

use PersonalizeWP\Admin\Admin;
use PersonalizeWP\Admin\Admin_Base_Page;
use PersonalizeWP\Rules_Conditions;
use PersonalizeWP\Model\Rule;

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

/**
 * Class Rules Admin
 */
class PageRules extends Admin_Base_Page {

	/**
	 * Slug for Rules admin page
	 *
	 * @since 1.2.0
	 */
	const SLUG = 'personalizewp/rules';

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

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

	/**
	 * URL for admin page, editing a rule.
	 * Requires rule ID to be appended.
	 *
	 * @since 3.2.0
	 *
	 * @var string
	 */
	public static $edit_url = 'admin.php?page=' . self::SLUG . '&' . Admin::URL_ACTION . '=edit&id=';

	/**
	 * URL for admin page, deleting a rule.
	 * Requires rule ID to be appended.
	 *
	 * @since 3.2.0
	 *
	 * @var string
	 */
	public static $delete_url = 'admin.php?page=' . self::SLUG . '&' . Admin::URL_ACTION . '=delete&id=';

	/**
	 * URL for admin page, duplicating a rule.
	 * Requires rule ID to be appended.
	 *
	 * @since 3.2.0
	 *
	 * @var string
	 */
	public static $duplicate_url = 'admin.php?page=' . self::SLUG . '&' . Admin::URL_ACTION . '=duplicate&id=';

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

		$this->page_title = __( 'Rules', 'personalizewp' );

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

	/**
	 * Display the Dashboard screen options.
	 *
	 * @since 1.2.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 Rules per page:', 'personalizewp' ),
			'default' => 20,
			'option'  => 'edit_pwp_rules_per_page',
		);
		add_screen_option( 'per_page', $args );
	}

	/**
	 * Store the screen options.
	 *
	 * @since 1.2.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.
	 *
	 * @return string
	 */
	public function set_screen_options( $screen_option, $option, $value ) {
		if ( 'edit_pwp_rules_per_page' === $option ) {
			return $value;
		}
		return $screen_option;
	}

	/**
	 * Process - typically used for processing POST data
	 *
	 * @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;
		}

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

		if ( ! empty( $this->form_post ) ) {
			switch ( $this->current_action ) {
				case 'create':
					check_admin_referer( 'rule-create' ); // Immediately dies if failing.
					$this->store_action( $this->form_post );
					break;

				case 'edit':
					if ( ! empty( $this->form_post['id'] ) ) {
						$rule_id = (int) $this->form_post['id'];
						check_admin_referer( 'rule-update-' . $rule_id );
						$this->update_action( $rule_id, $this->form_post );
					}
					break;
			}
		}

		switch ( $this->current_action ) {
			case 'delete':
				if ( ! empty( $_GET['id'] ) ) {
					$rule_id = (int) sanitize_text_field( wp_unslash( $_GET['id'] ) );
					check_admin_referer( 'rule-delete-' . $rule_id );
					$this->delete_action( $rule_id );
				}
				break;

			case 'duplicate':
				if ( ! empty( $_GET['id'] ) ) {
					$rule_id = (int) sanitize_text_field( wp_unslash( $_GET['id'] ) );
					check_admin_referer( 'rule-duplicate-' . $rule_id );
					$this->duplicate_action( $rule_id );
				}
				break;
		}
	}

	/**
	 * Route to the correct action within the page
	 *
	 * @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;
		}

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

			case 'edit':
				if ( ! empty( $_GET['id'] ) ) {
					$edit_id = (int) sanitize_text_field( wp_unslash( $_GET['id'] ) );
					$this->edit_screen( $edit_id );
					return;
				}
				break;
		}

		$this->index_screen();
	}

	/**
	 * Display onboarding message, only if not dismissed.
	 *
	 * @since 3.2.0
	 *
	 * @return void
	 */
	protected function maybe_display_onboarding() {
		$is_onboarding = ( '1' !== get_option( 'pwp_admin_onboarding_dismissed', '' ) );
		// Dismissing this updates the option globally, dismissing all the 'first time' messaging.
		if ( ! $is_onboarding ) {
			return;
		}
		// The option key ensures persistancy.
		wp_admin_notice(
			sprintf(
				'<span class="dashicons dashicons-info"></span><p><strong>%1$s</strong></p><p>%2$s</p>',
				esc_html__( 'First time configuration', 'personalizewp' ),
				esc_html__( 'PersonalizeWP provides a number of pre-built rules and categories that are ready to use on your site, which you can see below. If you want to add your own, just click on the Create Rule or Create Category button.', 'personalizewp' ),
			),
			array(
				'type'               => 'info',
				'dismissible'        => true,
				'paragraph_wrap'     => false,
				'additional_classes' => array( 'onboarding' ),
				'attributes'         => array( 'data-dismiss-type' => 'onboarding' ),
			)
		);
	}

	/**
	 * Index Screen
	 */
	protected function index_screen() {

		$this->display_header();

		$status_message = '';
		$status_type    = 'success';
		$icon_type      = 'yes-alt';
		// 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 = __( 'Rule created', 'personalizewp' );
		}
		if ( isset( $_REQUEST['deleted'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this is only displaying a message with no dynamic values.
			$status_message = __( 'Rule deleted', 'personalizewp' );
		}
		if ( isset( $_REQUEST['duplicate_error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this is only displaying a message with no dynamic values.
			$status_message = __( 'Error duplicating rule', 'personalizewp' );
			$status_type    = 'error';
			$icon_type      = 'error';
		}
		if ( $status_message ) {
			wp_admin_notice(
				sprintf(
					'<span class="dashicons dashicons-%1$s"></span>%2$s',
					$icon_type,
					$status_message
				),
				array(
					'type'        => $status_type,
					'id'          => 'message',
					'dismissible' => true,
				)
			);
		}

		$this->maybe_display_onboarding();

		require_once plugin_dir_path( __DIR__ ) . 'admin/partials/rules/index.php';
		$this->display_footer();
	}

	/**
	 * Create Screen - display form
	 *
	 * @param array $data Form data
	 */
	protected function create_screen( $data ) {

		$this->display_header();

		$rule = new Rule();

		if ( $data ) {
			$rule->populateFromArray( $data );
		}

		require_once plugin_dir_path( __DIR__ ) . 'admin/partials/rules/create.php';
		$this->display_footer();
	}

	/**
	 * Store action - process data from form submitted on "create" page
	 *
	 * @param array $data Form data
	 */
	protected function store_action( $data ) {
		$this->validate( $data );

		if ( empty( $this->getError() ) ) {
			$this->saveRule( $data );

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

	/**
	 * Edit Screen - display form for existing rule
	 *
	 * @param integer $id Rule ID
	 */
	protected function edit_screen( $id ) {

		// This is used within the partial template below.
		$rule = Rule::find( $id );

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

		$this->display_header();

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

		require_once plugin_dir_path( __DIR__ ) . 'admin/partials/rules/edit.php';
		$this->display_footer();
	}

	/**
	 * Update action - process data from form submitted on "edit" page
	 *
	 * @param  integer $id   Rule ID
	 * @param  array   $data Form data
	 */
	protected function update_action( $id, $data ) {
		$rule = Rule::find( $id );

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

		$this->validate( $data, true );

		if ( empty( $this->getError() ) ) {
			$this->saveRule( $data, $id );

			wp_admin_notice(
				'<span class="dashicons dashicons-yes-alt"></span>' . __( 'Rule updated', 'personalizewp' ),
				array(
					'type'        => 'success',
					'id'          => 'message',
					'dismissible' => true,
				)
			);

			// No redirect as re-loading the same edit screen.
		}
	}

	/**
	 * Delete action
	 *
	 * @param  integer $id Rule ID
	 */
	protected function delete_action( $id ) {
		$rule = Rule::find( $id );

		if ( ! $rule ) {
			// Nothing to do
			wp_safe_redirect( admin_url( self::$index_url ) );
			exit;
		}

		if ( ! $rule->can_delete ) {
			$this->showError( __( 'Rule cannot be deleted', 'personalizewp' ) );
		}

		Rule::delete( $id );

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

	/**
	 * Duplicate action
	 *
	 * @param  integer $id Rule ID
	 */
	protected function duplicate_action( $id ) {
		$rule = Rule::find( $id );

		if ( ! $rule ) {
			wp_safe_redirect( add_query_arg( array( 'duplicate_error' => 1 ), admin_url( self::$index_url ) ) );
			exit;
		}

		$duplicate_rule = $rule->clone();

		if ( $duplicate_rule instanceof Rule ) {
			wp_safe_redirect( add_query_arg( array( 'duplicated' => 1 ), $duplicate_rule->getEditUrlAttribute() ) );
		} else {
			wp_safe_redirect( add_query_arg( array( 'duplicate_error' => 1 ), admin_url( self::$index_url ) ) );
		}
		exit;
	}

	/**
	 * Save rule to DB (either create or update)
	 *
	 * @param  array   $data Form data
	 * @param  integer $id   Rule ID
	 * @return Rule
	 */
	protected function saveRule( $data, $id = null ) {
		if ( $id ) {
			$rule = Rule::find( $id );
		}

		if ( empty( $rule ) ) {
			$rule = new Rule();
		}

		$rule->populateFromArray( $data );

		// Is new rule
		if ( empty( $rule->id ) ) {
			$rule->type       = 'custom';
			$rule->created_by = get_current_user_id();
		}

		$rule->save();

		return $rule;
	}

	/**
	 * Validate data, likely from submitted form
	 *
	 * @param  array   $data           Form data
	 * @param  boolean $existing_model True if we're editing an existing model
	 * @return void
	 */
	protected function validate( $data, $existing_model = false ) {
		if ( empty( $data ) ) {
			$this->addValidationError( 'form', __( 'Form is not valid', 'personalizewp' ) );
			return;
		}

		if ( $existing_model ) {
			if ( empty( $data['id'] ) ) {
				$this->addValidationError( 'form', __( 'ID is required to update a rule', 'personalizewp' ) );
			}
		}

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

		if ( Rule::check_name( $data['name'], $data['id'] ) ) {
			$this->addValidationError( 'name', __( 'This rule name already exists. Please choose a different name.', 'personalizewp' ) );
		}

		if ( ! is_numeric( $data['category_id'] ) ) {
			$this->addValidationError( 'category_id', __( 'Please select a valid category', 'personalizewp' ) );
		}

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

		if ( ! empty( $data['conditions']['measure'] ) ) {
			foreach ( $data['conditions']['measure'] as $i => $measure ) {
				if ( empty( $measure ) ) {
					$this->addValidationError( 'conditions[' . $i . ']', __( 'A measure for the condition should be selected', 'personalizewp' ) );
				}
			}
		}

		if ( ! empty( $data['conditions']['meta_value'] ) ) {
			foreach ( $data['conditions']['meta_value'] as $i => $meta_value ) {
				$condition_class = Rules_Conditions::get_class( $data['conditions']['measure'][ $i ] );

				if ( empty( $meta_value ) && $condition_class && ! empty( $condition_class->measure_key ) ) {
					$this->addValidationError( 'conditions[' . $i . ']', __( 'The meta value for the condition should not be empty', 'personalizewp' ) );
				}
			}
		}

		if ( ! empty( $data['conditions']['comparator'] ) ) {
			foreach ( $data['conditions']['comparator'] as $i => $comparator ) {
				if ( empty( $comparator ) ) {
					$this->addValidationError( 'conditions[' . $i . ']', __( 'A comparator for the condition should be selected', 'personalizewp' ) );
				}
			}
		}

		if ( ! empty( $data['conditions']['raw_value'] ) ) {
			foreach ( $data['conditions']['raw_value'] as $i => $value ) {
				$comparitor_value = $data['conditions']['comparator'][ $i ];

				if ( empty( $value ) && 'any_value' !== $comparitor_value && 'no_value' !== $comparitor_value ) {
					if ( $data['conditions'] ) {
						$this->addValidationError( 'conditions[' . $i . ']', __( 'Please select a value', 'personalizewp' ) );
					}
				}
			}
		}

		// Validate days_number for date-based conditions with days_after/days_before comparators
		if ( ! empty( $data['conditions']['days_number'] ) ) {
			foreach ( $data['conditions']['days_number'] as $i => $days_number ) {
				$comparator = isset( $data['conditions']['comparator'][ $i ] ) ? $data['conditions']['comparator'][ $i ] : '';

				if ( in_array( $comparator, [ 'days_after', 'days_before' ], true ) ) {
					if ( empty( $days_number ) || ! is_numeric( $days_number ) || (int) $days_number <= 0 ) {
						$this->addValidationError( 'conditions[' . $i . ']', __( 'Please enter a valid positive number of days', 'personalizewp' ) );
					}
				}
			}
		}
	}
}
