<?php

namespace SugarCalendarBookings\Appointments\Admin;

use DateTimeImmutable;
use SugarCalendarBookings\Admin\AbstractListTable;
use SugarCalendarBookings\Appointments\AppointmentModel;
use SugarCalendarBookings\Appointments\AppointmentService;
use SugarCalendarBookings\Customers\CustomerModel;
use SugarCalendarBookings\DeleteManager;
use SugarCalendarBookings\Locations\OnlineLocationProviderFactory;
use SugarCalendarBookings\Payments\Admin\PaymentsDetailsPage;
use SugarCalendarBookings\Payments\PaymentsHelpers;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Services\ServiceModel;
use SugarCalendarBookings\Support\Admin\Notices;
use SugarCalendarBookings\Support\Database\QueryBuilder;
use SugarCalendarBookings\Support\Helpers\Date;
use SugarCalendarBookings\Support\Helpers\Time;
use SugarCalendarBookings\Support\Helpers\UI;
use SugarCalendarBookings\Options;

/**
 * Appointments list table.
 *
 * @since 1.0.0
 * @since 1.2.1 Switched to extend abstract list table.
 */
class AppointmentsListTable extends AbstractListTable {

	/**
	 * Domain-specific default filters.
	 *
	 * @since 1.0.0
	 * @since 1.2.1 Only domain-specific filters (s, status, order, orderby moved to parent).
	 *
	 * @var array
	 */
	protected $active_filters = [
		'month'       => null,
		'year'        => null,
		'service_id'  => null,
		'customer_id' => null,
	];

	/**
	 * Domain-specific filter rules.
	 *
	 * @since 1.2.1
	 *
	 * @var array
	 */
	protected $filter_rules = [
		'month'       => 'numeric',
		'year'        => 'numeric',
		'service_id'  => 'id',
		'customer_id' => 'id',
	];

