<?php
/**
 * WordPress privacy exporter, eraser, policy content, and retention purge.
 *
 * @package Carticy\Conversions\Privacy
 */

declare(strict_types=1);

namespace Carticy\Conversions\Privacy;

use Carticy\Conversions\Settings\ConsentPostureSettings;
use Carticy\Conversions\Storage\OrderMetaKeys;
use Carticy\Conversions\Storage\OrderMetaStore;

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

/**
 * Provides WordPress privacy primitives for captured conversion data.
 */
final class PrivacyService {

	public const RETENTION_HOOK     = 'carticy_conversions_retention_purge';
	private const PRIVACY_PAGE_SIZE = 100;

	/**
	 * Storage service.
	 *
	 * @var OrderMetaStore
	 */
	private OrderMetaStore $store;

	/**
	 * Settings service.
	 *
	 * @var ConsentPostureSettings
	 */
	private ConsentPostureSettings $settings;

	/**
	 * Constructor.
	 *
	 * @param OrderMetaStore         $store Storage service.
	 * @param ConsentPostureSettings $settings Settings service.
	 */
	public function __construct( OrderMetaStore $store, ConsentPostureSettings $settings ) {
		$this->store    = $store;
		$this->settings = $settings;
	}

	/**
	 * Register privacy hooks.
	 */
	public function register_hooks(): void {
		add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporter' ) );
		add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_eraser' ) );
		add_action( 'admin_init', array( $this, 'add_privacy_policy_content' ) );
		add_action( self::RETENTION_HOOK, array( $this, 'purge_expired_data' ) );
	}

