<?php
/**
 * Email health reporting service.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Mail;

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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Service reads plugin-owned mail event tables for admin analytics.
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names are plugin-owned $wpdb->prefix values.

/**
 * Provides the canonical Email Health report used by admin and Cloud surfaces.
 */
class EmailHealthService {
	const SIGNAL_CACHE_KEY        = 'subin_email_health_signal_v2';
	const LEGACY_SIGNAL_CACHE_KEY = 'subin_email_health_signal_v1';
	const SIGNAL_CACHE_TTL        = 300;
	const SIGNAL_VERSION          = 2;
	const INCIDENT_EVENT_LIMIT    = 2000;
	const STATE_NO_DATA           = 'no_data';
	const STATE_HEALTHY           = 'healthy';
	const STATE_DEGRADED          = 'degraded';
	const STATE_CRITICAL          = 'critical';
	const STATE_RECOVERING        = 'recovering';
	const STATE_UNKNOWN           = 'unknown';

	/**
	 * Email error classifier.
	 *
	 * @var EmailErrorClassifier
	 */
	private $error_classifier;

	/**
	 * Deterministic incident lifecycle projector.
	 *
	 * @var EmailHealthIncidentProjector
	 */
	private $incident_projector;

	/** @var bool Whether the latest bounded incident projection saw the full window. */
	private $incident_projection_complete = true;

	/**
	 * Constructor.
	 *
	 * @param EmailErrorClassifier|null         $error_classifier Classifier.
	 * @param EmailHealthIncidentProjector|null $incident_projector Lifecycle projector.
	 */
	public function __construct( ?EmailErrorClassifier $error_classifier = null, ?EmailHealthIncidentProjector $incident_projector = null ) {
		$this->error_classifier   = $error_classifier ? $error_classifier : new EmailErrorClassifier();
		$this->incident_projector = $incident_projector ? $incident_projector : new EmailHealthIncidentProjector();
	}

	/**
	 * Build one stable report so every consumer uses the same health semantics.
	 *
	 * @param array<string,mixed> $filters Filters.
	 * @return array<string,mixed>
	 */
	public function build_report( array $filters = array() ) {
		$filters       = $this->normalize_filters( $filters );
		$summary       = $this->summary_counts( $filters );
		$all_incidents = $this->current_incidents( $filters, 0, true );
		$incidents     = $this->actionable_incidents( $all_incidents );
		$sources       = $this->apply_incident_states_to_sources( $this->source_summary( $filters ), $incidents );
		$previous      = $this->previous_summary( $filters );
		$active        = array();
		$inactive      = 0;

		foreach ( $sources as $source ) {
			if ( self::STATE_NO_DATA === ( $source['state'] ?? '' ) ) {
				++$inactive;
				continue;
			}
			$active[] = $source;
		}

		usort(
			$active,
			static function ( array $left, array $right ): int {
				$weights = array(
					self::STATE_CRITICAL   => 5,
					self::STATE_DEGRADED   => 4,
					self::STATE_RECOVERING => 3,
					self::STATE_UNKNOWN    => 2,
					self::STATE_HEALTHY    => 1,
					self::STATE_NO_DATA    => 0,
				);
				$left_weight  = $weights[ $left['state'] ?? self::STATE_UNKNOWN ] ?? 0;
				$right_weight = $weights[ $right['state'] ?? self::STATE_UNKNOWN ] ?? 0;
				if ( $left_weight !== $right_weight ) {
					return $right_weight <=> $left_weight;
				}

				return absint( $right['total'] ?? 0 ) <=> absint( $left['total'] ?? 0 );
			}
		);

		$state = $this->state_from_incidents( $summary, $incidents );

		$attempted       = absint( $summary['sent'] ?? 0 ) + absint( $summary['failed'] ?? 0 );
		$acceptance_rate = $attempted > 0 ? round( ( absint( $summary['sent'] ?? 0 ) / $attempted ) * 100, 1 ) : null;
		$previous_rate   = null;
		if ( is_array( $previous ) ) {
			$previous_attempted = absint( $previous['sent'] ?? 0 ) + absint( $previous['failed'] ?? 0 );
			$previous_rate      = $previous_attempted > 0 ? round( ( absint( $previous['sent'] ?? 0 ) / $previous_attempted ) * 100, 1 ) : null;
		}

		return array(
			'filters'                => $filters,
			'state'                  => $state,
			'summary'                => $summary,
			'acceptance_rate'        => $acceptance_rate,
			'previous_acceptance_rate' => $previous_rate,
			'acceptance_change'      => null !== $acceptance_rate && null !== $previous_rate ? round( $acceptance_rate - $previous_rate, 1 ) : null,
			'previous_summary'       => $previous,
			'sources'                => $sources,
			'active_sources'         => $active,
			'inactive_source_count'  => $inactive,
			'trend'                  => $this->daily_trend( $filters ),
			'trend_limited_to_days'  => 'all' === $filters['date_range'] ? 90 : 0,
			'issues'                 => array_slice( $incidents, 0, 8 ),
			'incident_summary'       => $this->incident_summary( $all_incidents ),
			'incident_count_is_lower_bound' => ! $this->incident_projection_complete,
			'latest_event'           => $this->latest_email_event( $filters ),
			'latest_failed'          => $this->most_recent_failed_error( $filters ),
			'form_options'           => $this->form_options( $filters['source'] ),
		);
	}

