<?php
/**
 * Coupon abuse detection module.
 *
 * @package TrustLens\Modules
 * @since   1.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Coupons module class.
 *
 * Detects coupon abuse patterns:
 * - First-order coupon abuse (new account + coupon + refund)
 * - Excessive coupon usage
 * - Coupon stacking patterns
 * - Multi-account coupon abuse (Pro)
 * - New account discount fraud (Pro)
 *
 * @since 1.0.0
 */
class TrustLens_Module_Coupons extends TrustLens_Module {

	/**
	 * Module ID.
	 *
	 * @var string
	 */
	protected string $id = 'coupons';

	/**
	 * Module name.
	 *
	 * @var string
	 */
	protected string $name = 'Coupon Abuse Detection';

	/**
	 * Module description.
	 *
	 * @var string
	 */
	protected string $description = 'Tracks coupon usage patterns and identifies abuse including multi-account fraud.';

	/**
	 * Is Pro feature.
	 *
	 * @var bool
	 */
	protected bool $is_pro = false;

	/**
	 * Register hooks.
	 *
	 * @since 1.0.0
	 */
	public function register_hooks(): void {
		// Track coupon usage when order is created.
		add_action( 'woocommerce_checkout_order_created', array( $this, 'handle_order_coupons' ), 20 );

		// Track coupon-then-refund pattern.
		add_action( 'woocommerce_order_refunded', array( $this, 'handle_coupon_refund' ), 20, 2 );

		// Advanced: Validate coupon on checkout (Pro).
		add_filter( 'woocommerce_coupon_is_valid', array( $this, 'validate_coupon_abuse' ), 20, 3 );

		// Advanced: Track first-order coupons across linked accounts (Pro).
		add_action( 'trustlens/linked_accounts_detected', array( $this, 'check_linked_account_coupons' ), 10, 3 );
	}

	/**
	 * Handle coupon usage on order creation.
	 *
	 * @since 1.0.0
	 * @param WC_Order $order Order object.
	 */
	public function handle_order_coupons( WC_Order $order ): void {
		$coupons = $order->get_coupon_codes();

		if ( empty( $coupons ) ) {
			return;
		}

		$customer_key = wstl_get_customer_key( $order );
		$customer = wstl_get_customer( $customer_key['email_hash'] );

		if ( ! $customer ) {
			return;
		}

		// total_orders counts prior *completed* orders and is incremented on
		// order completion, not creation — so at this point (order created) a
		// genuine first order has no prior orders. Using === 0 (not <= 1) avoids
		// mis-flagging a customer's second order as their first.
		$is_first_order = ( 0 === (int) $customer->total_orders );
		$coupon_count = count( $coupons );
		$discount_total = (float) $order->get_discount_total();

		// Update customer coupon stats.
		$this->update_coupon_stats( $customer_key['email_hash'], $coupon_count, $is_first_order );

		// Log event.
		$this->log_event(
			$customer_key['email_hash'],
			'coupon_used',
			array(
				'order_id'       => $order->get_id(),
				'coupons'        => $coupons,
				'discount_total' => $discount_total,
				'is_first_order' => $is_first_order,
			),
			$order->get_id()
		);

		// Queue score recalculation if suspicious pattern detected.
		if ( $is_first_order || $coupon_count > 1 ) {
			$this->queue_score_update( $customer_key['email_hash'], 30 );
		}
	}

	/**
	 * Handle refund on order with coupons.
	 *
	 * @since 1.0.0
	 * @param int $order_id  Order ID.
	 * @param int $refund_id Refund ID.
	 */
	public function handle_coupon_refund( int $order_id, int $refund_id ): void {
		$order = wc_get_order( $order_id );

		if ( ! $order ) {
			return;
		}

		$coupons = $order->get_coupon_codes();

		if ( empty( $coupons ) ) {
			return;
		}

		$customer_key = wstl_get_customer_key( $order );

		// Increment coupon-then-refund counter.
		$repo = new TrustLens_Customer_Repository();
		$repo->increment( $customer_key['email_hash'], 'coupon_then_refund' );

		// Log event.
		$this->log_event(
			$customer_key['email_hash'],
			'coupon_refund',
			array(
				'order_id'  => $order_id,
				'refund_id' => $refund_id,
				'coupons'   => $coupons,
			),
			$order_id
		);

		// Queue score update.
		$this->queue_score_update( $customer_key['email_hash'], 30 );
	}

