<?php

namespace WeeConnectPay;

use WeeConnectPay\Integrations\LogService;

/**
 * Maps raw Clover 3-D Secure response fields to merchant-facing labels,
 * colors, and HTML fragments. Single source of truth so the order-note line
 * and the charges-metabox badge stay aligned.
 *
 * Mirrors the dashboard's `ThreeDSBadge.tsx` value-to-label mapping and color
 * classification — see weeconnect-api/resources/js/Pages/Dashboard/components/ThreeDSBadge.tsx.
 *
 * NOTE: We read these values from the raw Clover response forwarded by the
 * backend (`charge_response.threeds.*`) rather than from a normalized top-level
 * field. A future backend ticket will normalize the shape; when that lands,
 * only `extractFromChargeResponse()` needs to change.
 */
class ThreeDSecureFormatter {

	const CLASS_PROTECTED = 'protected';
	const CLASS_FAILED    = 'failed';
	const CLASS_ATTEMPTED = 'attempted';
	const CLASS_NONE      = 'none';

	/**
	 * Pull the 5 3DS fields out of a decoded charge response.
	 *
	 * @param object|null $response The decoded response object returned by
	 *                              WeeConnectPayOrderProcessor::decodeResponse().
	 *                              Expected to carry a nested
	 *                              `charge_response->threeds` object on
	 *                              3DS-enabled charges.
	 * @return array|null Associative array with keys validation_result,
	 *                    liability_protection_status, server_transaction_id,
	 *                    acs_transaction_id, flow_status. Null when no 3DS
	 *                    data is present.
	 */
	public static function extractFromChargeResponse( $response ): ?array {
		if ( ! is_object( $response ) ) {
			return null;
		}

		$threeds = $response->charge_response->threeds ?? null;
		if ( ! is_object( $threeds ) && ! is_array( $threeds ) ) {
			return null;
		}

		// Surface the raw payload to logs whenever present — even when the
		// extract bails on empty fields — so we can debug shape mismatches
		// without having to re-run the transaction.
		LogService::debug( 'Raw threeds payload from charge response: ' . wp_json_encode( $threeds ) );

		$threeds = (object) $threeds;
		$data    = isset( $threeds->data ) ? (object) $threeds->data : null;

		$extracted = array(
			'validation_result'           => isset( $threeds->validation_result ) ? (string) $threeds->validation_result : null,
			'liability_protection_status' => isset( $threeds->liability_protection_status ) ? (string) $threeds->liability_protection_status : null,
			'server_transaction_id'       => $data && isset( $data->threeds_server_transaction_id ) ? (string) $data->threeds_server_transaction_id : null,
			'acs_transaction_id'          => $data && isset( $data->acs_transaction_id ) ? (string) $data->acs_transaction_id : null,
			'flow_status'                 => isset( $threeds->flow_status ) ? (string) $threeds->flow_status : null,
		);

		foreach ( $extracted as $value ) {
			if ( $value !== null && $value !== '' ) {
				return $extracted;
			}
		}

		return null;
	}

	/**
	 * Classify a 3DS outcome into one of four buckets that drive both the
	 * badge color and the order-note phrasing.
	 */
	public static function classify( ?array $threeds ): string {
		if ( empty( $threeds ) ) {
			return self::CLASS_NONE;
		}

		$liability  = $threeds['liability_protection_status'] ?? null;
		$validation = $threeds['validation_result'] ?? null;

		if ( $liability === 'PROTECTED' ) {
			return self::CLASS_PROTECTED;
		}
		if ( $validation === 'AUTHENTICATION_FAILED' || $validation === 'NOT_AUTHENTICATED' ) {
			return self::CLASS_FAILED;
		}
		return self::CLASS_ATTEMPTED;
	}

