<?php
/**
 * Idempotency key contract.
 *
 * @package Carticy\Conversions\Domain
 */

declare(strict_types=1);

namespace Carticy\Conversions\Domain;

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

/**
 * Stable dispatch key for order, event type, and destination.
 */
final class IdempotencyKey {

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

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

	/**
	 * Provider or client destination key.
	 *
	 * @var string
	 */
	private string $destination;

	/**
	 * Optional event instance identifier for repeatable events.
	 *
	 * @var string
	 */
	private string $event_id;

	/**
	 * Constructor.
	 *
	 * @param int    $order_id WooCommerce order ID.
	 * @param string $event_type Canonical event type.
	 * @param string $destination Provider or client destination key.
	 * @param string $event_id Optional event instance identifier for repeatable events.
	 *
	 * @throws \InvalidArgumentException When the order ID, event type, or destination is invalid.
	 */
	public function __construct( int $order_id, string $event_type, string $destination, string $event_id = '' ) {
		if ( $order_id <= 0 ) {
			throw new \InvalidArgumentException( 'Order ID must be positive.' );
		}

		if ( ! ConversionEventType::is_valid( $event_type ) ) {
			throw new \InvalidArgumentException( 'Unsupported event type.' );
		}

		$normalized_destination = self::normalize_segment( $destination );
		if ( '' === $normalized_destination ) {
			throw new \InvalidArgumentException( 'Destination is required.' );
		}

		$this->order_id    = $order_id;
		$this->event_type  = $event_type;
		$this->destination = $normalized_destination;
		$this->event_id    = self::normalize_segment( $event_id );
	}

	/**
	 * Normalize a key segment without losing uniqueness.
	 *
	 * The sanitize_key() helper maps distinct inputs to one value (e.g. "Ref#1" and
	 * "Ref@1" both become "ref1"), which would silently merge two different
	 * dispatch claims. Whenever sanitization changed the input, a short
	 * content hash is appended so distinct inputs always stay distinct.
	 *
	 * @param string $value Raw segment value.
	 * @return string
	 */
	public static function normalize_segment( string $value ): string {
		if ( '' === trim( $value ) ) {
			return '';
		}

		$sanitized = sanitize_key( $value );
		if ( '' === $sanitized ) {
			return 'k' . substr( md5( $value ), 0, 12 );
		}

		if ( strtolower( trim( $value ) ) !== $sanitized ) {
			return $sanitized . '_' . substr( md5( $value ), 0, 8 );
		}

		return $sanitized;
	}

	/**
	 * Rebuild from serialized string.
	 *
	 * @param string $key Serialized idempotency key.
	 * @return self
	 */
	public static function from_string( string $key ): self {
		$parts = array();
		foreach ( explode( '|', $key ) as $segment ) {
			$pair = explode( ':', $segment, 2 );
			if ( 2 === count( $pair ) ) {
				$parts[ $pair[0] ] = $pair[1];
			}
		}

		return new self(
			isset( $parts['order'] ) ? (int) $parts['order'] : 0,
			(string) ( $parts['event'] ?? '' ),
			(string) ( $parts['destination'] ?? '' ),
			(string) ( $parts['instance'] ?? '' )
		);
	}

	/**
	 * Serialize to a stable string.
	 *
	 * @return string
	 */
	public function to_string(): string {
		$key = sprintf(
			'order:%d|event:%s|destination:%s',
			$this->order_id,
			$this->event_type,
			$this->destination
		);

		return '' !== $this->event_id ? $key . '|instance:' . $this->event_id : $key;
	}

	/**
	 * Serialize for storage.
	 *
	 * @return array<string, mixed>
	 */
	public function to_array(): array {
		return array(
			'order_id'    => $this->order_id,
			'event_type'  => $this->event_type,
			'destination' => $this->destination,
			'event_id'    => $this->event_id,
			'key'         => $this->to_string(),
		);
	}
}
