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

namespace Hafenstudios\Kurato\Campaign;

use Hafenstudios\Kurato\Db\CampaignRepository;
use Hafenstudios\Kurato\Db\ContactRepository;
use Hafenstudios\Kurato\Db\QueueRepository;
use Hafenstudios\Kurato\Db\SegmentRepository;
use Hafenstudios\Kurato\Db\SubscriptionRepository;

defined( 'ABSPATH' ) || exit;

/**
 * Materialisiert die Empfaengerauswahl einer Kampagne EINMAL in die Versand-
 * Warteschlange (K4-Konzept Abschnitt 4).
 *
 * Empfaenger = (Vereinigung der bestaetigten Abos der Ziel-Listen) vereinigt mit
 * (Segment-Treffer geschnitten mit den irgendwo bestaetigten Abonnenten), minus die
 * Ausschluss-Listen, dedupliziert. Marketing-Mails gehen nur an bestaetigte, nicht
 * supprimierte Adressen; leere Mails werden uebersprungen. Diese INVARIANTE gilt auch
 * ueber Segmente: ein Segment-Treffer ohne bestaetigtes Abo erhaelt keine Mail. Der so
 * eingefrorene Snapshot ist gegen spaetere Kontakt-Aenderungen stabil (nur die
 * Suppression wird beim tatsaechlichen Send noch einmal geprueft, im
 * CampaignMailBuilder).
 *
 * Die Auswahl-Logik ist frei von direkten WP-DB-Aufrufen (alles ueber die
 * Repositories und den SegmentQuery), damit sie mit Test-Doubles ohne echtes WP
 * testbar bleibt.
 */
final class CampaignMaterializer {

	private CampaignRepository $campaigns;

	private SubscriptionRepository $subscriptions;

	private ContactRepository $contacts;

	private QueueRepository $queue;

	private ?SegmentRepository $segments;

	private ?SegmentQuery $segment_query;

	/**
	 * @param CampaignRepository     $campaigns     Kampagnen-Repository.
	 * @param SubscriptionRepository $subscriptions Abo-Repository.
	 * @param ContactRepository      $contacts      Kontakt-Repository.
	 * @param QueueRepository        $queue         Queue-Repository.
	 * @param SegmentRepository|null $segments      Segment-Repository (K4d; null = keine Segmente).
	 * @param SegmentQuery|null      $segment_query Segment-Auswertung (K4d; null = keine Segmente).
	 */
	public function __construct(
		CampaignRepository $campaigns,
		SubscriptionRepository $subscriptions,
		ContactRepository $contacts,
		QueueRepository $queue,
		?SegmentRepository $segments = null,
		?SegmentQuery $segment_query = null
	) {
		$this->campaigns     = $campaigns;
		$this->subscriptions = $subscriptions;
		$this->contacts      = $contacts;
		$this->queue         = $queue;
		$this->segments      = $segments;
		$this->segment_query = $segment_query;
	}

	/**
	 * Baut den Empfaenger-Snapshot einer Kampagne und setzt sie auf "sending".
	 *
	 * @param int $campaign_id Kampagnen-ID.
	 * @return int Anzahl materialisierter Empfaenger (0, wenn Kampagne fehlt oder leer).
	 */
	public function materialize( int $campaign_id ): int {
		$campaign = $this->campaigns->get( $campaign_id );
		if ( null === $campaign ) {
			return 0;
		}

		$audience    = $this->parse_audience( (string) ( $campaign['audience'] ?? '' ) );
		$include_ids = $this->collect_confirmed( $audience['lists'] );
		$exclude_ids = $this->collect_confirmed( $audience['exclude_lists'] );

		// Segment-Treffer, geschnitten mit den irgendwo bestaetigten Abonnenten,
		// in die Einschluss-Menge mischen (INVARIANTE: nur confirmed).
		foreach ( $this->collect_segment_matches( $audience['segments'] ) as $contact_id ) {
			$include_ids[ (int) $contact_id ] = true;
		}

		// Vereinigung minus Ausschluss, dedupliziert (contact_id als Schluessel).
		$candidate_ids = array_diff( array_keys( $include_ids ), array_keys( $exclude_ids ) );

		$recipients = array();
		foreach ( $candidate_ids as $contact_id ) {
			$contact = $this->contacts->get( (int) $contact_id );
			if ( null === $contact ) {
				continue;
			}
			// Suppression (Bounce/Beschwerde): nur aktive Adressen erhalten Post.
			if ( 'active' !== (string) ( $contact['email_status'] ?? 'active' ) ) {
				continue;
			}
			$email = sanitize_email( (string) ( $contact['email'] ?? '' ) );
			if ( '' === $email ) {
				continue;
			}
			$recipients[] = array(
				'contact_id' => (int) $contact_id,
				'to_email'   => $email,
			);
		}

		// TODO K4b/1.1: Chunking fuer sehr grosse Listen (z.B. 500er-Schritte als
		// Action-Scheduler-Jobs). In v1 ein einzelner enqueue-Aufruf.
		$now = current_time( 'mysql', true );
		$this->queue->enqueue( $campaign_id, $recipients, $now );

		$total = count( $recipients );
		$this->campaigns->update(
			$campaign_id,
			array(
				'status'           => 'sending',
				'started_at'       => $now,
				'total_recipients' => $total,
			)
		);

		return $total;
	}

