<?php
/**
 * Deterministic Email Health incident projection.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Mail;

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

/**
 * Projects append-only mail outcomes into actionable incident episodes.
 *
 * The projector is intentionally free of WordPress and database calls. Its
 * caller owns normalization, classification, privacy, and the bounded query.
 */
class EmailHealthIncidentProjector {
	const LIFECYCLE_ACTIVE     = 'active';
	const LIFECYCLE_RECOVERING = 'recovering';
	const LIFECYCLE_RESOLVED   = 'resolved';

	/** Three accepted handoffs provide deterministic automatic recovery. */
	const RECOVERY_ACCEPTANCES_REQUIRED = 3;

	/** One accepted handoff plus a day without recurrence also resolves. */
	const RECOVERY_CLEAN_SECONDS = 86400;

	/**
	 * Project normalized events into stable incident episodes.
	 *
	 * Problem events must contain a classified `issue`, `scope_type`, and
	 * `scope_key`. Accepted events are applied to every matching open incident
	 * for a source that incident actually affected.
	 *
	 * @param array<int,array<string,mixed>> $events Normalized event rows.
	 * @param int|null                       $now Unix timestamp used for clean-window recovery.
	 * @return array<int,array<string,mixed>>
	 */
	public function project( array $events, $now = null ) {
		$now        = is_numeric( $now ) ? (int) $now : time();
		$normalized = array();

		foreach ( array_values( $events ) as $index => $event ) {
			if ( ! is_array( $event ) ) {
				continue;
			}
			$status = strtolower( trim( (string) ( $event['status'] ?? '' ) ) );
			if ( ! in_array( $status, array( 'sent', 'accepted', 'success', 'failed', 'no_email_generated', 'unknown', 'pending', 'problem' ), true ) ) {
				continue;
			}
			$event['status']         = $status;
			$event['_timestamp']     = $this->event_timestamp( $event );
			$event['_original_index'] = $index;
			$normalized[]            = $event;
		}

		usort(
			$normalized,
			static function ( array $left, array $right ): int {
				if ( $left['_timestamp'] !== $right['_timestamp'] ) {
					return $left['_timestamp'] <=> $right['_timestamp'];
				}
				$left_id  = (int) ( $left['id'] ?? 0 );
				$right_id = (int) ( $right['id'] ?? 0 );
				if ( $left_id !== $right_id ) {
					return $left_id <=> $right_id;
				}

				return $left['_original_index'] <=> $right['_original_index'];
			}
		);

		$groups = array();
		foreach ( $normalized as $event ) {
			if ( $this->is_accepted( $event['status'] ) ) {
				$this->apply_acceptance( $groups, $event );
				continue;
			}
			if ( empty( $event['issue'] ) || ! is_array( $event['issue'] ) ) {
				continue;
			}

			$issue      = $event['issue'];
			$issue_code = $this->safe_key( $issue['code'] ?? 'unknown_email_error', 'unknown_email_error' );
			$scope_type = $this->safe_key( $event['scope_type'] ?? 'source', 'source' );
			$scope_type = in_array( $scope_type, array( 'site', 'form', 'source' ), true ) ? $scope_type : 'source';
			$scope_key  = trim( (string) ( $event['scope_key'] ?? '' ) );
			if ( '' === $scope_key ) {
				$scope_key = 'source:' . $this->source_key( $event );
			}
			$fingerprint = hash( 'sha256', $issue_code . "\0" . $scope_type . "\0" . $scope_key );

			if ( isset( $groups[ $fingerprint ] ) && self::LIFECYCLE_RESOLVED === $this->lifecycle_for_group( $groups[ $fingerprint ], (int) $event['_timestamp'] ) ) {
				unset( $groups[ $fingerprint ] );
			}
			if ( ! isset( $groups[ $fingerprint ] ) ) {
				$groups[ $fingerprint ] = $this->new_group( $fingerprint, $scope_type, $scope_key, $issue, $event );
			}

			$this->apply_problem( $groups[ $fingerprint ], $issue, $event );
		}

		$output = array();
		foreach ( $groups as $group ) {
			$group['lifecycle'] = $this->lifecycle_for_group( $group, $now );
			$output[]           = $this->public_group( $group );
		}

		usort(
			$output,
			static function ( array $left, array $right ): int {
				$weight = array(
					self::LIFECYCLE_ACTIVE     => 3,
					self::LIFECYCLE_RECOVERING => 2,
					self::LIFECYCLE_RESOLVED   => 1,
				);
				$left_weight  = $weight[ $left['lifecycle'] ] ?? 0;
				$right_weight = $weight[ $right['lifecycle'] ] ?? 0;
				if ( $left_weight !== $right_weight ) {
					return $right_weight <=> $left_weight;
				}
				if ( $left['occurrences'] !== $right['occurrences'] ) {
					return $right['occurrences'] <=> $left['occurrences'];
				}
				if ( $left['latest_timestamp'] !== $right['latest_timestamp'] ) {
					return $right['latest_timestamp'] <=> $left['latest_timestamp'];
				}

				return strcmp( $left['fingerprint'], $right['fingerprint'] );
			}
		);

		return $output;
	}

