<?php
/**
 * Data Export functionality
 *
 * Handles data export requests, CSV generation, and file management
 * for PersonalizeWP visitor activity data.
 *
 * @link https://personalizewp.com/
 * @since 3.3.0
 *
 * @package PersonalizeWP
 * @subpackage PersonalizeWP/Admin
 */

namespace PersonalizeWP\Admin;

use PersonalizeWP\Traits\SingletonTrait;
use PersonalizeWP\Admin\PageDataExport;


defined( 'ABSPATH' ) || exit; // Exit if accessed directly.

/**
 * Data Export functionality
 */
class Data_Export {

	use SingletonTrait;

	/**
	 * The post type for export requests
	 */
	const POST_TYPE = 'pwp_export_request';

	/**
	 * The meta key for export status
	 */
	const META_EXPORT_STATUS = '_export_status';

	/**
	 * The meta key for completed status date
	 */
	const META_EXPORT_PROCESSING_DATE = '_export_processing_date';

	/**
	 * The meta key for completed status date
	 */
	const META_EXPORT_COMPLETED_DATE = '_export_completed_date';

	/**
	 * The meta key for export file path
	 */
	const META_EXPORT_FILE = '_export_file';

	/**
	 * The meta key for export requester email
	 */
	const META_EXPORT_EMAIL = '_export_email';

	/**
	 * The meta key for export requester email sent
	 */
	const META_EXPORT_EMAIL_SENT = '_export_email_sent';

	/**
	 * Export statuses
	 */
	const STATUS_PENDING    = 'pending'; // When created
	const STATUS_PROCESSING = 'processing'; // Once download data selected for first time
	const STATUS_COMPLETED  = 'completed'; // Once downloaded once
	const STATUS_FAILED     = 'failed'; // Any failure

	/**
	 * Setup hooks and initialize Data Export functionality
	 */
	public function __construct() {
		add_action( 'init', array( $this, 'register_export_request_cpt' ) );
	}

	/**
	 * Register the hidden custom post type for export requests
	 */
	public function register_export_request_cpt() {
		register_post_type(
			self::POST_TYPE,
			array(
				'labels'              => array(
					'name'          => __( 'Export Requests', 'personalizewp' ),
					'singular_name' => __( 'Export Request', 'personalizewp' ),
				),
				'public'              => false,
				'publicly_queryable'  => false,
				'show_ui'             => false,
				'show_in_menu'        => false,
				'show_in_nav_menus'   => false,
				'show_in_admin_bar'   => false,
				'show_in_rest'        => false,
				'has_archive'         => false,
				'exclude_from_search' => true,
				'capability_type'     => 'post',
				'supports'            => array( 'title', 'author' ),
				'can_export'          => false,
			)
		);
	}

	/**
	 * Handles exporting an export request via AJAX.
	 *
	 * @since 3.5.0
	 */
	public function wp_ajax_process_export_data() {

		if ( empty( $_POST['id'] ) ) {
			wp_send_json_error( __( 'Missing request ID.', 'personalizewp' ) );
		}

		$request_id = (int) $_POST['id'];

		if ( $request_id < 1 ) {
			wp_send_json_error( __( 'Invalid request ID.', 'personalizewp' ) );
		}

		// Restricted to 'admin' users, same as the admin screen.
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( __( 'Sorry, you are not allowed to perform this action.' ) );
		}

		check_ajax_referer( 'pwp-export-data-' . $request_id, 'security' );

		// Get the request.
		$request = get_post( $request_id );

		if ( ! $request || self::POST_TYPE !== $request->post_type ) {
			wp_send_json_error( __( 'Invalid request type.', 'personalizewp' ) );
		}

		if ( ! isset( $_POST['page'] ) ) {
			wp_send_json_error( __( 'Missing page index.', 'personalizewp' ) );
		}

		$page = (int) $_POST['page'];

		if ( $page < 1 ) {
			wp_send_json_error( __( 'Page index cannot be less than one.', 'personalizewp' ) );
		}

		$filename  = null;
		$_filename = get_post_meta( $request->ID, self::META_EXPORT_FILE, true );
		// Check and restore previously stored filename.
		if ( ! empty( $_filename ) ) {
			$filename = $_filename;
		}

