<?php
/**
 * Canonical conversion event.
 *
 * @package Carticy\Conversions\Domain
 */

declare(strict_types=1);

namespace Carticy\Conversions\Domain;

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

/**
 * Provider-neutral event snapshot consumed by later provider and UI layers.
 */
final class ConversionEvent {

	/**
	 * Canonical event type.
	 *
	 * @var string
	 */
	private string $event_type;

	/**
	 * WooCommerce order ID.
	 *
	 * @var int
	 */
	private int $order_id;

	/**
	 * Stable transaction identifier.
	 *
	 * @var string
	 */
	private string $transaction_id;

	/**
	 * Deterministic event ID.
	 *
	 * @var string
	 */
	private string $event_id;

	/**
	 * Event value. Refund values are positive.
	 *
	 * @var Money
	 */
	private Money $value;

	/**
	 * Identity snapshot.
	 *
	 * @var IdentityData
	 */
	private IdentityData $identity;

	/**
	 * Attribution snapshot.
	 *
	 * @var AttributionData
	 */
	private AttributionData $attribution;

	/**
	 * Consent snapshot.
	 *
	 * @var ConsentRecord
	 */
	private ConsentRecord $consent;

	/**
	 * GMT timestamp.
	 *
	 * @var string
	 */
	private string $occurred_at;

	/**
	 * Event source label.
	 *
	 * @var string
	 */
	private string $source;

	/**
	 * Event items.
	 *
	 * @var array<int, ConversionItem>
	 */
	private array $items;

	/**
	 * Optional numeric event context (tax, shipping).
	 *
	 * @var array<string, float>
	 */
	private array $context;

	/**
	 * Constructor.
	 *
	 * @param string                     $event_type Canonical event type.
	 * @param int                        $order_id WooCommerce order ID.
	 * @param string                     $transaction_id Stable transaction identifier.
	 * @param Money                      $value Event value. Refund values are positive.
	 * @param array<int, ConversionItem> $items Event items.
	 * @param IdentityData               $identity Identity snapshot.
	 * @param AttributionData            $attribution Attribution snapshot.
	 * @param ConsentRecord              $consent Consent snapshot.
	 * @param string                     $occurred_at GMT timestamp.
	 * @param string                     $source Event source label.
	 * @param string                     $event_id Optional deterministic event ID override.
	 * @param array<string, mixed>       $context Optional numeric context (tax, shipping).
	 *
	 * @throws \InvalidArgumentException When the event type, order ID, transaction ID, value, items, or consent linkage is invalid.
	 */
	public function __construct(
		string $event_type,
		int $order_id,
		string $transaction_id,
		Money $value,
		array $items = array(),
		?IdentityData $identity = null,
		?AttributionData $attribution = null,
		?ConsentRecord $consent = null,
		string $occurred_at = '',
		string $source = 'woocommerce_order',
		string $event_id = '',
		array $context = array()
	) {
		if ( ! ConversionEventType::is_valid( $event_type ) ) {
			throw new \InvalidArgumentException( 'Unsupported event type.' );
		}

		if ( $order_id <= 0 ) {
			throw new \InvalidArgumentException( 'Order ID must be positive.' );
		}

		if ( '' === trim( $transaction_id ) ) {
			throw new \InvalidArgumentException( 'Transaction ID is required.' );
		}

		if ( $value->amount_float() <= 0.0 ) {
			throw new \InvalidArgumentException( 'Conversion event value must be positive.' );
		}

		foreach ( $items as $item ) {
			if ( ! $item instanceof ConversionItem ) {
				throw new \InvalidArgumentException( 'Event items must be ConversionItem instances.' );
			}
		}

		$this->event_type     = $event_type;
		$this->order_id       = $order_id;
		$this->transaction_id = trim( $transaction_id );
		$normalized_event_id  = IdempotencyKey::normalize_segment( $event_id );
		$this->event_id       = '' !== $normalized_event_id ? $normalized_event_id : self::deterministic_event_id( $event_type, $order_id, $this->transaction_id );
		$this->value          = $value;
		$this->items          = array_values( $items );
		$this->identity       = $identity ?? new IdentityData();
		$this->attribution    = $attribution ?? new AttributionData();
		$this->consent        = $consent ?? ConsentRecord::unset_for_order( $order_id );
		if ( $this->consent->order_id() !== $order_id ) {
			throw new \InvalidArgumentException( 'Consent record order ID must match the conversion event order ID.' );
		}
		$this->occurred_at = '' !== trim( $occurred_at ) ? trim( $occurred_at ) : gmdate( 'Y-m-d H:i:s' );
		$this->source      = trim( $source );
		$this->context     = $this->normalize_context( $context );
	}

