<?php
namespace Pay4All\Pro\Frontend;

use Pay4All\Pro\Activator;
use Pay4All\Form\Admin\Settings\Schema;
use Pay4All\Pro\Domain\AbandonedCart;
use Pay4All\Pro\Domain\Category;
use Pay4All\Pro\Services\Mailer;

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

/**
 * Wiring for the cart-abandonment feature.
 *
 *  - `wp_ajax_(_nopriv)_p4all_form_save_progress`
 *      Captures the cart when the customer moves from the contact-info step
 *      to the validation step (they've entered their email + have items).
 *
 *  - GET ?p4all_recover={token}
 *      Stores the cart payload in a transient keyed by the visitor session
 *      and redirects to the original form page with a `p4all_recovered=1`
 *      query arg. The shortcode picks it up and pre-fills the form.
 *
 *  - cron hook `p4all_abandoned_cart_tick`
 *      Sends recovery emails for carts older than the configured delay,
 *      then deletes carts older than 90 days.
 */
final class AbandonedCarts {

	public const ACTION_SAVE     = 'p4all_form_save_progress';
	public const RECOVER_QUERY   = 'p4all_recover';
	public const RECOVER_FLAG    = 'p4all_recovered';
	public const RECOVER_TRANSIENT = 'p4all_recover_';
	public const PURGE_DAYS      = 90;

	public static function register() : void {
		add_action( 'wp_ajax_'        . self::ACTION_SAVE, [ self::class, 'handle_save' ] );
		add_action( 'wp_ajax_nopriv_' . self::ACTION_SAVE, [ self::class, 'handle_save' ] );
		add_action( 'init',                                [ self::class, 'maybe_handle_recovery' ] );
		add_action( Activator::CRON_HOOK_ABANDONED,        [ self::class, 'cron_tick' ] );
		// Wipe the abandoned-cart row(s) for a customer as soon as they
		// actually complete a checkout - see on_order_created() for the
		// full rationale (avoids stale entries + accidental recovery
		// emails for shoppers who already paid).
		add_action( 'p4all_after_order_created',           [ self::class, 'on_order_created' ], 10, 2 );
		// Belt-and-braces cleanup : any lingering row still visible when
		// the customer actually reaches ?p4all_thankyou=... (autosave
		// arrived AFTER the order was placed, form deferred payment,
		// gateway async race) gets wiped on the thank-you page render.
		add_action( 'p4all_thankyou_view',                 [ self::class, 'on_thankyou_view' ], 10, 2 );
		// Feeds the free plugin's shortcode with the recovered cart payload
		// stashed in a transient by maybe_handle_recovery(). Runs at default
		// priority 10 — the shortcode's `apply_filters` call is the only
		// consumer, so priority ordering doesn't matter.
		add_filter( 'p4all_form_recovered_payload',        [ self::class, 'filter_recovered_payload' ], 10, 2 );
	}

	/**
	 * Reads `?p4all_recovered={transient_key}` from the current request and
	 * returns the stashed payload if it still matches the requested form.
	 * Returns the untouched fallback (usually `[]`) otherwise.
	 *
	 * @param array $default
	 * @param int   $form_id
	 * @return array{customer?:array,items?:array,delivery_id?:int,payment_method?:string}
	 */
	public static function filter_recovered_payload( array $default, int $form_id ) : array {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only handler.
		if ( empty( $_GET[ self::RECOVER_FLAG ] ) ) { return $default; }
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$key = sanitize_text_field( wp_unslash( (string) $_GET[ self::RECOVER_FLAG ] ) );
		if ( strpos( $key, self::RECOVER_TRANSIENT ) !== 0 ) { return $default; }

		$payload = get_transient( $key );
		if ( ! is_array( $payload ) ) { return $default; }

		// Refuse to leak another form's payload if the merchant renders two
		// shortcodes on the same page — the transient carries the source
		// form_id it was captured for.
		if ( $form_id > 0 && (int) ( $payload['form_id'] ?? 0 ) !== $form_id ) {
			return $default;
		}

		return [
			'customer'       => (array) ( $payload['customer']       ?? [] ),
			'items'          => (array) ( $payload['items']          ?? [] ),
			'delivery_id'    => (int)   ( $payload['delivery_id']    ?? 0 ),
			'payment_method' => (string)( $payload['payment_method'] ?? '' ),
		];
	}

