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

namespace Hafenstudios\Kurato\Insight;

use Hafenstudios\Kurato\Db\Schema;

defined( 'ABSPATH' ) || exit;

/**
 * Der eine Filter der Seekarte (CP6). Ein Wertobjekt, das aus den Request-
 * Parametern eine **kontaktbezogene WHERE-Ergaenzung mit Platzhaltern** baut.
 *
 * Warum ein eigenes Objekt und nicht je Abfrage ein paar ifs: Die Funnel-Ansicht
 * hat drei Zoom-Stufen, und der Inhaber-Auftrag verlangt, dass auf allen dreien
 * **dieselben** Filter wirken. Ein gemeinsames Objekt ist der einzige Weg, das zu
 * garantieren - jede zweite Filter-Implementierung waere die naechste Stelle, an
 * der sich Zahlen zwischen Uebersicht und Liste widersprechen.
 *
 * Verbindliche Linien:
 * - **Serverseitig.** Der Browser filtert nichts; er schickt Parameter und
 *   bekommt fertige Aggregate. Sonst muesste er bei 10.000 Kontakten erst 10.000
 *   Zeilen laden, um 5 zu zeigen.
 * - **Prepared.** {@see where()} liefert ausschliesslich Platzhalter (%d/%s) und
 *   die Werte getrennt; zusammengesetzt wird nur mit $wpdb->prepare(). Tabellen-
 *   namen kommen aus {@see Schema::table()}, der Alias aus einer Weissliste -
 *   nie aus Eingaben.
 * - **Leer heisst leer.** Ohne aktiven Filter ist die Ergaenzung der leere String;
 *   die bestehenden Abfragen bleiben dann byte-gleich zu vorher (Live-System).
 */
final class FunnelFilter {

	/** Erlaubte Sortierungen der Hafenliste (Zoom 3). Alles andere faellt auf rang. */
	public const SORTS = array( 'rang', 'name', 'still' );

	private int $stage_id;

	private int $tag_id;

	private string $source;

	private bool $overdue;

	private bool $no_next_step;

	private bool $with_booking;

	/**
	 * @param int    $stage_id     Stufe (0 = alle).
	 * @param int    $tag_id       Tag (0 = alle).
	 * @param string $source       Quelle (leer = alle).
	 * @param bool   $overdue      Nur Kontakte mit ueberfaelliger offener Wiedervorlage.
	 * @param bool   $no_next_step Nur Kontakte ohne offene Wiedervorlage.
	 * @param bool   $with_booking Nur Kontakte mit kommendem bestaetigtem Termin.
	 */
	private function __construct( int $stage_id, int $tag_id, string $source, bool $overdue, bool $no_next_step, bool $with_booking ) {
		$this->stage_id     = $stage_id;
		$this->tag_id       = $tag_id;
		$this->source       = $source;
		$this->overdue      = $overdue;
		$this->no_next_step = $no_next_step;
		$this->with_booking = $with_booking;
	}

	/**
	 * Der leere Filter (alles sehen).
	 *
	 * @return self
	 */
	public static function none(): self {
		return new self( 0, 0, '', false, false, false );
	}

	/**
	 * Baut den Filter aus Request-Parametern (alles bereinigt, nichts vertraut).
	 *
	 * @param array<string, mixed> $params Roh-Parameter.
	 * @return self
	 */
	public static function from_params( array $params ): self {
		return new self(
			max( 0, (int) ( $params['stage_id'] ?? 0 ) ),
			max( 0, (int) ( $params['tag_id'] ?? 0 ) ),
			substr( sanitize_text_field( (string) ( $params['source'] ?? '' ) ), 0, 100 ),
			self::flag( $params['overdue'] ?? null ),
			self::flag( $params['no_next_step'] ?? null ),
			self::flag( $params['with_booking'] ?? null )
		);
	}

	/**
	 * Liest einen Schalter aus einem Query-Parameter (1/true/yes/on = an).
	 *
	 * @param mixed $value Roh-Wert.
	 * @return bool
	 */
	private static function flag( $value ): bool {
		if ( is_bool( $value ) ) {
			return $value;
		}
		return in_array( strtolower( trim( (string) $value ) ), array( '1', 'true', 'yes', 'on' ), true );
	}

	/**
	 * Normalisiert einen Sortier-Wunsch auf die Weissliste.
	 *
	 * @param mixed $sort Roh-Wert.
	 * @return string
	 */
	public static function normalize_sort( $sort ): string {
		$sort = strtolower( trim( (string) $sort ) );
		return in_array( $sort, self::SORTS, true ) ? $sort : self::SORTS[0];
	}