	/**
	 * Return a cached, minimal signal for menu badges and other lightweight UI.
	 *
	 * @param bool $force Ignore the cached value.
	 * @return array<string,mixed>
	 */
	public function current_signal( $force = false ) {
		if ( ! $force ) {
			$cached = get_transient( self::SIGNAL_CACHE_KEY );
			if ( is_array( $cached ) && self::SIGNAL_VERSION === absint( $cached['version'] ?? 0 ) ) {
				return $cached;
			}
		}

		$filters       = array( 'date_range' => '30' );
		$summary       = $this->summary_counts( $filters );
		$all_incidents = $this->current_incidents( $filters, 0, true );
		$incidents     = $this->actionable_incidents( $all_incidents );
		$sources       = $this->apply_incident_states_to_sources( $this->source_summary( $filters ), $incidents );
		$incident_summary     = $this->incident_summary( $all_incidents );
		$state              = $this->state_from_incidents( $summary, $incidents );
		$problem_sources    = 0;
		$recovering_sources = 0;
		$active_sources     = array();
		$last_success_at    = '';
		$last_problem_at    = '';

		foreach ( $sources as $source ) {
			$source_state = sanitize_key( $source['state'] ?? self::STATE_NO_DATA );
			$state        = $this->stronger_state( $state, $source_state );
			if ( self::STATE_NO_DATA !== $source_state ) {
				$active_sources[] = array(
					'key'                => sanitize_key( $source['key'] ?? 'unknown' ),
					'state'              => $source_state,
					'total'              => absint( $source['total'] ?? 0 ),
					'sent'               => absint( $source['sent'] ?? 0 ),
					'failed'             => absint( $source['failed'] ?? 0 ),
					'no_email_generated' => absint( $source['no_email_generated'] ?? 0 ),
					'unknown'            => absint( $source['unknown'] ?? 0 ),
					'latest_event_at'    => sanitize_text_field( $source['latest_event_at'] ?? '' ),
				);
			}
			if ( (string) ( $source['latest_sent_at'] ?? '' ) > $last_success_at ) {
				$last_success_at = sanitize_text_field( $source['latest_sent_at'] );
			}
			if ( (string) ( $source['latest_problem_at'] ?? '' ) > $last_problem_at ) {
				$last_problem_at = sanitize_text_field( $source['latest_problem_at'] );
			}
			if ( in_array( $source_state, array( self::STATE_CRITICAL, self::STATE_DEGRADED, self::STATE_UNKNOWN ), true ) ) {
				++$problem_sources;
			} elseif ( self::STATE_RECOVERING === $source_state ) {
				++$recovering_sources;
			}
		}
		$attempted = absint( $summary['sent'] ?? 0 ) + absint( $summary['failed'] ?? 0 );

		$signal = array(
			'version'            => self::SIGNAL_VERSION,
			'state'              => $state,
			'active_issue_count' => $incident_summary['active_issue_count'],
			'recovering_issue_count' => $incident_summary['recovering_issue_count'],
			'affected_source_count' => $incident_summary['affected_source_count'],
			'recovering_source_count' => $incident_summary['recovering_source_count'],
			'problem_event_count' => $incident_summary['problem_event_count'],
			'recovering_event_count' => $incident_summary['recovering_event_count'],
			'resolved_issue_count' => $incident_summary['resolved_issue_count'],
			'count_is_lower_bound' => ! $this->incident_projection_complete,
			'problem_sources'    => $problem_sources,
			'recovering_sources' => $recovering_sources,
			'total'              => absint( $summary['total'] ?? 0 ),
			'sent'               => absint( $summary['sent'] ?? 0 ),
			'failed'             => absint( $summary['failed'] ?? 0 ),
			'no_email_generated' => absint( $summary['no_email_generated'] ?? 0 ),
			'unknown'            => absint( $summary['unknown'] ?? 0 ),
			'acceptance_rate'    => $attempted > 0 ? round( ( absint( $summary['sent'] ?? 0 ) / $attempted ) * 100, 1 ) : null,
			'last_success_at'    => $last_success_at,
			'last_problem_at'    => $last_problem_at,
			'sources'            => $active_sources,
			'generated_at'       => gmdate( 'Y-m-d H:i:s' ),
		);
		set_transient( self::SIGNAL_CACHE_KEY, $signal, self::SIGNAL_CACHE_TTL );

		return $signal;
	}

	/** Invalidate the lightweight signal after a mail outcome changes. */
	public static function invalidate_signal_cache( ...$unused ) {
		unset( $unused );
		delete_transient( self::SIGNAL_CACHE_KEY );
		delete_transient( self::LEGACY_SIGNAL_CACHE_KEY );
	}

	/**
	 * Summary counts.
	 *
	 * @param array<string,mixed> $filters Filters.
	 * @return array<string,int>
	 */
	public function summary_counts( array $filters = array() ) {
		global $wpdb;

		$filters    = $this->normalize_filters( $filters );
		$table      = $wpdb->prefix . 'subin_mail_events';
		$lead_table = $wpdb->prefix . 'subin_leads';
		$where      = $this->where_for_filters( $filters );
		$sql        = "SELECT
				COUNT(*) AS total,
				SUM(CASE WHEN e.status = 'sent' THEN 1 ELSE 0 END) AS sent,
				SUM(CASE WHEN e.status = 'failed' THEN 1 ELSE 0 END) AS failed,
				SUM(CASE WHEN e.status = 'no_email_generated' THEN 1 ELSE 0 END) AS no_email_generated,
				SUM(CASE WHEN e.status IN ('unknown','pending') THEN 1 ELSE 0 END) AS unknown
			FROM {$table} e
			LEFT JOIN {$lead_table} leads ON leads.id = e.lead_id
			{$where['sql']}";
		$row        = $wpdb->get_row( $this->prepare_sql( $sql, $where['params'] ), ARRAY_A );
		$row        = is_array( $row ) ? $row : array();

		return array(
			'total'              => absint( $row['total'] ?? 0 ),
			'sent'               => absint( $row['sent'] ?? 0 ),
			'failed'             => absint( $row['failed'] ?? 0 ),
			'no_email_generated' => absint( $row['no_email_generated'] ?? 0 ),
			'unknown'            => absint( $row['unknown'] ?? 0 ),
		);
	}

