<?php
/**
 * Provider-neutral attribution data.
 *
 * @package Carticy\Conversions\Domain
 */

declare(strict_types=1);

namespace Carticy\Conversions\Domain;

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

/**
 * Stores first-touch and last-touch attribution without provider payload fields.
 */
final class AttributionData {

	private const ALLOWED_KEYS = array(
		'utm_source',
		'utm_medium',
		'utm_campaign',
		'utm_content',
		'utm_term',
		'gclid',
		'fbclid',
		'wbraid',
		'gbraid',
		'gad_source',
		'dclid',
		'landing_page',
		'referrer',
		'timestamp',
	);

	/**
	 * First-touch attribution.
	 *
	 * @var array<string, string>
	 */
	private array $first_touch;

	/**
	 * Last-touch attribution.
	 *
	 * @var array<string, string>
	 */
	private array $last_touch;

	/**
	 * Constructor.
	 *
	 * @param array<string, mixed> $first_touch First-touch attribution.
	 * @param array<string, mixed> $last_touch Last-touch attribution.
	 */
	public function __construct( array $first_touch = array(), array $last_touch = array() ) {
		$this->first_touch = $this->normalize_touch( $first_touch );
		$this->last_touch  = $this->normalize_touch( $last_touch );
	}

	/**
	 * Rebuild from serialized data.
	 *
	 * @param array<string, mixed> $data Serialized attribution data.
	 * @return self
	 */
	public static function from_array( array $data ): self {
		return new self(
			is_array( $data['first_touch'] ?? null ) ? $data['first_touch'] : array(),
			is_array( $data['last_touch'] ?? null ) ? $data['last_touch'] : array()
		);
	}

	/**
	 * Serialize for storage.
	 *
	 * @return array<string, mixed>
	 */
	public function to_array(): array {
		return array(
			'first_touch' => $this->first_touch,
			'last_touch'  => $this->last_touch,
		);
	}

	/**
	 * Normalize one attribution touch.
	 *
	 * @param array<string, mixed> $touch Raw touch data.
	 * @return array<string, string>
	 */
	private function normalize_touch( array $touch ): array {
		$normalized = array();

		foreach ( self::ALLOWED_KEYS as $key ) {
			if ( ! array_key_exists( $key, $touch ) ) {
				continue;
			}

			$value = $this->sanitize_value( (string) $touch[ $key ], $key );
			if ( '' === $value ) {
				continue;
			}

			if ( 0 === strpos( $key, 'utm_' ) || 'gad_source' === $key ) {
				$value = strtolower( $value );
			}

			$normalized[ $key ] = $value;
		}

		return $normalized;
	}

	/**
	 * Sanitize and cap one attribution value.
	 *
	 * Touch payloads arrive from visitor-controlled cookies; without
	 * sanitization and length caps they are a stored-XSS and meta-bloat
	 * vector for every later admin surface and DSAR export.
	 *
	 * @param string $value Raw value.
	 * @param string $key Attribution key.
	 * @return string
	 */
	private function sanitize_value( string $value, string $key ): string {
		if ( function_exists( 'sanitize_text_field' ) ) {
			$value = sanitize_text_field( $value );
		} else {
			$value = trim( (string) preg_replace( '/[\x00-\x1F\x7F]/', '', $value ) );
		}

		$max = in_array( $key, array( 'landing_page', 'referrer' ), true ) ? 500 : 255;

		return substr( $value, 0, $max );
	}
}
