<?php
/* 
 * don't extend TD_API_Model
 * instead extend TD_DB_Model
 */
abstract class TD_API_Model extends TD_Model {
	protected $plugin;

	protected $api_enabled = true;
	protected $api_namespace;
	protected $api_version = '1';
	protected $api_base;

	public function __construct( $plugin ) {
		parent::__construct( $plugin );

		$this->plugin = $plugin;
		$this->api_hooks();
	}

	public function api_hooks() {
		if ( empty( $this->api_enabled ) ) {
			return;
		}

		if ( empty( $this->api_namespace ) ) {
			die( 'define $this->api_namespace in ' . get_class( $this ) ); // phpcs:ignore
		}

		add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
	}

	public function rest_api_init() {
		$this->register_routes();
	}

	public function get_api_base() {
		if ( !empty( $this->api_base ) ) {
			return $this->api_base;
		}

		$this->api_base = $this->get_pluralized_slug();
		return $this->api_base;
	}

	/**
	 * Register the routes for the objects of the controller.
	 */
	public function register_routes() {
		$namespace = $this->api_namespace.'/v' . $this->api_version;
		$base = $this->get_api_base();

		register_rest_route( $namespace, '/' . $base, array(
			array(
				'methods'         => WP_REST_Server::READABLE,
				'callback'        => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'            => array(

				),
			),
			array(
				'methods'         => WP_REST_Server::CREATABLE,
				'callback'        => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'            => array(),
			),
		) );

		register_rest_route( $namespace, '/' . $base . '/bulk', array(
			array(
				'methods'         => WP_REST_Server::EDITABLE,
				'callback'        => array( $this, 'update_items' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'            => array(),
			),
		) );

		register_rest_route( $namespace, '/' . $base . '/(?P<id>[\d]+)', array(
			array(
				'methods'         => WP_REST_Server::READABLE,
				'callback'        => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'            => array(
					'context'          => array(
						'default'      => 'view',
					),
				),
			),
			array(
				'methods'         => WP_REST_Server::EDITABLE,
				'callback'        => array( $this, 'update_item' ),
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
				'args'            => array(),
			),
			array(
				'methods'  => WP_REST_Server::DELETABLE,
				'callback' => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'     => array(
					'force'    => array(
						'default'      => false,
					),
				),
			),
		) );

		// Some servers have DELETE method disabled for some reason.
		// It's common enough that this workaround is nice to have as an option.
		register_rest_route( $namespace, '/' . $base . '/(?P<id>[\d]+)/delete', array(
			array(
				'methods'  => WP_REST_Server::CREATABLE,
				'callback' => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'     => array(
					'force'    => array(
						'default'      => false,
					),
				),
			),
		) );
		
		$this->register_custom_routes();
		// register_rest_route( $namespace, '/' . $base . '/schema', array(
		// 	'methods'         => WP_REST_Server::READABLE,
		// 	'callback'        => array( $this, 'get_public_item_schema' ),
		// ) );
	}

	public function register_custom_routes() {
		
	}

	/**
	 * Get a collection of items
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$params = $request->get_params();

		$schema = $this->get_schema();

		$data = $this->query( $params );
		$data = $this->prepare_collection_for_api_response( $data );

		$response = array(
			'response_code' => 200,
			'error' => '',
			'data' => $data,
		);

		return new WP_REST_Response( $response, 200 );
	}

	/**
	 * Get one item from the collection
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$params = $request->get_params();
		$recursive = ( !empty( $params['recursive'] ) ) ? $params['recursive'] : 0;
		$data = $this->get( $params['id'], $recursive );
		$data = $this->prepare_item_for_api_response( $data );

		$response = array(
			'response_code' => 200,
			'error' => '',
			'data' => $data,
		);

		return new WP_REST_Response( $response, 200 );
	}

	public function prepare_item_for_api_response( $item, $recursive=0 ) {
		$keys_to_redact = array();
		foreach ($item as $key => $value) {
			if ( empty( $this->schema[$key]['required_capability'] ) ) {
				continue;
			}

			if ( current_user_can( $this->schema[$key]['required_capability'] ) ) {
				continue;
			}

			$keys_to_redact[$key] = $key;
		}

		$item = array_diff_key( $item, $keys_to_redact );
		return $item;
	}

	public function prepare_collection_for_api_response( $items, $recursive=0 ) {
		$prepared_items = array();
		foreach ($items as $key => $item) {
			$prepared_items[$key] = $this->prepare_item_for_api_response( $item );
		}

		return $prepared_items;
	}

	/**
	 * Create one item from the collection
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|WP_REST_Request
	 */
	public function create_item( $request ) {
		$params = $request->get_params();
		$insert_id = $this->insert( $params );

		if ( empty( $insert_id ) ) {
			$response = array(
				'response_code' => '500',
				'error' => 'Not created',
				'data' => array(),
			);
		} elseif( is_wp_error( $insert_id ) ) {
			$response = array(
				'response_code' => '500',
				'error' => true,
				'data' => $insert_id,
			);
		} elseif( is_array( $insert_id ) && ! empty( $insert_id['error']['code'] ) ) {
			$response = array(
				'response_code' => $insert_id['error']['code'],
				'error' => $insert_id['error']['message'],
				'data' => $insert_id['error']['data'],
			);
		} else {
			$response = array(
				'response_code' => 200,
				'error' => '',
				'data' => $this->get( $insert_id ),
			);
		}

		return new WP_REST_Response( $response, 200 );

	}

	/**
	 * Update one item from the collection
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|WP_REST_Request
	 */
	public function update_item( $request ) {
		$item_id = $request['id'];
		$params = $request->get_params();
		// To remove afte testing (this should happen in the db abstraction level)
		// $schema = $this->get_schema();

		// $default_item = $this->get_field_defaults();
		// if ( isset( $default_item[$this->primary_key] ) ) {
		// 	unset( $default_item[$this->primary_key] );
		// }

		// $updated_item = array();
		// foreach ($default_item as $key => $value) {
		// 	if ( isset( $params[$key] ) ) {
		// 		$updated_item[$key] = $params[$key];
		// 	}
		// }

		// if ( !empty( $schema['date_modified'] ) && empty( $updated_item['date_modified'] ) ) {
		// 	$updated_item['date_modified'] = date('Y-m-d H:i:s' );
		// }
		$response = $this->update( $item_id, $params );

		// Note that $this->update will not update the db if an error key is detected
		// It just returns the data back to frontend
		if ( ! empty( $response['error'] ) ) {
			$error = $response['error'];
			unset($response['error']);

			$response = array(
				'response_code' => 500,
				'error' => $error,
				'data' => $response,
			);
		} else {
			$response = array(
				'response_code' => 200,
				'error' => '',
				'data' => $this->get( $item_id ),
			);
		}
		
		return new WP_REST_Response( $response, 200 );
	}