	/**
	 * Translated friendly label for a `threeds.validation_result` value.
	 */
	public static function validationResultLabel( ?string $value ): string {
		switch ( $value ) {
			case 'AUTHENTICATION_SUCCESSFUL':
				return __( 'Authentication successful', 'weeconnectpay' );
			case 'AUTHENTICATION_ATTEMPTED':
				return __( 'Authentication attempted', 'weeconnectpay' );
			case 'AUTHENTICATION_FAILED':
				return __( 'Authentication failed', 'weeconnectpay' );
			case 'NOT_AUTHENTICATED':
				return __( 'Not authenticated', 'weeconnectpay' );
			case 'NOT_ENROLLED':
				return __( 'Card not enrolled in 3-D Secure', 'weeconnectpay' );
			case 'AUTHENTICATION_UNAVAILABLE':
				return __( 'Authentication unavailable', 'weeconnectpay' );
			case 'BYPASSED':
				return __( 'Authentication bypassed', 'weeconnectpay' );
			default:
				return $value !== null ? $value : '';
		}
	}

	/**
	 * Translated friendly label for a `threeds.liability_protection_status` value.
	 */
	public static function liabilityLabel( ?string $value ): string {
		switch ( $value ) {
			case 'PROTECTED':
				return __( 'Protected — liability shifted to issuer', 'weeconnectpay' );
			case 'NOT_PROTECTED':
				return __( 'Not protected', 'weeconnectpay' );
			case 'UNKNOWN':
				return __( 'Unknown', 'weeconnectpay' );
			default:
				return $value !== null ? $value : '';
		}
	}

	/**
	 * Translated friendly label for an EMVCo 3DS2 transStatus letter.
	 * Letters come through unchanged in the badge; this is the explanatory
	 * suffix shown beside them.
	 */
	public static function flowStatusLabel( ?string $value ): string {
		switch ( $value ) {
			case 'Y':
				return __( 'Authenticated successfully', 'weeconnectpay' );
			case 'N':
				return __( 'Not authenticated', 'weeconnectpay' );
			case 'U':
				return __( 'Authentication could not be performed', 'weeconnectpay' );
			case 'A':
				return __( 'Attempted — proof of authentication', 'weeconnectpay' );
			case 'C':
				return __( 'Challenge required', 'weeconnectpay' );
			case 'D':
				return __( 'Decoupled authentication', 'weeconnectpay' );
			case 'R':
				return __( 'Authentication rejected by issuer', 'weeconnectpay' );
			default:
				return $value !== null ? $value : '';
		}
	}