	/** Initialize one incident episode. */
	private function new_group( $fingerprint, $scope_type, $scope_key, array $issue, array $event ) {
		$timestamp = (int) $event['_timestamp'];
		$created   = trim( (string) ( $event['created_at'] ?? '' ) );

		return array(
			'fingerprint'       => (string) $fingerprint,
			'scope_type'       => (string) $scope_type,
			'scope_key'        => (string) $scope_key,
			'issue'            => $issue,
			'occurrences'      => 0,
			'first_at'         => $created,
			'first_timestamp'  => $timestamp,
			'latest_at'        => $created,
			'latest_timestamp' => $timestamp,
			'latest_recovery_at' => '',
			'primary_status'   => (string) ( $event['status'] ?? 'unknown' ),
			'sources'          => array(),
			'source_evidence'  => array(),
			'lifecycle'        => self::LIFECYCLE_ACTIVE,
		);
	}

	/** Add or reopen a problem and invalidate older recovery evidence. */
	private function apply_problem( array &$group, array $issue, array $event ) {
		$timestamp = (int) $event['_timestamp'];
		$event_id  = (int) ( $event['id'] ?? 0 );
		$created   = trim( (string) ( $event['created_at'] ?? '' ) );
		$source    = $this->source_key( $event );

		foreach ( $group['source_evidence'] as &$evidence ) {
			$evidence['acceptances']       = 0;
			$evidence['latest_problem_at'] = $created;
			$evidence['problem_timestamp'] = $timestamp;
			$evidence['problem_id']        = $event_id;
			$evidence['latest_recovery_at'] = '';
		}
		unset( $evidence );

		if ( ! isset( $group['source_evidence'][ $source ] ) ) {
			$group['sources'][] = $source;
			$group['source_evidence'][ $source ] = array();
		}
		$group['source_evidence'][ $source ] = array(
			'acceptances'        => 0,
			'latest_problem_at'  => $created,
			'problem_timestamp'  => $timestamp,
			'problem_id'         => $event_id,
			'latest_recovery_at' => '',
		);
		$group['issue']            = $issue;
		$group['occurrences']      = (int) $group['occurrences'] + 1;
		$group['latest_at']        = $created;
		$group['latest_timestamp'] = $timestamp;
		$group['latest_recovery_at'] = '';
		if ( $this->status_weight( $event['status'] ) >= $this->status_weight( $group['primary_status'] ) ) {
			$group['primary_status'] = (string) $event['status'];
		}
		$group['lifecycle'] = self::LIFECYCLE_ACTIVE;
	}

	/** Apply one accepted handoff to all matching unresolved incidents. */
	private function apply_acceptance( array &$groups, array $event ) {
		$source    = $this->source_key( $event );
		$timestamp = (int) $event['_timestamp'];
		$event_id  = (int) ( $event['id'] ?? 0 );
		$created   = trim( (string) ( $event['created_at'] ?? '' ) );

		foreach ( $groups as &$group ) {
			if ( ! isset( $group['source_evidence'][ $source ] ) || ! $this->scope_matches( $group, $event ) ) {
				continue;
			}
			$evidence = &$group['source_evidence'][ $source ];
			if ( $timestamp < (int) $evidence['problem_timestamp'] || ( $timestamp === (int) $evidence['problem_timestamp'] && $event_id <= (int) $evidence['problem_id'] ) ) {
				unset( $evidence );
				continue;
			}
			++$evidence['acceptances'];
			$evidence['latest_recovery_at'] = $created;
			$group['latest_recovery_at']    = $created;
			$group['lifecycle']             = $this->lifecycle_for_group( $group, $timestamp );
			unset( $evidence );
		}
		unset( $group );
	}