		// IF first page, generate CSV headers and store filename.
		// Note even if already generated.
		if ( 1 === $page ) {
			update_post_meta( $request_id, self::META_EXPORT_STATUS, self::STATUS_PROCESSING );
			update_post_meta( $request_id, self::META_EXPORT_PROCESSING_DATE, time() );

			// Just in case clear previously stored file as re-generating.
			if ( ! empty( $filename ) ) {
				$export_dir = $this->get_export_directory();
				if ( ! empty( $export_dir ) ) {
					$file_path = $export_dir . '/' . $filename;
					if ( $file_path && file_exists( $file_path ) ) {
						wp_delete_file( $file_path );
					}
				}
			}

			$filename = $this->generate_csv_header( $request->ID );
			if ( is_wp_error( $filename ) ) {
				wp_send_json_error( $filename );

			} else {
				// Store filename for future calls.
				update_post_meta( $request->ID, self::META_EXPORT_FILE, $filename );
			}
		}

		if ( empty( $filename ) ) {
			wp_send_json_error( __( 'Invalid data export file.', 'personalizewp' ) );
		}

		// Export params should be direct in post_content, might be in meta.
		$export_params = ! empty( $request->post_content ) ? json_decode( $request->post_content, true ) : get_post_meta( $request->ID, '_export_params', true );

		if ( empty( $export_params ) ) {
			wp_send_json_error( __( 'Invalid data export request.', 'personalizewp' ) );
		}

		// Generate next set of data
		list( 'data' => $data_to_export, 'total' => $total, 'done' => $done ) = $this->get_export_rows( $request_id, $export_params, $page );

		if ( ! empty( $data_to_export ) ) {
			// Store the results as CSV rows
			$result = $this->populate_csv_from_data( $request_id, $data_to_export );

			if ( is_wp_error( $result ) ) {
				wp_send_json_error( $result );
			}
		}

		$response = array(
			'data'     => array(),
			'total'    => $total,
			'done'     => $done,
			'download' => '',
		);

		if ( $done ) {
			$response['download'] = admin_url( PageDataExport::$download_url . (int) $request_id );

			$success = true;
			// As done, check and send email notification
			$email = $this->get_export_email( $request_id );
			if ( ! empty( $email ) ) {
				$success = $this->send_export_notification( $request_id, $email );
			}

			if ( $success ) {
				// Mark as completed
				update_post_meta( $request_id, self::META_EXPORT_STATUS, self::STATUS_COMPLETED );
				update_post_meta( $request_id, self::META_EXPORT_COMPLETED_DATE, time() );
			}

			// If DONE zip the csv up to decrease server overheads?
		}

