<?php
/**
 * Provider webhook ingestion for Mail Doctor.
 *
 * @package MailDoctor
 */

namespace MailDoctor;

use MailDoctor\Helpers\Settings;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;

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

/**
 * Registers REST endpoints for provider callbacks.
 */
class Webhooks {


	/**
	 * Bootstrap webhook routes.
	 *
	 * @return void
	 */
	public static function init(): void {
		add_action( 'rest_api_init', array( __CLASS__, 'register_routes' ) );
	}

	/**
	 * Register webhook endpoints.
	 *
	 * @return void
	 */
	public static function register_routes(): void {
		register_rest_route(
			'mail-doctor/v1',
			'/webhook/(?P<provider>[a-z0-9_-]+)',
			array(
				'methods'             => array( 'POST' ),
				'callback'            => array( __CLASS__, 'dispatch' ),
				'permission_callback' => array( __CLASS__, 'authorize' ),
				'args'                => array(
					'provider' => array(
						'required' => true,
						'type'     => 'string',
					),
				),
			)
		);
	}

	/**
	 * Basic authorization check leveraging shared secret token.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return true|WP_Error
	 */
	public static function authorize( WP_REST_Request $request ) {
		if ( ! Settings::get( 'webhook_enabled', false ) ) {
			return new WP_Error(
				'mail_doctor_webhook_disabled',
				__( 'Webhook ingestion is disabled for this site.', 'mail-doctor' ),
				array( 'status' => 403 )
			);
		}

		$token = trim( (string) Settings::get( 'webhook_token', '' ) );

		if ( '' === $token ) {
			return new WP_Error( 'mail_doctor_webhook_disabled', __( 'Webhook secret is not configured.', 'mail-doctor' ), array( 'status' => 403 ) );
		}

		$provided = trim( (string) $request->get_header( 'x-maildoctor-token' ) );

		if ( '' === $provided ) {
			$authorization = trim( (string) $request->get_header( 'authorization' ) );

			if ( '' !== $authorization ) {
				if ( stripos( $authorization, 'Bearer ' ) === 0 ) {
					$provided = trim( substr( $authorization, 7 ) );
				} elseif ( stripos( $authorization, 'Basic ' ) === 0 ) {
					$decoded = base64_decode( substr( $authorization, 6 ) );
					if ( $decoded && false !== strpos( $decoded, ':' ) ) {
						list($user, $pass) = explode( ':', $decoded, 2 );
						$provided          = trim( $pass !== '' ? $pass : $user );
					} elseif ( false !== $decoded ) {
						$provided = trim( $decoded );
					}
				}
			}
		}

		if ( '' === $provided ) {
			$provided = trim( (string) $request->get_param( 'token' ) );
		}

		if ( '' === $provided || ! hash_equals( $token, $provided ) ) {
			return new WP_Error( 'mail_doctor_webhook_forbidden', __( 'Invalid webhook token.', 'mail-doctor' ), array( 'status' => 403 ) );
		}

		return true;
	}