	/**
	 * Normalize the numeric context map to the allow-listed keys.
	 *
	 * @param array<string, mixed> $context Raw context.
	 * @return array<string, float>
	 */
	private function normalize_context( array $context ): array {
		$normalized = array();
		foreach ( array( 'tax', 'shipping' ) as $key ) {
			if ( isset( $context[ $key ] ) && is_numeric( $context[ $key ] ) && (float) $context[ $key ] > 0 ) {
				$normalized[ $key ] = round( (float) $context[ $key ], 2 );
			}
		}

		return $normalized;
	}

	/**
	 * Rebuild from serialized data.
	 *
	 * @param array<string, mixed> $data Serialized event.
	 * @return self
	 */
	public static function from_array( array $data ): self {
		$items = array();
		foreach ( is_array( $data['items'] ?? null ) ? $data['items'] : array() as $item ) {
			if ( is_array( $item ) ) {
				$items[] = ConversionItem::from_array( $item );
			}
		}

		$order_id = isset( $data['order_id'] ) ? (int) $data['order_id'] : 0;

		return new self(
			(string) ( $data['event_type'] ?? '' ),
			$order_id,
			(string) ( $data['transaction_id'] ?? '' ),
			Money::from_array( is_array( $data['value'] ?? null ) ? $data['value'] : array() ),
			$items,
			IdentityData::from_array( is_array( $data['identity'] ?? null ) ? $data['identity'] : array() ),
			AttributionData::from_array( is_array( $data['attribution'] ?? null ) ? $data['attribution'] : array() ),
			ConsentRecord::from_array( is_array( $data['consent'] ?? null ) ? $data['consent'] : array(), $order_id ),
			(string) ( $data['occurred_at'] ?? '' ),
			(string) ( $data['source'] ?? 'woocommerce_order' ),
			(string) ( $data['event_id'] ?? '' ),
			is_array( $data['context'] ?? null ) ? $data['context'] : array()
		);
	}

	/**
	 * Build a deterministic event ID.
	 *
	 * Purchase IDs stay order-bound. Refund and renewal IDs include the transaction
	 * context so legitimate later events do not collide with the purchase.
	 *
	 * @param string $event_type Canonical event type.
	 * @param int    $order_id WooCommerce order ID.
	 * @param string $transaction_id Transaction or lifecycle identifier.
	 * @return string
	 */
	public static function deterministic_event_id( string $event_type, int $order_id, string $transaction_id ): string {
		if ( ConversionEventType::PURCHASE === $event_type ) {
			return 'purchase_order_' . $order_id;
		}

		$context = IdempotencyKey::normalize_segment( str_replace( array( ':', '|' ), '_', $transaction_id ) );

		return $event_type . '_order_' . $order_id . ( '' !== $context ? '_' . $context : '' );
	}

	/**
	 * Get event ID.
	 *
	 * @return string
	 */
	public function event_id(): string {
		return $this->event_id;
	}

	/**
	 * Get event type.
	 *
	 * @return string
	 */
	public function event_type(): string {
		return $this->event_type;
	}

	/**
	 * Get the WooCommerce order ID.
	 *
	 * @return int
	 */
	public function order_id(): int {
		return $this->order_id;
	}

	/**
	 * Serialize for storage.
	 *
	 * @return array<string, mixed>
	 */
	public function to_array(): array {
		return array(
			'event_type'     => $this->event_type,
			'order_id'       => $this->order_id,
			'transaction_id' => $this->transaction_id,
			'event_id'       => $this->event_id,
			'value'          => $this->value->to_array(),
			'items'          => array_map(
				static function ( ConversionItem $item ): array {
					return $item->to_array();
				},
				$this->items
			),
			'identity'       => $this->identity->to_array(),
			'attribution'    => $this->attribution->to_array(),
			'consent'        => $this->consent->to_array(),
			'occurred_at'    => $this->occurred_at,
			'source'         => $this->source,
			'context'        => $this->context,
		);
	}
}
