<?php
/**
 * @package Hafenstudios\Kurato
 */

namespace Hafenstudios\Kurato\Db;

defined( 'ABSPATH' ) || exit;

/**
 * Ereignis-Log des Versands (hs_mail_events, K4-Konzept Abschnitte 2, 9).
 *
 * Sammelt Server-Ereignisse je Kontakt/Kampagne: `sent` (erfolgreicher Versand),
 * `failed` (permanenter Sendefehler), Bounce-/Complaint-/Abmelde-Ereignisse aus
 * dem ESP-Webhook (K4e-1) und spaeter open/click bei Tracking-Consent (K4e-2).
 * Es werden bewusst KEINE IP-Adressen gespeichert (BAUPLAN 28); je Ereignis nur
 * Typ, ein optionales Detail und der Zeitstempel.
 *
 * `campaign_id` ist NULL-faehig: Ereignisse ohne Kampagnenbezug (z.B. eine
 * Abmeldung ueber den Abmeldelink oder ein Webhook-Bounce) tragen keinen
 * Kampagnen-Bezug; der Aufrufer uebergibt dann 0, was als NULL gespeichert wird.
 *
 * Nicht final, damit Tests die DB-Methoden per Test-Double ueberschreiben koennen.
 */
class MailEventRepository {

	/**
	 * Bekannte Ereignistypen (Konzept-Tabelle hs_mail_events).
	 *
	 * @var array<int, string>
	 */
	public const TYPES = array( 'sent', 'failed', 'bounce_hard', 'bounce_soft', 'complaint', 'open', 'click', 'unsubscribe', 'welcome' );

	/**
	 * Schreibt ein Ereignis.
	 *
	 * @param int                  $campaign_id Kampagne (0 = ohne Bezug -> NULL).
	 * @param int                  $contact_id  Kontakt.
	 * @param string               $type        Ereignistyp (siehe TYPES).
	 * @param array<string, mixed> $meta        Optional: `detail` (string), `queue_id` (int).
	 * @return int Ereignis-ID (0 bei Fehler).
	 */
	public function record( int $campaign_id, int $contact_id, string $type, array $meta = array() ): int {
		if ( $contact_id <= 0 ) {
			return 0;
		}
		global $wpdb;

		$queue_id = isset( $meta['queue_id'] ) ? absint( $meta['queue_id'] ) : 0;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$ok = $wpdb->insert(
			Schema::table( 'mail_events' ),
			array(
				'campaign_id' => $campaign_id > 0 ? $campaign_id : null,
				'contact_id'  => $contact_id,
				'queue_id'    => $queue_id > 0 ? $queue_id : null,
				'type'        => substr( $type, 0, 30 ),
				'detail'      => substr( (string) ( $meta['detail'] ?? '' ), 0, 255 ),
				'created_at'  => current_time( 'mysql', true ),
			)
		);
		return $ok ? (int) $wpdb->insert_id : 0;
	}

