<?php
/**
 * WC_Bookings_CSV_Exporter class
 *
 * @package WooCommerce Bookings
 * @since 3.6.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WC_CSV_Batch_Exporter', false ) ) {
	include_once WC_ABSPATH . 'includes/export/abstract-wc-csv-batch-exporter.php';
}

/**
 * Booking instances CSV exporter.
 *
 * Mirrors WC_GC_CSV_Exporter from WooCommerce Gift Cards: extends WC_CSV_Batch_Exporter and
 * exposes 1:1 column hooks so 3rd parties can swap or extend any field via filter without
 * subclassing. Exports booking-related fields only (not bookable product configuration —
 * that is handled by WC_Booking_Products_Export / WC_Bookings_Single_Export).
 *
 * @version 3.6.0
 */
class WC_Bookings_CSV_Exporter extends WC_CSV_Batch_Exporter {

	/**
	 * Type of export used in filter names.
	 *
	 * @var string
	 */
	protected $export_type = 'bookings';

	/**
	 * Should meta data be exported?
	 *
	 * @var bool
	 */
	protected $enable_meta_export = false;

	/**
	 * Query filters captured from the modal form.
	 *
	 * @var array|false
	 */
	protected $filters = false;

	/**
	 * Explicit list of booking IDs to export, bypassing filters.
	 *
	 * @var int[]
	 */
	protected $booking_ids = array();

	/**
	 * Force-cast percent to integer (matches gift cards override; avoids client-side string compare quirks).
	 *
	 * @return int
	 */
	public function get_percent_complete() {
		return absint( parent::get_percent_complete() );
	}

	/**
	 * Limit the export to a specific set of booking IDs (selected from the list table).
	 *
	 * @param array $ids Booking post IDs.
	 */
	public function set_booking_ids_to_export( $ids ) {
		$this->booking_ids = array_values( array_filter( array_map( 'absint', (array) $ids ) ) );
	}

	/**
	 * Toggle meta export.
	 *
	 * @param mixed $export_meta Whether to export meta data.
	 */
	public function set_export_meta( $export_meta ) {
		$this->enable_meta_export = wc_string_to_bool( $export_meta );
	}

	/**
	 * Default columns. All Booking-related fields.
	 *
	 * @return array
	 */
	public function get_default_column_names() {
		/**
		 * Filters the default columns for the bookings CSV export.
		 *
		 * @since 3.6.0
		 *
		 * @param array $columns Map of column id to header label.
		 */
		return apply_filters(
			"woocommerce_{$this->export_type}_export_default_columns",
			array(
				'id'                       => __( 'ID', 'woocommerce-bookings' ),
				'status'                   => __( 'Status', 'woocommerce-bookings' ),
				'attendance_status'        => __( 'Attendance status', 'woocommerce-bookings' ),
				'all_day'                  => __( 'All day', 'woocommerce-bookings' ),
				'start'                    => __( 'Start date', 'woocommerce-bookings' ),
				'end'                      => __( 'End date', 'woocommerce-bookings' ),
				'local_timezone'           => __( 'Customer timezone', 'woocommerce-bookings' ),
				'cost'                     => __( 'Cost', 'woocommerce-bookings' ),
				'product_id'               => __( 'Product ID', 'woocommerce-bookings' ),
				'product_name'             => __( 'Product name', 'woocommerce-bookings' ),
				'resource_id'              => __( 'Resource ID', 'woocommerce-bookings' ),
				'resource_name'            => __( 'Resource name', 'woocommerce-bookings' ),
				'person_counts'            => __( 'Person counts', 'woocommerce-bookings' ),
				'persons_total'            => __( 'Persons total', 'woocommerce-bookings' ),
				'order_id'                 => __( 'Order ID', 'woocommerce-bookings' ),
				'order_item_id'            => __( 'Order item ID', 'woocommerce-bookings' ),
				'order_status'             => __( 'Order status', 'woocommerce-bookings' ),
				'parent_id'                => __( 'Parent booking ID', 'woocommerce-bookings' ),
				'customer_id'              => __( 'Customer user ID', 'woocommerce-bookings' ),
				'woo_customer_id'          => __( 'Analytics customer ID', 'woocommerce-bookings' ),
				'customer_name'            => __( 'Customer name', 'woocommerce-bookings' ),
				'customer_email'           => __( 'Customer email', 'woocommerce-bookings' ),
				'google_calendar_event_id' => __( 'Google Calendar event ID', 'woocommerce-bookings' ),
				'note'                     => __( 'Customer note', 'woocommerce-bookings' ),
				'date_created'             => __( 'Created date', 'woocommerce-bookings' ),
				'date_modified'            => __( 'Modified date', 'woocommerce-bookings' ),
				'date_cancelled'           => __( 'Cancelled date', 'woocommerce-bookings' ),
			)
		);
	}