		wp_send_json_success( $response );
	}

	/**
	 * Create a new export request
	 *
	 * @param array  $export_params Export parameters
	 * @param string $email         Requester email
	 * @return int|\WP_Error Post ID on success, WP_Error on failure
	 */
	public function create_export_request( $export_params, $email ) {

		$email     = sanitize_email( $email );
		$post_data = array(
			'post_type'    => self::POST_TYPE,
			'post_title'   => ( ! empty( $email ) && is_email( $email ) ) ? $email : __( 'Export request', 'personalizewp' ),
			'post_status'  => 'publish',
			'post_author'  => get_current_user_id(),
			'post_content' => wp_json_encode( $export_params ),
		);

		$post_id = wp_insert_post( $post_data );

		if ( is_wp_error( $post_id ) ) {
			return $post_id;
		}

		// Save export parameters
		update_post_meta( $post_id, self::META_EXPORT_STATUS, self::STATUS_PENDING );

		/**
		 * Fires after export request is created
		 *
		 * @since 3.3.0
		 *
		 * @param int    $post_id       Export request post ID
		 * @param array  $export_params Export parameters
		 * @param string $email         Requester email
		 */
		do_action( 'personalizewp_export_request_created', $post_id, $export_params, $email );

		return $post_id;
	}

	/**
	 * Gets the email to send an export notification to.
	 *
	 * @since 3.5.0
	 *
	 * @param int     $request_id Export request ID
	 * @return string $exporter   Email address or empty if none set
	 */
	public function get_export_email( int $post_id ) {

		$export = get_post( $post_id );
		if ( ! $export instanceof \WP_Post || self::POST_TYPE !== $export->post_type ) {
			return '';
		}

		$_email = is_email( $export->post_title ) ? $export->post_title : get_post_meta( $export->ID, self::META_EXPORT_EMAIL, true );

		$return = ! empty( $_email ) ? sanitize_email( $_email ) : '';

		return $return;
	}

	/**
	 * Checks if there is a valid download file for the export
	 *
	 * @since 3.5.0
	 *
	 * @param int $request_id Export request ID
	 * @return bool True if a valid export file exist
	 */
	public function has_export_file( int $post_id ): bool {

		$export_dir = $this->get_export_directory();
		if ( ! $export_dir ) {
			return false;
		}

		$filename = get_post_meta( $post_id, self::META_EXPORT_FILE, true );
		if ( ! empty( $filename ) ) {
			$file_path = $export_dir . '/' . $filename;

			return file_exists( $file_path );
		}
		return false;
	}

	/**
	 * Validate and sanitize export form data
	 *
	 * @param array $data Raw form data
	 * @return array|false Sanitized data on success, false on validation failure
	 */
	public function validate_sanitize_export_data( array $data ) {

		$validation_errors = array();

		$data = wp_unslash( $data );

		/**
		 * Filters the sanitized export parameters to be used for an export.
		 *
		 * @since 3.4.0
		 *
		 * @param array[] $export_params Default export params
		 * @param array[] $data          Raw form POST data
		 * @return array[]
		 */
		$export_params = apply_filters(
			'personalizewp_export_params',
			array(
				'export_type'       => isset( $data['export_type'] ) ? sanitize_text_field( $data['export_type'] ) : 'all_visitors',
				'wp_user_id'        => isset( $data['wp_user_id'] ) ? (int) $data['wp_user_id'] : 0,
				'include_anonymous' => ! empty( $data['include_anonymous'] ),
				'event_filter'      => isset( $data['event_filter'] ) ? sanitize_text_field( $data['event_filter'] ) : 'all_events',
				'event_type'        => isset( $data['event_type'] ) ? sanitize_text_field( $data['event_type'] ) : '',
				'start_date'        => isset( $data['start_date'] ) ? sanitize_text_field( $data['start_date'] ) : '',
				'end_date'          => isset( $data['end_date'] ) ? sanitize_text_field( $data['end_date'] ) : '',
			),
			$data
		);

		$email = isset( $data['email'] ) ? sanitize_email( wp_unslash( $data['email'] ) ) : '';

		// Validate email if provided
		if ( ! empty( $email ) && ! is_email( $email ) ) {
			$validation_errors[] = array(
				'input'   => 'email',
				'message' => __( 'Please enter a valid email address.', 'personalizewp' ),
			);
		}

		// Validate single visitor selection
		if ( 'single_visitor' === $export_params['export_type'] && empty( $export_params['wp_user_id'] ) ) {
			$validation_errors[] = array(
				'input'   => 'visitor-search',
				'message' => __( 'Please select a valid WordPress User.', 'personalizewp' ),
			);
		}

		// Validate single event selection
		if ( 'single_event' === $export_params['event_filter'] && empty( $export_params['event_type'] ) ) {
			$validation_errors[] = array(
				'input'   => 'event_type',
				'message' => __( 'Please select an event type.', 'personalizewp' ),
			);
		}

		// Validate date range if provided
		if ( ! empty( $export_params['start_date'] ) && ! empty( $export_params['end_date'] ) ) {
			if ( strtotime( $export_params['start_date'] ) > strtotime( $export_params['end_date'] ) ) {
				$validation_errors[] = array(
					'input'   => 'date_range',
					'message' => __( 'Start date must be before end date.', 'personalizewp' ),
				);
			}
		}

		return array(
			'params' => $export_params,
			'email'  => $email,
			'errors' => $validation_errors,
		);
	}

	/**
	 * Generate CSV file with headers
	 *
	 * @since 3.5.0
	 *
	 * @param int $request_id Export request ID
	 * @return string|\WP_Error File name on success, WP_Error on failure
	 */
	private function generate_csv_header( int $request_id ) {

		// Cannot generate csv without a directory.
		$export_dir = $this->get_export_directory();
		if ( ! $export_dir ) {
			return new \WP_Error( 'personalizewp_invalid_export_directory', __( 'Invalid data export directory', 'personalizewp' ) );
		}

		$obscura    = wp_generate_password( 32, false, false );
		$filename   = 'personalizewp_export_' . $obscura . '_' . gmdate( 'Y-m-d' ) . '.csv';
		$csv_handle = fopen( $export_dir . '/' . $filename, 'w' ); // Creates new file, truncating data.
		if ( ! $csv_handle ) {
			return new \WP_Error( 'personalizewp_invalid_export_file', __( 'Invalid data export file.', 'personalizewp' ) );
		}

		$headers = array(
			__( 'Visitor First Name', 'personalizewp' ),
			__( 'Visitor Last Name', 'personalizewp' ),
			__( 'Email', 'personalizewp' ),
			__( 'Event Type', 'personalizewp' ),
			__( 'URL', 'personalizewp' ),
			__( 'Lead Score', 'personalizewp' ),
			__( 'Event Key', 'personalizewp' ),
			__( 'Event Value', 'personalizewp' ),
			__( 'Meta Key', 'personalizewp' ),
			__( 'Meta Value', 'personalizewp' ),
			__( 'Event Date / Time', 'personalizewp' ),
		);

		/**
		 * Filter CSV headers for export
		 *
		 * @since 3.3.0
		 *
		 * @param array $headers CSV headers
		 * @param int   $request_id Export request ID
		 */
		$headers = apply_filters( 'personalizewp_export_csv_headers', $headers, $request_id );

		fputcsv( $csv_handle, $headers );

		fclose( $csv_handle );

		return $filename;
	}

	/**
	 * Generates the export database query and returns rows.
	 *
	 * @since 3.5.0
	 *
	 * @param int    $request_id     Export request ID
	 * @param array  $export_params  Export parameters
	 * @param int    $page           Data page number
	 * @return array {
	 *     An array of personal data.
	 *
	 *     @type array[] $data An array of export data rows.
	 *     @type bool    $done Whether the export has more rows.
	 * }
	 */
	private function get_export_rows( int $request_id, array $export_params, $page = 1 ) {
		global $wpdb;

		// Limit us to 500 rows at a time to avoid timing out.
		$number = 500;
		$page   = absint( $page );

		// Generate next set of data
		$data_to_export = array();

		$default_fields = 'c.ID as contact_id, c.first_name, c.last_name, c.email, c.is_known, a.activity_type, a.url, a.lead_score, a.object_id, a.object_type, m.meta_key, m.meta_value, a.created';
		$default_join   = "LEFT JOIN {$wpdb->prefix}pwp_contacts c ON a.contact_id = c.ID LEFT JOIN {$wpdb->prefix}pwp_activity_meta AS m ON a.ID = m.activity_id";

		$fields  = $default_fields;
		$join    = $default_join;
		$where   = '';
		$orderby = 'a.created DESC';
		$limits  = '';

		// Filtering by visitor (WordPress user ID, which can be multiple Visitor IDs).
		if ( ! empty( $export_params['export_type'] ) && 'single_visitor' === $export_params['export_type'] && ! empty( $export_params['wp_user_id'] ) ) {
			// Find the contact ID(s) for this WordPress user ID
			$contact_ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT contact_id FROM {$wpdb->prefix}pwp_contacts_meta WHERE meta_key = 'wp_user_id' AND meta_value = %s",
					$export_params['wp_user_id']
				)
			);

			if ( ! empty( $contact_ids ) ) {
				// Audit the Contact IDs exported.
				add_post_meta( $request_id, '_export_contact_ids', $contact_ids, true );

				$placeholders = implode( ', ', array_fill( 0, count( $contact_ids ), '%d' ) );
				$where       .= $wpdb->prepare( " AND a.contact_id IN ( {$placeholders} )", $contact_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: using placeholders
			} else {
				// If no contacts found for this user, return no results
				$where .= ' AND 1=0 ';
			}
		}

		// Allow excluding unknown/anonymous visitors when exporting all visitors.
		if ( ! empty( $export_params['export_type'] ) && 'all_visitors' === $export_params['export_type'] && empty( $export_params['include_anonymous'] ) ) {
			$where .= ' AND c.is_known = 1';
		}

		// Filtering by event type.
		if ( ! empty( $export_params['event_filter'] ) && 'single_event' === $export_params['event_filter'] && ! empty( $export_params['event_type'] ) ) {
			$where .= $wpdb->prepare( ' AND a.activity_type = %s', $export_params['event_type'] );
		}

		// Filtering by date range.
		if ( ! empty( $export_params['start_date'] ) ) {
			$start = $export_params['start_date'];
			// Append time if only a date was provided
			if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $start ) ) {
				$start .= ' 00:00:00';
			}
			$where .= $wpdb->prepare( ' AND a.created >= %s', $start );
		}
		if ( ! empty( $export_params['end_date'] ) ) {
			$end = $export_params['end_date'];
			// Append time if only a date was provided
			if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $end ) ) {
				$end .= ' 23:59:59';
			}
			$where .= $wpdb->prepare( ' AND a.created <= %s', $end );
		}

		$pieces = array( 'fields', 'join', 'where', 'orderby' );
		/**
		 * Filters export query clauses at once, for convenience.
		 *
		 * @since 3.5.0 - Removed 'limits' as now hardcoded
		 * @since 3.3.0
		 *
		 * @param string[] $clauses {
		 *     Associative array of the clauses for the query.
		 *
		 *     @type string $fields  The SELECT clause of the query.
		 *     @type string $join    The JOIN clause of the query.
		 *     @type string $where   The WHERE clause of the query.
		 *     @type string $orderby The ORDER BY clause of the query.
		 * }
		 * @param array $export_params  Export parameters
		 * @param int   $request_id     Export request ID
		 */
		$clauses = (array) apply_filters( 'personalizewp_export_request_clauses', compact( $pieces ), $export_params, $request_id );
		$fields  = isset( $clauses['fields'] ) ? $clauses['fields'] : $default_fields;
		$join    = isset( $clauses['join'] ) ? $clauses['join'] : $default_join;
		$where   = isset( $clauses['where'] ) ? $clauses['where'] : '';
		$orderby = isset( $clauses['orderby'] ) ? $clauses['orderby'] : 'a.created DESC';

		if ( ! empty( $orderby ) ) {
			$orderby = 'ORDER BY ' . $orderby;
		}

		// Limits cannot be overwritten.
		$limits = 'LIMIT ' . ( $number * ( $page - 1 ) ) . ',' . $number;

		// Execute query and get raw results.
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: custom db table
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: inputs sanitised/validated above
		$results = $wpdb->get_results(
			"SELECT SQL_CALC_FOUND_ROWS $fields FROM {$wpdb->prefix}pwp_activity a $join WHERE 1=1 $where $orderby $limits",  // nosemgrep: audit.php.wp.security.sqli.input-in-sinks -- Reason: inputs sanitised/validated above
			ARRAY_A
		);
		// phpcs:enable

		$found_entries = (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' );

		// Assume there's more if exactly the limit. Worst case, there's zero more.
		$done = count( $results ) < $number;

		return array(
			'data'  => $results,
			'total' => ceil( $found_entries / $number ), // Total pages to process
			'done'  => $done,
		);
	}

	/**
	 * Populate CSV file from export data
	 *
	 * @since 3.5.0
	 *
	 * @param int   $request_id Export request ID
	 * @param array $results    Export data results
	 * @return boolean|\WP_Error True on success, WP_Error on failure
	 */
	private function populate_csv_from_data( int $request_id, array $results ) {

		// Cannot generate csv without a directory.
		$export_dir = $this->get_export_directory();
		if ( ! $export_dir ) {
			return new \WP_Error( 'personalizewp_invalid_export_directory', __( 'Invalid data export directory', 'personalizewp' ) );
		}

		$filename = get_post_meta( $request_id, self::META_EXPORT_FILE, true );
		if ( empty( $filename ) || ! is_file( $export_dir . '/' . $filename ) ) {
			return new \WP_Error( 'personalizewp_invalid_export_file', __( 'Invalid data export file.', 'personalizewp' ) );
		}

		$csv_handle = fopen( $export_dir . '/' . $filename, 'a' ); // Open for writing data, appending.
		if ( ! $csv_handle ) {
			return new \WP_Error( 'personalizewp_invalid_export_file', __( 'Invalid data export file.', 'personalizewp' ) );
		}

		// Write data rows
		foreach ( $results as $row ) {
			$csv_row = $this->format_csv_row( $row );

			/**
			 * Filter CSV row data for export
			 *
			 * @since 3.3.0
			 *
			 * @param array $csv_row    CSV row data
			 * @param array $row        Database row
			 * @param int   $request_id Export request ID
			 */
			$csv_row = apply_filters( 'personalizewp_export_csv_row', $csv_row, $row, $request_id );

			fputcsv( $csv_handle, $csv_row );
		}

		fclose( $csv_handle );

		return true;
	}

	/**
	 * Format database row for CSV output
	 *
	 * @param array $row Database row
	 *
	 * @return array[] {
	 *     Associative array of CSV row.
	 *
	 *     @type array ...$0 {
	 *         Data for the CSV row.
	 *
	 *         @type string $first_name    First name of the visitor, or empty if not known
	 *         @type string $last_name     Last name of the visitor, or empty if not known
	 *         @type string $email         Email address of the visitor, or empty if not known
	 *         @type string $activity_type Activity type, from all known registered types
	 *         @type string $url           URL of the Activity
	 *         @type string $lead_score    Lead score given to the Activity, defaults zero
	 *         @type string $object_key    Type of the primary data, ID of the linked object, or empty if not known
	 *         @type string $object_value  Value of the linked object ID or URL
	 *         @type string $meta_key      Key for the meta data, if set
	 *         @type string $meta_value    Value of the meta data
	 *         @type string $created       Date of the activity log, in YYYY-MM-DD HH:MM:SS format.
	 *     }
	 */
	private function format_csv_row( array $row ): array {
		$obj_key    = '';
		$obj_value  = '';
		$meta_key   = '';
		$meta_value = '';

		// Add linked object ID/type.
		if ( ! empty( $row['object_id'] ) ) {
			$obj_key   = 'object_id';
			$obj_value = $row['object_id'];
		}

		// Add meta data if in result. Other cols are repeated for each meta.
		if ( ! empty( $row['meta_key'] ) && 'NULL' !== $row['meta_key'] ) {
			$meta_key = $row['meta_key'];
		}
		if ( ! empty( $row['meta_value'] ) && 'NULL' !== $row['meta_value'] ) {
			$meta_value = $row['meta_value'];
		}

		return array(
			$row['first_name'] ?? '',
			$row['last_name'] ?? '',
			$row['email'] ?? '',
			$row['activity_type'] ?? '',
			$row['url'] ?? '',
			$row['lead_score'],
			$obj_key,
			$obj_value,
			$meta_key,
			$meta_value,
			$row['created'] ?? '',
		);
	}

	/**
	 * Get or create secure export directory
	 *
	 * Creates a secure directory for storing export files
	 *
	 * @return string|false Directory path on success, false on failure
	 */
	private function get_export_directory() {
		$upload_dir  = wp_upload_dir();
		$exports_dir = $upload_dir['basedir'] . '/personalizewp-exports';

		if ( ! file_exists( $exports_dir ) ) {
			if ( ! wp_mkdir_p( $exports_dir ) ) {
				// Unable to create data export folder.
				return false;
			}
		}

		// Create .htaccess to prevent direct access
		$htaccess_file = $exports_dir . '/.htaccess';
		if ( ! file_exists( $htaccess_file ) ) {
			$file = fopen( $htaccess_file, 'w' );
			if ( false === $file ) {
				// Unable to protect data export folder from direct access.
				return false;
			}
			fwrite( $file, "deny from all\n" );
			fclose( $file );
		}

		// Create index.php to prevent directory listing
		$index_file = $exports_dir . '/index.php';
		if ( ! file_exists( $index_file ) ) {
			$file = fopen( $index_file, 'w' );
			if ( false === $file ) {
				// Unable to protect data export folder from browsing.
				return false;
			}
			fwrite( $file, "<?php\n// Silence is golden.\n" );
			fclose( $file );
		}

		return is_dir( $exports_dir ) ? $exports_dir : false;
	}

	/**
	 * Send export notification email to the requester
	 *
	 * @param int    $post_id Export request post ID
	 * @param string $email   Recipient email
	 * @return bool True if email sent, false otherwise
	 */
	public function send_export_notification( int $post_id, string $email ): bool {
		if ( empty( $email ) ) {
			return false;
		}

		$download_url    = admin_url( PageDataExport::$download_url . (int) $post_id );
		$expiration_date = date_i18n( get_option( 'date_format' ), time() + ( 7 * DAY_IN_SECONDS ) );

		$site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
		$site_url  = home_url();

		$default_subject = sprintf(
			/* translators: %s: Site name */
			__( '[%s] Data Export Ready', 'personalizewp' ),
			$site_name
		);

		/**
		 * Filter the export notification email subject.
		 *
		 * @since 3.3.0
		 *
		 * @param string $subject   Email subject.
		 * @param int    $post_id   Export request post ID.
		 * @param string $email     Recipient email address.
		 */
		$subject = apply_filters( 'personalizewp_export_email_subject', $default_subject, $post_id, $email );

		/* translators: Do not translate LINK, EXPIRATION, SITENAME, SITEURL: those are placeholders. */
		$default_message = __(
			'Howdy,

Your PersonalizeWP data export is ready for download.

Download link: ###LINK###

We will automatically delete the file on ###EXPIRATION###.

Regards,
All at ###SITENAME###
###SITEURL###',
			'personalizewp'
		);

		/**
		 * Filter the export notification email message.
		 *
		 * The following strings have a special meaning and will get replaced dynamically:
		 *
		 *  - `###EXPIRATION###` The date when the URL will be automatically deleted.
		 *  - `###LINK###`       URL of the data export file for the user.
		 *  - `###SITENAME###`   The name of the site.
		 *  - `###SITEURL###`    The URL to the site.
		 *
		 * @since 3.3.0
		 *
		 * @param string $message      Email message body (plain text by default).
		 * @param int    $post_id      Export request post ID.
		 * @param string $email        Recipient email address.
		 * @param string $download_url Download URL for the export file.
		 */
		$message = apply_filters( 'personalizewp_export_email_message', $default_message, $post_id, $email, $download_url );

		$message = str_replace( '###EXPIRATION###', $expiration_date, $message );
		$message = str_replace( '###LINK###', sanitize_url( $download_url ), $message );
		$message = str_replace( '###SITENAME###', $site_name, $message );
		$message = str_replace( '###SITEURL###', sanitize_url( $site_url ), $message );

		// Default to plain text; allow customization (e.g., text/html) via filter
		$default_headers = array( 'Content-Type: text/plain; charset=UTF-8' );

		/**
		 * Filter the headers for the export notification email.
		 *
		 * @since 3.3.0
		 *
		 * @param array  $headers Headers array. Defaults to plain text content type.
		 * @param int    $post_id Export request post ID.
		 * @param string $email   Recipient email address.
		 */
		$headers = apply_filters( 'personalizewp_export_email_headers', $default_headers, $post_id, $email );

		/**
		 * Fires before the export notification email is sent.
		 *
		 * @since 3.3.0
		 *
		 * @param int    $post_id Export request post ID.
		 * @param string $email   Recipient email address.
		 * @param string $subject Email subject.
		 * @param string $message Email message body.
		 * @param array  $headers Email headers.
		 */
		do_action( 'personalizewp_before_send_export_email', $post_id, $email, $subject, $message, $headers );

		$sent = wp_mail( $email, $subject, $message, $headers );

		/**
		 * Fires after the export notification email is sent.
		 *
		 * @since 3.3.0
		 *
		 * @param int    $post_id Export request post ID.
		 * @param string $email   Recipient email address.
		 * @param bool   $sent    Whether the email was sent successfully.
		 */
		do_action( 'personalizewp_after_send_export_email', $post_id, $email, $sent );

		// Mark sent
		update_post_meta( $post_id, self::META_EXPORT_EMAIL_SENT, '1' );

		return $sent;
	}

	/**
	 * Delete export request and associated file
	 *
	 * @param int $post_id Export request post ID
	 * @return bool Success
	 */
	public function delete_export_request( int $post_id ): bool {

		// Only fully processed export requests have filenames.
		$filename = get_post_meta( $post_id, self::META_EXPORT_FILE, true );
		if ( ! empty( $filename ) ) {
			$export_dir = $this->get_export_directory();
			if ( ! empty( $export_dir ) ) {
				// If the export directory doesn't exist anymore there are larger issues.
				$file_path = $export_dir . '/' . $filename;
				if ( $file_path && file_exists( $file_path ) ) {
					wp_delete_file( $file_path );
				}
			}
		}

		// Always delete the entry, irregardless of actual file.
		return (bool) wp_delete_post( $post_id, true );
	}

	/**
	 * Handle file download
	 *
	 * Consolidated download method with comprehensive security checks
	 * and proper header handling following PersonalizeWP patterns.
	 *
	 * @param int $post_id Export request post ID
	 * @return void
	 */
	public function handle_download( int $post_id ): void {

		// Security checks using PersonalizeWP standard capability
		if ( ! current_user_can( Admin::CAP ) ) {
			wp_die( esc_html__( 'You do not have permission to download this export.', 'personalizewp' ) );
		}

		// Verify this is a valid export request
		$export_post = get_post( $post_id );
		if ( ! $export_post || self::POST_TYPE !== $export_post->post_type ) {
			wp_die( esc_html__( 'Invalid export request.', 'personalizewp' ) );
		}

		$export_dir = $this->get_export_directory();
		if ( empty( $export_dir ) ) {
			wp_die( esc_html__( 'Export file not found or not ready.', 'personalizewp' ) );
		}

		$filename  = get_post_meta( $post_id, self::META_EXPORT_FILE, true );
		$file_path = $export_dir . '/' . $filename;
		$status    = get_post_meta( $post_id, self::META_EXPORT_STATUS, true );

		if ( self::STATUS_COMPLETED !== $status || empty( $filename ) || ! file_exists( $file_path ) ) {
			wp_die( esc_html__( 'Export file not found or not ready.', 'personalizewp' ) );
		}

		// Clear any existing output
		if ( ob_get_level() ) {
			ob_end_clean();
		}
		// Set comprehensive headers for secure file download
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
		header( 'Content-Length: ' . filesize( $file_path ) );
		header( 'Cache-Control: no-cache, no-store, must-revalidate, max-age=0' );
		header( 'Expires: 0' );
		flush();

		// Output the file
		readfile( $file_path ); // Note: Replace with WP_Filesystem
		exit;
	}

	/**
	 * Cleanup old export files
	 *
	 * @return void
	 */
	public function cleanup_old_export_files(): void {

		$export_dir = $this->get_export_directory();
		if ( ! $export_dir ) {
			return;
		}

		// Get requests that are older than 7 days.
		$query_args = array(
			'post_type'      => self::POST_TYPE,
			'posts_per_page' => -1, // Need to get all
			'orderby'        => 'date',
			'order'          => 'DESC',
			'date_query'     => array(
				'before' => '-7 days',
			),
			'meta_query'     => array(
				'has_file' => array(
					'key'     => self::META_EXPORT_FILE,
					'compare' => 'EXISTS',
				),
			),
		);

		$query    = new \WP_Query();
		$requests = $query->query( $query_args );

		foreach ( $requests as $export_request ) {
			// Doesn't matter what the status of the request is,
			// only that there is an export filename available.
			$filename = get_post_meta( $export_request->ID, self::META_EXPORT_FILE, true );
			if ( ! empty( $filename ) ) {
				$file_path = $export_dir . '/' . $filename;

				if ( file_exists( $file_path ) ) {
					wp_delete_file( $file_path );
				}
			}

			// Always clear the export filename.
			delete_post_meta( $export_request->ID, self::META_EXPORT_FILE );
		}
	}
}