	/**
	 * Update customer coupon stats.
	 *
	 * @since 1.0.0
	 * @param string $email_hash     Customer email hash.
	 * @param int    $coupon_count   Number of coupons used.
	 * @param bool   $is_first_order Whether this is customer's first order.
	 */
	private function update_coupon_stats( string $email_hash, int $coupon_count, bool $is_first_order ): void {
		global $wpdb;

		$table = $wpdb->prefix . 'trustlens_customers';

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, safe.
		$wpdb->query( $wpdb->prepare(
			"UPDATE {$table}
			 SET
			     total_coupons_used = total_coupons_used + %d,
			     first_order_coupons = first_order_coupons + %d,
			     updated_at = %s
			 WHERE email_hash = %s",
			$coupon_count,
			$is_first_order ? $coupon_count : 0,
			current_time( 'mysql' ),
			$email_hash
		) );
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
	}

	/**
	 * Calculate signal score.
	 *
	 * @since 1.0.0
	 * @param string $email_hash Customer email hash.
	 * @return array Signal data.
	 */
	public function get_signal( string $email_hash, ?object $customer = null ): array {
		$customer = $customer ?? wstl_get_customer( $email_hash );

		if ( ! $customer ) {
			return array( 'score' => 0, 'reason' => '' );
		}

		$score = 0;
		$reasons = array();

		$total_coupons = (int) $customer->total_coupons_used;
		$first_order_coupons = (int) $customer->first_order_coupons;
		$coupon_refunds = (int) $customer->coupon_then_refund;
		$total_orders = (int) $customer->total_orders;

		// Coupon-then-refund pattern (strong abuse indicator).
		if ( $coupon_refunds >= 3 ) {
			$score -= 25;
			$reasons[] = sprintf(
				/* translators: %d: number of refunded coupon orders */
				__( '%d coupon orders refunded (abuse pattern)', 'trustlens' ),
				$coupon_refunds
			);
		} elseif ( $coupon_refunds >= 2 ) {
			$score -= 15;
			$reasons[] = sprintf(
				/* translators: %d: number of refunded coupon orders */
				__( '%d coupon orders refunded', 'trustlens' ),
				$coupon_refunds
			);
		} elseif ( $coupon_refunds >= 1 ) {
			$score -= 5;
		}

		// First-order coupon abuse.
		if ( $first_order_coupons > 0 && $coupon_refunds > 0 ) {
			$score -= 10;
			$reasons[] = __( 'First-order coupon abuse pattern', 'trustlens' );
		}

		// High coupon usage rate.
		if ( $total_orders >= 5 ) {
			$coupon_rate = ( $total_coupons / $total_orders ) * 100;

			if ( $coupon_rate >= 80 ) {
				$score -= 10;
				$reasons[] = sprintf(
					/* translators: %.0f: coupon usage percentage */
					__( 'High coupon usage: %.0f%% of orders', 'trustlens' ),
					$coupon_rate
				);
			}
		}

		// Positive: Uses coupons legitimately without refunds.
		if ( $total_coupons >= 3 && $coupon_refunds === 0 ) {
			$score += 5;
			$reasons[] = __( 'Legitimate coupon user', 'trustlens' );
		}

		return array(
			'score'  => $score,
			'reason' => implode( '; ', $reasons ),
		);
	}

	/**
	 * Get settings fields.
	 *
	 * @since 1.0.0
	 * @return array Settings fields.
	 */
	public function get_settings_fields(): array {
		return array(
			array(
				'id'      => 'coupons_block_linked_abuse',
				'title'   => __( 'Block linked account coupon abuse', 'trustlens' ),
				'type'    => 'checkbox',
				'default' => false,
				'desc'    => __( 'Block checkout if first-order coupon detected on linked account (Pro)', 'trustlens' ),
			),
			array(
				'id'      => 'coupons_max_first_order',
				'title'   => __( 'Max first-order coupons', 'trustlens' ),
				'type'    => 'number',
				'default' => 2,
				'desc'    => __( 'Maximum times a customer can use first-order coupons before flagging', 'trustlens' ),
			),
		);
	}