	/**
	 * Summary counts grouped by Email Health source.
	 *
	 * @param array<string,mixed> $filters Filters.
	 * @return array<string,array<string,mixed>>
	 */
	public function source_summary( array $filters = array() ) {
		global $wpdb;

		$filters     = $this->normalize_filters( $filters );
		$table       = $wpdb->prefix . 'subin_mail_events';
		$lead_table  = $wpdb->prefix . 'subin_leads';
		$where       = $this->where_for_filters( $filters );
		$source_expr = $this->source_group_sql();
		$sql         = "SELECT {$source_expr} AS source_group,
				COUNT(*) AS total,
				SUM(CASE WHEN e.status = 'sent' THEN 1 ELSE 0 END) AS sent,
				SUM(CASE WHEN e.status = 'failed' THEN 1 ELSE 0 END) AS failed,
				SUM(CASE WHEN e.status = 'no_email_generated' THEN 1 ELSE 0 END) AS no_email_generated,
				SUM(CASE WHEN e.status IN ('unknown','pending') THEN 1 ELSE 0 END) AS unknown,
				MAX(CASE WHEN e.status = 'sent' THEN e.id ELSE NULL END) AS latest_sent_id,
				MAX(CASE WHEN e.status IN ('failed','no_email_generated') THEN e.id ELSE NULL END) AS latest_problem_id,
				MAX(CASE WHEN e.status IN ('unknown','pending') THEN e.id ELSE NULL END) AS latest_unknown_id,
				MAX(CASE WHEN e.status = 'sent' THEN e.created_at ELSE NULL END) AS latest_sent_at,
				MAX(CASE WHEN e.status IN ('failed','no_email_generated') THEN e.created_at ELSE NULL END) AS latest_problem_at,
				MAX(e.created_at) AS latest_event_at
			FROM {$table} e
			LEFT JOIN {$lead_table} leads ON leads.id = e.lead_id
			{$where['sql']}
			GROUP BY source_group";
		$rows        = $wpdb->get_results( $this->prepare_sql( $sql, $where['params'] ), ARRAY_A );
		$errors      = $this->latest_failed_errors_by_source( $filters );
		$summary     = array();

		foreach ( $this->source_labels() as $key => $label ) {
			$summary[ $key ] = array(
				'key'                => $key,
				'name'               => $label,
				'total'              => 0,
				'sent'               => 0,
				'failed'             => 0,
				'no_email_generated' => 0,
				'unknown'            => 0,
				'acceptance_rate'    => null,
				'latest_sent_id'      => 0,
				'latest_problem_id'   => 0,
				'latest_unknown_id'   => 0,
				'latest_sent_at'     => '',
				'latest_problem_at'  => '',
				'latest_event_at'    => '',
				'latest_error'       => '',
				'state'              => self::STATE_NO_DATA,
			);
		}

		foreach ( is_array( $rows ) ? $rows : array() as $row ) {
			$key = sanitize_key( $row['source_group'] ?? 'unknown' );
			if ( ! isset( $summary[ $key ] ) ) {
				$key = 'unknown';
			}
			$sent      = absint( $row['sent'] ?? 0 );
			$failed    = absint( $row['failed'] ?? 0 );
			$attempted = $sent + $failed;
			$data      = array(
				'key'                => $key,
				'name'               => $summary[ $key ]['name'],
				'total'              => absint( $row['total'] ?? 0 ),
				'sent'               => $sent,
				'failed'             => $failed,
				'no_email_generated' => absint( $row['no_email_generated'] ?? 0 ),
				'unknown'            => absint( $row['unknown'] ?? 0 ),
				'acceptance_rate'    => $attempted > 0 ? round( ( $sent / $attempted ) * 100, 1 ) : null,
				'latest_sent_id'      => absint( $row['latest_sent_id'] ?? 0 ),
				'latest_problem_id'   => absint( $row['latest_problem_id'] ?? 0 ),
				'latest_unknown_id'   => absint( $row['latest_unknown_id'] ?? 0 ),
				'latest_sent_at'     => sanitize_text_field( $row['latest_sent_at'] ?? '' ),
				'latest_problem_at'  => sanitize_text_field( $row['latest_problem_at'] ?? '' ),
				'latest_event_at'    => sanitize_text_field( $row['latest_event_at'] ?? '' ),
				'latest_error'       => sanitize_textarea_field( $errors[ $key ] ?? '' ),
			);
			$data['state'] = $this->state_for_source( $data );
			$summary[ $key ] = $data;
		}

		return $summary;
	}

	/**
	 * Return form/provider options observed in mail history.
	 *
	 * @param string $source Optional source group.
	 * @return array<int,array<string,string|int>>
	 */
	public function form_options( $source = '' ) {
		global $wpdb;

		$table         = $wpdb->prefix . 'subin_mail_events';
		$lead_table    = $wpdb->prefix . 'subin_leads';
		$provider_expr = "COALESCE(NULLIF(e.provider, ''), NULLIF(leads.provider, ''), '')";
		$form_expr     = "COALESCE(NULLIF(e.form_id, ''), NULLIF(leads.form_id, ''), '')";
		$where         = array( "{$form_expr} != ''" );
		$params        = array();
		$source        = $this->normalize_source( $source );
		if ( '' !== $source ) {
			$where[]  = $this->source_group_sql() . ' = %s';
			$params[] = $source;
		}
		$sql  = "SELECT {$provider_expr} AS provider, {$form_expr} AS form_id,
				MAX(CASE WHEN leads.form_name IS NOT NULL AND leads.form_name != '' THEN leads.form_name ELSE '' END) AS form_name,
				COUNT(*) AS event_count
			FROM {$table} e
			LEFT JOIN {$lead_table} leads ON leads.id = e.lead_id
			WHERE " . implode( ' AND ', $where ) . "
			GROUP BY provider, form_id
			ORDER BY form_name ASC, provider ASC, form_id ASC
			LIMIT 250";
		$rows = $wpdb->get_results( $this->prepare_sql( $sql, $params ), ARRAY_A );
		$out  = array();

		foreach ( is_array( $rows ) ? $rows : array() as $row ) {
			$provider  = sanitize_key( $row['provider'] ?? '' );
			$form_id   = sanitize_text_field( $row['form_id'] ?? '' );
			$form_name = sanitize_text_field( $row['form_name'] ?? '' );
			if ( '' === $provider || '' === $form_id ) {
				continue;
			}
			$out[] = array(
				'key'         => $provider . ':' . $form_id,
				'provider'    => $provider,
				'form_id'     => $form_id,
				/* translators: %1$s: form provider name, %2$s: provider-specific form ID. */
				'label'       => '' !== $form_name ? $form_name : sprintf( __( '%1$s form %2$s', 'submission-inbox' ), $this->provider_label( $provider ), $form_id ),
				'event_count' => absint( $row['event_count'] ?? 0 ),
			);
		}

		return $out;
	}