	/**
	 * Prepare data to export.
	 */
	public function prepare_data_to_export() {
		// Explicit selection short-circuits all filtering and pagination by ID list.
		if ( ! empty( $this->booking_ids ) ) {
			$this->total_rows = count( $this->booking_ids );
			$offset           = $this->get_limit() * ( $this->get_page() - 1 );
			$page_ids         = array_slice( $this->booking_ids, $offset, $this->get_limit() );
			$this->row_data   = array();

			foreach ( $page_ids as $booking_id ) {
				try {
					$booking = new WC_Booking( $booking_id );
				} catch ( Exception $e ) {
					continue;
				}

				$this->row_data[] = $this->generate_row_data( $booking );
			}

			return;
		}

		$args        = $this->get_query_args();
		$booking_ids = WC_Booking_Data_Store::get_booking_ids_by( $args );

		// Total count (re-run without limit/offset).
		$count_args           = $args;
		$count_args['limit']  = -1;
		$count_args['offset'] = 0;
		$this->total_rows     = count( WC_Booking_Data_Store::get_booking_ids_by( $count_args ) );
		$this->row_data       = array();

		foreach ( $booking_ids as $booking_id ) {
			try {
				$booking = new WC_Booking( $booking_id );
			} catch ( Exception $e ) {
				continue;
			}

			$this->row_data[] = $this->generate_row_data( $booking );
		}
	}