	/**
	 * Dispatch provider-specific handler.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response
	 */
	public static function dispatch( WP_REST_Request $request ): WP_REST_Response {
		$provider = sanitize_key( (string) $request['provider'] );

		switch ( $provider ) {
			case 'mailgun':
				$result = self::handle_mailgun( $request );
				break;
			case 'sendgrid':
				$result = self::handle_sendgrid( $request );
				break;
			case 'ses':
			case 'ses-sns':
			case 'amazon':
				$result = self::handle_ses( $request );
				break;
			default:
				$result = new WP_Error( 'mail_doctor_unknown_provider', __( 'Unsupported provider.', 'mail-doctor' ), array( 'status' => 404 ) );
				break;
		}

		if ( is_wp_error( $result ) ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'error'   => $result->get_error_message(),
				),
				$result->get_error_data()['status'] ?? 400
			);
		}

		return new WP_REST_Response(
			array(
				'success'   => true,
				'processed' => $result,
			),
			200
		);
	}

	/**
	 * Process Mailgun webhook payload.
	 *
	 * @param WP_REST_Request $request Request containing body.
	 * @return int Count of processed events.
	 */
	protected static function handle_mailgun( WP_REST_Request $request ): int {
		$body = self::decode_json_body( $request );

		if ( empty( $body['event-data'] ) ) {
			return 0;
		}

		$data      = $body['event-data'];
		$event     = isset( $data['event'] ) ? sanitize_key( $data['event'] ) : '';
		$recipient = isset( $data['recipient'] ) ? sanitize_email( $data['recipient'] ) : '';

		if ( '' === $recipient || ! is_email( $recipient ) ) {
			return 0;
		}

		$reason = 'bounce';
		if ( in_array( $event, array( 'complained', 'complaint' ), true ) ) {
			$reason = 'complaint';
		} elseif ( in_array( $event, array( 'failed', 'bounced' ), true ) ) {
			$reason = 'bounce';
		} elseif ( 'unsubscribed' === $event ) {
			$reason = 'unsubscribe';
		}

		$meta = array(
			'event'       => $event,
			'timestamp'   => isset( $data['timestamp'] ) ? (int) $data['timestamp'] : time(),
			'description' => $data['delivery-status']['description'] ?? '',
			'code'        => $data['delivery-status']['code'] ?? '',
		);

		Suppression::record( $recipient, $reason, $meta );
		Logger::annotate_suppression( $recipient, $reason, $meta );

		return 1;
	}

	/**
	 * Process SendGrid event webhooks.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return int|WP_Error Processed count or error when verification fails.
	 */
	protected static function handle_sendgrid( WP_REST_Request $request ) {
		$raw_body        = (string) $request->get_body();
		$signature_check = self::verify_sendgrid_signature( $request, $raw_body );

		if ( is_wp_error( $signature_check ) ) {
			return $signature_check;
		}

		$body = self::decode_json_body( $request );

		if ( empty( $body ) || ! is_array( $body ) ) {
			if ( Settings::debug_logging_enabled() ) {
				self::debug_log( 'SendGrid webhook received empty or invalid payload.', array( 'provider' => 'sendgrid' ) );
			}
			return 0;
		}

		self::debug_log(
			sprintf(
				'SendGrid webhook payload: %s',
				wp_json_encode( $body, JSON_PARTIAL_OUTPUT_ON_ERROR )
			),
			array( 'provider' => 'sendgrid' )
		);

		$count = 0;

		foreach ( $body as $event ) {
			if ( ! is_array( $event ) ) {
				continue;
			}

			$email = isset( $event['email'] ) ? sanitize_email( $event['email'] ) : '';

			if ( '' === $email || ! is_email( $email ) ) {
				continue;
			}

			$type = isset( $event['event'] ) ? sanitize_key( $event['event'] ) : '';

			if ( '' === $type ) {
				continue;
			}

			if ( ! in_array( $type, array( 'bounce', 'dropped', 'spamreport', 'blocked' ), true ) ) {
				continue;
			}

			$reason = 'bounce';
			if ( 'spamreport' === $type ) {
				$reason = 'complaint';
			} elseif ( 'blocked' === $type || 'dropped' === $type ) {
				$reason = 'blocked';
			}

			$meta = array(
				'event'   => $type,
				'smtp_id' => $event['smtp-id'] ?? '',
				'reason'  => $event['reason'] ?? '',
				'status'  => $event['status'] ?? '',
			);

			Suppression::record( $email, $reason, $meta );
			Logger::annotate_suppression( $email, $reason, $meta );
			++$count;
		}

		return $count;
	}

	/**
	 * Verify SendGrid webhook signatures when configured.
	 *
	 * @param WP_REST_Request $request Request instance.
	 * @param string          $raw_body Raw JSON payload.
	 * @return true|WP_Error
	 */
	protected static function verify_sendgrid_signature( WP_REST_Request $request, string $raw_body ) {
		$public_key_b64 = trim( (string) Settings::get( 'sendgrid_webhook_signing_key', '' ) );

		if ( '' === $public_key_b64 ) {
			return true;
		}

		$signature_header = trim( (string) $request->get_header( 'x-twilio-email-event-webhook-signature' ) );
		$timestamp_header = trim( (string) $request->get_header( 'x-twilio-email-event-webhook-timestamp' ) );

		if ( '' === $signature_header || '' === $timestamp_header ) {
			if ( Settings::debug_logging_enabled() ) {
				self::debug_log( 'SendGrid webhook missing signature or timestamp headers.', array( 'provider' => 'sendgrid' ) );
			}

			return new WP_Error(
				'mail_doctor_webhook_signature_missing',
				__( 'SendGrid webhook signature headers are required when verification is enabled.', 'mail-doctor' ),
				array( 'status' => 401 )
			);
		}

		if ( ! ctype_digit( $timestamp_header ) ) {
			if ( Settings::debug_logging_enabled() ) {
				self::debug_log( sprintf( 'SendGrid webhook timestamp header is invalid: %s', $timestamp_header ), array( 'provider' => 'sendgrid' ) );
			}

			return new WP_Error(
				'mail_doctor_webhook_signature_invalid_timestamp',
				__( 'SendGrid webhook timestamp format is invalid.', 'mail-doctor' ),
				array( 'status' => 401 )
			);
		}

		$timestamp = (int) $timestamp_header;
		$tolerance = (int) apply_filters( 'mail_doctor_sendgrid_signature_tolerance', 300 );
		if ( $tolerance > 0 && abs( time() - $timestamp ) > $tolerance ) {
			if ( Settings::debug_logging_enabled() ) {
				self::debug_log( sprintf( 'SendGrid webhook timestamp outside tolerance window: %d', $timestamp ), array( 'provider' => 'sendgrid' ) );
			}

			return new WP_Error(
				'mail_doctor_webhook_signature_stale',
				__( 'SendGrid webhook timestamp is outside the allowable tolerance window.', 'mail-doctor' ),
				array( 'status' => 401 )
			);
		}

		$public_key_raw = base64_decode( $public_key_b64, true );
		if ( false === $public_key_raw ) {
			if ( Settings::debug_logging_enabled() ) {
				self::debug_log( 'SendGrid webhook signing key is not valid base64.', array( 'provider' => 'sendgrid' ) );
			}

			return new WP_Error(
				'mail_doctor_webhook_invalid_signing_key',
				__( 'The configured SendGrid webhook signing key is invalid. Update the key in settings.', 'mail-doctor' ),
				array( 'status' => 500 )
			);
		}

		$signature = base64_decode( $signature_header, true );
		if ( false === $signature ) {
			if ( Settings::debug_logging_enabled() ) {
				self::debug_log( 'SendGrid webhook signature header is not valid base64.', array( 'provider' => 'sendgrid' ) );
			}

			return new WP_Error(
				'mail_doctor_webhook_signature_invalid',
				__( 'SendGrid webhook signature could not be decoded.', 'mail-doctor' ),
				array( 'status' => 401 )
			);
		}

		$signed_payload = $timestamp_header . $raw_body;

		// Ed25519 (recommended) uses 32-byte raw public keys.
		if ( strlen( $public_key_raw ) === 32 ) {
			if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ) {
				if ( Settings::debug_logging_enabled() ) {
					self::debug_log( 'SendGrid webhook signing requires the Sodium PHP extension.', array( 'provider' => 'sendgrid' ) );
				}

				return new WP_Error(
					'mail_doctor_webhook_signature_unavailable',
					__( 'SendGrid webhook signature verification requires the Sodium PHP extension.', 'mail-doctor' ),
					array( 'status' => 500 )
				);
			}

			if ( ! sodium_crypto_sign_verify_detached( $signature, $signed_payload, $public_key_raw ) ) {
				if ( Settings::debug_logging_enabled() ) {
					self::debug_log( 'SendGrid webhook signature verification (Ed25519) failed.', array( 'provider' => 'sendgrid' ) );
				}

				return new WP_Error(
					'mail_doctor_webhook_signature_failed',
					__( 'SendGrid webhook signature verification failed.', 'mail-doctor' ),
					array( 'status' => 401 )
				);
			}

			return true;
		}

		// SendGrid historically issued ES256 (P-256) keys; support them as well.
		if ( ! function_exists( 'openssl_verify' ) ) {
			if ( Settings::debug_logging_enabled() ) {
				self::debug_log( 'OpenSSL extension is required for SendGrid ES256 webhook verification.', array( 'provider' => 'sendgrid' ) );
			}

			return new WP_Error(
				'mail_doctor_webhook_signature_unavailable',
				__( 'OpenSSL is required to verify SendGrid webhook signatures.', 'mail-doctor' ),
				array( 'status' => 500 )
			);
		}

		// Convert base64-encoded DER key into PEM.
		$pem_public_key = "-----BEGIN PUBLIC KEY-----\n" . chunk_split( $public_key_b64, 64, "\n" ) . "-----END PUBLIC KEY-----\n";

		$public_key_res = openssl_pkey_get_public( $pem_public_key );
		if ( false === $public_key_res ) {
			if ( Settings::debug_logging_enabled() ) {
				self::debug_log( 'Unable to parse SendGrid webhook signing key via OpenSSL.', array( 'provider' => 'sendgrid' ) );
			}

			return new WP_Error(
				'mail_doctor_webhook_invalid_signing_key',
				__( 'The configured SendGrid webhook signing key could not be parsed. Confirm the value and try again.', 'mail-doctor' ),
				array( 'status' => 500 )
			);
		}

		$verified = openssl_verify( $signed_payload, $signature, $public_key_res, OPENSSL_ALGO_SHA256 );

		if ( 1 !== $verified ) {
			if ( Settings::debug_logging_enabled() ) {
				self::debug_log( 'SendGrid webhook signature verification (ES256) failed.', array( 'provider' => 'sendgrid' ) );
			}

			return new WP_Error(
				'mail_doctor_webhook_signature_failed',
				__( 'SendGrid webhook signature verification failed.', 'mail-doctor' ),
				array( 'status' => 401 )
			);
		}

		return true;
	}

	/**
	 * Process Amazon SES / SNS notifications.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return int Processed count.
	 */
	protected static function handle_ses( WP_REST_Request $request ): int {
		$body = self::decode_json_body( $request );

		if ( empty( $body ) ) {
			return 0;
		}

		// SNS wrapper support.
		if ( isset( $body['Message'] ) && is_string( $body['Message'] ) ) {
			$decoded = json_decode( $body['Message'], true );
			if ( is_array( $decoded ) ) {
				$body = $decoded;
			}
		}

		$type = isset( $body['notificationType'] ) ? sanitize_key( $body['notificationType'] ) : '';

		if ( '' === $type ) {
			return 0;
		}

		$records = array();

		if ( 'bounce' === $type && ! empty( $body['bounce']['bouncedRecipients'] ) ) {
			foreach ( (array) $body['bounce']['bouncedRecipients'] as $recipient ) {
				if ( empty( $recipient['emailAddress'] ) ) {
					continue;
				}

				$records[] = array(
					'email'  => $recipient['emailAddress'],
					'reason' => 'bounce',
					'meta'   => array(
						'action'     => $recipient['action'] ?? '',
						'status'     => $recipient['status'] ?? '',
						'diagnostic' => $recipient['diagnosticCode'] ?? '',
					),
				);
			}
		} elseif ( 'complaint' === $type && ! empty( $body['complaint']['complainedRecipients'] ) ) {
			foreach ( (array) $body['complaint']['complainedRecipients'] as $recipient ) {
				if ( empty( $recipient['emailAddress'] ) ) {
					continue;
				}

				$records[] = array(
					'email'  => $recipient['emailAddress'],
					'reason' => 'complaint',
					'meta'   => array(
						'user_agent'  => $body['complaint']['userAgent'] ?? '',
						'feedback_id' => $body['complaint']['feedbackId'] ?? '',
					),
				);
			}
		}

		$count = 0;
		foreach ( $records as $record ) {
			$email = sanitize_email( $record['email'] );
			if ( '' === $email || ! is_email( $email ) ) {
				continue;
			}

			Suppression::record( $email, $record['reason'], $record['meta'] );
			Logger::annotate_suppression( $email, $record['reason'], $record['meta'] );
			++$count;
		}

		return $count;
	}


	/**
	 * Debug logger helper that avoids using error_log() (flagged by WPCS/Plugin Check).
	 *
	 * If the plugin provides a Logger::debug() method, it will be used. Otherwise this
	 * emits an action developers can hook into.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional context.
	 * @return void
	 */
	protected static function debug_log( string $message, array $context = array() ): void {
		if ( ! Settings::debug_logging_enabled() ) {
			return;
		}

		// Prefer plugin logger if available.
		if ( class_exists( '\\MailDoctor\\Logger' ) && method_exists( '\\MailDoctor\\Logger', 'debug' ) ) {
			\MailDoctor\Logger::debug( $message, $context );
			return;
		}

		/**
		 * Fires when Mail Doctor wants to write a debug log entry.
		 *
		 * @param string $message Log message.
		 * @param array  $context Context array.
		 */
		do_action( 'mail_doctor_debug_log', $message, $context );
	}

	/**
	 * Decode JSON body from request.
	 *
	 * @param WP_REST_Request $request Request instance.
	 * @return mixed
	 */
	protected static function decode_json_body( WP_REST_Request $request ) {
		$body = $request->get_body();

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

		$decoded = json_decode( $body, true );

		if ( null === $decoded ) {
			return array();
		}

		return $decoded;
	}
}
