<?php
namespace Pay4All\Pro\Domain;

use Pay4All\Pro\PostTypes;

if ( ! defined( 'ABSPATH' ) ) { exit; }

/**
 * Read / write helpers for the p4all_newsletter CPT. A newsletter is a
 * multilingual campaign : one row per language holds { subject, body,
 * cta_label, cta_url }. A « template kind » selector decides whether the
 * campaign is a free announcement, tied to a specific product, or tied
 * to a promo code — the picked ID lives alongside the language rows so
 * the spooler can inject product / promo variables at send time.
 *
 * Progress tracking : the spooler advances `last_customer_id` (highest
 * customer.id already dispatched) after every batch. When the cursor
 * reaches the end of the audience the campaign flips to `status=done`.
 */
final class Newsletter {

	public const META_KIND               = '_p4all_nl_kind';
	public const META_KIND_CONFIRMED     = '_p4all_nl_kind_confirmed'; // '1' once the merchant validated the template picker
	public const META_PRODUCT_ID         = '_p4all_nl_product_id';
	public const META_PROMO_ID           = '_p4all_nl_promo_id';
	public const META_CONTENT            = '_p4all_nl_content';         // [ lang => [ subject, body, cta_label, cta_url ] ]
	public const META_STATUS             = '_p4all_nl_status';
	public const META_SCHEDULED_AT       = '_p4all_nl_scheduled_at';    // gmdate ISO or ''
	public const META_LAST_CUSTOMER_ID   = '_p4all_nl_last_customer_id';
	public const META_STARTED_AT         = '_p4all_nl_started_at';
	public const META_FINISHED_AT        = '_p4all_nl_finished_at';
	public const META_AUDIENCE_TOTAL     = '_p4all_nl_audience_total';

	public const KIND_FREE    = 'free';
	public const KIND_PRODUCT = 'product';
	public const KIND_PROMO   = 'promo';
	public const KINDS        = [ self::KIND_FREE, self::KIND_PRODUCT, self::KIND_PROMO ];

	public const STATUS_DRAFT   = 'draft';
	public const STATUS_QUEUED  = 'queued';
	public const STATUS_SENDING = 'sending';
	public const STATUS_DONE    = 'done';
	public const STATUS_PAUSED  = 'paused';

	public const FIELDS = [ 'subject', 'body', 'cta_label', 'cta_url' ];

	public static function kind( int $id ) : string {
		$v = (string) get_post_meta( $id, self::META_KIND, true );
		return in_array( $v, self::KINDS, true ) ? $v : self::KIND_FREE;
	}

	/**
	 * True once the merchant validated the template picker on the
	 * two-step editor. Before that flip, only the Template metabox is
	 * rendered so the merchant is nudged into choosing free / product /
	 * promo before writing anything.
	 */
	public static function is_kind_confirmed( int $id ) : bool {
		return get_post_meta( $id, self::META_KIND_CONFIRMED, true ) === '1';
	}

	public static function product_id( int $id ) : int {
		return (int) get_post_meta( $id, self::META_PRODUCT_ID, true );
	}

	public static function promo_id( int $id ) : int {
		return (int) get_post_meta( $id, self::META_PROMO_ID, true );
	}

	/**
	 * Full per-language content bag. Missing languages fall through to an
	 * empty row so the caller never has to null-check the keys.
	 *
	 * @return array<string,array{subject:string,body:string,cta_label:string,cta_url:string}>
	 */
	public static function content( int $id ) : array {
		$raw = get_post_meta( $id, self::META_CONTENT, true );
		if ( ! is_array( $raw ) ) { return []; }
		$out = [];
		foreach ( $raw as $lang => $row ) {
			$lang = strtolower( sanitize_key( (string) $lang ) );
			if ( $lang === '' || ! is_array( $row ) ) { continue; }
			$out[ $lang ] = [
				'subject'   => (string) ( $row['subject']   ?? '' ),
				'body'      => (string) ( $row['body']      ?? '' ),
				'cta_label' => (string) ( $row['cta_label'] ?? '' ),
				'cta_url'   => (string) ( $row['cta_url']   ?? '' ),
			];
		}
		return $out;
	}

	/**
	 * Per-language content for a given locale, with fallback to the site's
	 * primary language then to the first non-empty row. Returned array
	 * always has all 4 keys.
	 *
	 * @return array{subject:string,body:string,cta_label:string,cta_url:string}
	 */
	public static function content_for_lang( int $id, string $lang ) : array {
		$bag = self::content( $id );
		$lang = strtolower( $lang );
		if ( isset( $bag[ $lang ] ) && trim( (string) $bag[ $lang ]['subject'] ) !== '' ) {
			return $bag[ $lang ];
		}
		// Fall back to the site's primary language (first language listed
		// in Réglages / Langue).
		$primary = self::primary_language();
		if ( $primary !== $lang && isset( $bag[ $primary ] ) && trim( (string) $bag[ $primary ]['subject'] ) !== '' ) {
			return $bag[ $primary ];
		}
		foreach ( $bag as $row ) {
			if ( trim( (string) $row['subject'] ) !== '' ) { return $row; }
		}
		return [ 'subject' => '', 'body' => '', 'cta_label' => '', 'cta_url' => '' ];
	}