	/**
	 * Aggregate daily outcomes for a compact trend chart.
	 *
	 * @param array<string,mixed> $filters Filters.
	 * @return array<int,array<string,int|string>>
	 */
	public function daily_trend( array $filters = array() ) {
		global $wpdb;

		$filters = $this->normalize_filters( $filters );
		if ( 'all' === $filters['date_range'] && empty( $filters['date_from'] ) ) {
			$filters['_date_from'] = gmdate( 'Y-m-d 00:00:00', strtotime( '-89 days' ) );
		}
		$table      = $wpdb->prefix . 'subin_mail_events';
		$lead_table = $wpdb->prefix . 'subin_leads';
		$where      = $this->where_for_filters( $filters );
		$sql        = "SELECT DATE(e.created_at) AS event_day,
				SUM(CASE WHEN e.status = 'sent' THEN 1 ELSE 0 END) AS sent,
				SUM(CASE WHEN e.status = 'failed' THEN 1 ELSE 0 END) AS failed,
				SUM(CASE WHEN e.status = 'no_email_generated' THEN 1 ELSE 0 END) AS no_email_generated,
				SUM(CASE WHEN e.status IN ('unknown','pending') THEN 1 ELSE 0 END) AS unknown
			FROM {$table} e
			LEFT JOIN {$lead_table} leads ON leads.id = e.lead_id
			{$where['sql']}
			GROUP BY event_day
			ORDER BY event_day ASC
			LIMIT 366";
		$rows       = $wpdb->get_results( $this->prepare_sql( $sql, $where['params'] ), ARRAY_A );
		$trend      = array();

		foreach ( is_array( $rows ) ? $rows : array() as $row ) {
			$trend[] = array(
				'date'               => sanitize_text_field( $row['event_day'] ?? '' ),
				'sent'               => absint( $row['sent'] ?? 0 ),
				'failed'             => absint( $row['failed'] ?? 0 ),
				'no_email_generated' => absint( $row['no_email_generated'] ?? 0 ),
				'unknown'            => absint( $row['unknown'] ?? 0 ),
			);
		}

		return $trend;
	}

	/**
	 * Build date/source/form/status filters.
	 *
	 * @param array<string,mixed> $filters Filters.
	 * @param string              $time_column Event time column for the projection.
	 * @return array{sql:string,params:array<int,mixed>}
	 */
	public function where_for_filters( array $filters, $time_column = 'created_at' ) {
		$filters     = $this->normalize_filters( $filters );
		$time_column = 'updated_at' === $time_column ? 'updated_at' : 'created_at';
		$event_time  = 'e.' . $time_column;
		$where       = array();
		$params      = array();

		if ( '' !== $filters['provider'] ) {
			$where[]  = "COALESCE(NULLIF(e.provider, ''), NULLIF(leads.provider, ''), '') = %s";
			$params[] = $filters['provider'];
		}
		if ( '' !== $filters['form_id'] ) {
			$where[]  = "COALESCE(NULLIF(e.form_id, ''), NULLIF(leads.form_id, ''), '') = %s";
			$params[] = $filters['form_id'];
		}
		if ( '' !== $filters['source'] ) {
			$where[]  = $this->source_group_sql() . ' = %s';
			$params[] = $filters['source'];
		}
		if ( '' !== $filters['status'] ) {
			if ( 'unknown' === $filters['status'] ) {
				$where[] = "e.status IN ('unknown','pending')";
			} else {
				$where[]  = 'e.status = %s';
				$params[] = $filters['status'];
			}
		}

		if ( ! empty( $filters['_date_from'] ) ) {
			$where[]  = $event_time . ' >= %s';
			$params[] = $filters['_date_from'];
		} elseif ( ! empty( $filters['date_from'] ) ) {
			$where[]  = $event_time . ' >= %s';
			$params[] = $filters['date_from'] . ' 00:00:00';
		} elseif ( in_array( $filters['date_range'], array( '7', '30', '90' ), true ) ) {
			$where[]  = $event_time . ' >= %s';
			$params[] = current_datetime()->modify( '-' . ( absint( $filters['date_range'] ) - 1 ) . ' days' )->format( 'Y-m-d 00:00:00' );
		}
		if ( ! empty( $filters['_date_to'] ) ) {
			$where[]  = $event_time . ' < %s';
			$params[] = $filters['_date_to'];
		} elseif ( ! empty( $filters['date_to'] ) ) {
			$where[]  = $event_time . ' < %s';
			$params[] = ( new \DateTimeImmutable( $filters['date_to'], wp_timezone() ) )->modify( '+1 day' )->format( 'Y-m-d 00:00:00' );
		}

		return array(
			'sql'    => ! empty( $where ) ? 'WHERE ' . implode( ' AND ', $where ) : '',
			'params' => $params,
		);
	}

	/**
	 * Prepare SQL when needed.
	 *
	 * @param string           $sql SQL.
	 * @param array<int,mixed> $params Params.
	 * @return string
	 */
	public function prepare_sql( $sql, array $params ) {
		global $wpdb;

		return empty( $params ) ? $sql : $wpdb->prepare( $sql, $params );
	}

	/**
	 * Classify an error.
	 *
	 * @param string $error Error text.
	 * @return array<string,string>
	 */
	public function classify_error( $error ) {
		return $this->error_classifier->classify( (string) $error );
	}

	/**
	 * Public source labels used by filters and reports.
	 *
	 * @return array<string,string>
	 */
	public function source_labels() {
		return array(
			'submission_inbox_forms' => __( 'InboxMend Forms', 'submission-inbox' ),
			'contact_form_7'         => __( 'Contact Form 7', 'submission-inbox' ),
			'wpforms'                => __( 'WPForms', 'submission-inbox' ),
			'fluent_forms'           => __( 'Fluent Forms', 'submission-inbox' ),
			'forminator'             => __( 'Forminator', 'submission-inbox' ),
			'ninja_forms'            => __( 'Ninja Forms', 'submission-inbox' ),
			'wordpress_core'         => __( 'WordPress Core', 'submission-inbox' ),
			'other_plugins'          => __( 'Other plugins', 'submission-inbox' ),
			'unknown'                => __( 'Unknown', 'submission-inbox' ),
		);
	}

