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

namespace Hafenstudios\Kurato\Db;

defined( 'ABSPATH' ) || exit;

/**
 * Repository fuer Kampagnen (hs_campaigns, K4-Konzept Abschnitt 2).
 *
 * Haelt den Versandzustand einer Kampagne getrennt vom Inhalt (CPT `hs_mail`,
 * referenziert ueber post_id). Die Empfaengerauswahl liegt als JSON in `audience`
 * ({lists:[ids], exclude_lists:[ids]}; Segmente kommen in K4d). Der eigentliche
 * Empfaenger-Snapshot entsteht bei der Materialisierung in `hs_mail_queue`.
 *
 * Nicht final, damit Tests die DB-Methoden per Test-Double ueberschreiben koennen.
 */
class CampaignRepository {

	/**
	 * Erlaubte Statuswerte einer Kampagne.
	 *
	 * @var array<int, string>
	 */
	public const STATUSES = array( 'draft', 'scheduled', 'sending', 'paused', 'sent', 'aborted' );

	/**
	 * Legt eine Kampagne (Zustandssatz) zu einem Inhalts-Post an.
	 *
	 * @param int $post_id CPT-`hs_mail`-Post mit Betreff/Inhalt.
	 * @return int Kampagnen-ID (0 bei Fehler).
	 */
	public function create( int $post_id ): int {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$ok = $wpdb->insert(
			Schema::table( 'campaigns' ),
			array(
				'post_id'    => max( 0, $post_id ),
				'status'     => 'draft',
				'created_at' => current_time( 'mysql', true ),
			)
		);
		return $ok ? (int) $wpdb->insert_id : 0;
	}

	/**
	 * Liefert eine Kampagne.
	 *
	 * @param int $id Kampagnen-ID.
	 * @return array<string, mixed>|null
	 */
	public function get( int $id ): ?array {
		global $wpdb;
		$table = Schema::table( 'campaigns' );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ), ARRAY_A );
		return $row ? $this->hydrate( $row ) : null;
	}

	/**
	 * Aktualisiert eine Kampagne (nur uebergebene, erlaubte Felder).
	 *
	 * @param int                  $id   Kampagnen-ID.
	 * @param array<string, mixed> $data Zu aendernde Felder.
	 * @return bool
	 */
	public function update( int $id, array $data ): bool {
		$allowed = array( 'post_id', 'status', 'audience', 'scheduled_at', 'started_at', 'finished_at', 'total_recipients', 'sent_count', 'failed_count' );
		$row     = array();
		foreach ( $allowed as $key ) {
			if ( ! array_key_exists( $key, $data ) ) {
				continue;
			}
			switch ( $key ) {
				case 'post_id':
				case 'total_recipients':
				case 'sent_count':
				case 'failed_count':
					$row[ $key ] = absint( $data[ $key ] );
					break;
				case 'status':
					$row[ $key ] = $this->normalize_status( (string) $data[ $key ] );
					break;
				case 'audience':
					// Bereits kodiertes JSON durchreichen, Arrays kodieren.
					$row[ $key ] = is_array( $data[ $key ] ) ? (string) wp_json_encode( $data[ $key ] ) : (string) $data[ $key ];
					break;
				default:
					// Datumsfelder: null erlaubt, sonst String.
					$row[ $key ] = null === $data[ $key ] ? null : (string) $data[ $key ];
					break;
			}
		}
		if ( array() === $row ) {
			return false;
		}
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return false !== $wpdb->update( Schema::table( 'campaigns' ), $row, array( 'id' => $id ) );
	}

	/**
	 * Setzt den Status einer Kampagne.
	 *
	 * @param int    $id     Kampagnen-ID.
	 * @param string $status Neuer Status (draft/scheduled/sending/paused/sent/aborted).
	 * @return bool
	 */
	public function set_status( int $id, string $status ): bool {
		return $this->update( $id, array( 'status' => $this->normalize_status( $status ) ) );
	}

	/**
	 * Schreibt die Versandzaehler.
	 *
	 * @param int $id     Kampagnen-ID.
	 * @param int $total  Gesamtzahl Empfaenger.
	 * @param int $sent   Gesendet.
	 * @param int $failed Fehlgeschlagen.
	 * @return bool
	 */
	public function set_counts( int $id, int $total, int $sent, int $failed ): bool {
		return $this->update(
			$id,
			array(
				'total_recipients' => max( 0, $total ),
				'sent_count'       => max( 0, $sent ),
				'failed_count'     => max( 0, $failed ),
			)
		);
	}

	/**
	 * Listet Kampagnen mit Pagination (neueste zuerst).
	 *
	 * @param int $page     Seite (1-basiert).
	 * @param int $per_page Eintraege je Seite.
	 * @return array{items:array<int,array<string,mixed>>,total:int,page:int,per_page:int}
	 */
	public function all( int $page = 1, int $per_page = 25 ): array {
		global $wpdb;
		$table    = Schema::table( 'campaigns' );
		$page     = max( 1, $page );
		$per_page = min( 100, max( 1, $per_page ) );
		$offset   = ( $page - 1 ) * $per_page;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
		$rows  = $wpdb->get_results(
			$wpdb->prepare( "SELECT * FROM {$table} ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d", $per_page, $offset ),
			ARRAY_A
		);
		// phpcs:enable

		return array(
			'items'    => array_map( array( $this, 'hydrate' ), $rows ?: array() ),
			'total'    => $total,
			'page'     => $page,
			'per_page' => $per_page,
		);
	}

	/**
	 * Kampagnen mit einem bestimmten Status (z.B. alle "sending" fuer den Worker).
	 *
	 * @param string $status Status.
	 * @return array<int, array<string, mixed>>
	 */
	public function by_status( string $status ): array {
		global $wpdb;
		$table = Schema::table( 'campaigns' );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE status = %s ORDER BY created_at DESC, id DESC", $this->normalize_status( $status ) ), ARRAY_A );
		return array_map( array( $this, 'hydrate' ), $rows ?: array() );
	}

	/**
	 * Faellt auf "draft" zurueck, wenn der Status unbekannt ist.
	 *
	 * @param string $status Roher Status.
	 * @return string
	 */
	private function normalize_status( string $status ): string {
		return in_array( $status, self::STATUSES, true ) ? $status : 'draft';
	}

	/**
	 * Normalisiert eine Datenbankzeile.
	 *
	 * @param array<string, mixed> $row Rohzeile.
	 * @return array<string, mixed>
	 */
	private function hydrate( array $row ): array {
		return array(
			'id'               => (int) $row['id'],
			'post_id'          => (int) $row['post_id'],
			'status'           => (string) $row['status'],
			'audience'         => null !== $row['audience'] ? (string) $row['audience'] : '',
			'scheduled_at'     => null !== $row['scheduled_at'] ? (string) $row['scheduled_at'] : '',
			'started_at'       => null !== $row['started_at'] ? (string) $row['started_at'] : '',
			'finished_at'      => null !== $row['finished_at'] ? (string) $row['finished_at'] : '',
			'total_recipients' => (int) $row['total_recipients'],
			'sent_count'       => (int) $row['sent_count'],
			'failed_count'     => (int) $row['failed_count'],
			'created_at'       => (string) $row['created_at'],
		);
	}
}