	/**
	 * Validate coupon for abuse patterns (Pro).
	 *
	 * @since 1.2.0
	 * @param bool      $valid  Is coupon valid.
	 * @param WC_Coupon $coupon Coupon object.
	 * @param mixed     $discounts Discounts object.
	 * @return bool
	 */
	public function validate_coupon_abuse( bool $valid, WC_Coupon $coupon, $discounts ): bool {
		if ( ! $valid ) {
			return $valid;
		}

		// Only run advanced checks if Pro is active.
		if ( ! $this->is_pro_active() ) {
			return $valid;
		}

		// Check if this is a first-order coupon.
		if ( ! $this->is_first_order_coupon( $coupon ) ) {
			return $valid;
		}

		// Get current customer email.
		$email = WC()->checkout ? WC()->checkout->get_value( 'billing_email' ) : '';
		if ( ! $email && WC()->customer ) {
			$email = WC()->customer->get_billing_email();
		}

		if ( ! $email ) {
			return $valid;
		}

		$email_hash = wstl_get_email_hash( $email );

		// Check if customer has linked accounts that have used first-order coupons.
		if ( get_option( 'trustlens_coupons_block_linked_abuse', false ) ) {
			$linked_abuse = $this->check_linked_account_coupon_abuse( $email_hash, $coupon->get_code() );

			if ( $linked_abuse ) {
				// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- WooCommerce escapes exception messages when displaying.
				throw new Exception(
					__( 'This coupon cannot be used. Please contact support if you believe this is an error.', 'trustlens' )
				);
				// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
			}
		}

		return $valid;
	}

	/**
	 * Check if coupon is a first-order type.
	 *
	 * @since 1.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 * @return bool
	 */
	private function is_first_order_coupon( WC_Coupon $coupon ): bool {
		$code = strtolower( $coupon->get_code() );

		// Check common first-order coupon patterns.
		$first_order_patterns = array(
			'first',
			'welcome',
			'new',
			'newcustomer',
			'signup',
			'register',
		);

		$is_first_order = false;
		foreach ( $first_order_patterns as $pattern ) {
			if ( strpos( $code, $pattern ) !== false ) {
				$is_first_order = true;
				break;
			}
		}

		// A per-user usage limit of 1 is the other strong first-order signal.
		if ( ! $is_first_order && 1 === $coupon->get_usage_limit_per_user() ) {
			$is_first_order = true;
		}

		/**
		 * Filter whether a coupon is treated as a first-order coupon.
		 *
		 * Replaces a former read of an undocumented '_trustlens_first_order_coupon'
		 * post-meta flag that nothing ever wrote — use this filter to force a
		 * specific coupon to count (or not) as a first-order coupon.
		 *
		 * @since 1.3.8
		 * @param bool      $is_first_order Heuristic result (code pattern / usage limit).
		 * @param WC_Coupon $coupon         Coupon being evaluated.
		 */
		return (bool) apply_filters( 'trustlens/is_first_order_coupon', $is_first_order, $coupon );
	}

