<?php
/**
 * Cart Recovery — Service (orchestrator)
 *
 * Thin wiring layer that observes WooCommerce cart activity and projects
 * it into the repository as cart-recovery candidate rows. Phase 1 scope:
 *
 *   - on add_to_cart / qty change / item removed → snapshot + upsert
 *   - on thankyou                                 → mark_recovered
 *   - on niwa_ai_contact_saved                    → attach email to shadow
 *
 * No scheduling, mail composition, coupon generation, or tracking is
 * implemented here — those are Phase 2/3/4 concerns.
 *
 * Session correlation uses the plugin's existing `niwa_ai_session`
 * cookie so cart activity and chat transcripts share the same key.
 * Requests that arrive without a session cookie are ignored — we
 * intentionally don't mint a new one here; cart-recovery is worthless
 * without the chat context we'd pair with later.
 *
 * @package Niwa_AI
 * @since   0.12.0
 */

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

class Niwa_AI_Cart_Recovery_Service {

	/**
	 * Re-entrancy guard. `woocommerce_add_to_cart` can fire during
	 * `woocommerce_cart_item_removed` callback chains on some themes,
	 * and we don't want to double-hash the same mutation.
	 */
	private bool $handling = false;

	public function __construct(
		private readonly Niwa_AI_Cart_Recovery_Repository $repository,
		private readonly Niwa_AI_Cart_Recovery_Snapshot_Builder $snapshot_builder,
		private readonly ?Niwa_AI_Cart_Recovery_Scheduler $scheduler = null
	) {}

	public function register_hooks(): void {
		add_action( 'woocommerce_add_to_cart', array( $this, 'handle_cart_change' ), 20, 0 );
		add_action( 'woocommerce_cart_item_removed', array( $this, 'handle_cart_change' ), 20, 0 );
		add_action( 'woocommerce_cart_item_restored', array( $this, 'handle_cart_change' ), 20, 0 );
		add_action( 'woocommerce_after_cart_item_quantity_update', array( $this, 'handle_cart_change' ), 20, 0 );
		add_action( 'woocommerce_cart_item_set_quantity', array( $this, 'handle_cart_change' ), 20, 0 );

		add_action( 'woocommerce_thankyou', array( $this, 'handle_thankyou' ), 20, 1 );

		// Upgrade shadow rows the moment the visitor types their email into
		// the checkout form — Woo fires this AJAX on every input change.
		add_action( 'woocommerce_checkout_update_order_review', array( $this, 'handle_checkout_contact_update' ), 20, 1 );

		add_action( 'niwa_ai_contact_saved', array( $this, 'handle_contact_saved' ), 20, 5 );
	}

	/**
	 * Snapshot the live cart and upsert a recovery row keyed by the
	 * visitor's session + (possibly empty) email.
	 */
	public function handle_cart_change(): void {
		if ( $this->handling ) {
			return;
		}

		$session_id = $this->resolve_session_id();
		if ( '' === $session_id ) {
			return;
		}

		$this->handling = true;

		try {
			$snapshot = $this->snapshot_builder->build_from_current_cart();

			if ( $snapshot->is_empty() ) {
				return;
			}

			$email  = $this->resolve_known_email( $session_id );
			$phone  = $this->resolve_known_phone( $session_id );
			$row_id = $this->repository->upsert_snapshot(
				$session_id,
				$email,
				$snapshot,
				$this->current_locale(),
				'',
				$phone
			);

			// Event-driven dispatch: the moment a cart row is created or
			// updated, queue the mail1 send for exactly `mail1_delay_minutes`
			// from now. Previously the row had to wait for the recurring
			// sweep to notice it — that's still the safety net, but the
			// primary path is now a precise per-row single-action.
			// Rescheduling is idempotent (unschedules the previous queued
			// action first), so subsequent cart activity resets the
			// inactivity clock, matching "abandoned-cart after N minutes
			// of no activity" semantics.
			$this->schedule_row_mail1_if_sendable( $row_id );
		} finally {
			$this->handling = false;
		}
	}

