<?php
/**
 * Allowlisted HTTP client for email API providers.
 *
 * @package EmailSmtp
 */

namespace EmailSmtp\Includes;

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

final class ApiHttpClient {

	/**
	 * Approved API hostnames (exact match).
	 *
	 * @var list<string>
	 */
	private const ALLOWED_HOSTS = [
		'api.sendgrid.com',
		'api.eu.sendgrid.com',
		'api.mailgun.net',
		'api.eu.mailgun.net',
		'api.brevo.com',
		'api.postmarkapp.com',
		'api.mailersend.com',
		'api.resend.com',
		'api.sparkpost.com',
		'api.eu.sparkpost.com',
		'us1.platform.bird.com',
		'eu1.platform.bird.com',
		'api.elasticemail.com',
		'api.smtp2go.com',
		'us-api.smtp2go.com',
		'eu-api.smtp2go.com',
		'au-api.smtp2go.com',
		'api.mailjet.com',
		'emailapi.netcorecloud.net',
		'apieu.netcorecloud.net',
		'mandrillapp.com',
		'api.emailit.com',
		'console.sendlayer.com',
		'api.smtp.com',
		'smtp.maileroo.com',
		'api.turbo-smtp.com',
		'api.eu.turbo-smtp.com',
		'api.mailbaby.net',
		'oauth2.googleapis.com',
		'gmail.googleapis.com',
		'www.googleapis.com',
		'graph.microsoft.com',
		'login.microsoftonline.com',
		// SES regional hosts are validated dynamically.
	];

	/**
	 * @param array<string, string>       $headers
	 * @param array<string, mixed>|string $body
	 * @return array{code:int, body:string, headers:array<string,string>, duration_ms:int, json:array<string,mixed>|null}
	 */
	public static function request( string $method, string $url, array $headers = [], $body = null, int $timeout = 30 ): array {
		$started = (int) round( microtime( true ) * 1000 );

		if ( ! self::is_url_allowed( $url ) ) {
			return [
				'code'        => 0,
				'body'        => '',
				'headers'     => [],
				'duration_ms' => 0,
				'json'        => null,
				'error'       => 'endpoint_not_allowed',
			];
		}

		$args = [
			'method'      => strtoupper( $method ),
			'timeout'     => max( 5, min( 120, $timeout ) ),
			// Do not follow redirects — a 3xx off the allowlisted host would bypass SSRF checks.
			'redirection' => 0,
			'headers'     => array_merge(
				[
					'Accept'     => 'application/json',
					'User-Agent' => self::userAgent(),
				],
				$headers
			),
		];

		if ( $body !== null ) {
			if ( is_array( $body ) ) {
				$content_type                    = (string) ( $args['headers']['Content-Type'] ?? 'application/json' );
				$args['headers']['Content-Type'] = $content_type;
				if ( stripos( $content_type, 'application/x-www-form-urlencoded' ) !== false ) {
					$args['body'] = http_build_query( $body, '', '&' );
				} else {
					$args['body'] = wp_json_encode( $body );
				}
			} else {
				$args['body'] = (string) $body;
			}
		}

		$response = wp_remote_request( $url, $args );
		$duration = (int) round( microtime( true ) * 1000 ) - $started;

		if ( is_wp_error( $response ) ) {
			return [
				'code'        => 0,
				'body'        => '',
				'headers'     => [],
				'duration_ms' => $duration,
				'json'        => null,
				'error'       => ErrorNormalizer::redact( $response->get_error_message() ),
			];
		}

		$code           = (int) wp_remote_retrieve_response_code( $response );
		$response_body  = (string) wp_remote_retrieve_body( $response );
		$response_hdrs  = wp_remote_retrieve_headers( $response );
		$normalized_hdr = [];
		if ( is_object( $response_hdrs ) && method_exists( $response_hdrs, 'getAll' ) ) {
			foreach ( $response_hdrs->getAll() as $name => $value ) {
				$normalized_hdr[ strtolower( (string) $name ) ] = is_array( $value ) ? implode( ', ', $value ) : (string) $value;
			}
		}

		$json = json_decode( $response_body, true );
		if ( ! is_array( $json ) ) {
			$json = null;
		}

		return [
			'code'        => $code,
			'body'        => $response_body,
			'headers'     => $normalized_hdr,
			'duration_ms' => $duration,
			'json'        => $json,
			'error'       => '',
		];
	}

