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

namespace Hafenstudios\Kurato\Insight;

use Hafenstudios\Kurato\Db\RecommendationRepository;
use Hafenstudios\Kurato\Db\Schema;

defined( 'ABSPATH' ) || exit;

/**
 * Outcome-Messung der Empfehlungen (HANS-KI-KONZEPT Schicht 4). Ein taeglicher
 * Cron misst fuer angenommene (accepted) Empfehlungen, die reif sind (Entscheidung
 * laenger als das Messfenster her, Default T+14 Tage, Filter
 * kurato_recommendation_outcome_days), ob sich im Fenster nach der Annahme etwas
 * bewegt hat, und schreibt das Ergebnis per set_outcome:
 *
 * - converted: es gab im Fenster einen Abschluss (Ankunft in einer gewonnenen
 *   Stufe oder eine neue Buchung),
 * - advanced: es gab einen Vorwaerts-Stufenwechsel (Ziel weiter vorne als Quelle),
 * - none: keins von beiden.
 *
 * WICHTIGE Ehrlichkeits-Grenze (K4e, dokumentiert in DECISIONS-ADDENDUM-H2): weil
 * hs_recommendations bewusst KEINE Kontaktdaten haelt, ist die Messung in H2
 * system-/regelweit (Signal im Zeitfenster), nicht auf die exakte betroffene
 * Kontaktmenge zurueckgerechnet. H4 verfeinert die Zurechnung und gewichtet die
 * Regeln aus diesen Ergebnissen; H2 schreibt nur ehrlich, was messbar ist.
 *
 * Die Roh-Zaehlungen kommen aus geschuetzten fetch_*-Methoden (SQL-Seams); die
 * Klassifizierung ist rein. Nicht final; Tests ersetzen Repo und Datenquellen.
 */
class OutcomeTracker {

	public const HOOK = 'kurato_recommendation_outcomes';

	/**
	 * Default-Messfenster in Tagen (T+X). Per Filter anpassbar.
	 */
	public const OUTCOME_DAYS = 14;

	private RecommendationRepository $recommendations;

	/**
	 * @param RecommendationRepository $recommendations Empfehlungs-Log.
	 */
	public function __construct( RecommendationRepository $recommendations ) {
		$this->recommendations = $recommendations;
	}

	/**
	 * Verdrahtet den Cron-Hook.
	 *
	 * @return void
	 */
	public function register(): void {
		add_action( self::HOOK, array( $this, 'run' ) );
	}

	/**
	 * Verarbeitet einen Mess-Durchlauf.
	 *
	 * @param int $now_ts "Jetzt" als Unix-Zeit (0 = time()).
	 * @return int Anzahl gemessener Empfehlungen.
	 */
	public function run( int $now_ts = 0 ): int {
		$now_ts = $now_ts > 0 ? $now_ts : time();
		$window = self::window_days();

		// Reif: entschieden vor now - window (das ganze Fenster ist verstrichen).
		$before = gmdate( 'Y-m-d H:i:s', $now_ts - $window * DAY_IN_SECONDS );

		$measured = 0;
		foreach ( $this->recommendations->accepted_awaiting_outcome( $before ) as $rec ) {
			$from = (string) $rec['decided_at'];
			$from_ts = strtotime( $from . ' UTC' );
			if ( false === $from_ts ) {
				continue;
			}
			$to = gmdate( 'Y-m-d H:i:s', $from_ts + $window * DAY_IN_SECONDS );

			$conversions = $this->fetch_conversions_between( $from, $to );
			$advances    = $this->fetch_advances_between( $from, $to );

			$this->recommendations->set_outcome( (int) $rec['id'], self::classify( $conversions, $advances ) );
			$measured++;
		}
		return $measured;
	}

	/**
	 * Klassifiziert ein Ergebnis aus den Signal-Zaehlungen (rein).
	 *
	 * @param int $conversions Abschluesse im Fenster.
	 * @param int $advances    Vorwaerts-Stufenwechsel im Fenster.
	 * @return string advanced | converted | none.
	 */
	public static function classify( int $conversions, int $advances ): string {
		if ( $conversions > 0 ) {
			return 'converted';
		}
		if ( $advances > 0 ) {
			return 'advanced';
		}
		return 'none';
	}

	/**
	 * Messfenster in Tagen (Filter kurato_recommendation_outcome_days).
	 *
	 * @return int
	 */
	public static function window_days(): int {
		$days = (int) apply_filters( 'kurato_recommendation_outcome_days', self::OUTCOME_DAYS );
		return $days > 0 ? $days : self::OUTCOME_DAYS;
	}

	// --- Datenquellen (SQL-Seams, in Tests ueberschrieben) -----------------

	/**
	 * Abschluss-Signale im Fenster: Ankuenfte in einer gewonnenen Stufe plus im
	 * Fenster angelegte bestaetigte Buchungen.
	 *
	 * @param string $from UTC-Fensteranfang (exklusiv).
	 * @param string $to   UTC-Fensterende (inklusiv).
	 * @return int
	 */
	protected function fetch_conversions_between( string $from, string $to ): int {
		global $wpdb;
		$history  = Schema::table( 'stage_history' );
		$stages   = Schema::table( 'stages' );
		$bookings = Schema::table( 'bookings' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$won = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$history} h INNER JOIN {$stages} s ON s.id = h.to_stage_id
			WHERE s.is_won = 1 AND h.changed_at > %s AND h.changed_at <= %s",
			$from,
			$to
		) );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$booked = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$bookings} WHERE status = 'confirmed' AND created_at > %s AND created_at <= %s",
			$from,
			$to
		) );

		return $won + $booked;
	}

	/**
	 * Vorwaerts-Stufenwechsel im Fenster: Ziel-Stufe hat eine hoehere sort_order als
	 * die Quell-Stufe (und ist nicht als verloren markiert).
	 *
	 * @param string $from UTC-Fensteranfang (exklusiv).
	 * @param string $to   UTC-Fensterende (inklusiv).
	 * @return int
	 */
	protected function fetch_advances_between( string $from, string $to ): int {
		global $wpdb;
		$history = Schema::table( 'stage_history' );
		$stages  = Schema::table( 'stages' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$history} h
			INNER JOIN {$stages} sf ON sf.id = h.from_stage_id
			INNER JOIN {$stages} st ON st.id = h.to_stage_id
			WHERE st.sort_order > sf.sort_order AND st.is_lost = 0 AND h.changed_at > %s AND h.changed_at <= %s",
			$from,
			$to
		) );
	}
}
