<?php
/**
 * Purchase Controller — Server-side Purchase event trigger.
 *
 * Dual-hook design (Hybrid Balanced workflow):
 *   - woocommerce_order_status_processing  → online payment gateways
 *   - woocommerce_order_status_wc-confirmed → COD manual/auto confirmation
 *
 * Deduplication via order meta lock (_capiflow_purchase_sent).
 * Analytics via 5-minute deferred AS action (race condition fix — Audit C3).
 *
 * @package CapiFlow
 */

namespace CapiFlow\Core;

defined( 'ABSPATH' ) || exit;

class Purchase_Controller {

	/**
	 * Register hooks — fixed dual triggers (no longer configurable).
	 *
	 * 1. processing   — online payment gateways (bKash, SSLCommerz, etc.)
	 * 2. wc-confirmed — COD manual verification by admin / auto-confirm by Pro
	 *
	 * Idempotency lock (_capiflow_purchase_sent) prevents double-fire
	 * when both hooks could theoretically trigger for the same order.
	 */
	public function register_hooks(): void {
		if ( ! (bool) get_option( 'capiflow_send_purchase', true ) ) {
			return;
		}

		// Hook 1: Online payment → processing (bKash, SSLCommerz auto-set this).
		add_action( 'woocommerce_order_status_processing', array( $this, 'handle_purchase' ), 10, 2 );

		// Hook 2: COD confirmed → wc-confirmed (admin manual or Pro auto-confirm).
		add_action( 'woocommerce_order_status_confirmed', array( $this, 'handle_purchase' ), 10, 2 );

		// Hook 3: Completed — safety net for COD orders where admin skips 'confirmed'.
		// Dedup lock prevents double-fire for online payment orders.
		add_action( 'woocommerce_order_status_completed', array( $this, 'handle_purchase' ), 10, 2 );

		// Payment complete hook as backup for gateways that skip status transitions.
		add_action( 'woocommerce_payment_complete', array( $this, 'handle_payment_complete' ), 10, 1 );
	}

	/**
	 * Handle Purchase event from order status change.
	 *
	 * @param int            $order_id Order ID.
	 * @param \WC_Order|null $order    Order object (may be null on some hooks).
	 */
	public function handle_purchase( int $order_id, $order = null ): void {
		if ( ! $order instanceof \WC_Order ) {
			$order = wc_get_order( $order_id );
		}

		if ( ! $order ) {
			return;
		}

		$this->process_purchase( $order );
	}

	/**
	 * Handle Purchase event from payment_complete hook.
	 *
	 * @param int $order_id Order ID.
	 */
	public function handle_payment_complete( int $order_id ): void {
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return;
		}