	/**
	 * Check linked accounts for coupon abuse.
	 *
	 * @since 1.2.0
	 * @param string $email_hash  Customer email hash.
	 * @param string $coupon_code Coupon code being used.
	 * @return bool True if abuse detected.
	 */
	private function check_linked_account_coupon_abuse( string $email_hash, string $coupon_code ): bool {
		global $wpdb;

		// Get linked accounts.
		$customer = wstl_get_customer( $email_hash );

		if ( ! $customer || ! $customer->shipping_address_hash ) {
			return false;
		}

		// Find other customers with same shipping address.
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, safe.
		$linked_hashes = $wpdb->get_col( $wpdb->prepare(
			"SELECT email_hash FROM {$wpdb->prefix}trustlens_customers
			 WHERE shipping_address_hash = %s
			   AND email_hash != %s
			   AND first_order_coupons > 0",
			$customer->shipping_address_hash,
			$email_hash
		) );
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

		if ( empty( $linked_hashes ) ) {
			return false;
		}

		// Check if any linked account used this or similar first-order coupons.
		foreach ( $linked_hashes as $linked_hash ) {
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, safe.
			$events = $wpdb->get_results( $wpdb->prepare(
				"SELECT event_data FROM {$wpdb->prefix}trustlens_events
				 WHERE email_hash = %s
				   AND event_type = 'coupon_used'
				 ORDER BY created_at DESC
				 LIMIT 10",
				$linked_hash
			) );
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

			foreach ( $events as $event ) {
				$data = json_decode( $event->event_data, true );
				if ( isset( $data['is_first_order'] ) && $data['is_first_order'] ) {
					// First-order coupon used by linked account.
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Handle linked account detection for coupon tracking.
	 *
	 * @since 1.2.0
	 * @param string $email_hash      Primary email hash.
	 * @param array  $linked_hashes   Array of linked email hashes.
	 * @param array  $linked_data     Full linked account data (unused).
	 */
	public function check_linked_account_coupons( string $email_hash, array $linked_hashes, array $linked_data = array() ): void {
		$linked_accounts = $linked_hashes; // Use the hashes array.
		global $wpdb;

		// Calculate total first-order coupons across all linked accounts.
		$total_first_order = 0;

		foreach ( $linked_accounts as $linked_hash ) {
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, safe.
			$first_order = $wpdb->get_var( $wpdb->prepare(
				"SELECT first_order_coupons FROM {$wpdb->prefix}trustlens_customers
				 WHERE email_hash = %s",
				$linked_hash
			) );
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
			$total_first_order += (int) $first_order;
		}

		// Also count current customer.
		$customer = wstl_get_customer( $email_hash );
		if ( $customer ) {
			$total_first_order += (int) $customer->first_order_coupons;
		}

		// If total exceeds threshold, log event and update score.
		$max_allowed = (int) get_option( 'trustlens_coupons_max_first_order', 2 );

		if ( $total_first_order > $max_allowed ) {
			$this->log_event(
				$email_hash,
				'multi_account_coupon_abuse',
				array(
					'linked_accounts'      => $linked_accounts,
					'total_first_order'    => $total_first_order,
					'threshold'            => $max_allowed,
				)
			);

			// Queue score update.
			$this->queue_score_update( $email_hash, 10 );
		}
	}

	/**
	 * Get coupon abuse stats for analytics.
	 *
	 * @since 1.2.0
	 * @return array Stats.
	 */
	public static function get_coupon_abuse_stats(): array {
		global $wpdb;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate stats query, table name from $wpdb->prefix is safe.
		$stats = $wpdb->get_row(
			"SELECT
				SUM(total_coupons_used) as total_coupons,
				SUM(first_order_coupons) as first_order_coupons,
				SUM(coupon_then_refund) as coupon_refunds,
				COUNT(CASE WHEN coupon_then_refund >= 2 THEN 1 END) as repeat_abusers
			 FROM {$wpdb->prefix}trustlens_customers"
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		// Get multi-account abuse events.
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate count query, table name from $wpdb->prefix is safe.
		$multi_abuse = $wpdb->get_var(
			"SELECT COUNT(*) FROM {$wpdb->prefix}trustlens_events
			 WHERE event_type = 'multi_account_coupon_abuse'"
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return array(
			'total_coupons'        => (int) ( $stats->total_coupons ?? 0 ),
			'first_order_coupons'  => (int) ( $stats->first_order_coupons ?? 0 ),
			'coupon_refunds'       => (int) ( $stats->coupon_refunds ?? 0 ),
			'repeat_abusers'       => (int) ( $stats->repeat_abusers ?? 0 ),
			'multi_account_abuse'  => (int) $multi_abuse,
		);
	}
}