	public static function is_url_allowed( string $url ): bool {
		$parts = wp_parse_url( $url );
		if ( ! is_array( $parts ) ) {
			return false;
		}
		$scheme = strtolower( (string) ( $parts['scheme'] ?? '' ) );
		$host   = strtolower( (string) ( $parts['host'] ?? '' ) );
		if ( $scheme !== 'https' || $host === '' ) {
			return false;
		}
		if ( in_array( $host, self::ALLOWED_HOSTS, true ) ) {
			return true;
		}
		// Amazon SES v2 regional endpoints: email.{region}.amazonaws.com
		if ( preg_match( '/^email\.([a-z0-9-]+)\.amazonaws\.com$/', $host ) ) {
			return true;
		}
		return false;
	}

	/**
	 * Convert an HTTP response into a DeliveryResult.
	 *
	 * @param array{code:int, body:string, headers:array<string,string>, duration_ms:int, json:array<string,mixed>|null, error?:string} $response
	 * @param callable|null                                                                                                                $success_extractor fn(array $response): array{message?:string, id?:string}
	 */
	public static function to_delivery_result( array $response, ?callable $success_extractor = null, int $success_min = 200, int $success_max = 299 ): DeliveryResult {
		if ( ! empty( $response['error'] ) ) {
			return ErrorNormalizer::from_text( (string) $response['error'] );
		}

		$code = (int) ( $response['code'] ?? 0 );
		if ( $code >= $success_min && $code <= $success_max ) {
			$message = __( 'Request accepted by provider.', 'wp-powersuite' );
			$id      = '';
			if ( $success_extractor ) {
				$extracted = $success_extractor( $response );
				if ( is_array( $extracted ) ) {
					$message = (string) ( $extracted['message'] ?? $message );
					$id      = (string) ( $extracted['id'] ?? '' );
				}
			}
			return DeliveryResult::success( $message, $id, (int) ( $response['duration_ms'] ?? 0 ) );
		}

		$technical = self::extract_error_text( $response );
		$result    = ErrorNormalizer::from_text( $technical, (string) $code );
		return DeliveryResult::failure(
			$result->get_user_message(),
			$result->get_technical_message(),
			$result->get_category(),
			(string) $code,
			$result->is_retry_safe(),
			$result->get_suggested_action(),
			(int) ( $response['duration_ms'] ?? 0 )
		);
	}