	/**
	 * Build the args passed to WC_Booking_Data_Store::get_booking_ids_by() for the current page.
	 *
	 * Kept separate from prepare_data_to_export() so the filter→query-args mapping can be
	 * unit tested without hitting the database.
	 *
	 * @return array
	 */
	protected function get_query_args() {
		$args = array(
			'limit'    => $this->get_limit(),
			'offset'   => $this->get_limit() * ( $this->get_page() - 1 ),
			'order_by' => 'date_created',
			'order'    => 'DESC',
			'status'   => $this->get_exportable_statuses(),
		);

		// Captured filters (from the list-screen URL params).
		if ( ! empty( $this->filters ) && is_array( $this->filters ) ) {
			// Multi-select status filter takes precedence over the single carried-over status.
			if ( ! empty( $this->filters['statuses'] ) && is_array( $this->filters['statuses'] ) ) {
				$args['status'] = array_values( array_filter( array_map( 'sanitize_key', $this->filters['statuses'] ) ) );
			} elseif ( ! empty( $this->filters['status'] ) ) {
				$args['status'] = array( $this->filters['status'] );
			}

			// Custom date range overrides the month filter when both are present.
			if ( ! empty( $this->filters['date_from'] ) || ! empty( $this->filters['date_to'] ) ) {
				$from = ! empty( $this->filters['date_from'] ) ? strtotime( $this->filters['date_from'] . ' 00:00:00' ) : false;
				$to   = ! empty( $this->filters['date_to'] ) ? strtotime( $this->filters['date_to'] . ' 23:59:59' ) : false;

				// Include any booking overlapping the range, not just those fully contained.
				if ( $from && $to ) {
					$args['date_between'] = array(
						'start' => $from,
						'end'   => $to,
					);
				} elseif ( $from ) {
					// Open upper bound [from, ∞): any booking ending on/after `from`.
					// date_end_after is a strict `>` in the data store; subtract a second so the
					// `from` boundary is inclusive, matching the two-sided date_between path.
					$args['date_end_after'] = $from - 1;
				} elseif ( $to ) {
					// Open lower bound (∞, to]: any booking starting on/before `to`.
					// date_start_before is a strict `<`; add a second so the `to` boundary is inclusive.
					$args['date_start_before'] = $to + 1;
				}
			} elseif ( ! empty( $this->filters['date'] ) && 6 === strlen( (string) $this->filters['date'] ) ) {
				// Month carryover filters the created date (post_date) to match the list table's `m` dropdown.
				$year                        = substr( (string) $this->filters['date'], 0, 4 );
				$month                       = substr( (string) $this->filters['date'], 4, 2 );
				$month_start                 = strtotime( $year . '/' . $month . '/1 00:00:00' );
				$args['date_created_after']  = $month_start;
				$args['date_created_before'] = strtotime( '+1 month', $month_start ) - 1;
			}

			// Multi-select product filter takes precedence over the single list-table dropdown.
			if ( ! empty( $this->filters['product_ids'] ) && is_array( $this->filters['product_ids'] ) ) {
				$args['object_id']   = array_filter( array_map( 'absint', $this->filters['product_ids'] ) );
				$args['object_type'] = 'product';
			} elseif ( ! empty( $this->filters['filter_bookings'] ) ) {
				$filter_id           = absint( $this->filters['filter_bookings'] );
				$is_resource         = ( 'bookable_resource' === get_post_type( $filter_id ) );
				$args['object_id']   = array( $filter_id );
				$args['object_type'] = $is_resource ? 'resource' : 'product';
			}
		}

		/**
		 * Filters the query args used to fetch booking IDs for the CSV export.
		 *
		 * @since 3.6.0
		 *
		 * @param array $args Args passed to WC_Booking_Data_Store::get_booking_ids_by().
		 */
		return apply_filters( "woocommerce_{$this->export_type}_export_query_args", $args );
	}

	/**
	 * Statuses considered exportable by default. Matches the post statuses the bookings list table shows.
	 *
	 * @return array
	 */
	protected function get_exportable_statuses() {
		$statuses = array_keys(
			array_merge(
				get_wc_booking_statuses( null, true ),
				get_wc_booking_statuses( 'user', true ),
				get_wc_booking_statuses( 'cancel', true )
			)
		);

		/**
		 * Filters the booking statuses considered exportable.
		 *
		 * @since 3.6.0
		 *
		 * @param string[] $statuses Booking post statuses.
		 */
		return apply_filters( "woocommerce_{$this->export_type}_export_statuses", array_unique( $statuses ) );
	}

	/**
	 * Build a single CSV row for a booking.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return array
	 */
	protected function generate_row_data( $booking ) {
		$columns = $this->get_column_names();
		$row     = array();

		foreach ( $columns as $column_id => $column_name ) {
			$column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id;
			$value     = '';

			if ( in_array( $column_id, array( 'meta' ), true ) || ! $this->is_column_exporting( $column_id ) ) {
				continue;
			}

			if ( has_filter( "woocommerce_{$this->export_type}_export_column_{$column_id}" ) ) {
				/**
				 * Filters a single column value before it is written to the CSV.
				 *
				 * @since 3.6.0
				 *
				 * @param string     $value     Default (empty) value.
				 * @param WC_Booking $booking   Booking object.
				 * @param string     $column_id Column id.
				 */
				$value = apply_filters( "woocommerce_{$this->export_type}_export_column_{$column_id}", '', $booking, $column_id );
			} elseif ( is_callable( array( $this, "get_column_value_{$column_id}" ) ) ) {
				$value = $this->{"get_column_value_{$column_id}"}( $booking );
			} elseif ( is_callable( array( $booking, "get_{$column_id}" ) ) ) {
				$value = $booking->{"get_{$column_id}"}();
			}

			$row[ $column_id ] = $value;
		}

		$this->prepare_meta_for_export( $booking, $row );

		/**
		 * Filters the full row data (column => value) before it is written to the CSV.
		 *
		 * @since 3.6.0
		 *
		 * @param array                     $row      Row data, keyed by column id.
		 * @param WC_Booking                $booking  Booking object.
		 * @param WC_Bookings_CSV_Exporter  $exporter Exporter instance.
		 */
		return apply_filters( "woocommerce_{$this->export_type}_export_row_data", $row, $booking, $this );
	}

