<?php
/**
 * Resolves many consent signals into one authoritative record.
 *
 * @package Carticy\Conversions\Domain
 */

declare(strict_types=1);

namespace Carticy\Conversions\Domain;

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

/**
 * Collapses the multi-source intake into a single per-order ConsentRecord.
 *
 * Policy (applied independently to the overall state and to every category and
 * service scope):
 *
 *   1. Any explicit DENIED  -> DENIED   (deny is dominant and sticky).
 *   2. Else any explicit GRANTED -> GRANTED.
 *   3. Else -> UNSET            (fail closed; defaults never grant or deny).
 *
 * The store posture is an overlay, not an exception to the gate: a
 * "consent not required" store contributes a synthetic explicit grant
 * (a logged merchant attestation) so the engine invariant - pass only on an
 * explicit grant - is never weakened, only fed.
 */
final class ConsentResolver {

	public const POSTURE_CMP_DRIVEN   = 'cmp_driven';
	public const POSTURE_REQUIRED     = 'required';
	public const POSTURE_NOT_REQUIRED = 'not_required';

	/**
	 * Resolve signals into the authoritative record for an order.
	 *
	 * @param int                       $order_id WooCommerce order ID.
	 * @param array<int, ConsentSignal> $signals Contributing signals.
	 * @param string                    $posture Store consent posture.
	 * @param string                    $plugin_version Plugin version (for attestation).
	 * @param array<string, mixed>      $attestation Stored attestation details (actor, timestamp).
	 * @return ConsentRecord
	 */
	public static function resolve(
		int $order_id,
		array $signals,
		string $posture = self::POSTURE_CMP_DRIVEN,
		string $plugin_version = '',
		array $attestation = array()
	): ConsentRecord {
		$signals = array_values(
			array_filter(
				$signals,
				static function ( $signal ): bool {
					return $signal instanceof ConsentSignal;
				}
			)
		);

		if ( self::POSTURE_NOT_REQUIRED === $posture ) {
			$signals[] = ConsentSignal::merchant_attestation( $plugin_version, '', $attestation );
		}

		$overall = self::resolve_scope( $signals, 'overall' );

		$categories = array();
		foreach ( self::collect_scopes( $signals, 'categories' ) as $category ) {
			$categories[ $category ] = self::resolve_scope( $signals, $category )['state'];
		}

		$services = array();
		foreach ( self::collect_scopes( $signals, 'services' ) as $service ) {
			$services[ $service ] = self::resolve_scope( $signals, $service )['state'];
		}

		$decisive = $overall['signal'];

		return new ConsentRecord(
			$order_id,
			$overall['state'],
			$categories,
			$services,
			$decisive instanceof ConsentSignal ? $decisive->source() : '',
			$decisive instanceof ConsentSignal ? $decisive->source_version() : '',
			$decisive instanceof ConsentSignal ? $decisive->captured_at() : '',
			self::first_consent_mode( $signals ),
			self::first_tcf_string( $signals ),
			array_map(
				static function ( ConsentSignal $signal ): array {
					return $signal->to_audit_array();
				},
				$signals
			)
		);
	}

	/**
	 * Apply the allow-list policy to one scope.
	 *
	 * @param array<int, ConsentSignal> $signals Contributing signals.
	 * @param string                    $scope Resolution scope.
	 * @return array{state: string, signal: ConsentSignal|null}
	 */
	private static function resolve_scope( array $signals, string $scope ): array {
		$granting = null;

		foreach ( $signals as $signal ) {
			$state = $signal->state_for_scope( $scope );

			if ( ConsentState::DENIED === $state ) {
				// Deny is dominant: first explicit denial decides the scope.
				return array(
					'state'  => ConsentState::DENIED,
					'signal' => $signal,
				);
			}

			if ( null === $granting && ConsentState::GRANTED === $state ) {
				$granting = $signal;
			}
		}

		if ( $granting instanceof ConsentSignal ) {
			return array(
				'state'  => ConsentState::GRANTED,
				'signal' => $granting,
			);
		}

		return array(
			'state'  => ConsentState::UNSET,
			'signal' => null,
		);
	}

	/**
	 * Gather the union of category or service keys across explicit signals.
	 *
	 * @param array<int, ConsentSignal> $signals Contributing signals.
	 * @param string                    $kind "categories" or "services".
	 * @return array<int, string>
	 */
	private static function collect_scopes( array $signals, string $kind ): array {
		$keys = array();

		foreach ( $signals as $signal ) {
			if ( ! $signal->is_explicit() ) {
				continue;
			}

			$scope_keys = 'services' === $kind ? $signal->service_keys() : $signal->category_keys();
			foreach ( $scope_keys as $key ) {
				$keys[ $key ] = true;
			}
		}

		return array_keys( $keys );
	}

	/**
	 * First non-empty Consent Mode snapshot among the signals.
	 *
	 * @param array<int, ConsentSignal> $signals Contributing signals.
	 * @return string
	 */
	private static function first_consent_mode( array $signals ): string {
		foreach ( $signals as $signal ) {
			if ( '' !== $signal->consent_mode() ) {
				return $signal->consent_mode();
			}
		}

		return '';
	}

	/**
	 * First non-empty TCF string among the signals.
	 *
	 * @param array<int, ConsentSignal> $signals Contributing signals.
	 * @return string
	 */
	private static function first_tcf_string( array $signals ): string {
		foreach ( $signals as $signal ) {
			if ( '' !== $signal->tcf_string() ) {
				return $signal->tcf_string();
			}
		}

		return '';
	}
}