	public static function status( int $id ) : string {
		$v = (string) get_post_meta( $id, self::META_STATUS, true );
		return in_array( $v, [ self::STATUS_DRAFT, self::STATUS_QUEUED, self::STATUS_SENDING, self::STATUS_DONE, self::STATUS_PAUSED ], true )
			? $v : self::STATUS_DRAFT;
	}

	public static function set_status( int $id, string $status ) : void {
		update_post_meta( $id, self::META_STATUS, $status );
	}

	public static function last_customer_id( int $id ) : int {
		return (int) get_post_meta( $id, self::META_LAST_CUSTOMER_ID, true );
	}

	public static function set_last_customer_id( int $id, int $customer_id ) : void {
		update_post_meta( $id, self::META_LAST_CUSTOMER_ID, $customer_id );
	}

	public static function audience_total( int $id ) : int {
		return (int) get_post_meta( $id, self::META_AUDIENCE_TOTAL, true );
	}

	/**
	 * Persists the whole content bag + kind + product/promo pick. Empty
	 * fields are stored as empty strings so re-editing preserves the
	 * per-language shape (helps the fallback chain in content_for_lang()).
	 *
	 * @param array<string,array<string,string>> $content
	 */
	public static function save( int $id, string $kind, int $product_id, int $promo_id, array $content ) : void {
		if ( ! in_array( $kind, self::KINDS, true ) ) { $kind = self::KIND_FREE; }
		update_post_meta( $id, self::META_KIND, $kind );
		update_post_meta( $id, self::META_PRODUCT_ID, max( 0, $product_id ) );
		update_post_meta( $id, self::META_PROMO_ID,   max( 0, $promo_id ) );

		$clean = [];
		$default_cta_url = home_url( '/' );
		foreach ( $content as $lang => $row ) {
			$lang = strtolower( sanitize_key( (string) $lang ) );
			if ( $lang === '' || ! is_array( $row ) ) { continue; }
			// CTA URL falls back to the site home URL when the merchant
			// leaves the field empty — a bare CTA button is more useful
			// than none, and « return to my site » is a sensible default.
			// URLs containing a « {token} » placeholder are stored verbatim
			// so the send-time substitutor can resolve them ; otherwise
			// esc_url_raw would rewrite « {product_url} » to the mangled
			// « http://product_url » literal that the user reported.
			$cta_url_raw = trim( (string) ( $row['cta_url'] ?? '' ) );
			if ( $cta_url_raw === '' ) {
				$cta_url = $default_cta_url;
			} elseif ( strpos( $cta_url_raw, '{' ) !== false && strpos( $cta_url_raw, '}' ) !== false ) {
				$cta_url = $cta_url_raw;
			} else {
				$cta_url = esc_url_raw( $cta_url_raw, [ 'http', 'https', 'mailto', 'tel' ] );
				if ( $cta_url === '' ) { $cta_url = $default_cta_url; }
			}
			$clean[ $lang ] = [
				'subject'   => sanitize_text_field( (string) ( $row['subject']   ?? '' ) ),
				// Body is HTML from TinyMCE — sanitized with post-kses so
				// <a>/<img>/<strong>/… are preserved but scripts are stripped.
				'body'      => wp_kses_post( (string) ( $row['body']            ?? '' ) ),
				'cta_label' => sanitize_text_field( (string) ( $row['cta_label'] ?? '' ) ),
				'cta_url'   => $cta_url,
			];
		}
		update_post_meta( $id, self::META_CONTENT, $clean );
	}

	/**
	 * Returns campaigns still pending dispatch (queued or sending) whose
	 * scheduled_at is either unset OR already in the past (i.e. ready to
	 * broadcast now). Used by the spooler on every cron tick.
	 *
	 * @return int[] campaign post IDs, ordered by post_date ASC
	 */
	public static function pending_campaigns() : array {
		$q = new \WP_Query( [
			'post_type'      => PostTypes::NEWSLETTER,
			'post_status'    => 'publish',
			'posts_per_page' => 20,
			'orderby'        => 'date',
			'order'          => 'ASC',
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- capped at 20 rows on a low-cardinality CPT (a merchant rarely has >100 newsletters ever) ; the spooler tick runs once per cron slice (default 15 min), not per request.
			'meta_query'     => [
				[
					'key'     => self::META_STATUS,
					'value'   => [ self::STATUS_QUEUED, self::STATUS_SENDING ],
					'compare' => 'IN',
				],
			],
			'fields'         => 'ids',
			'no_found_rows'  => true,
		] );
		$now = gmdate( 'Y-m-d H:i:s' );
		$out = [];
		foreach ( (array) $q->posts as $id ) {
			$sched = (string) get_post_meta( (int) $id, self::META_SCHEDULED_AT, true );
			// No schedule OR schedule is in the past → ready to broadcast.
			if ( $sched === '' || $sched <= $now ) { $out[] = (int) $id; }
		}
		return $out;
	}

	private static function primary_language() : string {
		if ( ! class_exists( '\\Pay4All\\Form\\Admin\\Settings\\Schema' ) ) { return 'fr'; }
		$langs = (array) \Pay4All\Form\Admin\Settings\Schema::get( 'languages', [] );
		$langs = array_values( array_intersect( $langs, \Pay4All\Form\Admin\Settings\Schema::SUPPORTED_LANGUAGES ) );
		return $langs[0] ?? 'fr';
	}
}