	/**
	 * Order completed → mark the recovery row for this email as recovered
	 * so no further mail is dispatched for this cycle.
	 *
	 * @param int $order_id Woo order id.
	 */
	public function handle_thankyou( $order_id ): void {
		$order_id = (int) $order_id;
		if ( $order_id <= 0 || ! function_exists( 'wc_get_order' ) ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( ! is_object( $order ) ) {
			return;
		}

		$email = method_exists( $order, 'get_billing_email' ) ? (string) $order->get_billing_email() : '';
		$phone = method_exists( $order, 'get_billing_phone' ) ? (string) $order->get_billing_phone() : '';
		$country = method_exists( $order, 'get_billing_country' ) ? (string) $order->get_billing_country() : '';
		$phone   = $this->repository->normalize_phone( $phone, $country );
		if ( '' === trim( $email ) && '' === $phone ) {
			return;
		}

		$this->repository->mark_recovered_by_contact( $email, $phone, $order_id );
	}

	/**
	 * `niwa_ai_contact_saved` hook → link any shadow row for the current
	 * session to the newly revealed email and/or phone.
	 *
	 * @param string $email  Email.
	 * @param string $name   Name (may be empty).
	 * @param string $phone  Phone (may be empty).
	 * @param string $source Consent source tag.
	 * @param int    $id     Row id in niwa_ai_contacts.
	 */
	public function handle_contact_saved( $email, $name = '', $phone = '', $source = '', $id = 0 ): void {
		$session_id = $this->resolve_session_id();
		$email      = trim( (string) $email );
		$phone      = $this->repository->normalize_phone( (string) $phone );
		$contact_id = max( 0, (int) $id );
		if ( '' !== $email && ! $this->is_valid_email( $email ) ) {
			$email = '';
		}

		if ( '' === $session_id || ( '' === $email && '' === $phone ) ) {
			return;
		}

		// 1) Upgrade an existing row for this session (if any) — covers
		//    cart-first and split email/phone capture orders.
		$linked = $this->repository->attach_contact( $session_id, $email, $phone, (string) $name, $contact_id );

		// 2) Contact-first, cart-later: plant a seed binding the session
		//    to whichever contact coordinates are already known.
		//    The next cart change will find this seed via
		//    `find_latest_by_session()` in `resolve_known_email()` and
		//    upsert the snapshot onto it instead of spawning a new
		//    shadow row that loses the email binding.
		if ( null === $linked ) {
			$linked = $this->repository->ensure_session_contact_seed( $session_id, $email, $phone, (string) $name, $contact_id );
		}

		// If a cart already exists, queue its first enabled reminder now.
		// Seed-only rows are skipped until a real snapshot arrives.
		$this->schedule_row_mail1_if_sendable( $linked );
	}

	/**
	 * Resolve the current visitor's verified server-owned chat session.
	 */
	private function resolve_session_id(): string {
		return Niwa_AI_Session_Utils::resolve_verified_session( false );
	}

	/**
	 * Best-effort resolution of an email already known for this visitor.
	 * Resolution order (first non-empty wins):
	 *   1. A previously upserted row for this session that already has an
	 *      email attached (via `attach_contact` or an earlier cart change
	 *      that saw WC/WP credentials).
	 *   2. WooCommerce customer session — `WC()->customer->get_billing_email()`
	 *      covers guest checkouts that have typed an email and logged-in
	 *      Woo customers (Woo seeds it from the user account).
	 *   3. The current WP user's account email — catches the case where
	 *      a logged-in shop admin or registered customer fills a cart
	 *      before visiting checkout.
	 *
	 * Returning '' is still valid; the row lives as a shadow until a
	 * contact save / checkout input supplies one.
	 */
	private function resolve_known_email( string $session_id ): string {
		if ( '' !== $session_id ) {
			$existing = $this->repository->find_latest_by_session( $session_id );
			if ( is_array( $existing ) && ! empty( $existing['email'] ) ) {
				$candidate = (string) $existing['email'];
				if ( $this->is_valid_email( $candidate ) ) {
					return $candidate;
				}
			}
		}

		if ( function_exists( 'WC' ) ) {
			$wc = WC();
			if ( is_object( $wc ) && isset( $wc->customer ) && is_object( $wc->customer )
				&& method_exists( $wc->customer, 'get_billing_email' ) ) {
				$billing = (string) $wc->customer->get_billing_email();
				if ( $this->is_valid_email( $billing ) ) {
					return $billing;
				}
			}
		}

		if ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
			$user = function_exists( 'wp_get_current_user' ) ? wp_get_current_user() : null;
			if ( is_object( $user ) && ! empty( $user->user_email ) ) {
				$ue = (string) $user->user_email;
				if ( $this->is_valid_email( $ue ) ) {
					return $ue;
				}
			}
		}

		return '';
	}