	// ------------------------------------------------------------------
	// AJAX save (called by JS just before the validation step)
	// ------------------------------------------------------------------

	public static function handle_save() : void {
		$form_id = isset( $_POST['p4all_form_id'] ) ? (int) $_POST['p4all_form_id'] : 0;
		$nonce   = isset( $_POST['p4all_nonce'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['p4all_nonce'] ) ) : '';
		if ( $form_id <= 0 || ! wp_verify_nonce( $nonce, 'p4all_form_submit_' . $form_id ) ) {
			wp_send_json_error( [ 'message' => 'nonce' ], 400 );
		}

		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- fields sanitized below per slot.
		$customer = ( isset( $_POST['p4all_fields'] )   && is_array( $_POST['p4all_fields'] ) )   ? wp_unslash( $_POST['p4all_fields'] )   : [];
		$items    = ( isset( $_POST['p4all_products'] ) && is_array( $_POST['p4all_products'] ) ) ? wp_unslash( $_POST['p4all_products'] ) : [];
		// phpcs:enable
		$delivery = isset( $_POST['p4all_delivery_id'] ) ? absint( wp_unslash( $_POST['p4all_delivery_id'] ) ) : 0;
		$payment  = isset( $_POST['p4all_payment_method'] ) ? sanitize_key( wp_unslash( (string) $_POST['p4all_payment_method'] ) ) : '';

		$email = '';
		$cleanCustomer = [];
		foreach ( $customer as $key => $value ) {
			$key = sanitize_key( (string) $key );
			$value = is_scalar( $value ) ? sanitize_text_field( (string) $value ) : '';
			if ( $value === '' ) { continue; }
			$cleanCustomer[ $key ] = $value;
			if ( $email === '' && is_email( $value ) ) { $email = $value; }
		}
		if ( $email === '' && isset( $cleanCustomer['email'] ) ) {
			$email = is_email( $cleanCustomer['email'] ) ? $cleanCustomer['email'] : '';
		}
		if ( $email === '' ) {
			// Pas d'email : pas la peine de stocker un panier qu'on ne pourra
			// jamais relancer. On répond 204-like.
			wp_send_json_success( [ 'stored' => false ] );
		}

		$cleanItems = [];
		foreach ( $items as $pid => $qty ) {
			$pid = (int) $pid;
			$qty = (int) $qty;
			if ( $pid > 0 && $qty > 0 ) { $cleanItems[ (string) $pid ] = $qty; }
		}
		if ( empty( $cleanItems ) ) {
			wp_send_json_success( [ 'stored' => false ] );
		}

		$lang = isset( $_POST['p4all_lang'] ) ? strtolower( sanitize_key( wp_unslash( (string) $_POST['p4all_lang'] ) ) ) : '';
		if ( ! in_array( $lang, Schema::SUPPORTED_LANGUAGES, true ) ) {
			$lang = Category::current_language();
		}

		// Calcule subtotal / delivery / total côté serveur à partir des items
		// et de la méthode de livraison choisie. Ces colonnes sont stockées
		// en centimes pour rester en accord avec le reste du plugin (et pour
		// éviter les arrondis flottants sur 6 mois de stockage).
		$totals_pkg     = \Pay4All\Form\Services\Totals::items( $cleanItems );
		$subtotal       = (float) ( $totals_pkg['subtotal'] ?? 0.0 );
		$delivery_price = \Pay4All\Form\Services\Totals::compute_delivery( $delivery, $subtotal );
		$total          = round( $subtotal + $delivery_price, 4 );
		$currency       = \Pay4All\Form\Services\Currency::code();

		AbandonedCart::save( [
			'form_id'        => $form_id,
			'email'          => $email,
			'customer_data'  => $cleanCustomer,
			'items'          => $cleanItems,
			'delivery_id'    => $delivery ?: null,
			'payment_method' => $payment,
			'subtotal_cents' => (int) round( $subtotal       * 100 ),
			'delivery_cents' => (int) round( $delivery_price * 100 ),
			'total_cents'    => (int) round( $total          * 100 ),
			'currency'       => $currency,
			'lang'           => $lang,
			'client_ip'      => self::client_ip(),
			'user_agent'     => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_USER_AGENT'] ) ) : '',
		] );

		wp_send_json_success( [ 'stored' => true ] );
	}