	/**
	 * Normalize raw event sources and legacy query values to one source group.
	 *
	 * @param string $source Source value.
	 * @return string
	 */
	public function normalize_source( $source ) {
		$source = sanitize_key( (string) $source );
		$aliases = array(
			'submission_inbox_form'       => 'submission_inbox_forms',
			'submission_inbox_auto_reply' => 'submission_inbox_forms',
			'wp_mail'                     => 'wordpress_core',
			'wordpress'                   => 'wordpress_core',
			'plugin'                      => 'other_plugins',
		);
		$source = $aliases[ $source ] ?? $source;

		return isset( $this->source_labels()[ $source ] ) ? $source : '';
	}

	/**
	 * Current issue groups with a deterministic active/recovering/resolved lifecycle.
	 *
	 * A transport configuration issue is site-wide, while notification and
	 * ambiguous issues are scoped to the observed form or source. An incident is
	 * active until every source it affected records a newer accepted handoff. It
	 * then recovers and is resolved after sustained accepted handoffs or a clean
	 * recovery window, as defined by EmailHealthIncidentProjector.
	 * Resolved history remains available in Email Log and is not shown as work.
	 *
	 * @param array<string,mixed> $filters Filters. Outcome is ignored for lifecycle evidence.
	 * @param int                 $limit Maximum current groups; zero returns all bounded groups.
	 * @param bool                $include_resolved Include resolved episodes for internal tests.
	 * @return array<int,object>
	 */
	public function current_incidents( array $filters = array(), $limit = 25, $include_resolved = false ) {
		global $wpdb;

		$filters           = $this->normalize_filters( $filters );
		$filters['status'] = '';
		$table             = $wpdb->prefix . 'subin_mail_events';
		$lead_table        = $wpdb->prefix . 'subin_leads';
		$where             = $this->where_for_filters( $filters, 'updated_at' );
		$source_expr       = $this->source_group_sql();
		$provider_expr     = "COALESCE(NULLIF(e.provider, ''), NULLIF(leads.provider, ''), '')";
		$form_expr         = "COALESCE(NULLIF(e.form_id, ''), NULLIF(leads.form_id, ''), '')";
		$sql               = "SELECT e.id, e.error_message, e.reason_code, e.status, e.created_at, e.updated_at,
				{$source_expr} AS source_group, {$provider_expr} AS provider, {$form_expr} AS form_id
			FROM {$table} e
			LEFT JOIN {$lead_table} leads ON leads.id = e.lead_id
			{$where['sql']}
			ORDER BY e.updated_at DESC, e.id DESC
			LIMIT " . ( self::INCIDENT_EVENT_LIMIT + 1 );
		$rows              = $wpdb->get_results( $this->prepare_sql( $sql, $where['params'] ), ARRAY_A );
		$this->incident_projection_complete = count( is_array( $rows ) ? $rows : array() ) <= self::INCIDENT_EVENT_LIMIT;
		if ( ! $this->incident_projection_complete ) {
			$rows = array_slice( $rows, 0, self::INCIDENT_EVENT_LIMIT );
		}
		$events            = array();
		$now_timestamp     = current_datetime()->getTimestamp();
		$pending_cutoff    = $now_timestamp - ( 10 * MINUTE_IN_SECONDS );

		foreach ( is_array( $rows ) ? $rows : array() as $row ) {
			$status = sanitize_key( $row['status'] ?? 'unknown' );
			if ( ! in_array( $status, array( 'sent', 'failed', 'no_email_generated', 'unknown', 'pending' ), true ) ) {
				continue;
			}

			$source            = sanitize_key( $row['source_group'] ?? 'unknown' );
			$provider          = sanitize_key( $row['provider'] ?? '' );
			$form_id           = sanitize_text_field( $row['form_id'] ?? '' );
			$updated_at        = sanitize_text_field( $row['updated_at'] ?? $row['created_at'] ?? '' );
			$updated_date      = \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $updated_at, wp_timezone() );
			$updated_timestamp = $updated_date ? $updated_date->getTimestamp() : 0;
			if ( 'pending' === $status && $updated_timestamp > $pending_cutoff ) {
				continue;
			}
			$event = array(
				'id'           => absint( $row['id'] ?? 0 ),
				'created_at'   => $updated_at,
				'timestamp'    => $updated_timestamp,
				'status'       => $status,
				'source_group' => '' !== $source ? $source : 'unknown',
				'provider'     => $provider,
				'form_id'      => $form_id,
			);

			if ( 'sent' !== $status ) {
				$error = sanitize_textarea_field( $row['error_message'] ?? '' );
				if ( 'failed' === $status ) {
					$issue = $this->error_classifier->classify( $error );
				} elseif ( 'no_email_generated' === $status ) {
					$issue = $this->missing_notification_issue( $error );
					$reason_code = sanitize_key( $row['reason_code'] ?? '' );
					if ( '' !== $reason_code ) {
						$issue['code'] .= '_' . substr( $reason_code, 0, 60 );
					}
				} else {
					$issue = $this->unknown_status_issue( $error );
				}
				$scope               = $this->incident_scope( $issue, $source, $provider, $form_id );
				$event['issue']       = $issue;
				$event['scope_type']  = $scope['type'];
				$event['scope_key']   = $scope['key'];
			}

			$events[] = $event;
		}

		$projected = $this->incident_projector->project( $events, $now_timestamp );
		if ( ! $include_resolved ) {
			$projected = array_values(
				array_filter(
					$projected,
					static function ( array $incident ): bool {
						return EmailHealthIncidentProjector::LIFECYCLE_RESOLVED !== ( $incident['lifecycle'] ?? '' );
					}
				)
			);
		}
		if ( absint( $limit ) > 0 ) {
			$projected = array_slice( $projected, 0, min( 100, absint( $limit ) ) );
		}