	/**
	 * Append booking post-meta as `meta:<key>` columns when meta export is enabled.
	 *
	 * @param WC_Booking $booking Booking.
	 * @param array      $row     Row data (by reference).
	 */
	protected function prepare_meta_for_export( $booking, &$row ) {
		if ( ! $this->enable_meta_export ) {
			return;
		}

		$meta = get_post_meta( $booking->get_id() );

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

		// Skip core booking props that are already exposed as first-class columns.
		// Inferred from the data store's meta-key → prop map so the two stay in sync.
		$data_store = WC_Data_Store::load( 'booking' );
		$core_keys  = array_keys( $data_store->get_meta_key_to_props_map() );
		$core_keys  = array_merge( $core_keys, array( '_edit_lock', '_edit_last' ) );

		/**
		 * Filters meta keys excluded from the per-booking meta export.
		 *
		 * @since 3.6.0
		 *
		 * @param string[]   $core_keys Meta keys to skip (already includes core booking props).
		 * @param WC_Booking $booking   Booking object.
		 */
		$meta_keys_to_skip = (array) apply_filters( "woocommerce_{$this->export_type}_export_skip_meta_keys", $core_keys, $booking );

		foreach ( $meta as $key => $values ) {
			if ( in_array( $key, $meta_keys_to_skip, true ) ) {
				continue;
			}

			$value = is_array( $values ) && 1 === count( $values ) ? maybe_unserialize( reset( $values ) ) : array_map( 'maybe_unserialize', (array) $values );

			if ( ! is_scalar( $value ) ) {
				$value = wp_json_encode( $value );
			}

			$column_key = 'meta:' . esc_attr( $key );
			/* translators: %s: meta key */
			$this->column_names[ $column_key ] = sprintf( __( 'Meta: %s', 'woocommerce-bookings' ), $key );
			$row[ $column_key ]                = $value;
		}
	}

	/**
	 * Set filters from the AJAX request payload.
	 *
	 * Accepts the same URL parameter names used on the Bookings list table
	 * (`m` for month, `post_status` for status, `filter_bookings` for product/resource),
	 * so the export form can pass them through verbatim from hidden inputs.
	 *
	 * @param array $post_data $_POST data.
	 */
	public function set_filters( $post_data ) {
		$product_ids = array();
		if ( ! empty( $post_data['product_ids'] ) ) {
			$raw         = is_array( $post_data['product_ids'] ) ? $post_data['product_ids'] : explode( ',', (string) $post_data['product_ids'] );
			$product_ids = array_filter( array_map( 'absint', $raw ) );
		}

		$statuses = array();
		if ( ! empty( $post_data['statuses'] ) ) {
			$raw      = is_array( $post_data['statuses'] ) ? $post_data['statuses'] : explode( ',', (string) $post_data['statuses'] );
			$statuses = array_filter( array_map( 'sanitize_key', $raw ) );
		}

		$date_from = ! empty( $post_data['date_from'] ) ? wc_clean( wp_unslash( $post_data['date_from'] ) ) : '';
		$date_to   = ! empty( $post_data['date_to'] ) ? wc_clean( wp_unslash( $post_data['date_to'] ) ) : '';

		// Y-m-d only — anything else is dropped to keep the SQL date math safe.
		if ( $date_from && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_from ) ) {
			$date_from = '';
		}

