<?php
/**
 * Authoritative provider evidence for mail events.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\MailLog;

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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Provider callbacks update the plugin-owned mail-event ledger directly.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- The table name is derived from the WordPress prefix.

/**
 * Stores a compact, traceable provider outcome without retaining webhook bodies.
 *
 * Calling code is responsible for authenticating the provider callback before
 * invoking this service. This service deliberately never changes the WordPress
 * handoff status: provider evidence is an independent fact.
 */
final class MailEventEvidenceService {
	/** @var array<int,string> */
	private const STATUSES = array( 'accepted', 'queued', 'delivered', 'bounced', 'deferred', 'dropped', 'complained', 'opened' );

	/**
	 * Record authenticated provider evidence.
	 *
	 * Evidence must include a provider-owned message id, a named authenticated
	 * source, and the provider event time in UTC. Older callbacks cannot replace
	 * newer evidence.
	 *
	 * @param int                 $event_id Mail event id.
	 * @param array<string,mixed> $evidence Authenticated provider evidence.
	 * @return true|\WP_Error
	 */
	public function record_authoritative( $event_id, array $evidence ) {
		global $wpdb;

		$event_id  = absint( $event_id );
		$status    = sanitize_key( (string) ( $evidence['status'] ?? '' ) );
		$source    = substr( sanitize_key( (string) ( $evidence['source'] ?? '' ) ), 0, 64 );
		$message_id = $this->truncate( sanitize_text_field( (string) ( $evidence['message_id'] ?? '' ) ), 191 );
		$occurred_at = sanitize_text_field( (string) ( $evidence['occurred_at_gmt'] ?? '' ) );

		if ( $event_id <= 0 || ! in_array( $status, self::STATUSES, true ) || '' === $source || '' === $message_id || ! $this->is_mysql_datetime( $occurred_at ) ) {
			return new \WP_Error(
				'subin_mail_provider_evidence_invalid',
				__( 'Provider delivery evidence is incomplete or invalid.', 'submission-inbox' )
			);
		}

		$table = $wpdb->prefix . 'subin_mail_events';
		$priority = $this->status_priority( $status );
		$stored_priority = "CASE provider_status
			WHEN 'accepted' THEN 10
			WHEN 'queued' THEN 20
			WHEN 'deferred' THEN 30
			WHEN 'delivered' THEN 40
			WHEN 'opened' THEN 50
			WHEN 'dropped' THEN 60
			WHEN 'bounced' THEN 70
			WHEN 'complained' THEN 80
			ELSE 0 END";
		$wpdb->last_error = '';
		$updated = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$table}
				SET provider_status = %s,
					provider_message_id = %s,
					provider_evidence_source = %s,
					provider_evidence_at = %s,
					updated_at = %s
				WHERE id = %d
					AND (
						provider_evidence_at IS NULL
						OR provider_evidence_at < %s
						OR (
							provider_evidence_at = %s
							AND (
								{$stored_priority} < %d
								OR (
									{$stored_priority} = %d
									AND CONCAT(%s, '|', %s) > CONCAT(COALESCE(provider_message_id, ''), '|', COALESCE(provider_evidence_source, ''))
								)
							)
						)
					)",
				$status,
				$message_id,
				$source,
				$occurred_at,
				current_time( 'mysql' ),
					$event_id,
					$occurred_at,
					$occurred_at,
					$priority,
					$priority,
					$message_id,
					$source
			)
		);

		if ( false === $updated || '' !== (string) $wpdb->last_error ) {
			return new \WP_Error(
				'subin_mail_provider_evidence_store_failed',
				__( 'Provider delivery evidence could not be stored.', 'submission-inbox' )
			);
		}

		if ( $updated > 0 ) {
			do_action( 'subin_mail_event_provider_evidence_updated', $event_id, $status );
			return true;
		}

		$existing = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT id, provider_evidence_at FROM {$table} WHERE id = %d LIMIT 1",
				$event_id
			)
		);
		if ( '' !== (string) $wpdb->last_error ) {
			return new \WP_Error(
				'subin_mail_provider_evidence_store_failed',
				__( 'Provider delivery evidence could not be stored.', 'submission-inbox' )
			);
		}
		if ( ! $existing ) {
			return new \WP_Error(
				'subin_mail_provider_evidence_event_missing',
				__( 'The email event for this provider evidence no longer exists.', 'submission-inbox' )
			);
		}

		// An authenticated callback older than the stored evidence is idempotent.
		return true;
	}

	/** @return bool */
	private function is_mysql_datetime( $value ) {
		if ( ! preg_match( '/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', (string) $value, $parts ) ) {
			return false;
		}

		return checkdate( (int) $parts[2], (int) $parts[3], (int) $parts[1] )
			&& (int) $parts[4] <= 23
			&& (int) $parts[5] <= 59
			&& (int) $parts[6] <= 59;
	}

	/** @return string */
	private function truncate( $value, $length ) {
		return function_exists( 'mb_substr' ) ? mb_substr( (string) $value, 0, $length ) : substr( (string) $value, 0, $length );
	}

	/** @return int */
	private function status_priority( $status ) {
		$priorities = array(
			'accepted'   => 10,
			'queued'     => 20,
			'deferred'   => 30,
			'delivered'  => 40,
			'opened'     => 50,
			'dropped'    => 60,
			'bounced'    => 70,
			'complained' => 80,
		);

		return absint( $priorities[ sanitize_key( (string) $status ) ] ?? 0 );
	}
}