		return array_map(
			static function ( array $incident ): object {
				$incident['sources'] = implode( ',', array_values( array_unique( (array) ( $incident['sources'] ?? array() ) ) ) );
				return (object) $incident;
			},
			$projected
		);
	}

	/** Backward-compatible name for current, actionable issue groups. */
	public function recent_issue_groups( array $filters, $limit = 8 ) {
		return $this->current_incidents( $filters, $limit );
	}

	/**
	 * Latest event in the current scope.
	 *
	 * @param array<string,mixed> $filters Filters.
	 * @return object|null
	 */
	public function latest_email_event( array $filters ) {
		global $wpdb;
		$table       = $wpdb->prefix . 'subin_mail_events';
		$lead_table  = $wpdb->prefix . 'subin_leads';
		$where       = $this->where_for_filters( $filters );
		$source_expr = $this->source_group_sql();
		$sql         = "SELECT e.id, e.status, e.error_message, e.created_at, {$source_expr} AS source_group
			FROM {$table} e LEFT JOIN {$lead_table} leads ON leads.id = e.lead_id
			{$where['sql']} ORDER BY e.created_at DESC, e.id DESC LIMIT 1";

		return $wpdb->get_row( $this->prepare_sql( $sql, $where['params'] ) );
	}

	/**
	 * Latest failed event in the current scope.
	 *
	 * @param array<string,mixed> $filters Filters.
	 * @return object|null
	 */
	public function most_recent_failed_error( array $filters ) {
		global $wpdb;
		$table       = $wpdb->prefix . 'subin_mail_events';
		$lead_table  = $wpdb->prefix . 'subin_leads';
		$where       = $this->where_for_filters( $filters );
		$where['sql'] = $where['sql'] ? $where['sql'] . " AND e.status = 'failed'" : "WHERE e.status = 'failed'";
		$sql         = "SELECT e.id, e.error_message, e.created_at FROM {$table} e
			LEFT JOIN {$lead_table} leads ON leads.id = e.lead_id
			{$where['sql']} ORDER BY e.created_at DESC, e.id DESC LIMIT 1";

		return $wpdb->get_row( $this->prepare_sql( $sql, $where['params'] ) );
	}

	/**
	 * Normalize supported filters while retaining internal previous-period bounds.
	 *
	 * @param array<string,mixed> $filters Filters.
	 * @return array<string,mixed>
	 */
	private function normalize_filters( array $filters ) {
		$date_range = (string) ( $filters['date_range'] ?? '30' );
		$date_range = in_array( $date_range, array( '7', '30', '90', 'all', 'custom' ), true ) ? $date_range : '30';
		$date_from  = $this->normalize_date( $filters['date_from'] ?? '' );
		$date_to    = $this->normalize_date( $filters['date_to'] ?? '' );
		if ( 'custom' === $date_range && ( '' === $date_from || '' === $date_to || $date_from > $date_to ) ) {
			$date_range = '30';
			$date_from  = '';
			$date_to    = '';
		}
		$status = sanitize_key( $filters['status'] ?? '' );
		$status = in_array( $status, array( 'sent', 'failed', 'no_email_generated', 'unknown' ), true ) ? $status : '';
		$form_id  = sanitize_text_field( $filters['form_id'] ?? '' );
		$provider = sanitize_key( $filters['provider'] ?? '' );
		if ( '' !== $form_id && '' === $provider ) {
			$provider = 'submission_inbox_form';
		}

		return array(
			'source'     => $this->normalize_source( $filters['source'] ?? '' ),
			'date_range' => $date_range,
			'date_from'  => $date_from,
			'date_to'    => $date_to,
			'provider'   => $provider,
			'form_id'    => $form_id,
			'status'     => $status,
			'_date_from' => sanitize_text_field( $filters['_date_from'] ?? '' ),
			'_date_to'   => sanitize_text_field( $filters['_date_to'] ?? '' ),
		);
	}

	/** Strictly normalize an ISO calendar date. */
	private function normalize_date( $date ) {
		$date = sanitize_text_field( (string) $date );
		$parsed = \DateTimeImmutable::createFromFormat( '!Y-m-d', $date );
		$errors = \DateTimeImmutable::getLastErrors();
		return $parsed && ( false === $errors || ( 0 === $errors['warning_count'] && 0 === $errors['error_count'] ) ) && $parsed->format( 'Y-m-d' ) === $date ? $date : '';
	}

	/** Build the previous period for relative and custom filters. */
	private function previous_summary( array $filters ) {
		if ( 'all' === $filters['date_range'] ) {
			return null;
		}

		if ( 'custom' === $filters['date_range'] ) {
			$from = strtotime( $filters['date_from'] . ' 00:00:00 UTC' );
			$to   = strtotime( $filters['date_to'] . ' +1 day 00:00:00 UTC' );
		} else {
			$days = absint( $filters['date_range'] );
			$to   = strtotime( gmdate( 'Y-m-d 00:00:00' ) . ' UTC' ) + DAY_IN_SECONDS;
			$from = $to - ( $days * DAY_IN_SECONDS );
		}
		if ( ! $from || ! $to || $from >= $to ) {
			return null;
		}
		$duration = $to - $from;
		$previous = $filters;
		$previous['date_range'] = 'all';
		$previous['date_from']  = '';
		$previous['date_to']    = '';
		$previous['_date_from'] = gmdate( 'Y-m-d H:i:s', $from - $duration );
		$previous['_date_to']   = gmdate( 'Y-m-d H:i:s', $from );

		return $this->summary_counts( $previous );
	}

	/** Determine one source state without treating no data as healthy. */
	private function state_for_source( array $source ) {
		$total   = absint( $source['total'] ?? 0 );
		$sent    = absint( $source['sent'] ?? 0 );
		$failed  = absint( $source['failed'] ?? 0 );
		$missing = absint( $source['no_email_generated'] ?? 0 );
		$unknown = absint( $source['unknown'] ?? 0 );
		if ( 0 === $total ) {
			return self::STATE_NO_DATA;
		}
		if ( 0 === $sent && 0 === $failed && 0 === $missing && $unknown > 0 ) {
			return self::STATE_UNKNOWN;
		}
		$latest_sent_id    = absint( $source['latest_sent_id'] ?? 0 );
		$latest_problem_id = absint( $source['latest_problem_id'] ?? 0 );
		if ( $failed + $missing > 0 && ( 0 === $latest_sent_id || $latest_problem_id > $latest_sent_id ) ) {
			return self::STATE_CRITICAL;
		}
		if ( $failed + $missing > 0 ) {
			return self::STATE_RECOVERING;
		}
		if ( $unknown > 0 ) {
			return self::STATE_DEGRADED;
		}

		return self::STATE_HEALTHY;
	}

	/** Return the more severe of two health states. */
	private function stronger_state( $left, $right ) {
		$weights = array(
			self::STATE_NO_DATA    => 0,
			self::STATE_HEALTHY    => 1,
			self::STATE_UNKNOWN    => 2,
			self::STATE_RECOVERING => 3,
			self::STATE_DEGRADED   => 4,
			self::STATE_CRITICAL   => 5,
		);

		return ( $weights[ $right ] ?? 0 ) > ( $weights[ $left ] ?? 0 ) ? $right : $left;
	}

	/**
	 * Scope an issue by the setting or object that can actually resolve it.
	 *
	 * @param array<string,string> $issue Classified issue.
	 * @param string               $source Source group.
	 * @param string               $provider Form provider.
	 * @param string               $form_id Form id.
	 * @return array{type:string,key:string}
	 */
	private function incident_scope( array $issue, $source, $provider, $form_id ) {
		$action   = sanitize_key( $issue['action_type'] ?? 'email_log' );
		$code     = sanitize_key( $issue['code'] ?? 'unknown_email_error' );
		$source   = sanitize_key( (string) $source );
		$provider = sanitize_key( (string) $provider );
		$form_id  = sanitize_text_field( (string) $form_id );

		$site_transport_codes = array( 'default_mail_unavailable', 'smtp_auth_failed', 'smtp_connection_failed', 'smtp_encryption_failed' );
		if ( in_array( $code, $site_transport_codes, true ) ) {
			return array( 'type' => 'site', 'key' => 'site' );
		}
		if ( in_array( $action, array( 'form_notification_settings', 'settings_sending' ), true ) && '' !== $provider && '' !== $form_id ) {
			return array( 'type' => 'form', 'key' => 'form:' . $provider . ':' . $form_id );
		}

		$source = '' !== $source ? $source : 'unknown';
		return array( 'type' => 'source', 'key' => 'source:' . $source );
	}

	/**
	 * Count current incidents without confusing causes, sources, and attempts.
	 *
	 * @param array<int,object> $incidents Current incident objects.
	 * @return array<string,int>
	 */
	private function incident_summary( array $incidents ) {
		$active_sources     = array();
		$recovering_sources = array();
		$summary            = array(
			'active_issue_count'     => 0,
			'recovering_issue_count' => 0,
			'resolved_issue_count'   => 0,
			'affected_source_count'  => 0,
			'recovering_source_count' => 0,
			'problem_event_count'    => 0,
			'recovering_event_count' => 0,
		);

		foreach ( $incidents as $incident ) {
			$lifecycle = sanitize_key( $incident->lifecycle ?? EmailHealthIncidentProjector::LIFECYCLE_ACTIVE );
			$sources   = array_filter( array_map( 'sanitize_key', explode( ',', (string) ( $incident->sources ?? '' ) ) ) );
			if ( EmailHealthIncidentProjector::LIFECYCLE_RESOLVED === $lifecycle ) {
				++$summary['resolved_issue_count'];
			} elseif ( EmailHealthIncidentProjector::LIFECYCLE_RECOVERING === $lifecycle ) {
				++$summary['recovering_issue_count'];
				$summary['recovering_event_count'] += absint( $incident->occurrences ?? 0 );
				$recovering_sources = array_merge( $recovering_sources, $sources );
			} elseif ( EmailHealthIncidentProjector::LIFECYCLE_ACTIVE === $lifecycle ) {
				++$summary['active_issue_count'];
				$summary['problem_event_count'] += absint( $incident->occurrences ?? 0 );
				$active_sources = array_merge( $active_sources, $sources );
			}
		}

		$summary['affected_source_count']   = count( array_unique( $active_sources ) );
		$summary['recovering_source_count'] = count( array_unique( $recovering_sources ) );

		return $summary;
	}

	/** Keep only incidents that still communicate current work or recovery. */
	private function actionable_incidents( array $incidents ) {
		return array_values(
			array_filter(
				$incidents,
				static function ( $incident ): bool {
					return EmailHealthIncidentProjector::LIFECYCLE_RESOLVED !== sanitize_key( $incident->lifecycle ?? '' );
				}
			)
		);
	}

	/**
	 * Make source badges use the same lifecycle as the global issue count.
	 *
	 * @param array<string,array<string,mixed>> $sources Source summaries.
	 * @param array<int,object>                  $incidents Current incidents.
	 * @return array<string,array<string,mixed>>
	 */
	private function apply_incident_states_to_sources( array $sources, array $incidents ) {
		foreach ( $sources as $key => $source ) {
			if ( 0 === absint( $source['total'] ?? 0 ) ) {
				$sources[ $key ]['state'] = self::STATE_NO_DATA;
				continue;
			}
			$latest_known   = max( absint( $source['latest_sent_id'] ?? 0 ), absint( $source['latest_problem_id'] ?? 0 ) );
			$latest_unknown = absint( $source['latest_unknown_id'] ?? 0 );
			$sources[ $key ]['state'] = $latest_unknown > $latest_known
				? ( absint( $source['sent'] ?? 0 ) > 0 ? self::STATE_DEGRADED : self::STATE_UNKNOWN )
				: self::STATE_HEALTHY;
		}

		foreach ( $incidents as $incident ) {
			$lifecycle = sanitize_key( $incident->lifecycle ?? EmailHealthIncidentProjector::LIFECYCLE_ACTIVE );
			$status    = sanitize_key( $incident->primary_status ?? 'unknown' );
			foreach ( explode( ',', (string) ( $incident->sources ?? '' ) ) as $source_key ) {
				$source_key = sanitize_key( $source_key );
				if ( '' === $source_key || ! isset( $sources[ $source_key ] ) ) {
					continue;
				}
				if ( EmailHealthIncidentProjector::LIFECYCLE_RECOVERING === $lifecycle ) {
					$incident_state = self::STATE_RECOVERING;
				} elseif ( in_array( $status, array( 'failed', 'no_email_generated' ), true ) ) {
					$incident_state = self::STATE_CRITICAL;
				} else {
					$incident_state = absint( $sources[ $source_key ]['sent'] ?? 0 ) > 0 ? self::STATE_DEGRADED : self::STATE_UNKNOWN;
				}
				$sources[ $source_key ]['state'] = $this->stronger_state( $sources[ $source_key ]['state'], $incident_state );
			}
		}

		return $sources;
	}

	/** Determine the global or scoped state from current incidents, not historical totals. */
	private function state_from_incidents( array $summary, array $incidents ) {
		if ( 0 === absint( $summary['total'] ?? 0 ) ) {
			return self::STATE_NO_DATA;
		}

		$state = self::STATE_HEALTHY;
		foreach ( $incidents as $incident ) {
			$lifecycle = sanitize_key( $incident->lifecycle ?? EmailHealthIncidentProjector::LIFECYCLE_ACTIVE );
			$status    = sanitize_key( $incident->primary_status ?? 'unknown' );
			if ( EmailHealthIncidentProjector::LIFECYCLE_RECOVERING === $lifecycle ) {
				$state = $this->stronger_state( $state, self::STATE_RECOVERING );
			} elseif ( EmailHealthIncidentProjector::LIFECYCLE_ACTIVE === $lifecycle ) {
				$incident_state = in_array( $status, array( 'failed', 'no_email_generated' ), true )
					? self::STATE_CRITICAL
					: ( absint( $summary['sent'] ?? 0 ) > 0 ? self::STATE_DEGRADED : self::STATE_UNKNOWN );
				$state = $this->stronger_state( $state, $incident_state );
			}
		}
		if ( self::STATE_HEALTHY === $state && absint( $summary['unknown'] ?? 0 ) > 0 ) {
			return absint( $summary['sent'] ?? 0 ) > 0 ? self::STATE_DEGRADED : self::STATE_UNKNOWN;
		}

		return $state;
	}

	/** Latest redacted failure per source group. */
	private function latest_failed_errors_by_source( array $filters ) {
		global $wpdb;
		$table       = $wpdb->prefix . 'subin_mail_events';
		$lead_table  = $wpdb->prefix . 'subin_leads';
		$where       = $this->where_for_filters( $filters );
		$where['sql'] = $where['sql'] ? $where['sql'] . " AND e.status = 'failed' AND e.error_message IS NOT NULL AND e.error_message != ''" : "WHERE e.status = 'failed' AND e.error_message IS NOT NULL AND e.error_message != ''";
		$source_expr = $this->source_group_sql();
		$sql         = "SELECT {$source_expr} AS source_group, e.error_message FROM {$table} e
			LEFT JOIN {$lead_table} leads ON leads.id = e.lead_id {$where['sql']}
			ORDER BY e.created_at DESC, e.id DESC LIMIT 500";
		$rows        = $wpdb->get_results( $this->prepare_sql( $sql, $where['params'] ), ARRAY_A );
		$errors      = array();
		foreach ( is_array( $rows ) ? $rows : array() as $row ) {
			$key = sanitize_key( $row['source_group'] ?? 'unknown' );
			if ( ! isset( $errors[ $key ] ) ) {
				$errors[ $key ] = $this->error_classifier->sanitize_technical_error( (string) ( $row['error_message'] ?? '' ) );
			}
		}

		return $errors;
	}

	/** Source grouping SQL shared by every report query. */
	private function source_group_sql() {
		return "CASE
			WHEN leads.provider = 'submission_inbox_form' OR e.source = 'submission_inbox_form' OR (e.source = 'submission_inbox_auto_reply' AND (leads.provider IS NULL OR leads.provider = '' OR leads.provider = 'submission_inbox_form')) THEN 'submission_inbox_forms'
			WHEN leads.provider = 'contact_form_7' OR e.source = 'contact_form_7' THEN 'contact_form_7'
			WHEN leads.provider = 'wpforms' OR e.source = 'wpforms' THEN 'wpforms'
			WHEN leads.provider = 'fluent_forms' OR e.source = 'fluent_forms' THEN 'fluent_forms'
			WHEN leads.provider = 'forminator' OR e.source = 'forminator' THEN 'forminator'
			WHEN leads.provider = 'ninja_forms' OR e.source = 'ninja_forms' THEN 'ninja_forms'
			WHEN e.source IN ('wp_mail','wordpress') THEN 'wordpress_core'
			WHEN e.source = 'plugin' THEN 'other_plugins'
			WHEN e.source IS NOT NULL AND e.source != '' AND e.source != 'unknown' THEN 'other_plugins'
			ELSE 'unknown'
		END";
	}

	/** Human label for raw provider keys. */
	private function provider_label( $provider ) {
		$labels = array(
			'submission_inbox_form' => __( 'InboxMend', 'submission-inbox' ),
			'contact_form_7'         => __( 'Contact Form 7', 'submission-inbox' ),
			'wpforms'                => __( 'WPForms', 'submission-inbox' ),
			'fluent_forms'           => __( 'Fluent Forms', 'submission-inbox' ),
			'forminator'             => __( 'Forminator', 'submission-inbox' ),
			'ninja_forms'            => __( 'Ninja Forms', 'submission-inbox' ),
		);

		return $labels[ sanitize_key( $provider ) ] ?? __( 'Form', 'submission-inbox' );
	}

	/** Missing notification diagnostic. */
	private function missing_notification_issue( $technical_error = '' ) {
		return array(
			'code'               => 'no_email_generated',
			'title'              => __( 'No email generated', 'submission-inbox' ),
			'explanation'        => __( 'A form submission was saved, but no notification email was created.', 'submission-inbox' ),
			'recommended_action' => __( 'Check the form notification settings.', 'submission-inbox' ),
			'action_type'        => 'form_notification_settings',
			'technical_error'    => $this->error_classifier->sanitize_technical_error( (string) $technical_error ),
		);
	}

	/** Unknown event diagnostic. */
	private function unknown_status_issue( $technical_error = '' ) {
		return array(
			'code'               => 'unknown_email_error',
			'title'              => __( 'Email status is unknown', 'submission-inbox' ),
			'explanation'        => __( 'InboxMend could not determine the email notification status.', 'submission-inbox' ),
			'recommended_action' => __( 'Review the related email events in Email Log.', 'submission-inbox' ),
			'action_type'        => 'email_log',
			'technical_error'    => $this->error_classifier->sanitize_technical_error( (string) $technical_error ),
		);
	}
}
