<?php
/**
 * Alert dispatch: one plain-text email per run, threshold-filtered, plus the
 * all-clear when previously-alerted findings resolve. Guideline 11 hygiene:
 * alerts go to email (a channel the admin chose), never to admin notices on
 * unrelated screens, and resolved findings clear themselves.
 *
 * The paid build layers Slack/webhooks onto the same dispatch point via the
 * `sfrec_alert_dispatched` action — this file stays email-only.
 *
 * @package SEO_Flight_Recorder
 */

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

/**
 * Class SFRec_Alerts
 */
class SFRec_Alerts {

	/**
	 * Send alerts for a finished run.
	 *
	 * @param int[]  $new_ids      Newly-inserted change IDs.
	 * @param int[]  $resolved_ids Change IDs resolved during the run.
	 * @param string $context      Run context.
	 */
	public static function dispatch( $new_ids, $resolved_ids, $context ) {
		global $wpdb;

		$settings  = get_option( 'sfrec_settings', array() );
		$threshold = isset( $settings['email_threshold'] ) ? $settings['email_threshold'] : 'warning';
		$rank      = array(
			'info'     => 0,
			'warning'  => 1,
			'critical' => 2,
		);
		$min_rank  = isset( $rank[ $threshold ] ) ? $rank[ $threshold ] : 1;

		$alertable = array();
		if ( ! empty( $new_ids ) ) {
			$placeholders = implode( ', ', array_fill( 0, count( $new_ids ), '%d' ) );
			$rows         = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}sfrec_changes WHERE id IN ( $placeholders )", $new_ids ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- own table; the %d placeholders are built by array_fill and passed to prepare; $new_ids are ints.
			foreach ( (array) $rows as $row ) {
				if ( isset( $rank[ $row->severity ] ) && $rank[ $row->severity ] >= $min_rank ) {
					$alertable[] = $row;
				}
			}
		}

		/**
		 * Fires once per finished run with the run's alert payload. The paid
		 * build's Slack/webhook senders hook here; nothing else ships on it.
		 *
		 * @param array  $alertable    Change rows at or above the email threshold.
		 * @param int[]  $resolved_ids Resolved change IDs.
		 * @param string $context      Run context.
		 */
		do_action( 'sfrec_alert_dispatched', $alertable, $resolved_ids, $context );

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

		$to = isset( $settings['alert_email'] ) && is_email( $settings['alert_email'] ) ? $settings['alert_email'] : get_option( 'admin_email' );

		if ( ! empty( $alertable ) ) {
			$critical_count = 0;
			foreach ( $alertable as $row ) {
				if ( 'critical' === $row->severity ) {
					$critical_count++;
				}
			}

			$host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
			if ( $critical_count > 0 ) {
				/* translators: 1: number of critical changes, 2: site host. */
				$subject = sprintf( __( '[SEO Flight Recorder] %1$d CRITICAL SEO change(s) on %2$s', 'buildsbyluke-seo-flight-recorder' ), $critical_count, $host );
			} else {
				/* translators: 1: number of changes, 2: site host. */
				$subject = sprintf( __( '[SEO Flight Recorder] %1$d SEO change(s) detected on %2$s', 'buildsbyluke-seo-flight-recorder' ), count( $alertable ), $host );
			}

			wp_mail( $to, $subject, self::body( $alertable, $context ) );
		}

