<?php
/**
 * Logger utility helpers.
 *
 * @package MailDoctor
 */

namespace MailDoctor;

use DateTimeImmutable;
use DateTimeZone;
use MailDoctor\Helpers\Settings;
use WP_Error;

trait Logger_Utils_Trait {

	/**
	 * Normalize status before persisting.
	 *
	 * @param string $status Raw status string.
	 * @return string
	 */
	protected static function normalize_status( string $status ): string {
		$status  = strtolower( sanitize_key( $status ) );
		$allowed = array( 'queued', 'sent', 'failed', 'skipped' );

		return in_array( $status, $allowed, true ) ? $status : 'queued';
	}

	/**
	 * Normalize date argument for queries.
	 *
	 * @param string $value       Date input.
	 * @param string $time_suffix Time suffix.
	 * @return string|null
	 */
	protected static function normalize_date_argument( string $value, string $time_suffix ): ?string {
		$value = trim( sanitize_text_field( $value ) );

		if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
			return null;
		}

		$combined = $value . ' ' . $time_suffix;
		$gmt      = get_gmt_from_date( $combined );

		return $gmt ? $gmt : null;
	}

	/**
	 * Convert recipients into deterministic string.
	 *
	 * @param mixed $to Recipient payload.
	 * @return string
	 */
	protected static function stringify_recipients( $to ): string {
		return implode( ', ', self::normalize_recipient_array( $to ) );
	}

	/**
	 * Normalize recipients into sanitized array.
	 *
	 * @param mixed $to Recipient payload.
	 * @return array<int, string>
	 */
	protected static function normalize_recipient_array( $to ): array {
		$items = array();

		if ( empty( $to ) ) {
			return array();
		}

		if ( is_array( $to ) ) {
			$items = $to;
		} else {
			$items = array_map( 'trim', preg_split( '/,|;/', (string) $to ) ?: array() );
		}

		$clean = array();

		foreach ( $items as $value ) {
			if ( is_array( $value ) ) {
				$clean = array_merge( $clean, self::normalize_recipient_array( $value ) );
				continue;
			}

			$email = sanitize_email( (string) $value );
			if ( $email ) {
				$clean[] = strtolower( $email );
			}
		}

		return array_values( array_unique( $clean ) );
	}

	/**
	 * Prepare headers for storage.
	 *
	 * @param mixed $headers Headers payload.
	 * @return string
	 */
	protected static function prepare_headers_for_storage( $headers ): string {
		if ( Settings::privacy_mode_enabled() ) {
			return __( '[redacted]', 'mail-doctor' );
		}

		if ( is_array( $headers ) ) {
			return implode( '\n', array_map( 'sanitize_text_field', $headers ) );
		}

		return sanitize_textarea_field( (string) $headers );
	}

	/**
	 * Prepare payload args for persistence.
	 *
	 * @param array<string, mixed> $mail_args Mail arguments.
	 * @return array<string, mixed>
	 */
	protected static function prepare_payload_args( array $mail_args ): array {
		if ( Settings::privacy_mode_enabled() ) {
			return array(
				'to'          => array(),
				'subject'     => $mail_args['subject'] ?? '',
				'message'     => '',
				'headers'     => array(),
				'attachments' => array(),
			);
		}

		return array(
			'to'          => $mail_args['to'] ?? '',
			'subject'     => $mail_args['subject'] ?? '',
			'message'     => $mail_args['message'] ?? '',
			'headers'     => $mail_args['headers'] ?? array(),
			'attachments' => $mail_args['attachments'] ?? array(),
		);
	}

	/**
	 * Extract log identifier from header collection.
	 *
	 * @param mixed $headers Headers payload.
	 * @return int
	 */
	protected static function extract_log_id_from_headers( $headers ): int {
		if ( is_array( $headers ) ) {
			foreach ( $headers as $key => $value ) {
				if ( is_string( $key ) && stripos( $key, 'X-Order-Mail-Doctor-ID' ) === 0 ) {
					return (int) trim( (string) $value );
				}
				if ( is_string( $value ) && stripos( $value, 'X-Order-Mail-Doctor-ID:' ) === 0 ) {
					$parts = explode( ':', $value );
					return isset( $parts[1] ) ? (int) trim( $parts[1] ) : 0;
				}
			}
		}

		$normalized = self::normalize_headers( $headers );

		foreach ( $normalized as $header ) {
			if ( stripos( $header, 'X-Order-Mail-Doctor-ID:' ) === 0 ) {
				$parts = explode( ':', $header );
				return isset( $parts[1] ) ? (int) trim( $parts[1] ) : 0;
			}
		}

		return 0;
	}

	/**
	 * Normalize headers into array of strings.
	 *
	 * @param mixed $headers Headers input.
	 * @return array<int, string>
	 */
	public static function normalize_headers( $headers ): array {
		if ( empty( $headers ) ) {
			return array();
		}

		if ( is_string( $headers ) ) {
			$headers = str_replace( array( "\r\n", "\r" ), "\n", $headers );
			return array_filter( array_map( 'trim', explode( "\n", $headers ) ) );
		}

		if ( is_array( $headers ) ) {
			return array_filter(
				array_map(
					static function ( $header ) {
						return is_string( $header ) ? trim( $header ) : '';
					},
					$headers
				)
			);
		}

		return array();
	}

	/**
	 * Extract a specific header value from a normalized header array.
	 *
	 * @param array<int,string> $headers Normalized headers.
	 * @param string            $needle  Header name to locate.
	 * @return string|null
	 */
	protected static function extract_header_value( array $headers, string $needle ): ?string {
		$target = strtolower( $needle );

		foreach ( $headers as $header ) {
			if ( false === strpos( $header, ':' ) ) {
				continue;
			}

			list( $name, $value ) = array_map( 'trim', explode( ':', $header, 2 ) );

			if ( strtolower( $name ) !== $target ) {
				continue;
			}

			$value = trim( $value );

			return '' === $value ? null : $value;
		}

		return null;
	}

	/**
	 * Build a short, safe preview of the message body.
	 *
	 * @param string $body Raw body.
	 * @param int    $limit Character limit.
	 * @return string
	 */
	protected static function trim_body_preview( string $body, int $limit = 240 ): string {
		$body = wp_strip_all_tags( $body );
		$body = trim( preg_replace( '/\s+/', ' ', $body ) );

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

		if ( strlen( $body ) > $limit ) {
			return substr( $body, 0, $limit - 1 ) . '…';
		}

		return $body;
	}

	/**
	 * Generate a short hash of the message body.
	 *
	 * @param string $body Raw body.
	 * @return string
	 */
	protected static function hash_body_preview( string $body ): string {
		if ( '' === $body ) {
			return '';
		}

		return substr( hash( 'sha256', $body ), 0, 12 );
	}

	/**
	 * Compute duration for active log.
	 *
	 * @param int $log_id Log identifier.
	 * @return int|null
	 */
	protected static function calculate_duration( int $log_id ): ?int {
		if ( ! isset( self::$active[ $log_id ] ) ) {
			return null;
		}

		$started = self::$active[ $log_id ]['started_at'] ?? 0.0;

		if ( ! $started ) {
			return null;
		}

		$duration = ( microtime( true ) - (float) $started ) * 1000;

		return (int) round( $duration );
	}

	/**
	 * Attempt to extract SMTP response code if present.
	 *
	 * @param array<string, mixed> $mail_data Mail data.
	 * @return string|null
	 */
	protected static function extract_smtp_code( array $mail_data ): ?string {
		if ( isset( $mail_data['mail_doctor_smtp_code'] ) ) {
			return sanitize_text_field( (string) $mail_data['mail_doctor_smtp_code'] );
		}

		return null;
	}

	/**
	 * Calculate median value.
	 *
	 * @param array<int, int|float> $values Values collection.
	 * @return float
	 */
	protected static function calculate_median( array $values ): float {
		$count = count( $values );
		if ( 0 === $count ) {
			return 0.0;
		}

		sort( $values, SORT_NUMERIC );

		$middle = (int) floor( ( $count - 1 ) / 2 );

		if ( $count % 2 ) {
			return (float) $values[ $middle ];
		}

		return ( $values[ $middle ] + $values[ $middle + 1 ] ) / 2.0;
	}

	/**
	 * Normalize error message and metadata.
	 *
	 * @param WP_Error|string $error Error representation.
	 * @return array{message: string, provider_code: string}
	 */
	protected static function normalize_error_info( $error ): array {
		$message       = '';
		$provider_code = null;
		$error_data    = array();

		if ( $error instanceof WP_Error ) {
			$message    = $error->get_error_message();
			$error_data = $error->get_error_data( 'wp_mail_failed' );
			if ( empty( $error_data ) ) {
				$error_data = $error->get_error_data();
			}
			if ( is_array( $error_data ) && isset( $error_data['phpmailer_exception_code'] ) ) {
				$provider_code = (string) $error_data['phpmailer_exception_code'];
			}
		} else {
			$message = (string) $error;
		}

		$message = trim( wp_strip_all_tags( $message ) );

		if ( empty( $message ) ) {
			$message = __( 'Unknown error reported by WordPress mail transport.', 'mail-doctor' );
		}

		if ( empty( $provider_code ) ) {
			$provider_code = self::extract_provider_code_from_message( $message );
		}

		$hint = self::derive_error_hint( strtolower( $message ) );

		if ( $hint ) {
			$message .= ' ' . sprintf(
				/* translators: %s: guidance text. */
				__( 'Hint: %s', 'mail-doctor' ),
				$hint
			);
		}

		return array(
			'message'       => $message,
			'provider_code' => $provider_code ? sanitize_text_field( $provider_code ) : '',
		);
	}

	/**
	 * Attempt to extract SMTP code from message text.
	 *
	 * @param string $message Error message.
	 * @return string|null
	 */
	protected static function extract_provider_code_from_message( string $message ): ?string {
		if ( preg_match( '/\b(\d{3})\b/', $message, $matches ) ) {
			return $matches[1];
		}

		return null;
	}

	/**
	 * Clear cached metrics transients.
	 *
	 * @return void
	 */
	protected static function flush_metrics_cache(): void {
		foreach ( array( 1, 7, 30 ) as $days ) {
			delete_transient( 'mail_doctor_metrics_' . $days );
		}
	}

	/**
	 * Provide human friendly hint based on known error fragments.
	 *
	 * @param string $message_lc Lowercase error message.
	 * @return string|null
	 */
	protected static function derive_error_hint( string $message_lc ): ?string {
		if ( strpos( $message_lc, 'invalid address' ) !== false || strpos( $message_lc, 'address rejected' ) !== false ) {
			return __( 'Verify the From email under Mail Doctor → Settings and ensure it matches your authenticated domain.', 'mail-doctor' );
		}

		if ( strpos( $message_lc, 'could not authenticate' ) !== false || strpos( $message_lc, 'authentication failed' ) !== false ) {
			return __( 'Check the SMTP username/password and confirm the account is allowed to send mail.', 'mail-doctor' );
		}

		if ( strpos( $message_lc, 'connection timed out' ) !== false || strpos( $message_lc, 'failed to connect to server' ) !== false || strpos( $message_lc, 'smtp connect() failed' ) !== false ) {
			return __( 'Confirm the SMTP host, port, and encryption settings; ensure the server can reach the provider through the firewall.', 'mail-doctor' );
		}

		if ( strpos( $message_lc, 'certificate verify failed' ) !== false ) {
			return __( 'The server cannot validate the SMTP certificate. Update CA bundles or switch to TLS/STARTTLS if supported.', 'mail-doctor' );
		}

		if ( strpos( $message_lc, 'quota exceeded' ) !== false || strpos( $message_lc, 'daily user sending quota exceeded' ) !== false ) {
			return __( 'The SMTP provider reports a quota issue. Review sending limits or upgrade the plan.', 'mail-doctor' );
		}

		if ( strpos( $message_lc, 'relay access denied' ) !== false ) {
			return __( 'The SMTP server rejected the sender. Ensure the From address is authorized and the domain is verified.', 'mail-doctor' );
		}

		return null;
	}

	/**
	 * Resolve origin log after successful retry.
	 *
	 * @param int                 $origin_id Origin log ID.
	 * @param int                 $resolved_by Resolver log ID.
	 * @param array<string,mixed> $meta Metadata.
	 * @return void
	 */
	protected static function resolve_origin_log( int $origin_id, int $resolved_by, array $meta ): void {
		if ( $origin_id <= 0 ) {
			return;
		}

		global $wpdb;

		$origin_payload = self::get_payload( $origin_id );

		if ( ! isset( $origin_payload['retry'] ) || ! is_array( $origin_payload['retry'] ) ) {
			$origin_payload['retry'] = array();
		}

		$origin_payload['retry']['next_attempt_gmt'] = null;
		$origin_payload['retry']['resolved']         = true;
		$origin_payload['retry']['resolved_by']      = $resolved_by;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Retry resolution update.
		$wpdb->update(
			self::table_name(),
			array(
				'status'        => 'sent',
				'error'         => sprintf(
					/* translators: %d: log ID that resolved the retry. */
					__( 'Resolved via retry log #%d.', 'mail-doctor' ),
					$resolved_by
				),
				'provider_code' => isset( $meta['provider_code'] ) ? sanitize_text_field( $meta['provider_code'] ) : null,
				'payload'       => wp_json_encode( $origin_payload ),
			),
			array( 'id' => $origin_id ),
			array( '%s', '%s', '%s', '%s' ),
			array( '%d' )
		);
	}

	/**
	 * Convert GMT datetime string to timestamp.
	 *
	 * @param string|null $datetime Datetime.
	 * @return int|null
	 */
	protected static function gmt_to_timestamp( ?string $datetime ): ?int {
		if ( empty( $datetime ) ) {
			return null;
		}

		$timestamp = strtotime( $datetime . ' UTC' );

		if ( false === $timestamp ) {
			return null;
		}

		return (int) $timestamp;
	}
}
