<?php
/**
 * Alerting engine for Mail Doctor.
 *
 * @package MailDoctor
 */

namespace MailDoctor;

use MailDoctor\Helpers\Settings;
use MailDoctor\Logger;
use MailDoctor\Mailer;

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

/**
 * Handles automated alert checks and notifications.
 */
class Alerts {


	/**
	 * Cron event name.
	 */
	const CRON_HOOK = 'mail_doctor_alert_cron';

	/**
	 * Option key for alert state (cooldowns).
	 */
	const OPTION_STATE = 'mail_doctor_alert_state';

	/**
	 * Initialise alert hooks.
	 *
	 * @return void
	 */
	public static function init(): void {
		add_action( self::CRON_HOOK, array( __CLASS__, 'evaluate' ) );
		add_filter( 'cron_schedules', array( __CLASS__, 'register_cron_interval' ) );
		add_action( 'mail_doctor_log_failed', array( __CLASS__, 'notify_failure' ), 20, 2 );

		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
			wp_schedule_event( time() + 300, 'mail_doctor_five_minutes', self::CRON_HOOK );
		}

		remove_filter( 'cron_schedules', array( __CLASS__, 'register_cron_interval' ) );
	}

	/**
	 * Ensure cron is scheduled on activation.
	 */
	public static function activate(): void {
		add_filter( 'cron_schedules', array( __CLASS__, 'register_cron_interval' ) );

		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
			wp_schedule_event( time() + 300, 'mail_doctor_five_minutes', self::CRON_HOOK );
		}
	}

	/**
	 * Clear cron on deactivation.
	 */
	public static function deactivate(): void {
		wp_clear_scheduled_hook( self::CRON_HOOK );
	}

	/**
	 * Ensure five minute interval exists.
	 *
	 * @param array<string, array<string, mixed>> $schedules Existing schedules.
	 * @return array<string, array<string, mixed>>
	 */
	public static function register_cron_interval( array $schedules ): array {
		if ( ! isset( $schedules['mail_doctor_five_minutes'] ) ) {
			$schedules['mail_doctor_five_minutes'] = array(
				'interval' => 300,
				'display'  => __( 'Every Five Minutes (Mail Doctor)', 'mail-doctor' ),
			);
		}

		return $schedules;
	}

	/**
	 * Evaluate alert conditions.
	 *
	 * @return void
	 */
	public static function evaluate(): void {
		$settings = Settings::all();

		if ( empty( $settings['alerts_enabled'] ) ) {
			return;
		}

		$window_minutes = max( 5, (int) ( $settings['alert_window_minutes'] ?? 30 ) );
		$threshold      = max( 1, (float) ( $settings['alert_threshold_percent'] ?? 20 ) );
		$cooldown       = max( 5, (int) ( $settings['alert_cooldown_minutes'] ?? 60 ) );

		$now   = time();
		$start = gmdate( 'Y-m-d H:i:s', $now - ( $window_minutes * MINUTE_IN_SECONDS ) );

		global $wpdb;
		$table = Logger::table_name();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$counts = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT status, COUNT(*) as total FROM {$table} WHERE created_at >= %s AND status IN ('sent','failed') GROUP BY status",
				$start
			),
			ARRAY_A
		);

		$total_sent   = 0;
		$total_failed = 0;
		foreach ( $counts as $row ) {
			if ( 'sent' === $row['status'] ) {
				$total_sent = (int) $row['total'];
			} elseif ( 'failed' === $row['status'] ) {
				$total_failed = (int) $row['total'];
			}
		}

		$total        = $total_sent + $total_failed;
		$failure_rate = $total > 0 ? ( $total_failed / $total ) * 100 : 0;

		$burst_minutes  = max( 1, (int) ( $settings['alert_burst_minutes'] ?? 5 ) );
		$burst_failures = max( 1, (int) ( $settings['alert_burst_failures'] ?? 5 ) );
		$burst_start    = gmdate( 'Y-m-d H:i:s', $now - ( $burst_minutes * MINUTE_IN_SECONDS ) );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$recent_failures = (int) $wpdb->get_var(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT COUNT(*) FROM {$table} WHERE created_at >= %s AND status = 'failed'",
				$burst_start
			)
		);

		$state = get_option( self::OPTION_STATE, array() );
		if ( ! is_array( $state ) ) {
			$state = array();
		}

		$alerts_fired = array();

		if ( $total > 0 && $failure_rate >= $threshold ) {
			$alerts_fired[] = self::trigger_alert( 'failure_rate', $failure_rate, $total_failed, $total, $settings, $state, $cooldown );
		}

		if ( $recent_failures >= $burst_failures ) {
			$alerts_fired[] = self::trigger_alert(
				'burst_failures',
				$recent_failures,
				$recent_failures,
				$total,
				$settings,
				$state,
				$cooldown,
				array(
					'window_minutes' => $burst_minutes,
				)
			);
		}

		$alerts_fired = array_filter( $alerts_fired );

		if ( ! empty( $alerts_fired ) ) {
			update_option( self::OPTION_STATE, $state, false );
		}
	}

	/**
	 * Trigger alert if cooldown passed.
	 *
	 * @param string               $type Alert type.
	 * @param float|int            $value Value measured.
	 * @param int                  $failures Failure count.
	 * @param int                  $total Total messages.
	 * @param array<string, mixed> $settings Settings.
	 * @param array<string, mixed> $state Alert state (referenced).
	 * @param int                  $cooldown Cooldown in minutes.
	 * @param array<string, mixed> $context Extra context.
	 * @return bool Whether alert dispatched.
	 */
	protected static function trigger_alert( string $type, $value, int $failures, int $total, array $settings, array &$state, int $cooldown, array $context = array() ): bool {
		$now  = time();
		$last = isset( $state[ $type ]['timestamp'] ) ? (int) $state[ $type ]['timestamp'] : 0;

		if ( $last && ( $now - $last ) < ( $cooldown * MINUTE_IN_SECONDS ) ) {
			return false;
		}

		$message = '';
		switch ( $type ) {
			case 'failure_rate':
				$message = sprintf(
					/* translators: 1: failure percentage, 2: failed count, 3: total */
					__( 'Mail Doctor detected a high failure rate: %1$.2f%% (%2$d of %3$d messages) in the last window.', 'mail-doctor' ),
					(float) $value,
					$failures,
					$total
				);
				break;
			case 'burst_failures':
				$window_minutes = (int) ( $context['window_minutes'] ?? 5 );
				$message        = sprintf(
					/* translators: 1: failure count, 2: minutes */
					__( 'Mail Doctor observed %1$d failures within the last %2$d minutes.', 'mail-doctor' ),
					$failures,
					$window_minutes
				);
				break;
		}

		$payload = array(
			'type'   => $type,
			'value'  => $value,
			'failed' => $failures,
			'total'  => $total,
			'time'   => gmdate( 'c', $now ),
		);

		self::dispatch_channels( $message, $payload, $settings );

		$state[ $type ] = array(
			'timestamp' => $now,
			'payload'   => $payload,
		);

		return true;
	}

	/**
	 * Send alert through configured channels.
	 *
	 * @param string               $message Human readable message.
	 * @param array<string, mixed> $payload Structured payload.
	 * @param array<string, mixed> $settings Plugin settings.
	 * @return void
	 */
	protected static function dispatch_channels( string $message, array $payload, array $settings ): void {
		$email = ! empty( $settings['alert_email'] ) ? sanitize_email( $settings['alert_email'] ) : sanitize_email( get_option( 'admin_email' ) );

		if ( $email ) {
			Mailer::force_next_override();
			Mailer::set_event_context( 'mail_doctor_alert' );
			Mailer::set_email_id( 'alert' );

			try {
				wp_mail(
					$email,
					__( 'Mail Doctor Alert', 'mail-doctor' ),
					$message . "\n\n" . __( 'Details:', 'mail-doctor' ) . ' ' . wp_json_encode( $payload )
				);
			} finally {
				Mailer::clear_event_context();
				Mailer::cleanup_context();
			}
		}

		$slack_hook = isset( $settings['alert_slack_webhook'] ) ? esc_url_raw( $settings['alert_slack_webhook'] ) : '';
		if ( $slack_hook ) {
			wp_remote_post(
				$slack_hook,
				array(
					'headers' => array( 'Content-Type' => 'application/json' ),
					'body'    => wp_json_encode( array( 'text' => sprintf( '*Mail Doctor Alert*: %s', $message ) ) ),
					'timeout' => 5,
				)
			);
		}

		$generic_hook = isset( $settings['alert_generic_webhook'] ) ? esc_url_raw( $settings['alert_generic_webhook'] ) : '';
		if ( $generic_hook ) {
			wp_remote_post(
				$generic_hook,
				array(
					'headers' => array( 'Content-Type' => 'application/json' ),
					'body'    => wp_json_encode(
						array(
							'source'  => 'mail-doctor',
							'message' => $message,
							'data'    => $payload,
						)
					),
					'timeout' => 5,
				)
			);
		}
	}

	/**
	 * Send an immediate alert through configured channels.
	 *
	 * @param string               $message Message to deliver.
	 * @param array<string, mixed> $payload Structured payload.
	 * @return void
	 */
	public static function notify( string $message, array $payload = array() ): void {
		self::dispatch_channels( $message, $payload, Settings::all() );
	}

	/**
	 * Send alerts when an individual email fails.
	 *
	 * @param int                  $log_id  Log identifier.
	 * @param array<string, mixed> $payload Stored payload.
	 * @return void
	 */
	public static function notify_failure( int $log_id, array $payload ): void {
		$settings = Settings::all();

		if ( empty( $settings['alerts_enabled'] ) ) {
			return;
		}

		$cooldown = max( 5, (int) ( $settings['alert_cooldown_minutes'] ?? 60 ) );
		$state    = get_option( self::OPTION_STATE, array() );

		if ( ! is_array( $state ) ) {
			$state = array();
		}

		$now     = time();
		$last    = isset( $state['single_failure']['timestamp'] ) ? (int) $state['single_failure']['timestamp'] : 0;
		$elapsed = $last ? ( $now - $last ) : $cooldown * MINUTE_IN_SECONDS;

		if ( $elapsed < ( $cooldown * MINUTE_IN_SECONDS ) ) {
			return;
		}

		$row      = Logger::get_entry( $log_id );
		$subject  = $row['subject'] ?? '';
		$error    = $row['error'] ?? '';
		$provider = $row['provider_code'] ?? '';

		if ( empty( $subject ) && ! empty( $payload['mail_args']['subject'] ) ) {
			$subject = (string) $payload['mail_args']['subject'];
		}

		$subject = $subject ? sanitize_text_field( $subject ) : __( '(no subject)', 'mail-doctor' );

		$message_bits = array(
			sprintf(
				/* translators: 1: email subject, 2: log id */
				__( 'Mail Doctor failed to deliver "%1$s" (log #%2$d).', 'mail-doctor' ),
				$subject,
				$log_id
			),
		);

		if ( $error ) {
			$message_bits[] = sprintf(
				/* translators: %s error message */
				__( 'Error: %s', 'mail-doctor' ),
				sanitize_text_field( $error )
			);
		}

		if ( $provider ) {
			$message_bits[] = sprintf(
				/* translators: %s provider code */
				__( 'Provider code: %s', 'mail-doctor' ),
				sanitize_text_field( $provider )
			);
		}

		$message = implode( ' ', $message_bits );

		$payload_details = array(
			'log_id'        => $log_id,
			'subject'       => $subject,
			'provider_code' => $provider,
			'error'         => $error,
		);

		self::dispatch_channels( $message, $payload_details, $settings );

		$state['single_failure'] = array(
			'timestamp' => $now,
			'payload'   => $payload_details,
		);

		update_option( self::OPTION_STATE, $state, false );
	}
}
