<?php
/**
 * Simply Schedule Appointments Db Model.
 *
 * @since   0.0.3
 * @package Simply_Schedule_Appointments
 */

/**
 * Simply Schedule Appointments Db Model.
 *
 * @since 0.0.3
 */
abstract class SSA_Db_Model extends TD_DB_Model {
	protected $hook_namespace = 'ssa';
	protected $db_namespace = 'ssa';
	protected $api_namespace = 'ssa';
	protected $api_version = '1';

	/**
	 * Parent plugin class.
	 *
	 * @since 0.0.3
	 *
	 * @var   Simply_Schedule_Appointments
	 */
	protected $plugin = null;

	/**
	 * Constructor.
	 *
	 * @since  0.0.3
	 *
	 * @param  Simply_Schedule_Appointments $plugin Main plugin object.
	 */
	public function __construct( $plugin ) {
		parent::__construct( $plugin );
		$this->ssa_model_hooks();

		add_filter( 'rest_authentication_errors', array( $this, 'whitelist_ssa_rest_api' ), 1000 );
	}

	public function whitelist_ssa_rest_api( $result ) {
		if ( isset( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
			$route = untrailingslashit( $GLOBALS['wp']->query_vars['rest_route'] );
			if ( 0 === strpos( $route, '/ssa/' ) ) {
				return true;
			}
		}

		return $result;
	}

	/**
	 * Use to retrieve the primary key for the model
	 * @return string
	 */
	public function primary_key() {
		return $this->primary_key;
	}
	
	/**
	 * Initiate our hooks.
	 *
	 * @since  0.0.3
	 */
	public function ssa_model_hooks() {
		add_filter( 'query_'.$this->slug.'_db_where_conditions', array( $this, 'ssa_filter_where_conditions' ), 10, 2 );
	}

	public function ssa_filter_where_conditions( $where, $args ) {
		global $wpdb;

		if( ! empty( $args['id'] ) ) {

			if( is_array( $args['id'] ) ) {
				$ids = implode( ',', array_map('intval', $args['id'] ) );
				$where .= " AND `".$this->primary_key."` IN( $ids ) ";
			} else {
				$ids = intval( $args['id'] );
				$where .= $wpdb->prepare( " AND `".$this->primary_key."` = %d ", $ids );
			}


		}

		if ( !empty( $this->schema['user_id'] ) ) {		
			// rows for specific user actions	
			if( ! empty( $args['user_id'] ) ) {

				if( is_array( $args['user_id'] ) ) {
					$user_ids = implode( ',', array_map('intval', $args['user_id'] ) );
					$where .=  " AND `user_id` IN( $user_ids ) ";
				} else {
					$user_ids = intval( $args['user_id'] );
					$where .=  $wpdb->prepare( " AND `user_id` = %d", $user_ids );
				}


			}
		}

		if ( !empty( $this->post_id_field ) && !empty( $this->schema[$this->post_id_field] ) ) {		
			if ( is_user_logged_in()
			 && !current_user_can( 'ssa_manage_appointments' ) ) {
				$args['author_id'] = get_current_user_id();
			}

			// rows for specific user accounts
			if( ! empty( $args['author_id'] ) ) {
				if( is_array( $args['author_id'] ) ) {
					$author_ids = implode( ',', array_map('intval', $args['author_id'] ) );
					$where .= $wpdb->prepare( " AND `%i` IN( SELECT ID FROM $wpdb->posts WHERE post_author IN ( $author_ids ) ) ", $this->post_id_field );
				} else {
					$author_ids = intval( $args['author_id'] );
					$where .= $wpdb->prepare( " AND `%i` IN( SELECT ID FROM $wpdb->posts WHERE post_author = $author_ids ) ", $this->post_id_field);
				}
				
			}

			// specific rows by name
			if( ! empty( $args[$this->post_id_field] ) ) {
				if ( is_array( $args[$this->post_id_field] ) ) {
					$post_ids = implode( ',', array_map('intval', $args[$this->post_id_field] ) );
					$where .= $wpdb->prepare( " AND `%i` IN( $post_ids ) ", $this->post_id_field );
				} else {
					$where .= $wpdb->prepare( " AND `%i` = %d ", $this->post_id_field, $args[$this->post_id_field] );
				}
			}
		}

		// specific rows by type
		if ( !empty( $this->schema['type'] ) ) {		
			if( ! empty( $args['type'] ) ) {
				$where .= $wpdb->prepare( " AND `type` = '" . '%s' . "' ", $args['type'] );
			}
		}


		// specific rows by name
		if ( !empty( $this->schema['name'] ) ) {		
			if( ! empty( $args['name'] ) ) {
				$where .= $wpdb->prepare( " AND `name` = '" . '%s' . "' ", $args['name'] );
			}
		}

		if ( !empty( $this->schema['start_date'] ) ) {		
			// Customers created for a specific date or in a date range
			if( ! empty( $args['start_date'] ) && false !== strtotime( $args['start_date'] ) ) {
				$where .= $wpdb->prepare( " AND `start_date` = '" . '%s' . "' ", $args['start_date'] );
			} else {

				if( ! empty( $args['start_date_min'] ) && false !== strtotime( $args['start_date_min'] )) {
					$where .= $wpdb->prepare( " AND `start_date` >= '" . '%s' . "' ", $args['start_date_min'] );
				}

				if( ! empty( $args['start_date_max'] ) && false !== strtotime( $args['start_date_max'] ) ) {
					$where .= $wpdb->prepare( " AND `start_date` <= '" . '%s' . "' ", $args["start_date_max"] );
				}

			}
		}

		if ( !empty( $this->schema['end_date'] ) ) {		
			// Customers created for a specific date or in a date range
			if( ! empty( $args['end_date'] ) ) {
				$where .= $wpdb->prepare( " AND `end_date` = '" . '%s' . "' ", $args['end_date'] );
			} else {

				if( ! empty( $args['end_date_min'] ) && false !== strtotime( $args['end_date_min'] ) ) {
					$where .= $wpdb->prepare( " AND `end_date` >= '" . '%s' . "' ",$args["end_date_min"] );
				}

				if( ! empty( $args['end_date_max'] ) && false !== strtotime( $args['end_date_max'] ) ) {
					$where .= $wpdb->prepare( " AND `end_date` <= '" . '%s' . "' ", $args["end_date_max"] );
				}

			}
		}

		if ( !empty( $this->schema['date_created'] ) ) {		
			// Customers created for a specific date or in a date range
			if( ! empty( $args['date_created'] ) ) {
				$where .= $wpdb->prepare( " AND `date_created` = '" . '%s' . "' ", $args['date_created'] );
			} else {

				if( ! empty( $args['date_created_min'] ) ) {
					$where .= $wpdb->prepare( " AND `date_created` >= '" . '%s' . "' ",$args["date_created_min"] );
				}

				if( ! empty( $args['date_created_max'] ) ) {
					$where .= $wpdb->prepare( " AND `date_created` <= '" . '%s' . "' ", $args["date_created_max"] );
				}

			}
		}

		if ( !empty( $this->schema['date_modified'] ) ) {		
			// Customers created for a specific date or in a date range
			if( ! empty( $args['date_modified'] ) ) {

				if( !is_array( $args['date_modified'] ) ) {

					$year  = date( 'Y', strtotime( $args['date_modified'] ) );
					$month = date( 'm', strtotime( $args['date_modified'] ) );
					$day   = date( 'd', strtotime( $args['date_modified'] ) );

					$where .= " AND $year = YEAR ( date_modified ) AND $month = MONTH ( date_modified ) AND $day = DAY ( date_modified )";
				}

			} else {

				if( ! empty( $args['date_modified_min'] ) ) {
					$where .= $wpdb->prepare( " AND `date_modified` >= '" . '%s' . "' ", $args["date_modified_min"] );
				}

				if( ! empty( $args['date_modified_max'] ) ) {
					$where .= $wpdb->prepare( " AND `date_modified` <= '" . '%s' . "' " , $args["date_modified_max"] );
				}

			}
		}

		return $where;
	}

	private function extract_id_from_input( $input ) {
		if ( is_numeric( $input ) ) {
				return $input;
		} elseif ( is_array( $input ) && isset( $input['id'] ) ) {
				return $input['id'];
		} elseif ( $input instanceof WP_REST_Request ) {
				return $input->get_param('id');
		} elseif ( is_object( $input ) && property_exists( $input, 'id' ) ) {
				return $input->id;
		}
		return false;
	}
	public function get_string_to_tokenize( $input ) {
		$id = $this->extract_id_from_input( $input );
		if ( empty( $id ) ) {
			return false;
		}

		$string_to_tokenize = $id;

		$model_entity = $this->get( $id, -1 );

		if ( ! empty( $model_entity['date_created'] ) ) {
			$string_to_tokenize .= $model_entity['date_created'];
		}

		return $string_to_tokenize;
	}

	/**
	 * New get_id_token that uses the id alongside the date_created field
	 */
	public function get_id_token( $input ) {
		$string_to_tokenize = $this->get_string_to_tokenize( $input );
		if ( empty( $string_to_tokenize ) ) {
			return false;
		}
		return SSA_Utils::site_unique_hash( $string_to_tokenize );
	}
	
	/**
	 * Consider removing this code after 2026-10-10
	 */
	public function deprecated_get_id_token( $input ) {
		$string_to_tokenize = $this->get_string_to_tokenize( $input );
		if ( empty( $string_to_tokenize ) ) {
			return false;
		}
		return SSA_Utils::deprecated_hash( $string_to_tokenize );
	}
	

	/**
	 * All tokens verification should be handled here
	 *
	 * @return bool
	 */
	public function verify_id_token( $input, $token_to_verify ) {

		if ( empty( $input ) || empty( $token_to_verify ) ) {
			return false;
		}

		$correct_token = $this->get_id_token( $input );
		if ( ! empty( $correct_token ) && hash_equals( (string) $correct_token, (string) $token_to_verify ) ) {
			return true;
		}

		// TODO remove this whole branch (including the throttle below) on 2026-09-30.
		$target_timestamp = strtotime( '2026-09-30 00:00:00' );
		$current_timestamp = current_time( 'timestamp' );

		if ( $current_timestamp < $target_timestamp ) {
			// The deprecated salt is in public source, so brute-force against
			// date_created is the only remaining barrier. Cap attempts per appointment
			// id well below what the attack needs; legitimate one-shot email-link clicks
			// never accumulate failures.
			$id = $this->extract_id_from_input( $input );
			if ( ! empty( $id ) ) {
				$throttle_key = 'ssa_dep_tok_fail_' . $id;
				$fails        = (int) get_transient( $throttle_key );
				if ( $fails >= 5 ) {
					return false;
				}

				$deprecated_correct_token = $this->deprecated_get_id_token( $input );
				if ( ! empty( $deprecated_correct_token ) && hash_equals( (string) $deprecated_correct_token, (string) $token_to_verify ) ) {
					delete_transient( $throttle_key );
					return true;
				}

				set_transient( $throttle_key, $fails + 1, 15 * MINUTE_IN_SECONDS );
			}
		}

		return false;
	}

	public function id_token_permissions_check( $request ) {

		$params = $request->get_params();
		if ( empty( $params['token'] ) ) {
			return false;
		}

		return $this->verify_id_token( $request, sanitize_text_field( $params['token'] ) );
	}

	/**
	 * Returns global SSA Token.
	 *
	 * @since 5.7.2
	 *
	 * @return string SSA Token.
	 */
	public function get_token() {
		return apply_filters( 'ssa/api/token', SSA_Utils::site_unique_hash( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' ) );
	}

	/**
	 * Validates global SSA Token.
	 *
	 * @since 5.7.2
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 *
	 * @return bool True if the token is valid, false if not.
	 */
	public function token_permissions_check( $request ) {
		$correct_token = $this->get_token();

		if ( empty( $correct_token ) ) {
			return false;
		}

		$params = $request->get_params();
		if ( empty( $params['token'] ) ) {
			return false;
		}

		if ( hash_equals( (string) $correct_token, (string) sanitize_text_field( $params['token'] ) ) ) {
			return true;
		}

		return false;
	}


}
