<?php
/**
 * Mailer debugging helpers.
 *
 * @package MailDoctor
 */

namespace MailDoctor;

use MailDoctor\Helpers\Settings;
use MailDoctor\Logger;
use WP_Error;

trait Mailer_Debug_Trait {

	/**
	 * Emit debug information when enabled.
	 *
	 * @param string              $message Message to log.
	 * @param array<string,mixed> $context Context data.
	 * @return void
	 */
	private static function debug_log( string $message, array $context = array() ): void {
		if ( ! Settings::debug_logging_enabled() ) {
			return;
		}

		$line = '[MailDoctor Mailer] ' . $message;

		if ( ! empty( $context ) ) {
			$encoded = wp_json_encode( $context );
			if ( false !== $encoded ) {
				$line .= ' :: ' . $encoded;
			}
		}

		error_log( $line ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
	}

	/**
	 * Retrieve configured transport.
	 *
	 * @return string
	 */
	protected static function current_transport(): string {
		$transport = Settings::get( 'transport', 'smtp' );

		return $transport ? (string) $transport : 'smtp';
	}

	/**
	 * Build logging context for current invocation.
	 *
	 * @return array<string, mixed>
	 */
	protected static function build_logging_context(): array {
		$transport       = self::current_transport();
		$profile_label   = isset( self::$active_profile['label'] ) ? (string) self::$active_profile['label'] : '';
		$transport_label = Settings::transport_label( $transport );
		$smtp_host       = isset( self::$active_profile['host'] ) ? (string) self::$active_profile['host'] : '';

		return array(
			'event'              => self::$current_event ?: 'wp_mail',
			'order_id'           => self::$current_order_id,
			'attempt'            => self::$retry_context['attempt'] ?? 0,
			'origin_log_id'      => self::$retry_context['origin_log_id'] ?? null,
			'manual'             => self::$retry_context['manual'] ?? false,
			'email_class'        => self::$current_email_class,
			'smtp_profile'       => self::$active_profile_key,
			'smtp_profile_label' => $profile_label ?: null,
			'smtp_host'          => $smtp_host ?: null,
			'transport'          => $transport,
			'transport_label'    => $transport_label,
			'idempotency_key'    => self::$idempotency_key,
			'idempotency_bucket' => self::$idempotency_bucket,
			'correlation_id'     => self::ensure_correlation_id(),
		);
	}

	/**
	 * Ensure a correlation identifier is available for the current send.
	 *
	 * @return string
	 */
	protected static function ensure_correlation_id(): string {
		if ( null === self::$correlation_id ) {
			self::$correlation_id = self::sanitize_correlation_value( self::generate_correlation_id() );
		}

		return self::$correlation_id;
	}

	/**
	 * Generate a deterministic-ish correlation identifier.
	 *
	 * @return string
	 */
	protected static function generate_correlation_id(): string {
		$order = self::$current_order_id ?: 0;
		$seed  = wp_rand();
		$token = substr( hash( 'sha256', microtime( true ) . '|' . $seed ), 0, 16 );

		return ( $order > 0 ? 'ord-' . $order : 'mail' ) . '-' . $token;
	}

	/**
	 * Sanitize correlation identifier for safe output.
	 *
	 * @param string $value Raw correlation value.
	 * @return string
	 */
	protected static function sanitize_correlation_value( string $value ): string {
		$value = preg_replace( '/[^\x20-\x7E]/', '', $value );

		return trim( (string) $value );
	}

	/**
	 * Provide folded correlation header value.
	 *
	 * @return string
	 */
	protected static function correlation_header_value(): string {
		$cid = self::sanitize_correlation_value( self::ensure_correlation_id() );

		if ( '' === $cid ) {
			return '';
		}

		return wordwrap( $cid, 76, "\r\n\t", true );
	}

	/**
	 * Emit structured JSON tracing payload guarded by MAILDBG.
	 *
	 * @param string               $stage   Trace stage identifier.
	 * @param array<string, mixed> $context Context data.
	 * @return void
	 */
	protected static function trace_event( string $stage, array $context = array() ): void {
		if ( ! defined( 'MAILDBG' ) || ! MAILDBG ) {
			return;
		}

		$payload = array_merge(
			array(
				'correlation' => self::sanitize_correlation_value( self::ensure_correlation_id() ),
				'event'       => self::$current_event ?: 'wp_mail',
				'order_id'    => self::$current_order_id,
				'email_id'    => self::$current_email_id,
				'stage'       => $stage,
				'time'        => gmdate( 'c' ),
				'transport'   => self::current_transport(),
			),
			self::sanitize_trace_payload( $context )
		);

		$encoded = wp_json_encode( $payload );
		if ( false === $encoded ) {
			return;
		}

		error_log( '[MAILDBG] ' . $encoded ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
	}

	/**
	 * Attach masked mail arguments to a failure error object.
	 *
	 * @param WP_Error            $error     Original error instance.
	 * @param array<string,mixed> $mail_args Mail arguments used for the attempt.
	 * @param int|null            $log_id    Associated log identifier.
	 * @return WP_Error
	 */
	protected static function augment_failed_error( WP_Error $error, array $mail_args, ?int $log_id = null ): WP_Error {
		$data = (array) $error->get_error_data();

		$data['maildoctor'] = array(
			'log_id'           => $log_id,
			'transport'        => self::current_transport(),
			'correlation'      => self::sanitize_correlation_value( self::ensure_correlation_id() ),
			'mail_args_masked' => self::sanitize_mail_args_for_error( $mail_args ),
		);

		if ( ! isset( $data['mail_doctor_raw'] ) ) {
			$data['mail_doctor_raw'] = $mail_args;
		}

		return new WP_Error( $error->get_error_code(), $error->get_error_message(), $data );
	}

	/**
	 * Create a masked snapshot of mail arguments for error telemetry.
	 *
	 * @param array<string,mixed> $mail_args Mail arguments used.
	 * @return array<string,mixed>
	 */
	protected static function sanitize_mail_args_for_error( array $mail_args ): array {
		$subset = array(
			'to'          => $mail_args['to'] ?? array(),
			'subject'     => $mail_args['subject'] ?? '',
			'headers'     => $mail_args['headers'] ?? array(),
			'attachments' => $mail_args['attachments'] ?? array(),
			'message'     => $mail_args['message'] ?? '',
		);

		return self::sanitize_trace_payload( $subset );
	}

	/**
	 * Sanitize trace payload to avoid leaking PII.
	 *
	 * @param array<string, mixed> $context Raw context.
	 * @return array<string, mixed>
	 */
	protected static function sanitize_trace_payload( array $context ): array {
		$sanitized = array();

		foreach ( $context as $key => $value ) {
			$normalized_key = is_string( $key ) ? strtolower( $key ) : '';

			if ( $value instanceof WP_Error ) {
				$sanitized[ $key ] = array(
					'code'    => $value->get_error_code(),
					'message' => self::truncate_string( $value->get_error_message(), 96 ),
				);
				continue;
			}

			if ( is_array( $value ) ) {
				if ( 'attachments' === $normalized_key ) {
					$sanitized[ $key ] = self::summarize_attachments( $value );
					continue;
				}

				if ( 'headers' === $normalized_key ) {
					$sanitized[ $key ] = self::sanitize_headers( $value );
					continue;
				}

				if ( self::is_address_key( $normalized_key ) ) {
					$sanitized[ $key ] = self::mask_address_list( $value );
					continue;
				}

				$sanitized[ $key ] = self::sanitize_trace_payload( $value );
				continue;
			}

			if ( is_bool( $value ) || is_int( $value ) || is_float( $value ) || null === $value ) {
				$sanitized[ $key ] = $value;
				continue;
			}

			if ( 'attachments' === $normalized_key ) {
				$sanitized[ $key ] = self::summarize_attachments( array( $value ) );
				continue;
			}

			if ( self::is_address_key( $normalized_key ) || is_email( (string) $value ) ) {
				$sanitized[ $key ] = self::mask_email( (string) $value );
				continue;
			}

			if ( 'correlation' === $normalized_key || 'correlation_id' === $normalized_key ) {
				$sanitized[ $key ] = self::sanitize_correlation_value( (string) $value );
				continue;
			}

			if ( 'subject' === $normalized_key ) {
				$sanitized[ $key ] = self::truncate_string( (string) $value, 120 );
				continue;
			}

			if ( self::is_message_key( $normalized_key ) ) {
				$sanitized[ $key ] = array( 'bytes' => strlen( (string) $value ) );
				continue;
			}

			$masked            = self::mask_emails_in_string( (string) $value );
			$sanitized[ $key ] = self::truncate_string( $masked, 96 );
		}

		return $sanitized;
	}

	/**
	 * Sanitize header collection for telemetry.
	 *
	 * @param mixed $headers Raw headers.
	 * @return array<string,mixed>
	 */
	protected static function sanitize_headers( $headers ): array {
		$normalized = Logger::normalize_headers( $headers );
		$sample     = array();

		foreach ( $normalized as $header ) {
			$line = (string) $header;

			if ( stripos( $line, 'authorization:' ) === 0 ) {
				$line = strtok( $line, ':' ) . ': [redacted]';
			}

			$sample[] = self::truncate_string( self::mask_emails_in_string( $line ), 96 );

			if ( count( $sample ) >= 5 ) {
				break;
			}
		}

		return array(
			'count'  => count( $normalized ),
			'sample' => $sample,
		);
	}

	/**
	 * Mask any addresses contained within a string.
	 *
	 * @param string $value Input value.
	 * @return string
	 */
	protected static function mask_emails_in_string( string $value ): string {
		return (string) preg_replace_callback(
			'/([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})/i',
			static function ( $matches ) {
				return self::mask_email( $matches[0] );
			},
			$value
		);
	}

	/**
	 * Mask an array or comma-separated list of addresses.
	 *
	 * @param mixed $value Raw value.
	 * @return array<int,string>
	 */
	protected static function mask_address_list( $value ): array {
		$items  = is_array( $value ) ? $value : array_map( 'trim', explode( ',', (string) $value ) );
		$masked = array();

		foreach ( $items as $item ) {
			if ( is_array( $item ) ) {
				if ( isset( $item['email'] ) ) {
					$masked[] = self::mask_email( (string) $item['email'] );
				}
				continue;
			}

			$string = trim( (string) $item );
			if ( '' === $string ) {
				continue;
			}

			if ( preg_match( '/<([^>]+)>/', $string, $matches ) && is_email( $matches[1] ) ) {
				$masked[] = self::mask_email( $matches[1] );
				continue;
			}

			if ( is_email( $string ) ) {
				$masked[] = self::mask_email( $string );
				continue;
			}

			$masked[] = self::truncate_string( $string, 32 );
		}

		$masked = array_values( array_unique( array_filter( $masked ) ) );

		return array_slice( $masked, 0, 10 );
	}

	/**
	 * Summarize attachment payload without leaking filenames.
	 *
	 * @param mixed $attachments Raw attachments.
	 * @return array<string,mixed>
	 */
	protected static function summarize_attachments( $attachments ): array {
		$count  = 0;
		$hashes = array();

		foreach ( (array) $attachments as $attachment ) {
			++$count;

			$label = 'attachment';
			if ( is_array( $attachment ) ) {
				foreach ( array( 'name', 'filename', 'file', 'path' ) as $candidate ) {
					if ( ! empty( $attachment[ $candidate ] ) ) {
						$label = (string) $attachment[ $candidate ];
						break;
					}
				}
			} else {
				$label = (string) $attachment;
			}

			$hashes[] = substr( hash( 'sha256', basename( $label ) ), 0, 12 );
		}

		return array(
			'count'  => $count,
			'hashes' => $hashes,
		);
	}

	/**
	 * Determine whether a key refers to address data.
	 *
	 * @param string $key Normalized key.
	 * @return bool
	 */
	protected static function is_address_key( string $key ): bool {
		return in_array( $key, array( 'to', 'cc', 'bcc', 'reply_to', 'recipients', 'recipient', 'emails' ), true );
	}

	/**
	 * Determine whether a key refers to message body content.
	 *
	 * @param string $key Normalized key.
	 * @return bool
	 */
	protected static function is_message_key( string $key ): bool {
		return in_array( $key, array( 'message', 'body', 'html', 'content', 'text' ), true );
	}

	/**
	 * Truncate string while keeping it readable.
	 *
	 * @param string $value Input string.
	 * @param int    $limit Maximum length.
	 * @return string
	 */
	protected static function truncate_string( string $value, int $limit = 96 ): string {
		$value = trim( preg_replace( '/\s+/', ' ', $value ) );

		if ( strlen( $value ) <= $limit ) {
			return $value;
		}

		return substr( $value, 0, $limit ) . '…';
	}

	/**
	 * Mask an email address for debug output.
	 *
	 * @param string $email Raw email.
	 * @return string
	 */
	protected static function mask_email( string $email ): string {
		$email = strtolower( trim( $email ) );

		if ( ! is_email( $email ) ) {
			return self::truncate_string( $email, 32 );
		}

		list($local, $domain) = explode( '@', $email, 2 );

		$user_mask = '' !== $local ? substr( $local, 0, 1 ) . '***' : 'u***';

		$segments  = explode( '.', $domain );
		$tld       = array_pop( $segments );
		$root      = implode( '.', $segments );
		$root_mask = '' !== $root ? substr( $root, 0, 1 ) . '***' : 'd***';
		$tld_mask  = $tld ? substr( $tld, 0, 3 ) : 'tld';

		return sprintf( '%s@%s.%s', $user_mask, $root_mask, strtolower( $tld_mask ) );
	}
}