	// ------------------------------------------------------------------
	// Recovery link handler — ?p4all_recover={token}
	// ------------------------------------------------------------------

	public static function maybe_handle_recovery() : void {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only handler.
		if ( empty( $_GET[ self::RECOVER_QUERY ] ) ) { return; }
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$token = sanitize_text_field( wp_unslash( (string) $_GET[ self::RECOVER_QUERY ] ) );
		if ( strlen( $token ) !== 40 ) { return; }

		$cart = AbandonedCart::find_by_token( $token );
		if ( $cart === null ) { return; }

		// Mark + stash the payload in a short-lived transient. The form's
		// JS will read it on render and pre-fill the inputs.
		AbandonedCart::mark_recovered( (int) $cart['id'] );

		$key = self::RECOVER_TRANSIENT . wp_generate_password( 12, false, false );
		set_transient( $key, [
			'cart_id'        => (int) $cart['id'],
			'form_id'        => (int) $cart['form_id'],
			'customer'       => json_decode( (string) $cart['customer_data'], true ) ?: [],
			'items'          => json_decode( (string) $cart['items'], true ) ?: [],
			'delivery_id'    => (int) ( $cart['delivery_id'] ?? 0 ),
			'payment_method' => (string) ( $cart['payment_method'] ?? '' ),
		], HOUR_IN_SECONDS );

		// Locate the first page that contains the form's shortcode, fall
		// back to home_url() if none found — better than a hard 404.
		$target = self::find_form_page_url( (int) $cart['form_id'] );
		if ( $target === '' ) { $target = home_url( '/' ); }
		$target = add_query_arg( self::RECOVER_FLAG, $key, $target );
		wp_safe_redirect( $target );
		exit;
	}

