<?php
/**
 * Segment model
 *
 * @link       https://personalizewp.com/
 * @since      3.2.0 Merged into PWP
 * @since      1.0.0
 *
 * @package    PersonalizeWP
 * @subpackage PersonalizeWP/Model
 */

namespace PersonalizeWP\Model;

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

/**
 * Segment model
 *
 * This class defines Segment structure, and has a selection of static utility functions.
 */
class Segment extends Model {

	/**
	 * Database table name storing segments
	 *
	 * @var string
	 */
	protected static string $table = 'pwp_segments';

	/**
	 * Matches to database columns, not including ID
	 *
	 * @var string[]
	 */
	protected static $attributes = [
		'name',
		'type',
		'conditions', // JSON encoded string of various data structures depending on Segment type.
		'operator', // String indicator of how multiple conditions are evaluated.
		'is_active',
		'created',
		'contact_count',
	];

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

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

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

		parent::__construct( $data );
	}

	/**
	 * Update a segment with new data
	 *
	 * @param array $data  Segment data to update
	 *
	 * @return bool|integer Database ID on success, false otherwise
	 */
	public function update( array $data ) {

		foreach ( $data as $property => $value ) {
			// Only store that which is expected.
			if ( in_array( $property, self::$attributes, true ) ) {
				$this->data[ $property ] = $value;
			}
		}

		return $this->save();
	}

	/**
	 * Deactivate Segment with specific ID
	 *
	 * @param  integer $id Segment ID
	 * @return bool
	 */
	public static function deactivate( $id ) {
		global $wpdb;

		self::clear_cache( $id );

		return $wpdb->update( "{$wpdb->prefix}pwp_segments", [ 'is_active' => 0 ], [ 'id' => $id ], [ '%d' ], [ '%d' ] );
	}

	/**
	 * Activate Segment with specific ID
	 *
	 * @param  integer $id Segment ID
	 * @return bool
	 */
	public static function activate( $id ) {
		global $wpdb;

		self::clear_cache( $id );

		return $wpdb->update( "{$wpdb->prefix}pwp_segments", [ 'is_active' => 1 ], [ 'id' => $id ], [ '%d' ], [ '%d' ] );
	}

	/**
	 * Save Model to database, extended to clear caching
	 *
	 * @since 2.7.0
	 *
	 * @return bool|integer Database ID on success, false otherwise
	 */
	public function save() {

		self::clear_cache();

		// Use parent delete to remove the Segment itself.
		return parent::save();
	}

	/**
	 * Delete Segment by specified ID and all related relationships to Contacts
	 *
	 * @param  integer $id Segment ID
	 * @return bool
	 */
	public static function delete( $id ) {
		global $wpdb;

		$segment = self::find( $id );
		if ( false === $segment ) {
			// Nothing to do.
			return;
		}

		/**
		 * Filters whether a Segment deletion should take place.
		 *
		 * @since 2.3.0
		 *
		 * @param false|null $delete  Whether to go forward with deletion.
		 * @param Segment    $segment Segment object.
		 */
		$check = apply_filters( 'personalizewp_pre_delete_segment', null, $segment );
		if ( null !== $check ) {
			return $check;
		}

		/**
		 * Fires before a Segment is deleted, at the start of Segment::delete().
		 *
		 * @since 2.3.0
		 *
		 * @param Segment $segment Segment object.
		 */
		do_action( 'personalizewp_before_delete_segment', $segment );

		// wp_cache_delete( $id, $segments_table );
		// // FIXME cache
		// wp_cache_delete( $id, $segments_table . $segments_contacts_table );

		$result = $wpdb->delete( "{$wpdb->prefix}pwp_segments_relationships", [ 'segment_id' => $id ], [ '%d' ] );
		// May already be zero Contacts for the Segment, so check for explicit error.
		if ( false === $result ) {
			return false;
		}

		self::clear_cache( $id );

		// Use parent delete to remove the Segment itself.
		return parent::delete( $id );
	}

	/**
	 * Retrieve Segment instance via ID
	 *
	 * @since 1.1.0 Added to ensure correct return for docs.
	 *
	 * @param  integer $id ID of database row
	 *
	 * @return Segment|false Instance of Segment on success, false on failure
	 */
	public static function find( $id ) { // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found -- Reason: adjusting return type in docs
		return parent::find( $id );
	}

	/**
	 * Returns the Segments for a given Contact
	 *
	 * @param int $contact_id Contact ID
	 *
	 * @return array Array of Segments
	 */
	public static function get_contact_segments( int $contact_id ) {
		$segments = wp_cache_get( 'pwp_pro_segments_by_contact_id_' . $contact_id, 'pwp_pro_contact_segments' );
		if ( false === $segments ) {
			global $wpdb;
			$segments = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT * FROM {$wpdb->prefix}pwp_segments
					INNER JOIN {$wpdb->prefix}pwp_segments_relationships ON {$wpdb->prefix}pwp_segments_relationships.segment_id = {$wpdb->prefix}pwp_segments.ID
					WHERE contact_id = %d",
					$contact_id
				)
			);
			wp_cache_set( 'pwp_pro_segments_by_contact_id_' . $contact_id, $segments, 'pwp_pro_contact_segments' );
		}
		return $segments;
	}

	/**
	 * Return the active Segments for a specific type
	 *
	 * @param array $types Types of Segment
	 *
	 * @return array Array of Segments
	 */
	public static function get_active_segments( $types = [] ) {
		global $wpdb;

		if ( empty( $types ) ) {
			return array();
		}

		// FIXME: Add caching?

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Reason: SQL is prepared with placeholders.
		$placeholders = implode( ', ', array_fill( 0, count( $types ), '%s' ) );
		$segments     = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}pwp_segments WHERE type IN ( {$placeholders} ) AND is_active = 1",
				$types
			),
			ARRAY_A
		);
		// phpcs:enable

		return $segments;
	}

	/**
	 * Clear any Segment cache, optionally with ID
	 *
	 * @since 2.7.0
	 *
	 * @param  integer $id Segment ID
	 * @return void
	 */
	public static function clear_cache( $id = false ) {

		if ( $id ) {
			wp_cache_delete( $id, self::$table );
		}

		// Delete all segments, used for Pro settings.
		wp_cache_delete( 'pwp_pro_segments', 'personalizewp' );

		// Used for counts on Segments listing screens
		wp_cache_delete( 'personalizewp_segments', 'counts' );

		// Used for getting segments by contact id.
		wp_cache_flush_group( 'pwp_pro_contact_segments' );
	}

	/**
	 * Adds a singular Contact to a Segment
	 *
	 * @param integer $segment_id id of Segment
	 * @param integer $contact_id id of Contact
	 * @return bool
	 */
	public static function add_contact( int $segment_id, int $contact_id ) {
		global $wpdb;

		// Does the row already exist?
		$result = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}pwp_segments_relationships WHERE segment_id = %d AND contact_id = %d LIMIT 1",
				[ $segment_id, $contact_id ]
			),
			ARRAY_N
		);
		if ( $result ) {
			return true;
		}

		$result = (bool) $wpdb->insert(
			"{$wpdb->prefix}pwp_segments_relationships",
			[
				'segment_id' => $segment_id,
				'contact_id' => $contact_id,
			],
			[ '%d', '%d' ]
		);
		if ( $result ) {
			// Update contact count
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->prefix}pwp_segments SET contact_count = contact_count + 1 WHERE id = %d;",
					$segment_id
				)
			);

			wp_cache_delete( 'pwp_pro_segments_by_contact_id_' . $contact_id, 'pwp_pro_contact_segments' );
		}
		return $result;
	}

	/**
	 * Removes a singular Contact from a Segment
	 *
	 * @param integer $segment_id id of Segment
	 * @param integer $contact_id id of Contact
	 * @return bool
	 */
	public static function remove_contact( int $segment_id, int $contact_id ) {
		global $wpdb;

		$result = (bool) $wpdb->delete(
			"{$wpdb->prefix}pwp_segments_relationships",
			[
				'segment_id' => $segment_id,
				'contact_id' => $contact_id,
			],
			[ '%d', '%d' ]
		);

		if ( $result ) {
			// Update contact count, use direct query as we don't want the 'value' to be escaped.
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->prefix}pwp_segments SET contact_count = contact_count - 1 WHERE id = %d;",
					$segment_id
				)
			);

			wp_cache_delete( 'pwp_pro_segments_by_contact_id_' . $contact_id, 'pwp_pro_contact_segments' );
		}
		return $result;
	}
}