/**
	 * Update one item from the collection
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|WP_REST_Request
	 */
	public function update_items( $request ) {
		$item_id = $request['id'];
		$params = $request->get_params();
		if ( empty( $params['items']['0']['id'] ) ) {
			return array(
				'response_code' => 500,
				'error' => 'Please specify data: { items: [{ id: 1, title: ...}, {id: 2, title: ...} ...]',
				'data' => '',
			);
		}

		$updated = array();
		foreach ($params['items'] as $key => $item) {
			$item_id = $item['id'];
			unset( $item['id'] );
			$this->update( $item_id, $item );
		}

		// Return all appointment types
		$schema = $this->get_schema();

		$data = $this->query( $params );
		$response = array(
			'response_code' => 200,
			'error' => '',
			'data' => $data,
		);

		return new WP_REST_Response( $response, 200 );
	}

	/**
	 * Delete one item from the collection
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|WP_REST_Request
	 */
	public function delete_item( $request ) {
		if ( empty( $request['id'] ) && $request['id'] !== 0 ) {
			return false;
		}

		$response = array(
			'response_code' => 200,
			'error' => '',
			'data' => $this->delete( $request['id'] ),
		);

		return new WP_REST_Response( $response, 200 );
	}

	public static function logged_in_permissions_check( $request ) {
		if ( is_user_logged_in() ) {
			return true;
		}

		return false;
	}

	public static function nonce_permissions_check( $request ) {
		if ( empty( $request->get_headers()['x_wp_nonce']['0'] ) ) {
			return false;
		}

		$nonce = $request->get_headers()['x_wp_nonce']['0'];

		return (bool) wp_verify_nonce( $nonce, 'wp_rest' );
	}

	/**
	 * Permission gate for routes that are intentionally reachable by anonymous
	 * booking-page visitors AND enforce their own per-request authorization
	 * downstream (id_token / appointment_token ownership, mass-assignment
	 * blocking, field-level redaction). Accepts a session-bound wp_rest nonce
	 * OR the static public_nonce.
	 *
	 * The static public_nonce branch exists so full-page-cached booking pages
	 * still complete a booking after the embedded X-WP-Nonce has aged past the
	 * wp_nonce_tick window (the historical reason public_nonce was introduced).
	 * It is safe ONLY here, because on these routes the static token is not the
	 * security boundary — the downstream check is. Never wire this gate to an
	 * admin or otherwise sensitive route; those use nonce_permissions_check.
	 *
	 * See project CLAUDE.md ("Permission gates default to strict").
	 */
	public static function public_booking_permissions_check( $request ) {
		if ( self::nonce_permissions_check( $request ) ) {
			return true;
		}

		if ( empty( $request->get_headers()['x_public_nonce']['0'] ) ) {
			return false;
		}

		$public_nonce = $request->get_headers()['x_public_nonce']['0'];

		return (bool) self::verify_nonce( $public_nonce, 'wp_rest' );
	}

	/**
	 * Check if a given request has access to get items
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|bool
	 */
	public function get_items_permissions_check( $request ) {
		if ( current_user_can( 'ssa_manage_site_settings' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if a given request has access to get a specific item
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|bool
	 */
	public function get_item_permissions_check( $request ) {
		if ( current_user_can( 'ssa_manage_site_settings' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if a given request has access to create items
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|bool
	 */
	public function create_item_permissions_check( $request ) {
		if ( current_user_can( 'ssa_manage_site_settings' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if a given request has access to update a specific item
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|bool
	 */
	public function update_item_permissions_check( $request ) {
		return $this->create_item_permissions_check( $request );
	}

	/**
	 * Check if a given request has access to delete a specific item
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|bool
	 */
	public function delete_item_permissions_check( $request ) {
		return $this->create_item_permissions_check( $request );
	}

	/**
	 * Get the query params for collections
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'page'                   => array(
				'description'        => 'Current page of the collection.',
				'type'               => 'integer',
				'default'            => 1,
				'sanitize_callback'  => 'absint',
			),
			'per_page'               => array(
				'description'        => 'Maximum number of items to be returned in result set.',
				'type'               => 'integer',
				'default'            => 10,
				'sanitize_callback'  => 'absint',
			),
			'search'                 => array(
				'description'        => 'Limit results to those matching a string.',
				'type'               => 'string',
				'sanitize_callback'  => 'sanitize_text_field',
			),
		);
	}

	/**
	 * Static, site-wide "you came from a booking page" token. This is NOT an
	 * authentication primitive — every visitor receives the same value, so it
	 * cannot differentiate authorized from unauthorized callers. The ONLY
	 * legitimate consumer is public_booking_permissions_check, which is wired
	 * exclusively to routes safe for any unauthenticated booking-page visitor
	 * (and which enforce per-request authorization downstream). Never honor it
	 * from nonce_permissions_check or any other gate. The historical bug
	 * (#1910, #1925, #1926) was using it as a fallback inside the shared
	 * nonce_permissions_check, which non-public routes also inherited.
	 *
	 * See project CLAUDE.md ("Permission gates default to strict").
	 */
	public static function create_nonce( $action = -1, $user_id = false ) {
		if ( $user_id !== false ) {
			if ( empty( $user_id ) ) {
				$user_id = get_current_user_id();
			}
		}

		return substr( wp_hash( $action . '|' . $user_id, 'nonce' ), -12, 10 );
	}

	public static function verify_nonce( $nonce, $action = -1, $user_id = false ) {
		$nonce = (string) $nonce;

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

		$expected = self::create_nonce( $action, $user_id );

		if ( hash_equals( $expected, $nonce ) ) {
			return 1;
		}

		return false;
	}
}