	/**
	 * Sammelt die bestaetigten Kontakt-IDs mehrerer Listen als Menge
	 * (contact_id => true), dedupliziert ueber Listen hinweg.
	 *
	 * @param array<int, int> $list_ids Listen-IDs.
	 * @return array<int, bool>
	 */
	private function collect_confirmed( array $list_ids ): array {
		$set = array();
		foreach ( $list_ids as $list_id ) {
			foreach ( $this->subscriptions->confirmed_contact_ids( (int) $list_id ) as $contact_id ) {
				$set[ (int) $contact_id ] = true;
			}
		}
		return $set;
	}

	/**
	 * Loest die Ziel-Segmente zu Kontakt-IDs auf und schneidet sie mit den
	 * irgendwo bestaetigten Abonnenten (INVARIANTE, K4-Konzept Abschnitt 4).
	 *
	 * Ohne verdrahtete Segment-Dienste (Konstruktor-Defaults null) oder ohne
	 * gewaehlte Segmente ist das Ergebnis leer.
	 *
	 * @param array<int, int> $segment_ids Segment-IDs.
	 * @return array<int, int> Kontakt-IDs.
	 */
	private function collect_segment_matches( array $segment_ids ): array {
		if ( array() === $segment_ids || null === $this->segments || null === $this->segment_query ) {
			return array();
		}

		$matches = array();
		foreach ( $segment_ids as $segment_id ) {
			$segment = $this->segments->get( (int) $segment_id );
			if ( null === $segment ) {
				continue;
			}
			foreach ( $this->segment_query->contact_ids( $segment['definition'] ) as $contact_id ) {
				$matches[ (int) $contact_id ] = (int) $contact_id;
			}
		}
		if ( array() === $matches ) {
			return array();
		}

		// Schnitt mit den irgendwo bestaetigten Abonnenten.
		$confirmed_any = array();
		foreach ( $this->subscriptions->confirmed_contact_ids_any() as $contact_id ) {
			$confirmed_any[ (int) $contact_id ] = true;
		}
		return array_values( array_intersect_key( $matches, $confirmed_any ) );
	}

	/**
	 * Liest die Empfaengerauswahl aus dem JSON der Kampagne (Listen, Ausschluesse,
	 * Segmente). Fehlende Schluessel = leeres Array (rueckwaertskompatibel).
	 *
	 * @param string $json Roh-JSON aus `audience`.
	 * @return array{lists:array<int,int>,exclude_lists:array<int,int>,segments:array<int,int>}
	 */
	private function parse_audience( string $json ): array {
		$data = '' !== $json ? json_decode( $json, true ) : array();
		$data = is_array( $data ) ? $data : array();

		return array(
			'lists'         => $this->int_list( $data['lists'] ?? array() ),
			'exclude_lists' => $this->int_list( $data['exclude_lists'] ?? array() ),
			'segments'      => $this->int_list( $data['segments'] ?? array() ),
		);
	}

	/**
	 * Normalisiert einen Wert zu einer Liste positiver Integer-IDs.
	 *
	 * @param mixed $value Rohwert.
	 * @return array<int, int>
	 */
	private function int_list( $value ): array {
		if ( ! is_array( $value ) ) {
			return array();
		}
		$out = array();
		foreach ( $value as $item ) {
			$id = absint( $item );
			if ( $id > 0 ) {
				$out[] = $id;
			}
		}
		return $out;
	}
}
