<?php
/**
 * GGM Commerce Downloads Class
 *
 * Handles downloadable product functionality including file management,
 * download permissions, and license key management.
 *
 * @package GGM_Commerce
 */

defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter

/**
 * Class GGM_Commerce_Downloads
 */
class GGM_Commerce_Downloads {

	/**
	 * Singleton instance.
	 *
	 * @var GGM_Commerce_Downloads|null
	 */
	private static $instance = null;

	/**
	 * Download endpoint slug.
	 *
	 * @var string
	 */
	const DOWNLOAD_ENDPOINT = 'ggmc-download';

	/**
	 * Get singleton instance.
	 *
	 * @return GGM_Commerce_Downloads
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	private function __construct() {
		// Register download endpoint.
		add_action( 'init', array( $this, 'add_download_endpoint' ) );
		add_action( 'template_redirect', array( $this, 'handle_download_request' ) );

		// Register AJAX actions.
		add_action( 'wp_ajax_ggmc_get_customer_downloads', array( $this, 'ajax_get_customer_downloads' ) );
	}

	/**
	 * Check if Downloads feature is activated.
	 *
	 * @return bool
	 */
	public static function is_activated() {
		if ( ! class_exists( 'GGM_Commerce_License' ) ) {
			return false;
		}
		return \GGM_Commerce_License::instance()->is_widget_activated( 'downloads' );
	}

	/**
	 * Add download endpoint.
	 */
	public function add_download_endpoint() {
		add_rewrite_endpoint( self::DOWNLOAD_ENDPOINT, EP_ROOT );
	}

	// =========================================================================
	// PRODUCT DOWNLOADS CRUD
	// =========================================================================

