<?php
/**
 * Rule model
 *
 * @link       https://personalizewp.com/
 * @since      1.0.0
 *
 * @package    PersonalizeWP
 * @subpackage PersonalizeWP/Model
 */

namespace PersonalizeWP\Model;

use PersonalizeWP\Rules_Conditions;

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

class Rule extends Base_Model {

	/**
	 * Database table name
	 *
	 * @var string
	 */
	protected static $table = 'pwp_rules';

	/**
	 * Default operator, used when multiple conditions are evaluated.
	 *
	 * @since 2.4.0
	 *
	 * @var string
	 */
	public static $default_operator = 'ALL';

	/**
	 * Matches to database columns
	 *
	 * @var array
	 */
	protected static $attributes = [
		'id',
		'name',
		'category_id',
		'type',
		'conditions_json',
		'operator', // String indicator of how multiple conditions are evaluated.
		'created_by',
		'created_at',
		'modified_at',
	];

	/**
	 * Format of database columns
	 *
	 * @var array
	 */
	protected $data_format = [
		'%d',
		'%s',
		'%d',
		'%s',
		'%s',
		'%s', // Operator
		'%s',
		'%s',
		'%s',
	];

	/**
	 * Constructor
	 *
	 * @param array $data Data to populate against attributes
	 */
	public function __construct( $data = [] ) {

		// Ensure database defaults are enforced.
		$data = wp_parse_args(
			$data,
			[
				'operator' => self::$default_operator,
			]
		);

		parent::__construct( $data );
	}

	/**
	 * Retrieve friendly name of category the rule is associated with
	 *
	 * @return string
	 */
	public function getCategoryNameAttribute() {
		// No category selected.
		if ( 0 === (int) $this->category_id ) {
			return '-';
		}
		$category = Category::find( $this->category_id );

		return $category ? $category->name : esc_html__( 'Unknown', 'personalizewp' );
	}

	/**
	 * Conditions in array, decoded from JSON
	 *
	 * @return array
	 */
	public function getConditionsAttribute() {
		$conditions = $this->conditions_json ? json_decode( $this->conditions_json ) : [];

		$return = [];
		foreach ( $conditions as $condition ) {
			$condition->raw_value = $condition->comparator === 'any' ? wp_json_encode( $condition->value ) : $condition->value;

			// Ensure existence of required data
			if ( ! isset( $condition->meta ) ) {
				$condition->meta = [];
			}

			$return[] = $condition;
		}

		return $return;
	}

	/**
	 * Return whether rule can be edited
	 *
	 * @return boolean
	 */
	public function getCanEditAttribute() {
		return true;
	}

	/**
	 * Return whether rule can be deleted
	 *
	 * @return boolean
	 */
	public function getCanDeleteAttribute() {
		return true;
	}

	/**
	 * Return whether rule can be duplicated
	 *
	 * @return boolean
	 */
	public function getCanDuplicateAttribute() {
		return $this->is_usable;
	}