		$this->process_purchase( $order );
	}

	/**
	 * Core purchase processing — all guards + queue.
	 *
	 * @param \WC_Order $order Order object.
	 */
	private function process_purchase( \WC_Order $order ): void {
		$order_id = $order->get_id();

		// ── Guard 1: Write-Once Dedup Lock ──────────────────────────────────
		// Prevents double-send from async gateways (bKash, SSLCommerz etc.)
		// that fire both woocommerce_payment_complete AND woocommerce_order_status_processing.
		$already_sent = $order->get_meta( '_capiflow_purchase_sent', true );
		if ( ! empty( $already_sent ) ) {
			return;
		}

		// ── Guard 1a: Pre-Installation Order Filter & Age Protection ─────────────
		// Orders created BEFORE CapiFlow was installed or >7 days ago cannot be
		// attributed properly to current Meta ad exposure and lack checkout context.
		// Meta CAPI also strictly rejects event_time >7 days in the past.
		$installed_at = (int) get_option( 'capiflow_installed_at', 0 );
		if ( 0 === $installed_at ) {
			$installed_at = time();
			add_option( 'capiflow_installed_at', $installed_at );
		}

		$order_created = $order->get_date_created();
		$created_ts    = $order_created ? $order_created->getTimestamp() : 0;

		if ( $created_ts > 0 ) {
			// Rule 1: Order created BEFORE plugin installation.
			$is_pre_install = ( $installed_at > 0 && $created_ts < $installed_at );

			// Rule 2: Order created > 7 days ago (Meta CAPI attribution/window limit).
			$is_expired_age = ( ( time() - $created_ts ) > ( 7 * DAY_IN_SECONDS ) );

			// Rule 3: Order lacks any CapiFlow tracking context AND was created prior to current request (pre-existing order fallback).
			$has_no_context = empty( $order->get_meta( '_capiflow_client_ip', true ) )
				&& empty( $order->get_meta( '_capiflow_user_agent', true ) )
				&& empty( $order->get_meta( '_capiflow_source_url', true ) )
				&& empty( $order->get_meta( '_capiflow_fbp', true ) );
			$is_untracked_pre_order = $has_no_context && ( $created_ts < ( time() - 300 ) );

			if ( $is_pre_install || $is_expired_age || $is_untracked_pre_order ) {
				$reason = $is_pre_install ? 'created before CapiFlow installation' : ( $is_expired_age ? 'created >7 days ago' : 'pre-existing order without tracking context' );
				Logger::log(
					'Purchase',
					'',
					null,
					0,
					sprintf( 'Order #%d: Skipped Purchase event (%s).', $order_id, $reason ),
					'info'
				);

				// Lock the order so it is not re-evaluated on future status transitions.
				$order->update_meta_data( '_capiflow_purchase_sent', 'pre_install_skip' );
				$order->save_meta_data();
				return;
			}
		}

		// ── Guard 1b: Consent Enforcement ─────────────────────────────────────
		// Only blocks if the consent toggle is enabled in Settings.
		// Option A behavior: no CMP = events continue normally (default true).
		if ( get_option( 'capiflow_consent_enforcement', false ) ) {
			if ( ! apply_filters( 'capiflow_has_tracking_consent', true ) ) {
				Logger::log( 'Purchase', '', null, 0,
					sprintf( 'Order #%d: Consent not granted — skipping CAPI.', $order_id ),
					'info'
				);
				return;
			}
		}

		// ── Read admin override ONCE (used by both COD Gate and Fraud Gate) ──
		$admin_override = $order->get_meta( '_capiflow_admin_override', true );

		// ── Guard 2: COD Purchase Gate ──────────────────────────────────────
		// Meta official: "Only fire Purchase when the transaction is definitively
		// finalized. Do not trigger it for pending, cancelled, or failed orders."
		// COD orders are not finalized until admin confirms → hold Purchase.
		// PRO: overrides filter to disable gate (fires immediately + negative on cancel).
		$cod_gate = (bool) apply_filters( 'capiflow_cod_gate_enabled', true );

		if ( $cod_gate && 'cod' === $order->get_payment_method() ) {
			// Allow COD Purchase when order reaches 'confirmed' OR 'completed'.
			// 'completed' = safety net for stores that don't use our custom 'confirmed' status.
			// Admin override also bypasses the gate.
			$allowed_statuses = array( 'confirmed', 'completed' );
			if ( empty( $admin_override ) && ! in_array( $order->get_status(), $allowed_statuses, true ) ) {
				Logger::log( 'Purchase', '', null, 0,
					sprintf( 'Order #%d: COD gate — holding Purchase until confirmed/completed.', $order_id ),
					'info'
				);
				return;
			}
		}

		// ── Guard 3: Minimum Value Threshold ────────────────────────────────
		$min_value = (float) apply_filters( 'capiflow_purchase_min_value', 0 );
		if ( $min_value > 0 && (float) $order->get_total() < $min_value ) {
			Logger::warning( 'Purchase', 'Below minimum value threshold for order #' . $order_id );
			return;
		}

		// ── Guard 4: Fraud check — respect admin override for gating ────────
		// FREE: no capiflow_is_order_gated filter registered → always false → CAPI fires.
		// PRO: Fraud_Gating registers at P20 → checks reputation + blacklist.
		// Admin override ignores the gating RESULT (already read above).

		// Run fraud gating filter.
		$is_gated = (bool) apply_filters( 'capiflow_is_order_gated', false, $order );

		if ( ! empty( $admin_override ) ) {
			// Admin explicitly confirmed — IGNORE gating result.
			Logger::log( 'Purchase', '', null, 0,
				sprintf(
					'Order #%d: Admin #%d override — fraud gate bypassed (gated: %s).',
					$order_id,
					(int) $admin_override,
					$is_gated ? 'yes' : 'no'
				),
				'info'
			);
			// Continue to Purchase CAPI — do NOT return.
		} else {
			// No admin override — respect gating result normally.
			if ( $is_gated ) {
				Logger::warning( 'Purchase', 'Order #' . $order_id . ' gated by fraud check — skipping CAPI.' );
				// Do NOT set dedup lock — admin may confirm later.
				return;
			}
		}

		// ── Guard 5: Order Source Whitelist ────────────────────────────────
		// Only fire CAPI for orders that originated from a tracked channel.
		// Default: 'checkout' (browser) + 'store-api' (Blocks checkout).
		// Admin-created and marketplace orders are opt-in — default OFF prevents
		// polluting Meta ad attribution data with off-platform sales.
		$allowed_sources = array( 'checkout', 'store-api' );

		// Option: include admin-panel orders (manual creation, WooAdmin).
		if ( get_option( 'capiflow_track_admin_orders', false ) ) {
			$allowed_sources[] = 'admin';
		}

		// Option: include marketplace/external channel orders.
		if ( get_option( 'capiflow_track_marketplace_orders', false ) ) {
			$allowed_sources = array_merge(
				$allowed_sources,
				array( 'rest-api', 'wplister_ebay', 'import', 'pos', 'amazon' )
			);
		}

		/**
		 * Filter the allowed order sources for Purchase event firing.
		 *
		 * @param string[]  $allowed_sources Whitelisted created_via values.
		 * @param \WC_Order $order           Order being evaluated.
		 */
		$allowed_sources = (array) apply_filters( 'capiflow_allowed_order_sources', $allowed_sources, $order );

		$created_via = $order->get_created_via(); // e.g. 'checkout', 'admin', 'rest-api', 'ebay'.

		// Only block if created_via is set AND not in the whitelist.
		// Empty created_via (legacy/unknown) is allowed through — safe default.
		if ( ! empty( $created_via ) && ! in_array( $created_via, $allowed_sources, true ) ) {
			Logger::log(
				'Purchase',
				'',
				null,
				0,
				sprintf( 'Order #%d: Skipped — created via "%s" not in allowed sources.', $order_id, $created_via ),
				'info'
			);
			// Set dedup lock so we don't re-evaluate on every status transition.
			$order->update_meta_data( '_capiflow_purchase_sent', 'source_skip' );
			$order->save_meta_data();
			return;
		}

		// ── Guard 6: Developer Filter ─────────────────────────────────────
		// Clean escape hatch for custom integrations. Return false to suppress CAPI.
		// No lock is set — allows retry via re-hooking or order note + status change.
		if ( ! (bool) apply_filters( 'capiflow_should_track_purchase', true, $order ) ) {
			Logger::log(
				'Purchase',
				'',
				null,
				0,
				sprintf( 'Order #%d: Suppressed by capiflow_should_track_purchase filter.', $order_id ),
				'info'
			);
			return;
		}


		// ── Build Event ID ───────────────────────────────────────────────────
		// Use existing event_id if Pixel_Injector already created one (dedup with browser pixel).
		$event_id = $order->get_meta( '_capiflow_purchase_event_id', true );
		if ( empty( $event_id ) ) {
			$event_id = Event_ID_Generator::generate();
			$order->update_meta_data( '_capiflow_purchase_event_id', $event_id );
			$order->save_meta_data();
		}

		// ── Retrieve stored customer context ─────────────────────────────────
		// Context_Capture stored these at checkout form submit time.
		$context = array(
			'client_ip'   => $order->get_meta( '_capiflow_client_ip', true ) ?: '',
			'user_agent'  => $order->get_meta( '_capiflow_user_agent', true ) ?: '',
			'fbc'         => $order->get_meta( '_capiflow_fbc', true ) ?: '',
			'fbp'         => $order->get_meta( '_capiflow_fbp', true ) ?: '',
			'external_id' => Cookie_Manager::get_external_id_for_order( $order ),
		);

		// ── Build payload ─────────────────────────────────────────────────────
		// Event_Builder handles PII hashing, value formatting, SKU extraction.
		$builder  = new Event_Builder();
		$payload  = $builder->build_purchase_event( $order, $event_id, $context );

		if ( empty( $payload ) ) {
			Logger::error( 'Purchase', 'Empty payload for order #' . $order_id );
			return; // No lock set — next status transition can retry.
		}

		// ── action_source: smart detection ───────────────────────────────────
		// If order has stored customer context (IP/UA/fbp from checkout),
		// the purchase originated from the website — admin is just confirming.
		// Only truly system-originated orders (CLI import, API) get 'system_generated'.
		$has_customer_context = ! empty( $context['client_ip'] ) || ! empty( $context['user_agent'] ) || ! empty( $context['fbp'] );
		$payload['action_source'] = $has_customer_context ? 'website' : $this->detect_action_source();

		// ── Resolve pixel ID (Pro multi-pixel via filter) ─────────────────────
		$first_product_id = 0;
		$order_items      = $order->get_items();
		if ( ! empty( $order_items ) ) {
			$first_item       = array_values( $order_items )[0];
			$first_product_id = $first_item ? $first_item->get_product_id() : 0;
		}
		$pixel_id = apply_filters( 'capiflow_resolve_pixel_id', get_option( 'capiflow_pixel_id', '' ), $first_product_id );

		// ── Queue the event ───────────────────────────────────────────────────
		$loader = capiflow();
		if ( $loader->queue ) {
			$insert_id = $loader->queue->enqueue(
				'Purchase',
				$event_id,
				time(),
				$pixel_id,
				$order_id,
				$payload
			);

			if ( false === $insert_id ) {
				Logger::error( 'Purchase', 'Queue insert failed for order #' . $order_id . ' — no lock set, retry possible.' );
				return; // No lock — retry possible on next status change.
			}
		}

		// ── Dedup Lock — set ONLY after confirmed enqueue ─────────────────────
		// Lock moved here from before payload build. If enqueue fails above,
		// no lock is set, allowing retry on next status transition.
		// Pro compatibility: Fraud_Gating::put_on_hold() deletes this meta
		// in COD Strict mode — still works because gated orders never reach here.
		$order->update_meta_data( '_capiflow_purchase_sent', gmdate( 'Y-m-d H:i:s' ) );

		// Store pixel_id used (for future Pro negative event pixel inheritance).
		$order->update_meta_data( '_capiflow_pixel_id', $pixel_id );
		$order->save_meta_data();

		Logger::log( 'Purchase', $event_id, null, 0, 'Queued for order #' . $order_id, 'info' );

		// ── Deferred analytics (Audit C3 — race condition fix) ───────────────
		// Schedule 5 minutes later so Pixel_Injector has time to set _capiflow_pixel_fired
		// on the thank-you page. Analytics_Tracker::record() reads that flag.
		if ( function_exists( 'as_schedule_single_action' ) ) {
			as_schedule_single_action(
				time() + ( 5 * MINUTE_IN_SECONDS ),
				'capiflow_record_purchase_analytics',
				array( $order_id ),
				'capiflow'
			);
		}

		// ── Pro extensibility hooks ───────────────────────────────────────────
		do_action( 'capiflow_purchase_queued', $order_id, $event_id, $payload );
	}

	/**
	 * Detect action_source dynamically.
	 *
	 * Admin / Cron / CLI / REST webhook = system_generated.
	 * Browser checkout = website.
	 *
	 * @return string 'website' or 'system_generated'.
	 */
	private function detect_action_source(): string {
		if ( is_admin() && ! wp_doing_ajax() ) {
			return 'system_generated';
		}

		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			return 'system_generated';
		}

		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
			return 'system_generated';
		}

		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			// Our own proxy endpoint is browser-initiated.
			$route = isset( $GLOBALS['wp']->query_vars['rest_route'] )
				? $GLOBALS['wp']->query_vars['rest_route']
				: '';

			if ( strpos( $route, '/capiflow/' ) !== false || strpos( $route, '/wc/store/' ) !== false ) {
				return 'website';
			}

			return 'system_generated';
		}

		return 'website';
	}
}