	/**
	 * Number of appointments by different statuses.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $counts = [];

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {

		// Set parent defaults.
		parent::__construct(
			[
				'singular' => 'appointment',
				'plural'   => 'appointments',
				'screen'   => 'bookings_page_sugar-calendar-bookings-appointments',
			]
		);

		// Process filters.
		$this->active_filters = $this->setup_active_filters();
	}

	/**
	 * Define the table columns.
	 *
	 * @since 1.0.0
	 *
	 * @return array Associative array of slug=>Name columns data.
	 */
	public function get_columns() {

		$columns = [];

		$columns['cb']             = '<input type="checkbox" />';
		$columns['id']             = esc_html__( 'ID', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		$columns['service']        = esc_html__( 'Service', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		$columns['appointment_on'] = esc_html__( 'Appointment On', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		$columns['duration']       = esc_html__( 'Duration', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		$columns['customer']       = esc_html__( 'Customer', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		$columns['location']       = esc_html__( 'Location', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		$columns['payment']        = esc_html__( 'Payment', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		$columns['status']         = esc_html__( 'Status', 'sugar-calendar-bookings-scheduling-appointments-lite' );

		return $columns;
	}

	/**
	 * Retrieve the view statuses.
	 *
	 * @since 1.0.0
	 */
	public function get_views() {

		$base_url       = $this->get_base_url();
		$current_status = $this->get_selected_status();

		$views = [];

		$views['all'] = sprintf(
			'<a href="%1$s" %2$s>%3$s&nbsp;<span class="count">(%4$d)</span></a>',
			esc_url( remove_query_arg( 'status', $base_url ) ),
			$current_status === null ? 'class="current"' : '',
			esc_html__( 'All', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			intval( $this->counts['total'] )
		);

		foreach ( $this->get_statuses() as $status => $status_label ) {
			$count = intval( $this->counts[ 'status_' . $status ] );

			// Skipping status with no appointments.
			if ( $count === 0 && $current_status !== $status ) {
				continue;
			}

			$views[ $status ] = sprintf(
				'<a href="%1$s" %2$s>%3$s&nbsp;<span class="count">(%4$d)</span></a>',
				esc_url( add_query_arg( 'status', $status, $base_url ) ),
				$current_status === $status ? 'class="current"' : '',
				esc_html( $status_label ),
				$count
			);
		}

		return $views;
	}

	/**
	 * Get the base url for the views links.
	 *
	 * @since 1.0.0
	 * @since 1.2.0 Switched to protected method.
	 *
	 * @return string
	 */
	// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
	protected function get_base_url() {

		$base_url = AppointmentsListPage::get_url();
		$filters  = $this->get_applied_filters();

		if ( isset( $filters['status'] ) ) {
			$base_url = add_query_arg( 'status', $filters['status'], $base_url );
		}

		if ( isset( $filters['search'] ) ) {
			$base_url = add_query_arg( 's', $filters['search'], $base_url );
		}

		if ( isset( $filters['month'] ) ) {
			$base_url = add_query_arg( 'month', $filters['month'], $base_url );
		}

		if ( isset( $filters['year'] ) ) {
			$base_url = add_query_arg( 'year', $filters['year'], $base_url );
		}

		if ( isset( $filters['service_id'] ) ) {
			$base_url = add_query_arg( 'service_id', $filters['service_id'], $base_url );
		}

		if ( isset( $filters['customer_id'] ) ) {
			$base_url = add_query_arg( 'customer_id', $filters['customer_id'], $base_url );
		}

		return $base_url;
	}

	/**
	 * Apply filters to the query.
	 *
	 * @since 1.0.0
	 * @since 1.2.0 Switched to protected method.
	 *
	 * @param QueryBuilder $query   Query builder.
	 * @param array        $filters Query parameters.
	 */
	// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
	protected function build_filters_query( $query, $filters ) {

		if ( ! empty( $filters['status'] ) ) {
			$query->where( 'appointments.status', '=', $filters['status'], '%s' );
		}

		if ( ! empty( $filters['search'] ) ) {
			$search_term = $filters['search'];

			$query
				->join( 'services', 'appointments.service_id', '=', 'services.id' )
				->join( 'customers', 'appointments.customer_id', '=', 'customers.id' )
				->join( 'locations', 'appointments.location_id', '=', 'locations.id' )
				->where(
					function ( $q ) use ( $search_term ) {

						$q->where( 'appointments.id', 'like', "%$search_term", '%s' )
							->or_where( 'services.name', 'like', "%$search_term%", '%s' )
							->or_where( 'customers.name', 'like', "%$search_term%", '%s' )
							->or_where( 'locations.name', 'like', "%$search_term%", '%s' );
					}
				);
		}

		if ( ! empty( $filters['month'] ) && ! empty( $filters['year'] ) && $filters['year'] >= 1 ) {
			$timezone = Date::get_default_timezone();

			$start_local = new DateTimeImmutable(
				sprintf( '%04d-%02d-01 00:00:00', $filters['year'], $filters['month'] ),
				$timezone
			);
			$end_local   = $start_local->modify( '+1 month' );

			$start_utc = Date::to_utc( $start_local )->format( Date::DB_DATETIME_FORMAT );
			$end_utc   = Date::to_utc( $end_local )->format( Date::DB_DATETIME_FORMAT );

			$query->where( 'appointments.start_at', '>=', $start_utc, '%s' );
			$query->where( 'appointments.start_at', '<', $end_utc, '%s' );
		} elseif ( ! empty( $filters['year'] ) && $filters['year'] >= 1 ) {
			$timezone = Date::get_default_timezone();

			$start_local = new DateTimeImmutable(
				sprintf( '%04d-01-01 00:00:00', $filters['year'] ),
				$timezone
			);
			$end_local   = $start_local->modify( '+1 year' );

			$start_utc = Date::to_utc( $start_local )->format( Date::DB_DATETIME_FORMAT );
			$end_utc   = Date::to_utc( $end_local )->format( Date::DB_DATETIME_FORMAT );

			$query->where( 'appointments.start_at', '>=', $start_utc, '%s' );
			$query->where( 'appointments.start_at', '<', $end_utc, '%s' );
		}

		if ( ! empty( $filters['service_id'] ) ) {
			$query->where( 'appointments.service_id', '=', $filters['service_id'], '%d' );
		}

		if ( ! empty( $filters['customer_id'] ) ) {
			$query->where( 'appointments.customer_id', '=', $filters['customer_id'], '%d' );
		}
	}

	/**
	 * Get current filters query parameters.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function get_applied_filters() {

		$filters = [
			'status'      => $this->get_selected_status(),
			'search'      => $this->get_search_term(),
			'month'       => $this->get_selected_month(),
			'year'        => $this->get_selected_year(),
			'service_id'  => $this->get_selected_service(),
			'customer_id' => $this->get_selected_customer(),
		];

		return array_filter( $filters, fn( $v ) => $v !== null );
	}

	/**
	 * Return status filter value or null.
	 *
	 * @since 1.0.0
	 *
	 * @return string|null
	 */
	public function get_selected_status() {

		return $this->get_active_filter( 'status' );
	}

	/**
	 * Return search filter values or null.
	 *
	 * @since 1.0.0
	 *
	 * @return string|null
	 */
	public function get_search_term() {

		return $this->get_active_filter( 's' );
	}

	/**
	 * Return month filter value or null.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null
	 */
	public function get_selected_month() {

		return $this->get_active_filter( 'month' );
	}

	/**
	 * Return year filter value or current year.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null
	 */
	public function get_selected_year() {

		$year = $this->get_active_filter( 'year' );

		if ( empty( $year ) ) {
			return $this->get_selected_month() ?
				(int) Date::format_display_date( new DateTimeImmutable(), 'Y' ) :
				null;
		}

		// Validate year is positive (years must be >= 1).
		if ( $year < 1 ) {
			return null;
		}

		return $year;
	}

	/**
	 * Return service filter value or null.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null
	 */
	public function get_selected_service() {

		return $this->get_active_filter( 'service_id' );
	}

	/**
	 * Return customer filter value or null.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null
	 */
	public function get_selected_customer() {

		return $this->get_active_filter( 'customer_id' );
	}

	/**
	 * Get the filter label map for the search results notice.
	 *
	 * @since 1.4.0
	 *
	 * @return array Filter label map.
	 */
	protected function get_filter_label_map() {

		return array_merge(
			parent::get_filter_label_map(),
			[
				'service_id'  => [
					'label' => esc_html__( 'Service', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					'model' => ServiceModel::class,
				],
				'customer_id' => [
					'label' => esc_html__( 'Customer', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					'model' => CustomerModel::class,
				],
			]
		);
	}

	/**
	 * Get the appointment statuses for filtering purpose.
	 *
	 * @since 1.0.0
	 *
	 * @return array Associative array of appointment statuses.
	 */
	private function get_statuses() {

		// In this order statuses will appear in filters bar.
		$statuses = [
			'all'       => esc_html__( 'All', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'approved'  => esc_html__( 'Approved', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'pending'   => esc_html__( 'Pending', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'cancelled' => esc_html__( 'Cancelled', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'draft'     => esc_html__( 'Draft', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'done'      => esc_html__( 'Done', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
		];

		return $statuses;
	}

	/**
	 * Get the items counts for various statuses of appointments.
	 *
	 * @since 1.0.0
	 */
	private function get_counts() {

		$this->counts = [];

		// Base params with applied filters.
		$filters = $this->get_applied_filters();

		unset( $filters['status'] );

		$query = ( new QueryBuilder() )
			->table( 'appointments' )
			->select( [ 'appointments.status', 'COUNT(DISTINCT id)' ] )
			->group_by( [ 'appointments.status' ] );

		$this->build_filters_query( $query, $filters );

		$stats = $query->get();

		$stats = array_combine( array_column( $stats, 'status' ), array_column( $stats, 'count' ) );

		$this->counts['total'] = array_sum( $stats ) - ( $stats['cancelled'] ?? 0 );

		foreach ( $this->get_statuses() as $status => $name ) {
			$count = $stats[ $status ] ?? 0;

			$this->counts[ 'status_' . $status ] = $count;
		}
	}

	/**
	 * Get the data, prepare pagination, process bulk actions.
	 * Prepare columns for display.
	 *
	 * @since 1.0.0
	 */
	public function prepare_items() {

		// Retrieve count.
		$this->get_counts();

		$items_per_page = 20;
		$offset         = ( $this->get_pagenum() - 1 ) * $items_per_page;
		$orderby        = 'id';
		$order          = 'DESC';
		$filters        = $this->get_applied_filters();

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if (
			! empty( $_REQUEST['orderby'] ) &&
			in_array( $_REQUEST['orderby'], array_keys( $this->get_sortable_columns() ), true )
		) {
			$orderby = sanitize_key( $_REQUEST['orderby'] );
		}

		if (
			! empty( $_REQUEST['order'] ) &&
			in_array( strtoupper( $_REQUEST['order'] ), [ 'ASC', 'DESC' ], true )
		) {
			$order = sanitize_key( $_REQUEST['order'] );
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		$query = AppointmentModel::query()
			->with(
				$this->get_appointment_model_relations()
			);

		$this->build_filters_query( $query, $filters );

		$query->order_by( 'appointments.' . $orderby, $order )
			->offset( $offset )
			->limit( $items_per_page )
			->group_by( [ 'appointments.id' ] );

		// Exclude cancelled appointments from 'all' view.
		if ( empty( $filters['status'] ) ) {
			$query->where( 'appointments.status', '!=', 'cancelled', '%s' );
		}

		$this->items = $query->get();

		$total_items = $this->counts['total'];

		if (
			! empty( $filters['status'] ) &&
			isset( $this->counts[ 'status_' . $filters['status'] ] )
		) {
			$total_items = $this->counts[ 'status_' . $filters['status'] ];
		}

		$this->set_pagination_args(
			[
				'total_items' => $total_items,
				'per_page'    => $items_per_page,
			]
		);
	}

	/**
	 * Get the appointment model relations.
	 *
	 * @since 1.2.0
	 *
	 * @return array
	 */
	protected function get_appointment_model_relations() {

		return [
			'service'  => fn( $q ) => $q->select( [ 'id', 'name', 'color' ] ),
			'customer' => fn( $q ) => $q->select( [ 'id', 'name' ] ),
			'location' => fn( $q ) => $q->select( [ 'id', 'name' ] ),
			'meta'     => fn( $q ) => $q->where( 'meta_key', 'IN', [ 'online_location' ], '%s' ),
			'payments' => fn( $q ) => $q->select( [ 'id', 'status' ] ),
		];
	}

	/**
	 * Whether the table has items to display or not.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public function has_items() {

		return count( $this->items ) > 0;
	}

	/**
	 * Message to be displayed when there are no items.
	 *
	 * @since 1.0.0
	 */
	public function no_items() {

		if ( ! empty( $this->get_applied_filters() ) ) {
			esc_html_e( 'No appointments found.', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		} else {
			?>
			<div class="wp-list-table__no-items">
				<div class="wp-list-table__no-items-icon">
					<img width="21" height="24" src="<?php echo esc_url( SCBOOKINGS_PLUGIN_URL . 'assets/images/icons/calendar-week.svg' ); ?>">
				</div>
				<div class="wp-list-table__no-items-title"><?php esc_html_e( 'No Appointments found!', 'sugar-calendar-bookings-scheduling-appointments-lite' ); ?></div>
				<?php
				// TODO: Add plus icon.
				UI::button(
					[
						'type'  => 'tertiary',
						'size'  => 'sm',
						'text'  => esc_html__( 'Add New', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
						'class' => 'sugar-calendar-bookings-btn-new-item',
					]
				);
				?>
			</div>
			<?php
		}
	}

	/**
	 * Extra controls to be displayed between bulk actions and pagination.
	 *
	 * @since 1.0.0
	 *
	 * @param string $which Which tablenav: top or bottom.
	 */
	// phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded
	protected function extra_tablenav( $which ) {

		if ( $which !== 'top' ) {
			return;
		}

		$selected_month = $this->get_selected_month();
		$selected_year  = $this->get_selected_year();
		?>
		<div class="alignleft actions">
			<select name="month">
				<option value=""><?php esc_html_e( 'All Months', 'sugar-calendar-bookings-scheduling-appointments-lite' ); ?></option>
				<?php for ( $month_index = 1; $month_index <= 12; $month_index++ ) : ?>
					<option value="<?php echo esc_attr( $month_index ); ?>" <?php selected( $month_index, $selected_month ); ?>>
						<?php echo esc_html( $GLOBALS['wp_locale']->get_month( $month_index ) ); ?>
					</option>
				<?php endfor; ?>
			</select>
			<input type="number" name="year" value="<?php echo esc_attr( $selected_year ); ?>" placeholder="<?php esc_html_e( 'Year', 'sugar-calendar-bookings-scheduling-appointments-lite' ); ?>" min="1">
			<?php submit_button( esc_html__( 'View', 'sugar-calendar-bookings-scheduling-appointments-lite' ), '', 'view_action', false ); ?>
		</div>
		<div class="alignleft actions">
			<?php $this->display_table_filters(); ?>
			<?php submit_button( esc_html__( 'Filter', 'sugar-calendar-bookings-scheduling-appointments-lite' ), '', 'filter_action', false ); ?>
		</div>
		<?php
	}

	/**
	 * Display table filters.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	private function display_table_filters() {

		$selected_filters = $this->get_selected_filters();
		$table_filters    = $this->get_table_filters();

		foreach ( $table_filters as $key => $filter ) {
			?>
			<select name="<?php echo esc_attr( $key ); ?>">
				<option value=""><?php echo esc_html( $filter['default'] ); ?></option>
				<?php foreach ( $filter['options'] as $option ) : ?>
					<option value="<?php echo esc_attr( $option->id ); ?>" <?php selected( $selected_filters[ $key ], $option->id ); ?>><?php echo esc_html( $option->name ); ?></option>
				<?php endforeach; ?>
			</select>
			<?php
		}
	}

	/**
	 * Get the selected filters.
	 *
	 * @since 1.2.0
	 *
	 * @return array
	 */
	protected function get_selected_filters() {

		return [
			'service_id'  => $this->get_selected_service(),
			'customer_id' => $this->get_selected_customer(),
		];
	}

	/**
	 * Get the table filters.
	 *
	 * @since 1.2.0
	 *
	 * @return array
	 */
	protected function get_table_filters() {

		// TODO: Fetch only services and customers that have appointments.
		$services  = ServiceModel::query()
			->select( [ 'id', 'name' ] )
			->order_by( 'name' )
			->get();
		$customers = CustomerModel::query()
			->select( [ 'id', 'name' ] )
			->order_by( 'name' )
			->get();

		return [
			'service_id'  => [
				'default' => esc_html__( 'All Services', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				'options' => $services,
			],
			'customer_id' => [
				'default' => esc_html__( 'All Customers', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				'options' => $customers,
			],
		];
	}

	/**
	 * Get the name of the primary column.
	 * Important for the mobile view.
	 *
	 * @since 1.0.0
	 *
	 * @return string The name of the primary column.
	 */
	protected function get_primary_column_name() {

		return 'appointment';
	}

	/**
	 * Allow users to select multiple appointments at once (to perform a bulk action, for example).
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment AppointmentModel object.
	 *
	 * @return string Checkbox for bulk selection.
	 */
	protected function column_cb( $appointment ) {

		return sprintf(
			'<input type="checkbox" name="appointment_id[]" value="%d" />',
			$appointment->get_id()
		);
	}

	/**
	 * Render the Appointment ID column.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The current row appointment.
	 *
	 * @return string
	 */
	protected function column_id( $appointment ) {

		return esc_html( $appointment->id );
	}

	/**
	 * Render the Service Name column.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The current row appointment.
	 *
	 * @return string
	 */
	protected function column_service( $appointment ) {

		$service       = $appointment->service;
		$service_name  = ! empty( $service ) ? $service->name : '';
		$service_color = ! empty( $service ) ? $service->color : '';

		$title = '<strong><i style="color: ' . $service_color . '"></i><a href="' . esc_url( $this->get_quick_action_url( $appointment, 'view' ) ) . '" class="row-title">' . esc_html( $service_name ) . '</a></strong>';

		$actions = [];

		if ( current_user_can( 'manage_options' ) ) {
			$actions['view']    = esc_html__( 'View', 'sugar-calendar-bookings-scheduling-appointments-lite' );
			$actions['edit']    = esc_html__( 'Edit', 'sugar-calendar-bookings-scheduling-appointments-lite' );
			$actions['payment'] = esc_html__( 'Payment Details', 'sugar-calendar-bookings-scheduling-appointments-lite' );

			if ( $this->get_selected_status() !== 'cancelled' ) {
				$actions['cancel'] = esc_html__( 'Cancel', 'sugar-calendar-bookings-scheduling-appointments-lite' );
			} else {
				$actions['delete'] = esc_html__( 'Delete', 'sugar-calendar-bookings-scheduling-appointments-lite' );
			}
		}

		$actions = array_combine(
			array_keys( $actions ),
			array_map( fn( $label, $action ) => sprintf( '<a href="%1$s">%2$s</a>', $this->get_quick_action_url( $appointment, $action ), $label ), $actions, array_keys( $actions ) )
		);

		return sprintf( '%1$s %2$s', $title, $this->row_actions( $actions ) );
	}

	/**
	 * Get the link to a certain action: "edit" or "delete" for row actions.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment AppointmentModel object.
	 * @param string           $link        The link type to create.
	 *
	 * @return string
	 */
	// phpcs:ignore Generic.Metrics.CyclomaticComplexity
	private function get_quick_action_url( $appointment, $link = 'edit' ) {

		$url  = '';
		$link = sanitize_key( $link );

		switch ( $link ) {
			case 'edit':
				$url = '#';
				break;

			case 'cancel':
				$url = esc_url(
					wp_nonce_url(
						add_query_arg(
							[
								'action'         => 'cancel',
								'appointment_id' => $appointment->id,
							],
							$this->get_base_url()
						),
						'bulk-appointments'
					)
				);
				break;

			case 'delete':
				$url = esc_url(
					wp_nonce_url(
						add_query_arg(
							[
								'action'         => 'delete',
								'appointment_id' => $appointment->id,
							],
							$this->get_base_url()
						),
						'bulk-appointments'
					)
				);
				break;

			case 'view':
				$url = AppointmentDetailsPage::get_url( $appointment->id );
				break;

			case 'payment':
				$payment = PaymentsHelpers::get_payment( $appointment );
				$url     = $payment ? PaymentsDetailsPage::get_url( $payment->id ) : '#';
				break;
			}

		return $url;
	}

	/**
	 * Render the Appointment On column.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The current row appointment.
	 *
	 * @return string
	 */
	protected function column_appointment_on( $appointment ) {

		$options = Plugin::get( Options::class );

		return sprintf(
			/* translators: %1$s - date; %2$s - time. */
			esc_html__( '%1$s at %2$s', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			Date::format_display_date( $appointment->start_at, $options->get( 'display.date_format' ) ),
			Date::format_display_date( $appointment->start_at, $options->get( 'display.time_format' ) )
		);
	}

	/**
	 * Render the Appointment Duration column.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The current row appointment.
	 *
	 * @return string
	 */
	protected function column_duration( $appointment ) {

		return Time::human_readable_duration( $appointment->get_duration() );
	}

	/**
	 * Render the Appointment Customer column.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The current row appointment.
	 *
	 * @return string
	 */
	protected function column_customer( $appointment ) {

		if ( ! empty( $appointment->customer ) ) {
			return sprintf( '<a href="#" data-customer-id="%1$s">%2$s</a>', esc_attr( $appointment->customer->id ), esc_html( $appointment->customer->name ) );
		}

		return '';
	}

	/**
	 * Render the Appointment Location column.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The current row appointment.
	 *
	 * @return string
	 */
	// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
	protected function column_location( $appointment ) {

		if ( ! empty( $appointment->location ) ) {
			return sprintf( '<a href="#" data-location-id="%1$s">%2$s</a>', esc_attr( $appointment->location->id ), esc_html( $appointment->location->name ) );
		} elseif ( ! empty( $appointment->online_location ) ) {
			$online_location = $appointment->online_location;

			// Zoom locations store join_url instead of url.
			$url = $online_location['join_url'] ?? $online_location['url'] ?? '';

			if ( empty( $url ) ) {
				$provider = OnlineLocationProviderFactory::from_array( $online_location );

				return esc_html( $provider ? $provider->get_display_name() : ( $online_location['provider'] ?? '' ) );
			}

			$location_name = wp_trim_words( $url, 30 );

			return sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>', esc_url( $url ), esc_html( $location_name ) );
		}

		return '';
	}

	/**
	 * Render the Appointment Payment Status column.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The current row appointment.
	 *
	 * @return string
	 */
	protected function column_payment( $appointment ) {

		$payment = PaymentsHelpers::get_payment( $appointment );

		if ( $payment ) {
			return UI::payment_status_badge( $payment, true );
		}
	}

	/**
	 * Render the Appointment Status column.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The current row appointment.
	 *
	 * @return string
	 */
	protected function column_status( $appointment ) {

		return '<span class="status-badge ' . $appointment->status . '">' . $appointment->get_status_label() . '</span>';
	}

	/**
	 * Define a list of available bulk actions.
	 *
	 * @since 1.0.0
	 *
	 * @return array List of actions: slug=>name.
	 */
	public function get_bulk_actions() {

		$actions = [];

		if ( current_user_can( 'manage_options' ) ) {
			if ( $this->get_selected_status() !== 'cancelled' ) {
				$actions['cancel'] = esc_html__( 'Cancel', 'sugar-calendar-bookings-scheduling-appointments-lite' );
			} else {
				$actions['delete'] = esc_html__( 'Delete', 'sugar-calendar-bookings-scheduling-appointments-lite' );
			}
		}

		return $actions;
	}

	/**
	 * Process the bulk actions.
	 *
	 * @since 1.0.0
	 */
	public function process_bulk_actions() {

		$this->display_bulk_action_message();

		if ( empty( $_REQUEST['_wpnonce'] ) ) {
			return;
		}

		if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'bulk-appointments' ) ) {
			return;
		}

		$this->process_bulk_action_single();
	}

	/**
	 * Display a bulk action result message.
	 *
	 * @since 1.0.0
	 */
	// phpcs:ignore Generic.Metrics.CyclomaticComplexity
	private function display_bulk_action_message() {

		$notices = Plugin::get( Notices::class );

		if ( ! $notices->get_query_notice() ) {
			return;
		}

		$message = '';
		$count   = (int) ( $notices->get_query_notice_data()[0] ?? 0 );

		switch ( $notices->get_query_notice() ) {
			case 'bulk_cancelled':
				/* translators: %d - number of cancelled appointments. */
				$message = _n( '%d appointment was successfully cancelled.', '%d appointments were successfully cancelled.', $count, 'sugar-calendar-bookings-scheduling-appointments-lite' );
				break;

			case 'bulk_deleted':
				/* translators: %d - number of deleted appointments. */
				$message = _n( '%d appointment was successfully deleted.', '%d appointments were successfully deleted.', $count, 'sugar-calendar-bookings-scheduling-appointments-lite' );
				break;
		}

		if ( ! empty( $message ) ) {
			Plugin::get( Notices::class )->success( sprintf( $message, $count ) );
		}
	}

	/**
	 * Process single bulk action.
	 *
	 * @since 1.0.0
	 */
	// phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, Generic.Metrics.CyclomaticComplexity.TooHigh
	private function process_bulk_action_single() {

		$action = $this->current_action();

		if ( empty( $action ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
		$ids = isset( $_GET['appointment_id'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['appointment_id'] ) ) : false;

		if ( empty( $ids ) ) {
			return;
		}

		$sendback = $this->get_base_url();

		switch ( $action ) {
			// Cancel appointments.
			case 'cancel':
				$sendback = $this->process_bulk_action_single_cancel( $ids, $sendback );
				break;

			// Delete appointments.
			case 'delete':
				$sendback = $this->process_bulk_action_single_delete( $ids, $sendback );
				break;
		}

		wp_safe_redirect( $sendback );
		exit();
	}

	/**
	 * Process the bulk action cancel.
	 *
	 * @since 1.0.0
	 *
	 * @param array  $ids      IDs to process.
	 * @param string $sendback URL query string.
	 *
	 * @return string
	 */
	private function process_bulk_action_single_cancel( $ids, $sendback ) {

		$cancelled    = 0;
		$appointments = AppointmentModel::query()
			->with( [ 'customer', 'employee', 'service', 'location', 'payments', 'meta' ] )
			->where( 'id', 'IN', $ids, '%d' )
			->get();

		foreach ( $appointments as $appointment ) {
			Plugin::get( AppointmentService::class )->cancel( $appointment );

			++$cancelled;
		}

		return add_query_arg(
			[
				'scbookings-notice'      => 'bulk_cancelled',
				'scbookings-notice-data' => $cancelled,
			],
			$sendback
		);
	}

	/**
	 * Process the bulk action delete.
	 *
	 * @since 1.0.0
	 *
	 * @param array  $ids      IDs to process.
	 * @param string $sendback URL query string.
	 *
	 * @return string
	 */
	private function process_bulk_action_single_delete( $ids, $sendback ) {

		Plugin::get( DeleteManager::class )->delete_appointments( $ids );

		return add_query_arg(
			[
				'scbookings-notice'      => 'bulk_deleted',
				'scbookings-notice-data' => count( $ids ),
			],
			$sendback
		);
	}
}