	/**
	 * Best-effort lookup : the first published post whose content embeds
	 * [pay4all_form id="X"]. WordPress doesn't track shortcode bindings,
	 * so we run a quick LIKE query (results cached by WP's MySQL layer).
	 */
	private static function find_form_page_url( int $form_id ) : string {
		global $wpdb;
		$needle = '%[pay4all_form%id="' . $form_id . '"%';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$post_id = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT ID FROM {$wpdb->posts}
			  WHERE post_status = 'publish'
			    AND post_content LIKE %s
			  ORDER BY ID DESC LIMIT 1",
			$needle
		) );
		return $post_id > 0 ? (string) get_permalink( $post_id ) : '';
	}

	// ------------------------------------------------------------------
	// Cron : envoi des mails + purge
	// ------------------------------------------------------------------

	/**
	 * Handler for `p4all_after_order_created` (fired by Submission once
	 * an order post lands in the DB). Deletes the abandoned-cart row(s)
	 * for the same (form_id, customer email) so :
	 *
	 *   - The row disappears from the admin "Abandoned carts" list.
	 *   - The recovery cron never tries to email a shopper who already
	 *     completed a purchase (the previous mark_converted_for() flag
	 *     used to leave the row visible, which confused merchants).
	 *
	 * The order id is optional here : we only need form_id + email, both
	 * carried in $context / on the order meta. Silent no-op if either is
	 * missing (e.g. custom code that fires the hook manually without a
	 * form context).
	 *
	 * @param int   $order_id
	 * @param array $context  Payload from Submission (form_id, payment_method, ...).
	 */
	public static function on_order_created( int $order_id, array $context = [] ) : void {
		$form_id = (int) ( $context['form_id'] ?? 0 );
		self::wipe_for_order( $order_id, $form_id );
	}

	/**
	 * Handler for `p4all_thankyou_view` (fired by Shortcode::render_thankyou
	 * on the confirmation page hit). Catches the corner cases the earlier
	 * `p4all_after_order_created` hook missed :
	 *
	 *   - The autosave AJAX arrived slightly AFTER the order was placed,
	 *     re-inserting a row the previous hook had just deleted.
	 *   - The order was created by a background flow (Stripe webhook,
	 *     TWINT auto...) that never fires p4all_after_order_created.
	 *
	 * form_id is looked up on the order meta since the thank-you hook
	 * only carries the post id + public number.
	 */
	public static function on_thankyou_view( int $order_id, string $number = '' ) : void {
		unset( $number );
		if ( $order_id <= 0 ) { return; }
		$form_id = class_exists( '\\Pay4All\\Form\\Domain\\Order' )
			? (int) \Pay4All\Form\Domain\Order::form_id( $order_id )
			: 0;
		self::wipe_for_order( $order_id, $form_id );
	}

	/**
	 * Shared cleanup routine : extract the customer email from the order
	 * meta and delete every abandoned-cart row that matches (form_id,
	 * email). The email lookup mirrors handle_save() so a merchant who
	 * renamed the field key ('mail', 'contact_email', 'courriel'...) still
	 * gets a clean wipe : we take the first customer value that passes
	 * is_email() and fall back to $customer['email'] as a last resort.
	 */
	private static function wipe_for_order( int $order_id, int $form_id ) : void {
		if ( $form_id <= 0 || $order_id <= 0 ) { return; }
		if ( ! class_exists( '\\Pay4All\\Form\\Domain\\Order' ) ) { return; }

		$customer = (array) \Pay4All\Form\Domain\Order::customer( $order_id );
		if ( empty( $customer ) ) { return; }

		$email = '';
		foreach ( $customer as $value ) {
			if ( ! is_scalar( $value ) ) { continue; }
			$str = trim( (string) $value );
			if ( $str !== '' && is_email( $str ) ) {
				$email = $str;
				break;
			}
		}
		if ( $email === '' && isset( $customer['email'] ) && is_email( (string) $customer['email'] ) ) {
			$email = (string) $customer['email'];
		}
		if ( $email === '' ) { return; }

		AbandonedCart::delete_by_email( $form_id, $email );
	}

	public static function cron_tick() : void {
		// `abandoned_cart_delay_days` = 0 signifie « Jamais » : le commerçant
		// a explicitement désactivé l'envoi automatique depuis la page
		// Abandon panier et souhaite envoyer les relances manuellement.
		$delay = (int) Schema::get( 'abandoned_cart_delay_days', 10 );
		if ( $delay > 0 ) {
			foreach ( AbandonedCart::due_for_email( $delay ) as $cart ) {
				$ok = Mailer::send_abandoned_cart_recovery( $cart );
				if ( $ok ) {
					AbandonedCart::mark_email_sent( (int) $cart['id'] );
				}
			}
		}
		AbandonedCart::purge_old( self::PURGE_DAYS );
	}

	private static function client_ip() : string {
		foreach ( [ 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR' ] as $k ) {
			if ( ! empty( $_SERVER[ $k ] ) ) {
				$v = sanitize_text_field( wp_unslash( (string) $_SERVER[ $k ] ) );
				return trim( explode( ',', $v )[0] );
			}
		}
		return '';
	}
}