	/**
	 * Build the inline HTML for the 3DS badge — a color-coded shield with a
	 * CSS-only tooltip listing every populated field. Returns the empty
	 * string when no 3DS data is present so callers can safely concatenate
	 * the result unconditionally.
	 *
	 * All output is pre-escaped — this method owns every byte it emits.
	 */
	public static function renderBadge( ?array $threeds ): string {
		$class = self::classify( $threeds );
		if ( $class === self::CLASS_NONE ) {
			return '';
		}

		$ariaLabel = '';
		$summary   = '';
		switch ( $class ) {
			case self::CLASS_PROTECTED:
				$ariaLabel = __( '3-D Secure: Protected', 'weeconnectpay' );
				$summary   = __( 'Liability shifted to issuer', 'weeconnectpay' );
				break;
			case self::CLASS_FAILED:
				$ariaLabel = __( '3-D Secure: Authentication failed', 'weeconnectpay' );
				$summary   = __( 'Authentication failed', 'weeconnectpay' );
				break;
			case self::CLASS_ATTEMPTED:
			default:
				$ariaLabel = __( '3-D Secure: Authentication attempted', 'weeconnectpay' );
				$summary   = __( 'Authentication attempted', 'weeconnectpay' );
				break;
		}

		// Same shield path data as the dashboard's ThreeDSBadge.tsx.
		$svg = '<svg class="wcp-3ds-badge-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3 4 6v6c0 4.5 3.2 8.2 8 9 4.8-.8 8-4.5 8-9V6l-8-3Z"/><path d="m9 12 2 2 4-4"/></svg>';

		$rows = '';

		if ( ! empty( $threeds['validation_result'] ) ) {
			$rows .= self::renderTooltipRow(
				__( 'Validation result', 'weeconnectpay' ),
				self::validationResultLabel( $threeds['validation_result'] )
			);
		}
		if ( ! empty( $threeds['liability_protection_status'] ) ) {
			$rows .= self::renderTooltipRow(
				__( 'Liability protection', 'weeconnectpay' ),
				self::liabilityLabel( $threeds['liability_protection_status'] )
			);
		}
		if ( ! empty( $threeds['flow_status'] ) ) {
			$letter = $threeds['flow_status'];
			$suffix = self::flowStatusLabel( $letter );
			$value  = '<span class="wcp-3ds-mono">' . esc_html( $letter ) . '</span>';
			if ( $suffix !== '' && $suffix !== $letter ) {
				$value .= ' — ' . esc_html( $suffix );
			}
			$rows .= self::renderTooltipRow(
				__( 'Flow status', 'weeconnectpay' ),
				$value,
				false
			);
		}
		if ( ! empty( $threeds['server_transaction_id'] ) ) {
			$rows .= self::renderTooltipRow(
				__( 'Server transaction ID', 'weeconnectpay' ),
				'<span class="wcp-3ds-mono">' . esc_html( $threeds['server_transaction_id'] ) . '</span>',
				false
			);
		}
		if ( ! empty( $threeds['acs_transaction_id'] ) ) {
			$rows .= self::renderTooltipRow(
				__( 'ACS transaction ID', 'weeconnectpay' ),
				'<span class="wcp-3ds-mono">' . esc_html( $threeds['acs_transaction_id'] ) . '</span>',
				false
			);
		}

		$tooltip = '<span class="wcp-3ds-tooltip" role="tooltip">'
			. '<span class="wcp-3ds-tooltip-title">' . esc_html__( '3-D Secure', 'weeconnectpay' ) . '</span>'
			. '<span class="wcp-3ds-tooltip-summary wcp-3ds-tooltip-summary-' . esc_attr( $class ) . '">' . esc_html( $summary ) . '</span>'
			. '<dl class="wcp-3ds-tooltip-list">' . $rows . '</dl>'
			. '</span>';

		return '<span class="wcp-3ds-badge wcp-3ds-badge-' . esc_attr( $class ) . '" tabindex="0" role="img" aria-label="' . esc_attr( $ariaLabel ) . '">'
			. $svg
			. $tooltip
			. '</span>';
	}

	/**
	 * Build a single tooltip definition row. `$preEscapedValue` lets callers
	 * pass HTML they've already escaped (used for the monospace transaction
	 * IDs); plain strings are escaped here.
	 */
	private static function renderTooltipRow( string $label, string $value, bool $escapeValue = true ): string {
		return '<div class="wcp-3ds-tooltip-row">'
			. '<dt>' . esc_html( $label ) . '</dt>'
			. '<dd>' . ( $escapeValue ? esc_html( $value ) : $value ) . '</dd>'
			. '</div>';
	}

	/**
	 * Build the `<br><b>3-D Secure:</b> ...` line to append to a charge
	 * success order note.
	 *
	 * Returns the empty string when no 3DS data is present AND the merchant
	 * has the opt-in toggle off — callers can append unconditionally.
	 *
	 * When the opt-in toggle IS on but the response carried no 3DS block
	 * (e.g. a card that didn't enter 3DS at all), still surface a line so
	 * merchants understand the transaction wasn't protected — silence would
	 * be indistinguishable from a successful 3DS authentication.
	 */
	public static function formatOrderNoteLine( ?array $threeds, bool $optInRequested = false, ?array $contextOnNoData = null ): string {
		$class = self::classify( $threeds );
		if ( $class === self::CLASS_NONE ) {
			if ( ! $optInRequested ) {
				return '';
			}
			$line = '<br><b>' . esc_html__( '3-D Secure:', 'weeconnectpay' ) . '</b> <em>'
				. esc_html__( 'Not applied — no authentication step ran on this charge.', 'weeconnectpay' )
				. '</em>';
			$line .= self::formatNoThreeDSContext( $contextOnNoData );

			return $line;
		}

		$validationLabel = ! empty( $threeds['validation_result'] )
			? self::validationResultLabel( $threeds['validation_result'] )
			: '';
		$liabilityLabel = ! empty( $threeds['liability_protection_status'] )
			? self::liabilityLabel( $threeds['liability_protection_status'] )
			: '';

		if ( $validationLabel !== '' && $liabilityLabel !== '' ) {
			$detail = $validationLabel . ' — ' . $liabilityLabel;
		} elseif ( $validationLabel !== '' ) {
			$detail = $validationLabel;
		} elseif ( $liabilityLabel !== '' ) {
			$detail = $liabilityLabel;
		} else {
			$detail = __( 'Outcome unavailable', 'weeconnectpay' );
		}

		return '<br><b>' . esc_html__( '3-D Secure:', 'weeconnectpay' ) . '</b> ' . esc_html( $detail );
	}