	/**
	 * Schedule retention purge on activation.
	 */
	public static function schedule_retention(): void {
		if ( function_exists( 'wp_next_scheduled' ) && function_exists( 'wp_schedule_event' ) && ! wp_next_scheduled( self::RETENTION_HOOK ) ) {
			wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::RETENTION_HOOK );
		}
	}

	/**
	 * Unschedule retention purge on deactivation.
	 */
	public static function unschedule_retention(): void {
		if ( function_exists( 'wp_next_scheduled' ) && function_exists( 'wp_unschedule_event' ) ) {
			$timestamp = wp_next_scheduled( self::RETENTION_HOOK );
			if ( $timestamp ) {
				wp_unschedule_event( $timestamp, self::RETENTION_HOOK );
			}
		}
	}

	/**
	 * Add exporter entry.
	 *
	 * @param array<string, array<string, mixed>> $exporters Exporters.
	 * @return array<string, array<string, mixed>>
	 */
	public function register_exporter( array $exporters ): array {
		$exporters['carticy-conversions'] = array(
			'exporter_friendly_name' => __( 'Carticy Conversions for WooCommerce', 'carticy-conversions-for-woocommerce' ),
			'callback'               => array( $this, 'export_personal_data' ),
		);

		return $exporters;
	}

	/**
	 * Add eraser entry.
	 *
	 * @param array<string, array<string, mixed>> $erasers Erasers.
	 * @return array<string, array<string, mixed>>
	 */
	public function register_eraser( array $erasers ): array {
		$erasers['carticy-conversions'] = array(
			'eraser_friendly_name' => __( 'Carticy Conversions for WooCommerce', 'carticy-conversions-for-woocommerce' ),
			'callback'             => array( $this, 'erase_personal_data' ),
		);

		return $erasers;
	}

	/**
	 * Export plugin-captured order data for a customer email.
	 *
	 * @param string $email_address Email address.
	 * @param int    $page Page number.
	 * @return array<string, mixed>
	 */
	public function export_personal_data( string $email_address, int $page = 1 ): array {
		$items = array();
		$query = $this->orders_for_email( $email_address, $page );

		foreach ( $query['orders'] as $order ) {
			$order_id    = method_exists( $order, 'get_id' ) ? (int) $order->get_id() : 0;
			$identity    = $this->store->get_identity( $order )->to_array();
			$attribution = $this->store->get_attribution( $order )->to_array();
			$consent     = $this->store->get_consent( $order )->to_array();

			if ( array() === $identity && array() === $attribution && 'unset' === $consent['state'] && array() === $consent['sources'] ) {
				continue;
			}

			$items[] = array(
				'group_id'    => 'carticy-conversions',
				'group_label' => __( 'Carticy Conversions', 'carticy-conversions-for-woocommerce' ),
				'item_id'     => 'carticy-conversions-order-' . $order_id,
				'data'        => array(
					array(
						'name'  => __( 'Identity data', 'carticy-conversions-for-woocommerce' ),
						'value' => wp_json_encode( $identity ),
					),
					array(
						'name'  => __( 'Attribution data', 'carticy-conversions-for-woocommerce' ),
						'value' => wp_json_encode( $attribution ),
					),
					array(
						'name'  => __( 'Consent record', 'carticy-conversions-for-woocommerce' ),
						'value' => wp_json_encode( $consent ),
					),
				),
			);
		}

		return array(
			'data' => $items,
			'done' => $query['done'],
		);
	}

	/**
	 * Erase plugin-captured order data for a customer email.
	 *
	 * @param string $email_address Email address.
	 * @param int    $page Page number.
	 * @return array<string, mixed>
	 */
	public function erase_personal_data( string $email_address, int $page = 1 ): array {
		$removed = false;
		$query   = $this->orders_for_email( $email_address, $page );

		foreach ( $query['orders'] as $order ) {
			if ( $this->erase_order_data( $order ) ) {
				$removed = true;
			}
		}

		return array(
			'items_removed'  => $removed,
			'items_retained' => false,
			'messages'       => array(),
			'done'           => $query['done'],
		);
	}

	/**
	 * Register plugin privacy policy copy.
	 */
	public function add_privacy_policy_content(): void {
		if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
			return;
		}

		wp_add_privacy_policy_content(
			__( 'Carticy Conversions for WooCommerce', 'carticy-conversions-for-woocommerce' ),
			wp_kses_post(
				__( 'Carticy Conversions stores order-bound attribution, browser identifiers, consent records, user agent, and IP data according to the configured IP storage mode. When tracking providers are configured, provider-bound data is eligible to leave the site only when the stored per-order consent record explicitly grants the relevant service.', 'carticy-conversions-for-woocommerce' )
			)
		);
	}

	/**
	 * Purge expired plugin data.
	 *
	 * The query filters on the plugin's own meta existing, so already-purged
	 * orders never re-match - erasing meta does not change date_created, and
	 * without this filter the purge would re-select the same first page of
	 * old orders forever while the backlog behind it is retained indefinitely.
	 *
	 * @return int Number of orders changed.
	 */
	public function purge_expired_data(): int {
		if ( ! function_exists( 'wc_get_orders' ) ) {
			return 0;
		}

		$cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $this->settings->retention_days() * DAY_IN_SECONDS ) );
		$count  = 0;

		// Bounded loop: each pass erases what it selects, so the next pass
		// naturally selects the following slice of the backlog.
		for ( $iteration = 0; $iteration < 20; $iteration++ ) {
			$orders = wc_get_orders(
				array(
					'limit'        => self::PRIVACY_PAGE_SIZE,
					'return'       => 'objects',
					'date_created' => '<' . $cutoff,
					'meta_query'   => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Bounded daily retention purge.
						'relation' => 'OR',
						array(
							'key'     => OrderMetaKeys::IDENTITY,
							'compare' => 'EXISTS',
						),
						array(
							'key'     => OrderMetaKeys::CONSENT,
							'compare' => 'EXISTS',
						),
						array(
							'key'     => OrderMetaKeys::EVENTS,
							'compare' => 'EXISTS',
						),
						array(
							'key'     => OrderMetaKeys::ATTRIBUTION,
							'compare' => 'EXISTS',
						),
					),
				)
			);

			$orders  = is_array( $orders ) ? array_values( array_filter( $orders, 'is_object' ) ) : array();
			$changed = 0;
			foreach ( $orders as $order ) {
				if ( $this->erase_order_data( $order ) ) {
					++$changed;
				}
			}

			$count += $changed;
			if ( count( $orders ) < self::PRIVACY_PAGE_SIZE || 0 === $changed ) {
				break;
			}
		}

		// Dispatch-queue rows carry order linkage and error diagnostics; they
		// follow the same retention window (settled rows only).
		$this->store->purge_dispatches_older_than( $cutoff );

		return $count;
	}

	/**
	 * Erase plugin order meta through WooCommerce CRUD.
	 *
	 * @param object $order WooCommerce order.
	 * @return bool True only when plugin data actually existed and was removed.
	 */
	public function erase_order_data( object $order ): bool {
		if ( ! method_exists( $order, 'delete_meta_data' ) || ! method_exists( $order, 'save' ) ) {
			return false;
		}

		$removed = false;
		foreach ( array( OrderMetaKeys::IDENTITY, OrderMetaKeys::ATTRIBUTION, OrderMetaKeys::CONSENT, OrderMetaKeys::EVENTS ) as $meta_key ) {
			// Duck-typed WooCommerce order contract: get_meta presence is implied by the CRUD methods checked above.
			// @phpstan-ignore-next-line
			$existing = $order->get_meta( $meta_key, true );
			if ( '' === $existing || array() === $existing || null === $existing ) {
				continue;
			}

			$order->delete_meta_data( $meta_key );
			$removed = true;
		}

		$order_id = method_exists( $order, 'get_id' ) ? (int) $order->get_id() : 0;
		if ( $order_id > 0 && $this->store->delete_dispatches_for_order( $order_id ) > 0 ) {
			$removed = true;
		}

		if ( $removed ) {
			$order->save();
		}

		return $removed;
	}

	/**
	 * Find WooCommerce orders by billing email.
	 *
	 * @param string $email_address Email.
	 * @param int    $page Page.
	 * @return array{orders: array<int, object>, done: bool}
	 */
	private function orders_for_email( string $email_address, int $page ): array {
		if ( ! function_exists( 'wc_get_orders' ) || '' === trim( $email_address ) ) {
			return array(
				'orders' => array(),
				'done'   => true,
			);
		}

		$orders = wc_get_orders(
			array(
				'billing_email' => sanitize_email( $email_address ),
				'limit'         => self::PRIVACY_PAGE_SIZE,
				'page'          => max( 1, $page ),
				'paginate'      => true,
				'return'        => 'objects',
			)
		);

		if ( is_object( $orders ) && isset( $orders->orders ) && is_array( $orders->orders ) ) {
			$max_pages = isset( $orders->max_num_pages ) ? (int) $orders->max_num_pages : 1;

			return array(
				'orders' => array_values( array_filter( $orders->orders, 'is_object' ) ),
				'done'   => max( 1, $page ) >= max( 1, $max_pages ),
			);
		}

		$fallback_orders = is_array( $orders ) ? array_values( array_filter( $orders, 'is_object' ) ) : array();

		return array(
			'orders' => $fallback_orders,
			'done'   => count( $fallback_orders ) < self::PRIVACY_PAGE_SIZE,
		);
	}
}