	/**
	 * @param array{json:array<string,mixed>|null, body:string} $response
	 */
	public static function extract_error_text( array $response ): string {
		$json = $response['json'] ?? null;
		if ( is_array( $json ) ) {
			// Bird error envelope
			if ( ! empty( $json['error'] ) && is_array( $json['error'] ) && isset( $json['error']['message'] ) && is_string( $json['error']['message'] ) ) {
				$err = $json['error'];
				$msg = $err['message'];
				if ( ! empty( $err['details'] ) && is_array( $err['details'] ) ) {
					$parts = [];
					foreach ( $err['details'] as $detail ) {
						if ( ! is_array( $detail ) ) {
							continue;
						}
						$dmsg = (string) ( $detail['message'] ?? '' );
						if ( $dmsg === '' ) {
							continue;
						}
						$param   = (string) ( $detail['param'] ?? '' );
						$parts[] = $param !== '' ? $param . ': ' . $dmsg : $dmsg;
					}
					if ( $parts !== [] ) {
						$msg .= ' — ' . implode( '; ', $parts );
					}
				} elseif ( ! empty( $err['code'] ) && is_string( $err['code'] ) ) {
					$msg .= ' (' . $err['code'] . ')';
				}
				return ErrorNormalizer::redact( $msg );
			}
			// TurboSMTP errors[]
			if (
				isset( $json['message'] )
				&& strtolower( (string) $json['message'] ) === 'error'
				&& ! empty( $json['errors'] )
				&& is_array( $json['errors'] )
			) {
				$parts = [];
				foreach ( $json['errors'] as $err ) {
					if ( is_string( $err ) && $err !== '' ) {
						$parts[] = $err;
					}
				}
				if ( $parts !== [] ) {
					return ErrorNormalizer::redact( implode( '; ', $parts ) );
				}
			}
			// Netcore error[]
			if ( ! empty( $json['error'] ) && is_array( $json['error'] ) ) {
				$parts = [];
				foreach ( $json['error'] as $err ) {
					if ( is_string( $err ) && $err !== '' ) {
						$parts[] = $err;
					} elseif ( is_array( $err ) ) {
						$msg = (string) ( $err['message'] ?? '' );
						if ( $msg === '' ) {
							continue;
						}
						$field = (string) ( $err['field'] ?? '' );
						$parts[] = $field !== '' ? $field . ': ' . $msg : $msg;
					}
				}
				if ( $parts !== [] ) {
					return ErrorNormalizer::redact( implode( '; ', $parts ) );
				}
			}
			// SMTP2GO data.error
			if ( ! empty( $json['data'] ) && is_array( $json['data'] ) ) {
				$data = $json['data'];
				if ( ! empty( $data['error'] ) && is_string( $data['error'] ) ) {
					$msg = $data['error'];
					if ( ! empty( $data['field_validation_errors'] ) && is_array( $data['field_validation_errors'] ) ) {
						$fve   = $data['field_validation_errors'];
						$extra = (string) ( $fve['message'] ?? '' );
						if ( $extra !== '' ) {
							$field = (string) ( $fve['fieldname'] ?? '' );
							$msg  .= ' — ' . ( $field !== '' ? $field . ': ' . $extra : $extra );
						}
					} elseif ( ! empty( $data['error_code'] ) && is_string( $data['error_code'] ) ) {
						$msg .= ' (' . $data['error_code'] . ')';
					}
					return ErrorNormalizer::redact( $msg );
				}
				if ( ! empty( $data['failures'] ) && is_array( $data['failures'] ) ) {
					$parts = [];
					foreach ( $data['failures'] as $failure ) {
						if ( is_string( $failure ) && $failure !== '' ) {
							$parts[] = $failure;
						} elseif ( is_array( $failure ) ) {
							$text = (string) ( $failure['error'] ?? $failure['message'] ?? '' );
							if ( $text !== '' ) {
								$parts[] = $text;
							}
						}
					}
					if ( $parts !== [] ) {
						return ErrorNormalizer::redact( implode( '; ', $parts ) );
					}
				}
			}
			// Emailit validation_errors
			if ( ! empty( $json['validation_errors'] ) && is_array( $json['validation_errors'] ) ) {
				$parts = [];
				foreach ( $json['validation_errors'] as $err ) {
					if ( is_string( $err ) && $err !== '' ) {
						$parts[] = $err;
					}
				}
				if ( $parts !== [] ) {
					$base = ! empty( $json['error'] ) && is_string( $json['error'] )
						? $json['error'] . ': '
						: '';
					return ErrorNormalizer::redact( $base . implode( '; ', $parts ) );
				}
			}
			// Postmark-style: { "ErrorCode": 300, "Message": "..." } (non-zero ErrorCode).
			if (
				isset( $json['ErrorCode'], $json['Message'] )
				&& is_string( $json['Message'] )
				&& $json['Message'] !== ''
				&& (int) $json['ErrorCode'] !== 0
			) {
				return ErrorNormalizer::redact( $json['Message'] . ' (ErrorCode ' . (int) $json['ErrorCode'] . ')' );
			}
			// Brevo code/message
			if (
				! empty( $json['code'] )
				&& is_string( $json['code'] )
				&& ! empty( $json['message'] )
				&& is_string( $json['message'] )
				&& ! isset( $json['ErrorCode'] )
			) {
				return ErrorNormalizer::redact( $json['message'] . ' (' . $json['code'] . ')' );
			}
			// Resend statusCode/message
			if (
				isset( $json['statusCode'], $json['message'] )
				&& is_string( $json['message'] )
				&& $json['message'] !== ''
			) {
				$msg = $json['message'];
				if ( ! empty( $json['name'] ) && is_string( $json['name'] ) ) {
					$msg .= ' (' . $json['name'] . ')';
				}
				return ErrorNormalizer::redact( $msg );
			}
			foreach ( [ 'message', 'error', 'Error', 'ErrorMessage', 'FriendlyErrorMessage', 'Message', 'title', 'detail' ] as $key ) {
				if ( ! empty( $json[ $key ] ) && is_string( $json[ $key ] ) ) {
					return ErrorNormalizer::redact( $json[ $key ] );
				}
			}
			// JSend fail/error: { "status": "fail", "data": { "field": "reason" } }
			if (
				isset( $json['status'] )
				&& in_array( (string) $json['status'], [ 'fail', 'error' ], true )
				&& ! empty( $json['data'] )
				&& is_array( $json['data'] )
			) {
				$parts = [];
				foreach ( $json['data'] as $err_key => $err_val ) {
					if ( is_string( $err_val ) && $err_val !== '' ) {
						$parts[] = is_string( $err_key ) && $err_key !== ''
							? $err_key . ': ' . $err_val
							: $err_val;
					} elseif ( is_array( $err_val ) && ! empty( $err_val['message'] ) ) {
						$parts[] = (string) $err_val['message'];
					}
				}
				if ( $parts !== [] ) {
					return ErrorNormalizer::redact( implode( '; ', $parts ) );
				}
			}
			// Laravel/MailerSend field errors
			if ( ! empty( $json['errors'] ) && is_array( $json['errors'] ) && ! array_is_list( $json['errors'] ) ) {
				$parts = [];
				foreach ( $json['errors'] as $field => $msgs ) {
					if ( is_string( $msgs ) && $msgs !== '' ) {
						$parts[] = is_string( $field ) && $field !== '' ? $field . ': ' . $msgs : $msgs;
						continue;
					}
					if ( ! is_array( $msgs ) ) {
						continue;
					}
					foreach ( $msgs as $m ) {
						if ( ! is_string( $m ) || $m === '' ) {
							continue;
						}
						$parts[] = is_string( $field ) && $field !== '' ? $field . ': ' . $m : $m;
					}
				}
				if ( $parts !== [] ) {
					return ErrorNormalizer::redact( implode( '; ', $parts ) );
				}
			}
			// SendLayer/SendGrid errors[]
			foreach ( [ 'Errors', 'errors' ] as $errors_key ) {
				if ( empty( $json[ $errors_key ] ) || ! is_array( $json[ $errors_key ] ) ) {
					continue;
				}
				$parts = [];
				foreach ( $json[ $errors_key ] as $err ) {
					if ( is_string( $err ) ) {
						$parts[] = $err;
					} elseif ( is_array( $err ) ) {
						$msg = (string) ( $err['Message'] ?? $err['message'] ?? '' );
						if ( $msg === '' ) {
							continue;
						}
						$code  = $err['Code'] ?? $err['code'] ?? '';
						$field = (string) ( $err['field'] ?? $err['Field'] ?? '' );
						if ( $field !== '' && strtolower( $field ) !== 'null' ) {
							$msg = $field . ': ' . $msg;
						}
						$parts[] = $code !== '' && $code !== null
							? (string) $code . ': ' . $msg
							: $msg;
					}
				}
				if ( $parts !== [] ) {
					return ErrorNormalizer::redact( implode( '; ', $parts ) );
				}
			}
		}

		$body = trim( (string) ( $response['body'] ?? '' ) );
		if ( $body !== '' ) {
			return ErrorNormalizer::redact( substr( $body, 0, 500 ) );
		}

		$code = (int) ( $response['code'] ?? 0 );
		return $code > 0
			/* translators: 1: number, 2: number. */
			? sprintf( /* translators: %d: HTTP status */ __( 'HTTP %d from provider.', 'wp-powersuite' ), $code )
			: __( 'Empty provider response.', 'wp-powersuite' );
	}

	/**
	 * Product User-Agent using white-label short name when available.
	 */
	private static function userAgent(): string {
		$short = function_exists( 'wp_powersuite_get_plugin_short_name' )
			? wp_powersuite_get_plugin_short_name()
			: 'PowerSuite';
		$slug = preg_replace( '/[^A-Za-z0-9]+/', '-', (string) $short );
		$slug = trim( (string) $slug, '-' );
		if ( $slug === '' ) {
			$slug = 'PowerSuite';
		}
		$version = defined( 'WP_POWERSUITE_VERSION' ) ? WP_POWERSUITE_VERSION : '1.0';

		return $slug . '-EmailDelivery/' . $version;
	}
}