		if ( $date_to && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_to ) ) {
			$date_to = '';
		}

		$this->filters = array(
			'date'            => isset( $post_data['m'] ) && '0' !== (string) $post_data['m'] ? absint( $post_data['m'] ) : false,
			'status'          => isset( $post_data['post_status'] ) && '' !== $post_data['post_status'] ? sanitize_key( wp_unslash( $post_data['post_status'] ) ) : false,
			'filter_bookings' => isset( $post_data['filter_bookings'] ) && '' !== $post_data['filter_bookings'] ? absint( $post_data['filter_bookings'] ) : false,
			'product_ids'     => $product_ids,
			'statuses'        => $statuses,
			'date_from'       => $date_from,
			'date_to'         => $date_to,
		);
	}

	/*
	|--------------------------------------------------------------------------
	| Column value resolvers.
	|--------------------------------------------------------------------------
	|
	| One method per non-trivial column. The dispatch in generate_row_data() will
	| pick `get_column_value_{column_id}` over the generic `get_{column_id}` getter,
	| so subclasses or filters can override the formatting per field.
	|
	*/

	/**
	 * Booking ID.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return int
	 */
	protected function get_column_value_id( $booking ) {
		return $booking->get_id();
	}

	/**
	 * All day flag.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_all_day( $booking ) {
		return $booking->is_all_day() ? 'yes' : 'no';
	}

	/**
	 * Start date as ISO 8601 with site offset.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_start( $booking ) {
		return wc_bookings_format_csv_datetime( $booking->get_start() );
	}

	/**
	 * End date as ISO 8601 with site offset.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_end( $booking ) {
		return wc_bookings_format_csv_datetime( $booking->get_end() );
	}

	/**
	 * Created date as ISO 8601 with site offset.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_date_created( $booking ) {
		return wc_bookings_format_csv_datetime( $booking->get_date_created() );
	}

	/**
	 * Modified date as ISO 8601 with site offset.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_date_modified( $booking ) {
		return wc_bookings_format_csv_datetime( $booking->get_date_modified() );
	}

	/**
	 * Cancelled date as ISO 8601 with site offset.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_date_cancelled( $booking ) {
		return wc_bookings_format_csv_datetime( $booking->get_date_cancelled() );
	}

	/**
	 * Person counts as JSON ("{type_id: count}").
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_person_counts( $booking ) {
		$counts = $booking->get_person_counts();
		return ! empty( $counts ) ? wp_json_encode( $counts ) : '';
	}

	/**
	 * Total person count.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return int
	 */
	protected function get_column_value_persons_total( $booking ) {
		return (int) $booking->get_persons_total();
	}

	/**
	 * Linked product name (resolved at export time — not stored on the booking).
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_product_name( $booking ) {
		$product = $booking->get_product();
		return $product ? $product->get_name() : '';
	}

	/**
	 * Linked resource name.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_resource_name( $booking ) {
		$resource = $booking->get_resource();
		return $resource ? $resource->post_title : '';
	}

	/**
	 * Order status (resolved from the linked order).
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_order_status( $booking ) {
		$order = $booking->get_order();
		return $order ? $order->get_status() : '';
	}

	/**
	 * Customer display name (uses WC_Booking::get_customer() — handles guests).
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_customer_name( $booking ) {
		$customer = $booking->get_customer();
		return isset( $customer->name ) ? $customer->name : '';
	}

	/**
	 * Customer email.
	 *
	 * @param WC_Booking $booking Booking object.
	 * @return string
	 */
	protected function get_column_value_customer_email( $booking ) {
		$customer = $booking->get_customer();
		return isset( $customer->email ) ? $customer->email : '';
	}
}
