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

namespace Hafenstudios\Kurato\Campaign;

defined( 'ABSPATH' ) || exit;

/**
 * Kompiliert eine Segment-Definition zu EINEM SQL und fuehrt es aus (K4-Konzept
 * Abschnitt 4).
 *
 * Statt einer PHP-Schleife ueber alle Kontakte entsteht ein einziges Statement mit
 * EXISTS-Unterabfragen auf hs_tag_rel und hs_subscriptions (status=confirmed) sowie
 * einem Vergleich auf die Spalte hs_contacts.stage_id. Alle Werte laufen als
 * gebundene Parameter ueber $wpdb->prepare.
 *
 * Die SQL-Erzeugung (compile) ist von der Ausfuehrung getrennt: compile() liefert
 * [sql, params] und ist ohne echtes wpdb pruefbar. Die Tabellennamen werden im
 * Konstruktor uebergeben (im Betrieb aus Schema::table aufgeloest), damit compile()
 * ohne WordPress laeuft.
 *
 * Nicht final, damit Tests (z.B. der Materializer) contact_ids per Test-Double
 * ersetzen koennen.
 */
class SegmentQuery {

	private string $contacts_table;

	private string $tag_rel_table;

	private string $subscriptions_table;

	/**
	 * @param string $contacts_table      Voller Name der hs_contacts-Tabelle.
	 * @param string $tag_rel_table       Voller Name der hs_tag_rel-Tabelle.
	 * @param string $subscriptions_table Voller Name der hs_subscriptions-Tabelle.
	 */
	public function __construct( string $contacts_table, string $tag_rel_table, string $subscriptions_table ) {
		$this->contacts_table      = $contacts_table;
		$this->tag_rel_table       = $tag_rel_table;
		$this->subscriptions_table = $subscriptions_table;
	}

	/**
	 * Kompiliert eine Definition zu [sql, params].
	 *
	 * Der Ausdruck fuer die SELECT-Liste ist parametrierbar (c.id fuer die IDs,
	 * COUNT(*) fuer den Zaehler), damit derselbe WHERE-Aufbau beide Faelle bedient.
	 *
	 * @param mixed  $definition Rohe oder sanitisierte Definition.
	 * @param string $select     SELECT-Ausdruck (Default c.id).
	 * @return array{0:string,1:array<int,mixed>}
	 */
	public function compile( $definition, string $select = 'c.id' ): array {
		$definition = SegmentDefinition::sanitize( $definition );

		$where  = array();
		$params = array();

		foreach ( $definition['conditions'] as $cond ) {
			$fragment = $this->condition_sql( (string) $cond['field'], (string) $cond['op'], $cond['value'], $params );
			if ( '' !== $fragment ) {
				$where[] = $fragment;
			}
		}

		$where_sql = array() === $where ? '1=1' : implode( ' AND ', $where );
		$sql       = 'SELECT ' . $select . ' FROM ' . $this->contacts_table . ' c WHERE ' . $where_sql;

		return array( $sql, $params );
	}

	/**
	 * Liefert die Kontakt-IDs, die ein Segment trifft.
	 *
	 * @param mixed $definition Definition.
	 * @return array<int, int>
	 */
	public function contact_ids( $definition ): array {
		list( $sql, $params ) = $this->compile( $definition, 'c.id' );

		global $wpdb;
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
		$ids = array() === $params
			? $wpdb->get_col( $sql )
			: $wpdb->get_col( $wpdb->prepare( $sql, ...$params ) );
		// phpcs:enable
		return array_map( 'intval', $ids ?: array() );
	}

	/**
	 * Liefert die Trefferzahl eines Segments (Vorschau-Zaehler).
	 *
	 * @param mixed $definition Definition.
	 * @return int
	 */
	public function count( $definition ): int {
		list( $sql, $params ) = $this->compile( $definition, 'COUNT(*)' );

		global $wpdb;
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
		$count = array() === $params
			? $wpdb->get_var( $sql )
			: $wpdb->get_var( $wpdb->prepare( $sql, ...$params ) );
		// phpcs:enable
		return (int) $count;
	}

	/**
	 * Baut das SQL-Fragment einer Bedingung und haengt seine Parameter an.
	 *
	 * @param string             $field  Feld (tag/stage/list).
	 * @param string             $op     Operator.
	 * @param mixed              $value  Wert (int oder Liste von ints).
	 * @param array<int, mixed>  $params Parameterliste (per Referenz erweitert).
	 * @return string
	 */
	private function condition_sql( string $field, string $op, $value, array &$params ): string {
		switch ( $field ) {
			case 'tag':
				$exists   = 'EXISTS (SELECT 1 FROM ' . $this->tag_rel_table . ' tr WHERE tr.contact_id = c.id AND tr.tag_id = %d)';
				$params[] = (int) $value;
				return 'has_not' === $op ? 'NOT ' . $exists : $exists;

			case 'stage':
				$ids = (array) $value;
				if ( array() === $ids ) {
					return '';
				}
				$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
				foreach ( $ids as $id ) {
					$params[] = (int) $id;
				}
				return 'not_in' === $op
					? '(c.stage_id IS NULL OR c.stage_id NOT IN (' . $placeholders . '))'
					: '(c.stage_id IN (' . $placeholders . '))';

			case 'list':
				$sql      = 'EXISTS (SELECT 1 FROM ' . $this->subscriptions_table . ' s WHERE s.contact_id = c.id AND s.list_id = %d AND s.status = %s)';
				$params[] = (int) $value;
				$params[] = 'confirmed';
				return $sql;
		}

		return '';
	}
}