	/**
	 * Get downloads for a product.
	 *
	 * @param int   $product_id Product ID.
	 * @param array $args       Query arguments.
	 * @return array Downloads.
	 */
	public function get_product_downloads( $product_id, $args = array() ) {
		global $wpdb;

		$defaults = array(
			'status' => 'active',
		);

		$args = wp_parse_args( $args, $defaults );

		$where = array( 'product_id = %d' );
		$values = array( $product_id );

		if ( ! empty( $args['status'] ) && 'all' !== $args['status'] ) {
			$where[] = 'status = %s';
			$values[] = $args['status'];
		}

		$where_clause = implode( ' AND ', $where );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_product_downloads WHERE {$where_clause} ORDER BY display_order ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				...$values
			),
			ARRAY_A
		) ?: array();
	}

	/**
	 * Get a single download by ID.
	 *
	 * @param int $download_id Download ID.
	 * @return array|null Download data or null.
	 */
	public function get_download( $download_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_product_downloads WHERE id = %d",
				$download_id
			),
			ARRAY_A
		);
	}

	/**
	 * Add a download to a product.
	 *
	 * @param int   $product_id Product ID.
	 * @param array $data       Download data.
	 * @return int|false New download ID or false on failure.
	 */
	public function add_download( $product_id, $data ) {
		global $wpdb;

		$defaults = array(
			'name'              => '',
			'file_url'          => '',
			'file_type'         => '',
			'file_size'         => 0,
			'download_limit'    => -1, // -1 = unlimited.
			'access_expiry_days' => -1, // -1 = never expires.
			'display_order'     => 0,
			'status'            => 'active',
		);

		$data = wp_parse_args( $data, $defaults );

		// Auto-detect file type if not provided.
		if ( empty( $data['file_type'] ) && ! empty( $data['file_url'] ) ) {
			$data['file_type'] = $this->get_file_type_from_url( $data['file_url'] );
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $wpdb->insert(
			"{$wpdb->prefix}ggmc_product_downloads",
			array(
				'product_id'         => $product_id,
				'name'               => sanitize_text_field( $data['name'] ),
				'file_url'           => esc_url_raw( $data['file_url'] ),
				'file_type'          => sanitize_text_field( $data['file_type'] ),
				'file_size'          => intval( $data['file_size'] ),
				'download_limit'     => intval( $data['download_limit'] ),
				'access_expiry_days' => intval( $data['access_expiry_days'] ),
				'display_order'      => intval( $data['display_order'] ),
				'status'             => sanitize_key( $data['status'] ),
			),
			array( '%d', '%s', '%s', '%s', '%d', '%d', '%d', '%d', '%s' )
		);

		return $result ? $wpdb->insert_id : false;
	}

	/**
	 * Update a download.
	 *
	 * @param int   $download_id Download ID.
	 * @param array $data        Updated data.
	 * @return bool Success or failure.
	 */
	public function update_download( $download_id, $data ) {
		global $wpdb;

		$update_data = array();
		$format = array();

		if ( isset( $data['name'] ) ) {
			$update_data['name'] = sanitize_text_field( $data['name'] );
			$format[] = '%s';
		}

		if ( isset( $data['file_url'] ) ) {
			$update_data['file_url'] = esc_url_raw( $data['file_url'] );
			$format[] = '%s';

			// Update file type if URL changed.
			if ( empty( $data['file_type'] ) ) {
				$update_data['file_type'] = $this->get_file_type_from_url( $data['file_url'] );
				$format[] = '%s';
			}
		}

		if ( isset( $data['file_type'] ) ) {
			$update_data['file_type'] = sanitize_text_field( $data['file_type'] );
			$format[] = '%s';
		}

		if ( isset( $data['file_size'] ) ) {
			$update_data['file_size'] = intval( $data['file_size'] );
			$format[] = '%d';
		}

		if ( isset( $data['download_limit'] ) ) {
			$update_data['download_limit'] = intval( $data['download_limit'] );
			$format[] = '%d';
		}

		if ( isset( $data['access_expiry_days'] ) ) {
			$update_data['access_expiry_days'] = intval( $data['access_expiry_days'] );
			$format[] = '%d';
		}

		if ( isset( $data['display_order'] ) ) {
			$update_data['display_order'] = intval( $data['display_order'] );
			$format[] = '%d';
		}

		if ( isset( $data['status'] ) ) {
			$update_data['status'] = sanitize_key( $data['status'] );
			$format[] = '%s';
		}

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

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->update(
			"{$wpdb->prefix}ggmc_product_downloads",
			$update_data,
			array( 'id' => $download_id ),
			$format,
			array( '%d' )
		);

		return false !== $result;
	}

	/**
	 * Delete a download.
	 *
	 * @param int $download_id Download ID.
	 * @return bool Success or failure.
	 */
	public function delete_download( $download_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->delete(
			"{$wpdb->prefix}ggmc_product_downloads",
			array( 'id' => $download_id ),
			array( '%d' )
		);

		return false !== $result;
	}

	/**
	 * Delete all downloads for a product.
	 *
	 * @param int $product_id Product ID.
	 * @return bool Success or failure.
	 */
	public function delete_product_downloads( $product_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->delete(
			"{$wpdb->prefix}ggmc_product_downloads",
			array( 'product_id' => $product_id ),
			array( '%d' )
		);

		return false !== $result;
	}

	/**
	 * Get file type from URL.
	 *
	 * @param string $url File URL.
	 * @return string File type/extension.
	 */
	private function get_file_type_from_url( $url ) {
		$path = wp_parse_url( $url, PHP_URL_PATH );
		return strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );
	}

	// =========================================================================
	// DOWNLOAD PERMISSIONS
	// =========================================================================

	/**
	 * Grant download permissions for an order.
	 *
	 * @param int    $order_id       Order ID.
	 * @param string $customer_email Customer email.
	 * @param int    $customer_id    Customer ID (optional).
	 * @return array Array of created permission IDs.
	 */
	public function grant_permissions( $order_id, $customer_email, $customer_id = null ) {
		global $wpdb;

		// Get order items.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$order_items = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_order_items WHERE order_id = %d AND is_downloadable = 1",
				$order_id
			),
			ARRAY_A
		);

		if ( empty( $order_items ) ) {
			return array();
		}

		$permission_ids = array();

		foreach ( $order_items as $item ) {
			// Get product downloads.
			$downloads = $this->get_product_downloads( $item['product_id'] );

			foreach ( $downloads as $download ) {
				// Calculate expiry date.
				$expires_at = null;
				if ( $download['access_expiry_days'] > 0 ) {
					$expires_at = gmdate( 'Y-m-d H:i:s', strtotime( '+' . $download['access_expiry_days'] . ' days' ) );
				}

				// Check if permission already exists.
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$existing = $wpdb->get_var(
					$wpdb->prepare(
						"SELECT id FROM {$wpdb->prefix}ggmc_download_permissions
						WHERE order_id = %d AND download_id = %d",
						$order_id,
						$download['id']
					)
				);

				if ( $existing ) {
					continue;
				}

				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
				$result = $wpdb->insert(
					"{$wpdb->prefix}ggmc_download_permissions",
					array(
						'order_id'          => $order_id,
						'order_item_id'     => $item['id'],
						'product_id'        => $item['product_id'],
						'download_id'       => $download['id'],
						'customer_id'       => $customer_id,
						'customer_email'    => sanitize_email( $customer_email ),
						'access_granted_at' => current_time( 'mysql', true ),
						'access_expires_at' => $expires_at,
						'download_count'    => 0,
						'download_limit'    => $download['download_limit'],
						'status'            => 'active',
					),
					array( '%d', '%d', '%d', '%d', '%d', '%s', '%s', '%s', '%d', '%d', '%s' )
				);

				if ( $result ) {
					$permission_ids[] = $wpdb->insert_id;
				}
			}
		}

		return $permission_ids;
	}

	/**
	 * Get downloads for a customer.
	 *
	 * @param string $customer_email Customer email.
	 * @param int    $customer_id    Customer ID (optional).
	 * @return array Customer downloads.
	 */
	public function get_customer_downloads( $customer_email, $customer_id = null ) {
		global $wpdb;

		$where = array( "dp.status = 'active'" );
		$values = array();

		if ( $customer_id ) {
			$where[] = '(dp.customer_email = %s OR dp.customer_id = %d)';
			$values[] = $customer_email;
			$values[] = $customer_id;
		} else {
			$where[] = 'dp.customer_email = %s';
			$values[] = $customer_email;
		}

		$where_clause = implode( ' AND ', $where );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT dp.*,
					pd.name as download_name,
					pd.file_type,
					pd.file_size,
					p.name as product_name
				FROM {$wpdb->prefix}ggmc_download_permissions dp
				INNER JOIN {$wpdb->prefix}ggmc_product_downloads pd ON dp.download_id = pd.id
				INNER JOIN {$wpdb->prefix}ggmc_products p ON dp.product_id = p.id
				WHERE {$where_clause}
				ORDER BY dp.access_granted_at DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				...$values
			),
			ARRAY_A
		);

		// Add download URL and status info.
		if ( $results ) {
			foreach ( $results as &$result ) {
				$result['download_url'] = $this->generate_download_url( $result['id'] );
				$result['is_expired'] = $this->is_permission_expired( $result );
				$result['downloads_remaining'] = $this->get_downloads_remaining( $result );
				$result['can_download'] = $this->can_download( $result['id'], $customer_email );
			}
		}

		return $results ?: array();
	}

	/**
	 * Check if customer can download a file.
	 *
	 * @param int    $permission_id Permission ID.
	 * @param string $email         Customer email.
	 * @return bool Whether download is allowed.
	 */
	public function can_download( $permission_id, $email ) {
		$permission = $this->get_permission( $permission_id );

		if ( ! $permission ) {
			return false;
		}

		// Check email matches.
		if ( strtolower( $permission['customer_email'] ) !== strtolower( $email ) ) {
			// Also check by customer ID if logged in.
			$current_user_id = get_current_user_id();
			if ( ! $current_user_id || intval( $permission['customer_id'] ) !== $current_user_id ) {
				return false;
			}
		}

		// Check status.
		if ( 'active' !== $permission['status'] ) {
			return false;
		}

		// Check expiry.
		if ( $this->is_permission_expired( $permission ) ) {
			return false;
		}

		// Check download limit.
		if ( $permission['download_limit'] > 0 && $permission['download_count'] >= $permission['download_limit'] ) {
			return false;
		}

		return true;
	}

	/**
	 * Get a permission by ID.
	 *
	 * @param int $permission_id Permission ID.
	 * @return array|null Permission data or null.
	 */
	public function get_permission( $permission_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_download_permissions WHERE id = %d",
				$permission_id
			),
			ARRAY_A
		);
	}

	/**
	 * Revoke a download permission.
	 *
	 * @param int $permission_id Permission ID.
	 * @return bool Success or failure.
	 */
	public function revoke_permission( $permission_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->update(
			"{$wpdb->prefix}ggmc_download_permissions",
			array( 'status' => 'revoked' ),
			array( 'id' => $permission_id ),
			array( '%s' ),
			array( '%d' )
		);

		return false !== $result;
	}

	/**
	 * Check if permission is expired.
	 *
	 * @param array $permission Permission data.
	 * @return bool Whether expired.
	 */
	private function is_permission_expired( $permission ) {
		if ( empty( $permission['access_expires_at'] ) ) {
			return false;
		}

		return strtotime( $permission['access_expires_at'] ) < time();
	}

	/**
	 * Get remaining downloads.
	 *
	 * @param array $permission Permission data.
	 * @return int|string Remaining count or 'unlimited'.
	 */
	private function get_downloads_remaining( $permission ) {
		if ( $permission['download_limit'] < 0 ) {
			return 'unlimited';
		}

		return max( 0, $permission['download_limit'] - $permission['download_count'] );
	}

	// =========================================================================
	// DOWNLOAD EXECUTION
	// =========================================================================

	/**
	 * Process a download request.
	 *
	 * @param int $permission_id Permission ID.
	 * @return bool|void Returns false on error, otherwise outputs file.
	 */
	public function process_download( $permission_id ) {
		$permission = $this->get_permission( $permission_id );

		if ( ! $permission ) {
			return false;
		}

		// Get the download file.
		$download = $this->get_download( $permission['download_id'] );

		if ( ! $download || empty( $download['file_url'] ) ) {
			return false;
		}

		// Log the download.
		$this->log_download( $permission_id );

		// Increment download count.
		$this->increment_download_count( $permission_id );

		// Get file path.
		$file_path = $this->get_file_path_from_url( $download['file_url'] );

		if ( $file_path && file_exists( $file_path ) ) {
			// Serve local file.
			$this->serve_file( $file_path, $download['name'] );
		} else {
			// Redirect to external URL.
			wp_redirect( $download['file_url'] ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
			exit;
		}
	}

	/**
	 * Generate secure download URL.
	 *
	 * @param int $permission_id Permission ID.
	 * @return string Download URL.
	 */
	public function generate_download_url( $permission_id ) {
		$permission = $this->get_permission( $permission_id );

		if ( ! $permission ) {
			return '';
		}

		// Generate a secure token.
		$token = $this->generate_download_token( $permission_id, $permission['customer_email'] );

		return add_query_arg(
			array(
				self::DOWNLOAD_ENDPOINT => $permission_id,
				'token'                 => $token,
			),
			home_url( '/' )
		);
	}

	/**
	 * Generate download token.
	 *
	 * @param int    $permission_id Permission ID.
	 * @param string $email         Customer email.
	 * @return string Token.
	 */
	private function generate_download_token( $permission_id, $email ) {
		return hash_hmac( 'sha256', $permission_id . '|' . $email, wp_salt( 'auth' ) );
	}

	/**
	 * Verify download token.
	 *
	 * @param int    $permission_id Permission ID.
	 * @param string $email         Customer email.
	 * @param string $token         Token to verify.
	 * @return bool Whether token is valid.
	 */
	private function verify_download_token( $permission_id, $email, $token ) {
		$expected = $this->generate_download_token( $permission_id, $email );
		return hash_equals( $expected, $token );
	}

	/**
	 * Log a download.
	 *
	 * @param int $permission_id Permission ID.
	 */
	public function log_download( $permission_id ) {
		global $wpdb;

		$permission = $this->get_permission( $permission_id );
		$customer_id = $permission ? $permission['customer_id'] : null;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$wpdb->insert(
			"{$wpdb->prefix}ggmc_download_logs",
			array(
				'permission_id' => $permission_id,
				'customer_id'   => $customer_id,
				'ip_address'    => $this->get_client_ip(),
				'user_agent'    => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
			),
			array( '%d', '%d', '%s', '%s' )
		);
	}

	/**
	 * Increment download count.
	 *
	 * @param int $permission_id Permission ID.
	 */
	private function increment_download_count( $permission_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->prefix}ggmc_download_permissions
				SET download_count = download_count + 1
				WHERE id = %d",
				$permission_id
			)
		);
	}

	/**
	 * Get client IP address.
	 *
	 * @return string IP address.
	 */
	private function get_client_ip() {
		$ip_headers = array(
			'HTTP_CF_CONNECTING_IP', // Cloudflare.
			'HTTP_X_FORWARDED_FOR',
			'HTTP_X_REAL_IP',
			'REMOTE_ADDR',
		);

		foreach ( $ip_headers as $header ) {
			if ( ! empty( $_SERVER[ $header ] ) ) {
				$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
				// Handle comma-separated IPs (X-Forwarded-For).
				if ( strpos( $ip, ',' ) !== false ) {
					$ip = trim( explode( ',', $ip )[0] );
				}
				if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
					return $ip;
				}
			}
		}

		return '';
	}

	/**
	 * Get file path from URL.
	 *
	 * @param string $url File URL.
	 * @return string|false File path or false.
	 */
	private function get_file_path_from_url( $url ) {
		$upload_dir = wp_upload_dir();

		// Check if URL is in uploads directory.
		if ( strpos( $url, $upload_dir['baseurl'] ) === 0 ) {
			$path = str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $url );
			if ( file_exists( $path ) ) {
				return $path;
			}
		}

		return false;
	}

	/**
	 * Serve a file for download.
	 *
	 * @param string $file_path Full path to file.
	 * @param string $filename  Display filename.
	 */
	private function serve_file( $file_path, $filename ) {
		// Ensure filename is safe.
		$filename = sanitize_file_name( $filename );

		// Get file extension from path if filename doesn't have one.
		if ( ! pathinfo( $filename, PATHINFO_EXTENSION ) ) {
			$ext = pathinfo( $file_path, PATHINFO_EXTENSION );
			if ( $ext ) {
				$filename .= '.' . $ext;
			}
		}

		// Get MIME type.
		$mime_type = wp_check_filetype( $file_path )['type'] ?: 'application/octet-stream';

		// Clean output buffer.
		if ( ob_get_level() ) {
			ob_end_clean();
		}

		// Set headers.
		nocache_headers();
		header( 'Content-Type: ' . $mime_type );
		header( 'Content-Description: File Transfer' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
		header( 'Content-Transfer-Encoding: binary' );
		header( 'Content-Length: ' . filesize( $file_path ) );

		// Read file.
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile
		readfile( $file_path );
		exit;
	}

	/**
	 * Handle download request from URL.
	 */
	public function handle_download_request() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_GET[ self::DOWNLOAD_ENDPOINT ] ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$permission_id = intval( $_GET[ self::DOWNLOAD_ENDPOINT ] );
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';

		if ( ! $permission_id || ! $token ) {
			wp_die( esc_html__( 'Invalid download link.', 'ggm-commerce' ), '', array( 'response' => 403 ) );
		}

		$permission = $this->get_permission( $permission_id );

		if ( ! $permission ) {
			wp_die( esc_html__( 'Download not found.', 'ggm-commerce' ), '', array( 'response' => 404 ) );
		}

		// Verify token.
		if ( ! $this->verify_download_token( $permission_id, $permission['customer_email'], $token ) ) {
			wp_die( esc_html__( 'Invalid download link.', 'ggm-commerce' ), '', array( 'response' => 403 ) );
		}

		// Check if download is allowed.
		if ( ! $this->can_download( $permission_id, $permission['customer_email'] ) ) {
			wp_die( esc_html__( 'Download access denied. Your download may have expired or reached its limit.', 'ggm-commerce' ), '', array( 'response' => 403 ) );
		}

		// Process the download.
		$this->process_download( $permission_id );
	}

	// =========================================================================
	// LICENSE KEYS
	// =========================================================================

	/**
	 * Generate a license key.
	 *
	 * @param int $product_id Product ID.
	 * @return int|false License ID or false.
	 */
	public function generate_license_key( $product_id ) {
		global $wpdb;

		$license_key = $this->create_license_key_string();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $wpdb->insert(
			"{$wpdb->prefix}ggmc_product_licenses",
			array(
				'product_id'  => $product_id,
				'license_key' => $license_key,
				'status'      => 'available',
			),
			array( '%d', '%s', '%s' )
		);

		return $result ? $wpdb->insert_id : false;
	}

	/**
	 * Bulk generate license keys.
	 *
	 * @param int $product_id Product ID.
	 * @param int $count      Number of keys to generate.
	 * @return array Array of created license IDs.
	 */
	public function bulk_generate_license_keys( $product_id, $count = 10 ) {
		$ids = array();

		for ( $i = 0; $i < $count; $i++ ) {
			$id = $this->generate_license_key( $product_id );
			if ( $id ) {
				$ids[] = $id;
			}
		}

		return $ids;
	}

	/**
	 * Create a unique license key string.
	 *
	 * @return string License key.
	 */
	private function create_license_key_string() {
		// Format: XXXX-XXXX-XXXX-XXXX.
		$segments = array();
		for ( $i = 0; $i < 4; $i++ ) {
			$segments[] = strtoupper( wp_generate_password( 4, false ) );
		}
		return implode( '-', $segments );
	}

	/**
	 * Assign a license key to an order.
	 *
	 * @param int    $order_id     Order ID.
	 * @param int    $order_item_id Order item ID.
	 * @param string $email        Customer email.
	 * @param int    $product_id   Product ID.
	 * @return array|false License data or false.
	 */
	public function assign_license( $order_id, $order_item_id, $email, $product_id ) {
		global $wpdb;

		// Get an available license.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$license = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_product_licenses
				WHERE product_id = %d AND status = 'available'
				ORDER BY id ASC LIMIT 1",
				$product_id
			),
			ARRAY_A
		);

		if ( ! $license ) {
			// No available license, generate one.
			$license_id = $this->generate_license_key( $product_id );
			$license = $this->get_license( $license_id );
		}

		if ( ! $license ) {
			return false;
		}

		// Assign the license.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->update(
			"{$wpdb->prefix}ggmc_product_licenses",
			array(
				'order_id'       => $order_id,
				'order_item_id'  => $order_item_id,
				'customer_email' => sanitize_email( $email ),
				'status'         => 'assigned',
				'assigned_at'    => current_time( 'mysql', true ),
			),
			array( 'id' => $license['id'] ),
			array( '%d', '%d', '%s', '%s', '%s' ),
			array( '%d' )
		);

		return $this->get_license( $license['id'] );
	}

	/**
	 * Get a license by ID.
	 *
	 * @param int $license_id License ID.
	 * @return array|null License data or null.
	 */
	public function get_license( $license_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_product_licenses WHERE id = %d",
				$license_id
			),
			ARRAY_A
		);
	}

	/**
	 * Get license by key.
	 *
	 * @param string $license_key License key.
	 * @return array|null License data or null.
	 */
	public function get_license_by_key( $license_key ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_product_licenses WHERE license_key = %s",
				$license_key
			),
			ARRAY_A
		);
	}

	/**
	 * Get licenses for an order.
	 *
	 * @param int $order_id Order ID.
	 * @return array Licenses.
	 */
	public function get_order_licenses( $order_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT l.*, p.name as product_name
				FROM {$wpdb->prefix}ggmc_product_licenses l
				INNER JOIN {$wpdb->prefix}ggmc_products p ON l.product_id = p.id
				WHERE l.order_id = %d
				ORDER BY l.id ASC",
				$order_id
			),
			ARRAY_A
		) ?: array();
	}

	/**
	 * Get available license count for a product.
	 *
	 * @param int $product_id Product ID.
	 * @return int Count of available licenses.
	 */
	public function get_available_license_count( $product_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->prefix}ggmc_product_licenses
				WHERE product_id = %d AND status = 'available'",
				$product_id
			)
		);
	}

	/**
	 * Get all licenses for a product.
	 *
	 * @param int   $product_id Product ID.
	 * @param array $args       Query arguments.
	 * @return array Licenses.
	 */
	public function get_product_licenses( $product_id, $args = array() ) {
		global $wpdb;

		$defaults = array(
			'status'   => '',
			'per_page' => 50,
			'page'     => 1,
		);

		$args = wp_parse_args( $args, $defaults );

		$where = array( 'product_id = %d' );
		$values = array( $product_id );

		if ( ! empty( $args['status'] ) ) {
			$where[] = 'status = %s';
			$values[] = $args['status'];
		}

		$where_clause = implode( ' AND ', $where );

		$offset = ( $args['page'] - 1 ) * $args['per_page'];

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_product_licenses
				WHERE {$where_clause}
				ORDER BY id DESC
				LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				array_merge( $values, array( $args['per_page'], $offset ) )
			),
			ARRAY_A
		) ?: array();
	}

	// =========================================================================
	// AJAX HANDLERS
	// =========================================================================

	/**
	 * AJAX handler for getting customer downloads.
	 */
	public function ajax_get_customer_downloads() {
		check_ajax_referer( 'ggmc_nonce', 'nonce' );

		$user = wp_get_current_user();

		if ( ! $user->ID ) {
			wp_send_json_error( 'Not logged in' );
		}

		$downloads = $this->get_customer_downloads( $user->user_email, $user->ID );

		wp_send_json_success( array( 'downloads' => $downloads ) );
	}
}