		if ( ! empty( $resolved_ids ) && ! empty( $settings['email_all_clear'] ) ) {
			$placeholders  = implode( ', ', array_fill( 0, count( $resolved_ids ), '%d' ) );
			$resolved_rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}sfrec_changes WHERE id IN ( $placeholders ) AND severity = 'critical'", $resolved_ids ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- own table; the %d placeholders are built by array_fill and passed to prepare; $resolved_ids are ints.
			if ( ! empty( $resolved_rows ) ) {
				$host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
				/* translators: %s: site host. */
				$subject = sprintf( __( '[SEO Flight Recorder] All clear: critical SEO finding(s) resolved on %s', 'buildsbyluke-seo-flight-recorder' ), $host );
				$lines   = array( __( 'The following previously-reported critical findings are no longer detected:', 'buildsbyluke-seo-flight-recorder' ), '' );
				foreach ( $resolved_rows as $row ) {
					$lines[] = '- ' . self::describe( $row );
				}
				$lines[] = '';
				$lines[] = admin_url( 'admin.php?page=seo-flight-recorder' );
				wp_mail( $to, $subject, implode( "\n", $lines ) );
			}
		}
	}

	/**
	 * Plain-text email body.
	 *
	 * @param array  $rows    Change rows.
	 * @param string $context Run context.
	 * @return string
	 */
	private static function body( $rows, $context ) {
		$context_labels = array(
			'manual'      => __( 'a manual check', 'buildsbyluke-seo-flight-recorder' ),
			'daily'       => __( 'the daily scheduled check', 'buildsbyluke-seo-flight-recorder' ),
			'hourly'      => __( 'the hourly scheduled check', 'buildsbyluke-seo-flight-recorder' ),
			'post_update' => __( 'the automatic check after a site change (update/activation/setting)', 'buildsbyluke-seo-flight-recorder' ),
		);
		$context_label  = isset( $context_labels[ $context ] ) ? $context_labels[ $context ] : $context;

		$lines   = array();
		/* translators: %s: what triggered the check. */
		$lines[] = sprintf( __( 'SEO Flight Recorder detected changes during %s.', 'buildsbyluke-seo-flight-recorder' ), $context_label );
		$lines[] = '';

		foreach ( $rows as $row ) {
			$lines[] = strtoupper( $row->severity ) . ': ' . self::describe( $row );
			$suspects = json_decode( (string) $row->suspects, true );
			if ( is_array( $suspects ) && ! empty( $suspects ) ) {
				$lines[] = '  ' . __( 'Site events immediately before this change:', 'buildsbyluke-seo-flight-recorder' );
				foreach ( array_slice( $suspects, 0, 6 ) as $suspect ) {
					$lines[] = '   - ' . $suspect;
				}
			}
			$lines[] = '';
		}

		$lines[] = __( 'Full flight log:', 'buildsbyluke-seo-flight-recorder' ) . ' ' . admin_url( 'admin.php?page=seo-flight-recorder' );
		return implode( "\n", $lines );
	}

	/**
	 * One-line human description of a change row.
	 *
	 * @param object $row Change row.
	 * @return string
	 */
	public static function describe( $row ) {
		global $wpdb;

		$where = __( 'site-wide', 'buildsbyluke-seo-flight-recorder' );
		if ( (int) $row->page_id > 0 ) {
			$url = $wpdb->get_var( $wpdb->prepare( "SELECT url FROM {$wpdb->prefix}sfrec_pages WHERE id = %d", (int) $row->page_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own table.
			if ( $url ) {
				$where = $url;
			}
		}

		$before = (string) $row->before_val;
		$after  = (string) $row->after_val;
		if ( strlen( $before ) > 120 ) {
			$before = substr( $before, 0, 120 ) . '…';
		}
		if ( strlen( $after ) > 120 ) {
			$after = substr( $after, 0, 120 ) . '…';
		}

		if ( '' === $before ) {
			/* translators: 1: field name, 2: page URL, 3: new value. */
			return sprintf( __( '%1$s on %2$s is now: %3$s', 'buildsbyluke-seo-flight-recorder' ), $row->field, $where, $after );
		}
		if ( '' === $after ) {
			/* translators: 1: field name, 2: page URL, 3: old value. */
			return sprintf( __( '%1$s on %2$s was REMOVED (was: %3$s)', 'buildsbyluke-seo-flight-recorder' ), $row->field, $where, $before );
		}
		/* translators: 1: field name, 2: page URL, 3: old value, 4: new value. */
		return sprintf( __( '%1$s on %2$s changed: "%3$s" → "%4$s"', 'buildsbyluke-seo-flight-recorder' ), $row->field, $where, $before, $after );
	}
}
