<?php
/**
 * Recent-events diagnostics transformer.
 *
 * @package Carticy\Conversions\Diagnostics
 */

declare(strict_types=1);

namespace Carticy\Conversions\Diagnostics;

use Carticy\Conversions\Events\DispatchResult;

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

/**
 * Turns stored dispatch rows + recorded canonical events into the admin
 * recent-events contract.
 *
 * Pure transformer: it never queries - the controller supplies the dispatch
 * rows and the order-event list, and this class only maps and groups them. The
 * UI status is derived from the STORED dispatch status, never recomputed, so the
 * 06R lifecycle semantics hold: blocked states are recoverable ("waiting for
 * …"), only `permanent_failed` is a terminal failure, and `sent` is a terminal
 * success.
 */
final class RecentEventsReader {

	/**
	 * Map a stored dispatch status to its UI semantics.
	 *
	 * @param string $status Stored dispatch status.
	 * @return array{uiStatus: string, terminal: bool, recoverable: bool}
	 */
	public static function map_status( string $status ): array {
		switch ( $status ) {
			case DispatchResult::STATUS_SENT:
				return array(
					'uiStatus'    => 'delivered',
					'terminal'    => true,
					'recoverable' => false,
				);
			case DispatchResult::STATUS_PERMANENT_FAILED:
				return array(
					'uiStatus'    => 'failed',
					'terminal'    => true,
					'recoverable' => false,
				);
			case DispatchResult::STATUS_BLOCKED_CONSENT:
				return array(
					'uiStatus'    => 'waiting_for_consent',
					'terminal'    => false,
					'recoverable' => true,
				);
			case DispatchResult::STATUS_BLOCKED_MISSING_CONFIG:
				return array(
					'uiStatus'    => 'waiting_for_config',
					'terminal'    => false,
					'recoverable' => true,
				);
			case DispatchResult::STATUS_BLOCKED_MISSING_IDENTITY:
				return array(
					'uiStatus'    => 'waiting_for_identity',
					'terminal'    => false,
					'recoverable' => true,
				);
			case DispatchResult::STATUS_FAILED:
				return array(
					'uiStatus'    => 'retrying',
					'terminal'    => false,
					'recoverable' => true,
				);
			case DispatchResult::STATUS_QUEUED:
			case DispatchResult::STATUS_RETRY_SCHEDULED:
			case 'processing':
			case 'claimed':
				return array(
					'uiStatus'    => 'pending',
					'terminal'    => false,
					'recoverable' => true,
				);
			default:
				// Read from the table, never recompute: an unrecognized status is
				// surfaced verbatim rather than coerced into a verdict.
				return array(
					'uiStatus'    => $status,
					'terminal'    => false,
					'recoverable' => true,
				);
		}
	}

	/**
	 * Build the recent-events list.
	 *
	 * @param array<int, array<string, mixed>> $dispatch_rows Stored dispatch rows (newest first).
	 * @param array<int, array<string, mixed>> $order_events Recorded canonical events keyed by order+event.
	 * @param array<int, string>               $client_destinations Enabled client provider keys, resolved by the caller.
	 * @return array<int, array<string, mixed>>
	 */
	public function build( array $dispatch_rows, array $order_events, array $client_destinations = array() ): array {
		$events = array();

		// Seed from the recorded canonical events (the client-tracked baseline).
		foreach ( $order_events as $event ) {
			$key = $this->event_key( (int) ( $event['orderId'] ?? 0 ), (string) ( $event['eventId'] ?? '' ) );
			if ( '' === $key ) {
				continue;
			}

			$client_tracked = ! empty( $event['clientTracked'] );

			$events[ $key ] = array(
				'orderId'            => (int) ( $event['orderId'] ?? 0 ),
				'orderUrl'           => (string) ( $event['orderUrl'] ?? '' ),
				'eventType'          => (string) ( $event['eventType'] ?? '' ),
				'eventId'            => (string) ( $event['eventId'] ?? '' ),
				'occurredAt'         => (string) ( $event['occurredAt'] ?? '' ),
				'clientTracked'      => $client_tracked,
				// Resolved server-side at response time so the platforms column
				// never requires the client to compute provider status.
				'clientDestinations' => $client_tracked ? array_values( $client_destinations ) : array(),
				'destinations'       => array(),
			);
		}

		// Attach server dispatch destinations to their event (or surface a
		// dispatch-only row when no canonical event was supplied).
		foreach ( $dispatch_rows as $row ) {
			$order_id = (int) ( $row['order_id'] ?? 0 );
			$event_id = (string) ( $row['event_id'] ?? '' );
			$key      = $this->event_key( $order_id, $event_id );
			if ( '' === $key ) {
				continue;
			}

			if ( ! isset( $events[ $key ] ) ) {
				$events[ $key ] = array(
					'orderId'            => $order_id,
					'orderUrl'           => '',
					'eventType'          => (string) ( $row['event_type'] ?? '' ),
					'eventId'            => $event_id,
					'occurredAt'         => (string) ( $row['created_at'] ?? '' ),
					'clientTracked'      => false,
					'clientDestinations' => array(),
					'destinations'       => array(),
				);
			}

			$status                           = sanitize_key( (string) ( $row['status'] ?? '' ) );
			$mapped                           = self::map_status( $status );
			$events[ $key ]['destinations'][] = array(
				'destination'      => (string) ( $row['destination'] ?? '' ),
				'status'           => $status,
				'uiStatus'         => $mapped['uiStatus'],
				'terminal'         => $mapped['terminal'],
				'recoverable'      => $mapped['recoverable'],
				'attempts'         => (int) ( $row['attempts'] ?? 0 ),
				'lastHttpStatus'   => isset( $row['last_http_status'] ) ? (int) $row['last_http_status'] : null,
				'lastErrorCode'    => (string) ( $row['last_error_code'] ?? '' ),
				// Stored pre-redacted (the queue passes provider errors through
				// the Redactor before persisting) - the merchant-facing "why".
				'lastErrorMessage' => (string) ( $row['last_error_message'] ?? '' ),
				'nextAttemptAt'    => (string) ( $row['next_attempt_at'] ?? '' ),
				'updatedAt'        => (string) ( $row['updated_at'] ?? '' ),
			);
		}

		return array_values( $events );
	}

	/**
	 * Build a stable grouping key for an order + event.
	 *
	 * @param int    $order_id Order ID.
	 * @param string $event_id Event ID.
	 * @return string
	 */
	private function event_key( int $order_id, string $event_id ): string {
		if ( $order_id <= 0 || '' === $event_id ) {
			return '';
		}

		return $order_id . '|' . $event_id;
	}
}