	/**
	 * Is the rule usable with the conditions' current dependencies?
	 *
	 * @return boolean
	 */
	public function getIsUsableAttribute() {
		// $dependencies = $this->getConditionDependencies();

		foreach ( $this->conditions as $condition ) {
			$class_instance = Rules_Conditions::get_class( $condition->measure );
			if ( $class_instance ) {
				$is_usable = $class_instance->isUsable();

				if ( ! $is_usable ) {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Retrieve all dependencies from the conditions associated with the rule
	 *
	 * @return array
	 */
	protected function getConditionDependencies() {
		$return = [];
		foreach ( $this->conditions as $condition ) {
			$class_instance = Rules_Conditions::get_class( $condition->measure );
			if ( $class_instance ) {
				$dependencies = $class_instance->dependencies();

				$return = array_merge( $return, $dependencies );
			}
		}

		return $return;
	}

	public function getConditionDependencyIssues() {
		$issues = [];

		$dependencies = $this->getConditionDependencies();

		foreach ( $dependencies as $dependency ) {
			if ( ! $dependency->verify() ) {
				$issues[] = $dependency->get_failure_message();
			}
		}

		return $issues;
	}

	/**
	 * Clone this rule and save
	 *
	 * @return Rule
	 */
	public function clone() {
		$this->type       = 'custom';
		$this->created_by = get_current_user_id();
		$this->id         = null;
		$this->name       = $this->name . _x( ' copy', 'post fix added to name on duplication', 'personalizewp' );

		// Names should be unique.
		$name_check = $this->check_name( $this->name );
		if ( $name_check ) {
			$suffix = 2;
			do {
				$alt_name   = substr( $this->name, 0, 200 ) . "-$suffix";
				$name_check = $this->check_name( $alt_name );
				++$suffix;
			} while ( $name_check );
			$this->name = $alt_name;
		}

		$this->save();

		return $this;
	}

	/**
	 * Conditions match criteria - used when deciding whether to render a block
	 *
	 * @param  string $action Action to be taken when matching
	 * @return boolean
	 */
	public function conditionsMatched( $action ) {

		foreach ( $this->conditions as $condition ) {
			$class_instance = Rules_Conditions::get_class( $condition->measure );
			if ( $class_instance ) {
				$criteria_matched = $class_instance->matchesCriteria( $condition->comparator, $condition->value, $action, $condition->meta );

				switch ( $this->operator ) {
					default:
					case 'ALL':
						// Require all conditions to match, so a single failure causes no match.
						if ( ! $criteria_matched ) {
							return false;
						}
						break;

					case 'ANY':
						// Require any condition to match, so a single success triggers true.
						if ( $criteria_matched ) {
							return true;
						}
						break;
				}
			}
		}

		switch ( $this->operator ) {
			default:
			case 'ALL':
				// Required all conditions to match, no failures must indicate a success.
				return true;

			case 'ANY':
				// Required any condition to match, no matches must indicate a failure.
				return false;
		}
	}

	/**
	 * Populate model from an array of data
	 *
	 * @param  array $data
	 */
	public function populateFromArray( $data ) {
		$this->name = $data['name'];
		if ( ! empty( $data['category_id'] ) && is_numeric( $data['category_id'] ) ) {
			$this->category_id = (int) $data['category_id'];
		} else {
			$this->category_id = 0;
		}

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

		$condition_count = ! empty( $data['conditions']['measure'] ) ? count( $data['conditions']['measure'] ) : 0;

		$conditions = [];
		for ( $i = 0; $i < $condition_count; $i++ ) {

			$meta_array = [];
			$measure    = $data['conditions']['measure'][ $i ];
			$meta_value = $data['conditions']['meta_value'][ $i ];
			$comparator = $data['conditions']['comparator'][ $i ];
			$value      = $data['conditions']['value'][ $i ];
			$raw_value  = $data['conditions']['raw_value'][ $i ];

			if ( ! empty( $meta_value ) ) :
				$condition_class = Rules_Conditions::get_class( $measure );
				if ( $condition_class ) :
					$measure_key = $condition_class->measure_key;

					$meta_array[ $measure_key ] = $meta_value;
				endif;
			endif;

			// Handle days_number parameter for date-based conditions with days_after/days_before comparators
			if ( isset( $data['conditions']['days_number'][ $i ] ) && ! empty( $data['conditions']['days_number'][ $i ] ) ) {
				$days_number = (int) $data['conditions']['days_number'][ $i ];
				if ( $days_number > 0 && in_array( $comparator, [ 'days_after', 'days_before' ], true ) ) {
					$meta_array['days_number'] = $days_number;
				}
			}

			$conditions[] = (object) [
				'measure'    => $measure,
				'meta'       => $meta_array,
				'comparator' => $comparator,
				'value'      => $comparator === 'any' ? json_decode( stripslashes( $raw_value ) ) : $raw_value,
			];
		}

		$this->conditions_json = wp_json_encode( $conditions );
	}

	/**
	 * Array of tag attributes to be included in PWP tag
	 *
	 * @param string $action the resulting action of the rule
	 * @return array
	 */
	public function tagAttributes( $action ) {

		$attributes = [];

		foreach ( $this->conditions as $condition ) {
			$class_instance = Rules_Conditions::get_class( $condition->measure );
			if ( $class_instance ) {
				$attributes = $class_instance->tagAttributes( $condition, $action ) + $attributes;
			}
		}

		return $attributes;
	}
}