	/**
	 * Gewuenschte Stufe (0 = alle). Die Seekarte blendet damit Stufen aus, ohne
	 * dass die Liste eine zweite Filterregel braucht.
	 *
	 * @return int
	 */
	public function stage_id(): int {
		return $this->stage_id;
	}

	/**
	 * Wirkt ueberhaupt ein Filter?
	 *
	 * @return bool
	 */
	public function is_active(): bool {
		return $this->count_active() > 0;
	}

	/**
	 * Zahl der aktiven Filter - die Zeile "3 Filter aktiv" im Kopf der Ansicht.
	 * Niemand darf sich fragen, warum die Zahlen anders sind als gestern.
	 *
	 * @return int
	 */
	public function count_active(): int {
		$count = 0;
		foreach ( array( $this->stage_id > 0, $this->tag_id > 0, '' !== $this->source, $this->overdue, $this->no_next_step, $this->with_booking ) as $on ) {
			$count += $on ? 1 : 0;
		}
		return $count;
	}

	/**
	 * Der Filterzustand als Klartext-Struktur - die Antwort traegt ihn zurueck,
	 * damit die Ansicht zeigen kann, womit gerechnet wurde.
	 *
	 * @return array<string, mixed>
	 */
	public function to_array(): array {
		return array(
			'stage_id'     => $this->stage_id,
			'tag_id'       => $this->tag_id,
			'source'       => $this->source,
			'overdue'      => $this->overdue,
			'no_next_step' => $this->no_next_step,
			'with_booking' => $this->with_booking,
		);
	}

	/**
	 * Baut die WHERE-Ergaenzung fuer eine Abfrage auf der Kontakt-Tabelle.
	 *
	 * Rueckgabe: `sql` beginnt bei aktivem Filter mit " AND " und traegt nur
	 * Platzhalter; `params` sind die zugehoerigen Werte in derselben Reihenfolge.
	 * Ohne aktiven Filter ist `sql` leer und `params` das leere Array.
	 *
	 * @param string $alias Tabellen-Alias der Kontakte (Weissliste, Default c).
	 * @param string $now   UTC-"Jetzt" (Y-m-d H:i:s); leer = aktuelle Zeit.
	 * @return array{sql:string, params:array<int, mixed>}
	 */
	public function where( string $alias = 'c', string $now = '' ): array {
		$alias = preg_match( '/^[a-z][a-z0-9_]{0,9}$/', $alias ) ? $alias : 'c';
		$now   = '' !== $now ? $now : gmdate( 'Y-m-d H:i:s' );
		$today = substr( $now, 0, 10 );

		$sql    = '';
		$params = array();

		if ( $this->stage_id > 0 ) {
			$sql     .= " AND {$alias}.stage_id = %d";
			$params[] = $this->stage_id;
		}

		if ( $this->tag_id > 0 ) {
			$tag_rel  = Schema::table( 'tag_rel' );
			$sql     .= " AND EXISTS ( SELECT 1 FROM {$tag_rel} f_tr WHERE f_tr.contact_id = {$alias}.id AND f_tr.tag_id = %d )";
			$params[] = $this->tag_id;
		}

		if ( '' !== $this->source ) {
			$sql     .= " AND {$alias}.source = %s";
			$params[] = $this->source;
		}

		if ( $this->overdue ) {
			$tasks    = Schema::table( 'tasks' );
			$sql     .= " AND EXISTS ( SELECT 1 FROM {$tasks} f_to WHERE f_to.contact_id = {$alias}.id AND f_to.done = 0 AND f_to.due_date IS NOT NULL AND f_to.due_date < %s )";
			$params[] = $today;
		}

		if ( $this->no_next_step ) {
			$tasks = Schema::table( 'tasks' );
			$sql  .= " AND NOT EXISTS ( SELECT 1 FROM {$tasks} f_tn WHERE f_tn.contact_id = {$alias}.id AND f_tn.done = 0 )";
		}

		if ( $this->with_booking ) {
			$bookings = Schema::table( 'bookings' );
			$sql     .= " AND EXISTS ( SELECT 1 FROM {$bookings} f_bk WHERE f_bk.contact_id = {$alias}.id AND f_bk.status = 'confirmed' AND f_bk.start_utc >= %s )";
			$params[] = $now;
		}

		return array(
			'sql'    => $sql,
			'params' => $params,
		);
	}
}