	/** Resolve the lifecycle from evidence for every source the episode affected. */
	private function lifecycle_for_group( array $group, $now ) {
		if ( empty( $group['source_evidence'] ) ) {
			return self::LIFECYCLE_ACTIVE;
		}

		$all_recovered = true;
		$all_sustained = true;
		$latest_problem = 0;
		foreach ( $group['source_evidence'] as $evidence ) {
			$acceptances   = max( 0, (int) ( $evidence['acceptances'] ?? 0 ) );
			$latest_problem = max( $latest_problem, (int) ( $evidence['problem_timestamp'] ?? 0 ) );
			if ( $acceptances < 1 ) {
				$all_recovered = false;
			}
			if ( $acceptances < self::RECOVERY_ACCEPTANCES_REQUIRED ) {
				$all_sustained = false;
			}
		}

		if ( ! $all_recovered ) {
			return self::LIFECYCLE_ACTIVE;
		}
		if ( $all_sustained || (int) $now - $latest_problem >= self::RECOVERY_CLEAN_SECONDS ) {
			return self::LIFECYCLE_RESOLVED;
		}

		return self::LIFECYCLE_RECOVERING;
	}

	/** Flatten internal evidence without exposing per-event internals. */
	private function public_group( array $group ) {
		$issue             = is_array( $group['issue'] ?? null ) ? $group['issue'] : array();
		$recovered_sources = 0;
		$total_acceptances = 0;
		foreach ( $group['source_evidence'] as $evidence ) {
			$acceptances = max( 0, (int) ( $evidence['acceptances'] ?? 0 ) );
			$total_acceptances += $acceptances;
			if ( $acceptances > 0 ) {
				++$recovered_sources;
			}
		}

		return array(
			'fingerprint'         => (string) $group['fingerprint'],
			'scope_type'         => (string) $group['scope_type'],
			'scope_key'          => (string) $group['scope_key'],
			'lifecycle'          => (string) $group['lifecycle'],
			'issue'              => $issue,
			'issue_code'         => $this->safe_key( $issue['code'] ?? 'unknown_email_error', 'unknown_email_error' ),
			'issue_title'        => (string) ( $issue['title'] ?? '' ),
			'issue_explanation'  => (string) ( $issue['explanation'] ?? '' ),
			'recommended_action' => (string) ( $issue['recommended_action'] ?? '' ),
			'action_type'        => $this->safe_key( $issue['action_type'] ?? 'email_log', 'email_log' ),
			'technical_error'    => (string) ( $issue['technical_error'] ?? '' ),
			'occurrences'        => max( 0, (int) $group['occurrences'] ),
			'first_at'           => (string) $group['first_at'],
			'latest_at'          => (string) $group['latest_at'],
			'latest_timestamp'   => (int) $group['latest_timestamp'],
			'latest_recovery_at' => (string) $group['latest_recovery_at'],
			'primary_status'     => (string) $group['primary_status'],
			'sources'            => array_values( array_unique( $group['sources'] ) ),
			'source_count'       => count( array_unique( $group['sources'] ) ),
			'recovered_source_count' => $recovered_sources,
			'recovery_acceptance_count' => $total_acceptances,
			'required_acceptances' => self::RECOVERY_ACCEPTANCES_REQUIRED,
		);
	}

	/** Whether an accepted event belongs to an incident's remediation scope. */
	private function scope_matches( array $group, array $event ) {
		if ( 'site' === $group['scope_type'] ) {
			return true;
		}
		if ( 'form' === $group['scope_type'] ) {
			return $group['scope_key'] === 'form:' . $this->safe_key( $event['provider'] ?? '', '' ) . ':' . trim( (string) ( $event['form_id'] ?? '' ) );
		}

		return $group['scope_key'] === 'source:' . $this->source_key( $event );
	}

	/** Normalize a source group without WordPress helpers. */
	private function source_key( array $event ) {
		return $this->safe_key( $event['source_group'] ?? 'unknown', 'unknown' );
	}

	/** Normalize a machine key using the same conservative character set. */
	private function safe_key( $value, $fallback ) {
		$value = strtolower( (string) $value );
		$value = preg_replace( '/[^a-z0-9_\-]/', '', $value );
		$value = is_string( $value ) ? $value : '';

		return '' !== $value ? $value : (string) $fallback;
	}

	/** Stable event timestamp; callers should provide an explicit Unix value. */
	private function event_timestamp( array $event ) {
		if ( is_numeric( $event['timestamp'] ?? null ) ) {
			return (int) $event['timestamp'];
		}
		$parsed = strtotime( (string) ( $event['created_at'] ?? '' ) );

		return false === $parsed ? 0 : (int) $parsed;
	}

	/** Accepted status aliases used by integrations and fixtures. */
	private function is_accepted( $status ) {
		return in_array( $status, array( 'sent', 'accepted', 'success' ), true );
	}

	/** Preserve the most actionable problem status in a mixed incident. */
	private function status_weight( $status ) {
		$weights = array(
			'pending'            => 1,
			'unknown'            => 1,
			'problem'            => 2,
			'no_email_generated' => 3,
			'failed'             => 4,
		);

		return $weights[ (string) $status ] ?? 0;
	}
}