	private function resolve_known_phone( string $session_id ): string {
		if ( '' !== $session_id ) {
			$existing = $this->repository->find_latest_by_session( $session_id );
			if ( is_array( $existing ) ) {
				$phone = $this->repository->normalize_phone( (string) ( $existing['phone'] ?? '' ) );
				if ( '' !== $phone ) {
					return $phone;
				}
			}
		}

		if ( function_exists( 'WC' ) ) {
			$wc = WC();
			if ( is_object( $wc ) && isset( $wc->customer ) && is_object( $wc->customer )
				&& method_exists( $wc->customer, 'get_billing_phone' ) ) {
				return $this->repository->normalize_phone( (string) $wc->customer->get_billing_phone() );
			}
		}

		return '';
	}

	private function is_valid_email( string $email ): bool {
		$email = trim( $email );
		if ( '' === $email ) {
			return false;
		}
		if ( function_exists( 'is_email' ) ) {
			return (bool) is_email( $email );
		}
		return (bool) filter_var( $email, FILTER_VALIDATE_EMAIL );
	}

	/**
	 * Woo fires `woocommerce_checkout_update_order_review` (AJAX) on every
	 * keystroke / field change in the checkout form. If the visitor has
	 * typed a valid email or phone and we have a row for this session, link
	 * it here so the scheduler can promote the row before the user even
	 * reaches "Place order".
	 *
	 * @param string $post_data Serialized checkout form payload as sent by Woo.
	 */
	public function handle_checkout_contact_update( $post_data = '' ): void {
		$session_id = $this->resolve_session_id();
		if ( '' === $session_id ) {
			return;
		}

		$parsed = array();
		if ( is_string( $post_data ) && '' !== $post_data ) {
			parse_str( $post_data, $parsed );
		} elseif ( is_array( $post_data ) ) {
			$parsed = $post_data;
		}

		$email = '';
		$phone = '';
		if ( isset( $parsed['billing_email'] ) ) {
			$email = (string) $parsed['billing_email'];
		}
		if ( isset( $parsed['billing_phone'] ) ) {
			$phone = (string) $parsed['billing_phone'];
		}
		$name = trim(
			(string) ( $parsed['billing_first_name'] ?? '' ) . ' ' .
			(string) ( $parsed['billing_last_name'] ?? '' )
		);
		if ( ( '' === $email || '' === $phone ) && function_exists( 'WC' ) ) {
			$wc = WC();
			if ( is_object( $wc ) && isset( $wc->customer ) && is_object( $wc->customer ) ) {
				if ( '' === $email && method_exists( $wc->customer, 'get_billing_email' ) ) {
					$email = (string) $wc->customer->get_billing_email();
				}
				if ( '' === $phone && method_exists( $wc->customer, 'get_billing_phone' ) ) {
					$phone = (string) $wc->customer->get_billing_phone();
				}
			}
		}

		$email = function_exists( 'sanitize_email' ) ? sanitize_email( $email ) : trim( $email );
		if ( '' !== $email && ! $this->is_valid_email( $email ) ) {
			$email = '';
		}
		$country = isset( $parsed['billing_country'] ) ? (string) $parsed['billing_country'] : '';
		$phone   = $this->repository->normalize_phone( $phone, $country );
		if ( '' === $email && '' === $phone ) {
			return;
		}

		$row_id = $this->repository->attach_contact( $session_id, $email, $phone, $name );
		if ( null === $row_id ) {
			$row_id = $this->repository->ensure_session_contact_seed( $session_id, $email, $phone, $name );
		}
		$this->schedule_row_mail1_if_sendable( $row_id );
	}

	private function schedule_row_mail1_if_sendable( ?int $row_id ): void {
		if ( null === $this->scheduler || null === $row_id || $row_id <= 0 ) {
			return;
		}

		$row = $this->repository->find_by_id( $row_id );
		if ( ! is_array( $row ) || '' === trim( (string) ( $row['cart_snapshot_json'] ?? '' ) ) ) {
			return;
		}
		if ( empty( Niwa_AI_Cart_Recovery_Settings::delivery_channels_for_row( $row ) ) ) {
			return;
		}

		$this->scheduler->schedule_row_mail1( $row_id );
	}

	private function current_locale(): string {
		if ( function_exists( 'determine_locale' ) ) {
			return (string) determine_locale();
		}
		if ( function_exists( 'get_locale' ) ) {
			return (string) get_locale();
		}
		return '';
	}
}