	/**
	 * Prueft, ob fuer eine Kombination bereits ein Ereignis vorliegt (Dedup).
	 *
	 * Wird vom Tracking (je Kampagne+Kontakt nur das erste Open; je
	 * Kampagne+Kontakt+Link nur der erste Klick) und von der Willkommensmail
	 * (Idempotenz je Kontakt+Liste) genutzt. `campaign_id = 0` prueft auf
	 * kampagnenlose Ereignisse (Spalte NULL, z.B. Willkommensmail).
	 *
	 * @param int    $campaign_id Kampagne (0 = ohne Bezug -> NULL-Zeilen).
	 * @param int    $contact_id  Kontakt.
	 * @param string $type        Ereignistyp.
	 * @param string $detail      Optionales Detail (z.B. `link:3`, `list:5`).
	 * @return bool
	 */
	public function exists_for( int $campaign_id, int $contact_id, string $type, string $detail = '' ): bool {
		if ( $contact_id <= 0 ) {
			return false;
		}
		global $wpdb;
		$table = Schema::table( 'mail_events' );

		$where  = $campaign_id > 0 ? 'campaign_id = %d' : 'campaign_id IS NULL';
		$params = array();
		if ( $campaign_id > 0 ) {
			$params[] = $campaign_id;
		}
		$where   .= ' AND contact_id = %d AND type = %s';
		$params[] = $contact_id;
		$params[] = substr( $type, 0, 30 );
		if ( '' !== $detail ) {
			$where   .= ' AND detail = %s';
			$params[] = substr( $detail, 0, 255 );
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE {$where} LIMIT 1", ...$params ) );
		return null !== $id;
	}

	/**
	 * Zaehlt die Ereignisse einer Kampagne je Typ (Grundlage der K4e-2-Statistik).
	 *
	 * @param int $campaign_id Kampagne.
	 * @return array<string, int> type => Anzahl.
	 */
	public function counts_for_campaign( int $campaign_id ): array {
		global $wpdb;
		$table = Schema::table( 'mail_events' );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$rows = $wpdb->get_results( $wpdb->prepare( "SELECT type, COUNT(*) AS total FROM {$table} WHERE campaign_id = %d GROUP BY type", $campaign_id ), ARRAY_A );
		$out  = array();
		foreach ( (array) $rows as $row ) {
			$out[ (string) $row['type'] ] = (int) $row['total'];
		}
		return $out;
	}

	/**
	 * Ereignisse eines Kontakts (aufsteigend), fuer den Privacy-Export.
	 *
	 * @param int $contact_id Kontakt.
	 * @return array<int, array<string, mixed>>
	 */
	public function find_by_contact( int $contact_id ): array {
		global $wpdb;
		$table = Schema::table( 'mail_events' );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$rows = $wpdb->get_results( $wpdb->prepare( "SELECT campaign_id, type, detail, created_at FROM {$table} WHERE contact_id = %d ORDER BY created_at ASC, id ASC", $contact_id ), ARRAY_A );
		return array_map( array( $this, 'hydrate' ), $rows ?: array() );
	}

	/**
	 * Juengstes Ereignis eines Kontakts fuer eine Typenauswahl (fuer die
	 * Zustellstatus-Zeile im Kontaktprofil, z.B. "unzustellbar seit ...").
	 *
	 * @param int             $contact_id Kontakt.
	 * @param array<int, string> $types   Erlaubte Typen.
	 * @return array<string, mixed>|null
	 */
	public function last_event_for_contact( int $contact_id, array $types ): ?array {
		$types = array_values( array_filter( array_map( 'strval', $types ) ) );
		if ( array() === $types ) {
			return null;
		}
		global $wpdb;
		$table        = Schema::table( 'mail_events' );
		$placeholders = implode( ', ', array_fill( 0, count( $types ), '%s' ) );
		$params       = array_merge( array( $contact_id ), $types );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE contact_id = %d AND type IN ({$placeholders}) ORDER BY created_at DESC, id DESC LIMIT 1", ...$params ), ARRAY_A );
		return $row ? $this->hydrate( $row ) : null;
	}

	/**
	 * Loescht alle Ereignisse eines Kontakts (Recht auf Vergessen).
	 *
	 * @param int $contact_id Kontakt.
	 * @return void
	 */
	public function delete_by_contact( int $contact_id ): void {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->delete( Schema::table( 'mail_events' ), array( 'contact_id' => $contact_id ) );
	}

	/**
	 * Loescht Ereignisse aelter als die Aufbewahrungsfrist (Retention-Cron).
	 *
	 * @param int $days Frist in Tagen (<= 0 = keine Loeschung).
	 * @return int Anzahl geloeschter Zeilen.
	 */
	public function delete_older_than_days( int $days ): int {
		if ( $days <= 0 ) {
			return 0;
		}
		global $wpdb;
		$table  = Schema::table( 'mail_events' );
		$cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) );
	}

	/**
	 * Normalisiert eine Zeile.
	 *
	 * @param array<string, mixed> $row Rohzeile.
	 * @return array<string, mixed>
	 */
	private function hydrate( array $row ): array {
		return array(
			'campaign_id' => isset( $row['campaign_id'] ) && null !== $row['campaign_id'] ? (int) $row['campaign_id'] : 0,
			'type'        => (string) ( $row['type'] ?? '' ),
			'detail'      => (string) ( $row['detail'] ?? '' ),
			'created_at'  => (string) ( $row['created_at'] ?? '' ),
		);
	}
}