	/**
	 * Render the authorization / AVS / CVV sub-line that follows the
	 * "Not applied" 3DS line on charges where no 3DS step ran.
	 *
	 * Deliberately neutral: presents the facts Clover sent back without
	 * editorializing on *why* 3DS didn't fire. The reason is opaque to us
	 * — could be merchant 3DS off, amount-threshold rules, card-brand
	 * routing, the merchant's chosen 3DS provider (Clover offers two), or
	 * Clover's own risk-score routing. WSO has a follow-up card to map
	 * merchant fraud settings via the merchant-settings API; once that
	 * lands we can replace this with attributed reasoning.
	 *
	 * @param array{network_status?: string|null, outcome_type?: string|null, address_line1_check?: string|null, address_zip_check?: string|null, cvc_check?: string|null}|null $context Extracted from `charge_response.outcome` and `charge_response.source`.
	 */
	private static function formatNoThreeDSContext( ?array $context ): string {
		if ( empty( $context ) ) {
			return '';
		}

		$lines = array();

		$networkStatus = $context['network_status'] ?? null;
		$outcomeType   = $context['outcome_type'] ?? null;
		if ( ! empty( $networkStatus ) || ! empty( $outcomeType ) ) {
			$parts = array_filter( array(
				! empty( $networkStatus ) ? self::humanizeOutcome( $networkStatus ) : null,
				! empty( $outcomeType )   ? self::humanizeOutcome( $outcomeType )   : null,
			) );
			if ( ! empty( $parts ) ) {
				$lines[] = '<b>' . esc_html__( 'Authorization: ', 'weeconnectpay' ) . '</b>'
					. esc_html( implode( ', ', $parts ) );
			}
		}

		$avsLine1 = $context['address_line1_check'] ?? null;
		$avsZip   = $context['address_zip_check']   ?? null;
		$cvc      = $context['cvc_check']           ?? null;

		$avsParts = array();
		if ( ! empty( $avsLine1 ) ) {
			$avsParts[] = esc_html__( 'line1', 'weeconnectpay' ) . ' ' . esc_html( $avsLine1 );
		}
		if ( ! empty( $avsZip ) ) {
			$avsParts[] = esc_html__( 'ZIP', 'weeconnectpay' ) . ' ' . esc_html( $avsZip );
		}
		if ( ! empty( $avsParts ) ) {
			$lines[] = '<b>' . esc_html__( 'AVS: ', 'weeconnectpay' ) . '</b>' . implode( ', ', $avsParts );
		}

		if ( ! empty( $cvc ) ) {
			$lines[] = '<b>' . esc_html__( 'CVV: ', 'weeconnectpay' ) . '</b>' . esc_html( $cvc );
		}

		if ( empty( $lines ) ) {
			return '';
		}

		return '<br>' . implode( '<br>', $lines );
	}

	/**
	 * Tidy up snake_case outcome strings (e.g. `approved_by_network`,
	 * `not_sent_to_network`) into something readable in an order note.
	 */
	private static function humanizeOutcome( string $value ): string {
		return ucfirst( str_replace( '_', ' ', $value ) );
	}
}
