<?php

// phpcs:disable Generic.Commenting.DocComment.MissingShort
/** @noinspection AutoloadingIssuesInspection */
/** @noinspection PhpIllegalPsrClassPathInspection */
// phpcs:enable Generic.Commenting.DocComment.MissingShort

/**
 * Entry fields DB class.
 *
 * @since 1.4.3
 */
class WPForms_Entry_Fields_Handler extends WPForms_DB {

	/**
	 * Primary class constructor.
	 *
	 * @since 1.4.3
	 */
	public function __construct() {

		global $wpdb;

		parent::__construct();

		$this->table_name  = $wpdb->prefix . 'wpforms_entry_fields';
		$this->primary_key = 'id';
		$this->type        = 'entry_fields';
	}

	/**
	 * Get table columns.
	 *
	 * @since 1.4.3
	 */
	public function get_columns() {

		return [
			'id'       => '',
			'entry_id' => '%d',
			'form_id'  => '%d',
			'field_id' => '%s',
			'value'    => '%s',
			'date'     => '%s',
		];
	}

	/**
	 * Default column values.
	 *
	 * @since 1.4.3
	 */
	public function get_column_defaults() {

		return [
			'entry_id' => '',
			'form_id'  => '',
			'field_id' => '',
			'value'    => '',
			'date'     => date( 'Y-m-d H:i:s' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
		];
	}

	/**
	 * Get entry meta rows from the database.
	 *
	 * @since 1.4.3
	 *
	 * @param array $args  Modify the query with these params.
	 * @param bool  $count Whether to return only the number of rows, or rows themselves.
	 *
	 * @return array|int
	 */
	public function get_fields( $args = [], $count = false ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded

		global $wpdb;

		$defaults = [
			'select'        => 'all',
			'number'        => 30,
			'offset'        => 0,
			'id'            => 0,
			'entry_id'      => 0,
			'form_id'       => 0,
			'field_id'      => 0,
			'value'         => '',
			'value_compare' => '',
			'date'          => '',
			'orderby'       => 'id',
			'order'         => 'DESC',
		];

		$args = apply_filters(
			'wpforms_entry_fields_get_fields_args',
			wp_parse_args( $args, $defaults )
		);

		if ( $args['number'] < 1 ) {
			$args['number'] = PHP_INT_MAX;
		}

		/*
		 * Modify the SELECT.
		 */
		$select = '*';

		$possible_select_values = apply_filters(
			'wpforms_entries_fields_get_fields_select',
			[
				'all'       => '*',
				'entry_ids' => '`entry_id`',
			]
		);

		if ( array_key_exists( $args['select'], $possible_select_values ) ) {
			$select = esc_sql( $possible_select_values[ $args['select'] ] );
		}

		/*
		 * Modify the WHERE.
		 */
		$where = [
			'default' => '1=1',
		];

		// Allowed int arg items.
		$keys = [ 'id', 'entry_id', 'form_id', 'field_id' ];

		foreach ( $keys as $key ) {
			// Value `$args[ $key ]` can be a natural number and a numeric string.
			// We should skip empty string values, but continue working with '0'.
			if (
				! is_array( $args[ $key ] ) &&
				( ! is_numeric( $args[ $key ] ) || $args[ $key ] === 0 )
			) {
				continue;
			}

			if ( is_array( $args[ $key ] ) && ! empty( $args[ $key ] ) ) {
				$ids = implode( ',', array_map( 'intval', $args[ $key ] ) );
			} else {
				$ids = (int) $args[ $key ];
			}

			$where[ 'arg_' . $key ] = "`$key` IN ( $ids )";
		}

		// Processing value and value_compare.
		if ( isset( $args['value'] ) && ! wpforms_is_empty_string( $args['value'] ) ) {

			$escaped_value   = esc_sql( $args['value'] );
			$condition_value = '';

			switch ( $args['value_compare'] ) {
				case '': // Preserving backward compatibility.
				case 'is':
					$condition_value = " = '$escaped_value'";
					break;

				case 'is_not':
					$condition_value = " <> '$escaped_value'";
					break;

				case 'contains':
					$condition_value = " LIKE '%$escaped_value%'";
					break;

				case 'contains_not':
					$condition_value = " NOT LIKE '%$escaped_value%'";
					break;
			}

			$where['arg_value'] = '`value`' . $condition_value;

		// Empty value should be allowed in case certain comparisons are used.
		} elseif ( $args['value_compare'] === 'is' ) {

			$where['arg_value'] = "`value` = ''";

		} elseif ( $args['value_compare'] === 'is_not' ) {

			$where['arg_value'] = "`value` <> ''";

		} elseif ( $args['value_compare'] === 'empty' ) {

			$where['arg_value'] = "TRIM(`value`) = ''";

		} elseif ( $args['value_compare'] === 'not_empty' ) {

			$where['arg_value'] = "TRIM(`value`) <> '' AND `value` IS NOT NULL";

		}

		// Process dates.
		if ( ! empty( $args['date'] ) ) {
			// We can pass array and treat it as a range from:to.
			if ( is_array( $args['date'] ) && count( $args['date'] ) === 2 ) {
				$date_start = wpforms_get_day_period_date( 'start_of_day', strtotime( $args['date'][0] ), 'Y-m-d H:i:s', true );
				$date_end   = wpforms_get_day_period_date( 'end_of_day', strtotime( $args['date'][1] ), 'Y-m-d H:i:s', true );

				if ( ! empty( $date_start ) && ! empty( $date_end ) ) {
					$where['arg_date_start'] = "`date` >= '$date_start'";
					$where['arg_date_end']   = "`date` <= '$date_end'";
				}
			} elseif ( is_string( $args['date'] ) ) {
				/*
				 * If we pass the only string representation of a date -
				 * that means we want to get records of that day only.
				 * So we generate start and end MySQL dates for the specified day.
				 */
				$timestamp  = strtotime( $args['date'] );
				$date_start = wpforms_get_day_period_date( 'start_of_day', $timestamp, 'Y-m-d H:i:s', true );
				$date_end   = wpforms_get_day_period_date( 'end_of_day', $timestamp, 'Y-m-d H:i:s', true );

				if ( ! empty( $date_start ) && ! empty( $date_end ) ) {
					$where['arg_date_start'] = "`date` >= '$date_start'";
					$where['arg_date_end']   = "`date` <= '$date_end'";
				}
			}
		}

		// Give developers an ability to modify WHERE (unset clauses, add new, etc).
		$where     = (array) apply_filters( 'wpforms_entry_fields_get_fields_where', $where, $args );
		$where_sql = implode( ' AND ', $where );

		/*
		 * Modify the ORDER BY.
		 */
		$args['orderby'] = ! array_key_exists( $args['orderby'], $this->get_columns() ) ? $this->primary_key : $args['orderby'];

		if ( 'ASC' === strtoupper( $args['order'] ) ) {
			$args['order'] = 'ASC';
		} else {
			$args['order'] = 'DESC';
		}

		/*
		 * Modify the OFFSET / NUMBER.
		 */
		$args['offset'] = absint( $args['offset'] );
		$args['number'] = absint( $args['number'] );

		/*
		 * Retrieve the results.
		 */

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		if ( $count === true ) {
			$results = absint(
				$wpdb->get_var(
					"SELECT COUNT( $this->primary_key )
					FROM $this->table_name
					WHERE $where_sql;"
				)
			);
		} else {
			// Do not use an empty limit aka 'LIMIT 0, PHP_INT_MAX', because it may cause a performance issue.
			$limit   = $args['offset'] === 0 && $args['number'] === PHP_INT_MAX
				? ''
				: "LIMIT {$args['offset']}, {$args['number']}";
			$results = $wpdb->get_results(
				"SELECT $select
				FROM $this->table_name
				WHERE $where_sql
				ORDER BY {$args['orderby']} {$args['order']}
				$limit;"
			);
		}
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		return $results;
	}

	/**
	 * Save all fields of the entry.
	 *
	 * @since 1.7.3
	 *
	 * @param array $fields    Fields.
	 * @param array $form_data Form data.
	 * @param int   $entry_id  Entry id.
	 * @param bool  $update    Update field if it exists.
	 *
	 * @return void
	 */
	public function save( $fields, $form_data, $entry_id, $update = false ) {

		if ( ! $entry_id ) {
			return;
		}

		// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
		$date = $form_data['date'] ?? date( 'Y-m-d H:i:s' );

		foreach ( $fields as $field ) {
			$this->save_field( $field, $form_data, $entry_id, $update, $date );
		}
	}

	/**
	 * Save one field of the entry.
	 *
	 * @since 1.7.3
	 *
	 * @param array  $field     Field.
	 * @param array  $form_data Form data.
	 * @param int    $entry_id  Entry id.
	 * @param bool   $update    Update field if it exists.
	 * @param string $date      Date.
	 *
	 * @return void
	 */
	private function save_field( $field, $form_data, $entry_id, $update, $date ) {

		// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
		/**
		 * Filter entry field before saving.
		 *
		 * @since 1.4.3
		 *
		 * @param array $field     Field data.
		 * @param array $form_data Form data.
		 * @param int   $entry_id  Entry id.
		 */
		$field = apply_filters( 'wpforms_entry_save_fields', $field, $form_data, $entry_id );
		// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName

		// Check the required data.
		if ( ! isset( $field['id'] ) ) {
			return;
		}

		$value = $this->get_choice_value( $field, $form_data );

		if ( ! isset( $value ) || $value === '' ) {
			return;
		}

		$form_id        = $form_data['id'];
		$field_id       = $field['id'];
		$entry_field_id = null;

		if ( $update ) {
			$entry_field_id = $this->get_entry_field_id( $entry_id, $form_id, $field_id );
		}

		$data = [
			'entry_id' => $entry_id,
			'form_id'  => absint( $form_id ),
			'field_id' => wpforms_validate_field_id( $field_id ),
			'value'    => $value,
			'date'     => $date,
		];

		if ( $entry_field_id ) {
			$this->update( $entry_field_id, $data );

			return;
		}

		$this->add( $data );
	}

	/**
	 * Return choice value, but exclude fields with Other choices.
	 *
	 * @since 1.9.8.3
	 * @since 1.9.9 Exclude fields with Dynamic choices.
	 *
	 * @param array $field     Field data.
	 * @param array $form_data Form data.
	 *
	 * @return string
	 */
	private function get_choice_value( array $field, array $form_data ): string {

		if ( ! empty( $field['is_other'] ) || ! empty( $field['dynamic'] ) ) {
			return $field['value'] ?? '';
		}

		return wpforms_get_choices_value( $field, $form_data );
	}

	/**
	 * Get entry field id.
	 *
	 * @since 1.7.3
	 *
	 * @param int $entry_id Entry id.
	 * @param int $form_id  Form id.
	 * @param int $field_id Field id.
	 *
	 * @return string|null
	 */
	private function get_entry_field_id( $entry_id, $form_id, $field_id ) {

		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_var(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT id FROM $this->table_name WHERE entry_id = %d AND form_id = %d AND field_id = %s LIMIT 1",
				$entry_id,
				$form_id,
				$field_id
			)
		);
	}

	/**
	 * Create custom entry meta database table.
	 *
	 * @since 1.4.3
	 */
	public function create_table() {

		global $wpdb;

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		$charset_collate = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$this->table_name} (
			id bigint(20) NOT NULL AUTO_INCREMENT,
			entry_id bigint(20) NOT NULL,
			form_id bigint(20) NOT NULL,
			field_id varchar(16) NOT NULL,
			value longtext NOT NULL,
			date datetime NOT NULL,
			PRIMARY KEY  (id),
			KEY entry_id (entry_id),
			KEY form_id (form_id),
			KEY field_id (field_id)
		) {$charset_collate};";

		dbDelta( $sql );
	}
}
