<?php
/**
 * Cart Recovery — Tracking Controller
 *
 * REST endpoints consumed by recovery mails in the wild:
 *
 *   GET /wp-json/niwa-ai/v1/cart-recovery/open/{token}
 *     → 1×1 transparent GIF + opens++ (graceful even if token unknown)
 *
 *   GET /wp-json/niwa-ai/v1/cart-recovery/click/{token}?u=<b64url>
 *     → clicks++ + same-origin-safe 302 redirect
 *     → SSRF guard: rejects non-http(s), non-same-host targets.
 *
 *   GET /wp-json/niwa-ai/v1/cart-recovery/recover/{token}
 *     → restores the captured cart snapshot into WooCommerce, then
 *       redirects to the cart page.
 *
 *   GET /wp-json/niwa-ai/v1/cart-recovery/unsubscribe/{token}
 *     → suppression insert + row expire + simple HTML confirmation page.
 *
 * Public by design (no nonce, no capability) — tokens are
 * per-recipient random 32-char values acting as the bearer secret.
 *
 * Every delivery path is isolated behind a protected method so tests
 * can subclass + capture responses without actually sending headers
 * or exiting.
 *
 * @package Niwa_AI
 * @since   0.12.0
 */

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

class Niwa_AI_Cart_Recovery_Tracking_Controller {

	private const GIF_1X1 = "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\xFF\xFF\xFF\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B";

	public function __construct(
		private readonly Niwa_AI_Cart_Recovery_Repository $repository
	) {}

	public function register_hooks(): void {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	public function register_routes(): void {
		if ( ! function_exists( 'register_rest_route' ) ) {
			return;
		}

		register_rest_route(
			Niwa_AI_Constants::REST_NS_CART_RECOVERY,
			Niwa_AI_Constants::REST_ROUTE_CR_OPEN,
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'handle_open' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'token' => array( 'sanitize_callback' => 'sanitize_key' ),
				),
			)
		);

		register_rest_route(
			Niwa_AI_Constants::REST_NS_CART_RECOVERY,
			Niwa_AI_Constants::REST_ROUTE_CR_CLICK,
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'handle_click' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'token' => array( 'sanitize_callback' => 'sanitize_key' ),
					'u'     => array( 'sanitize_callback' => 'sanitize_text_field' ),
				),
			)
		);

		register_rest_route(
			Niwa_AI_Constants::REST_NS_CART_RECOVERY,
			Niwa_AI_Constants::REST_ROUTE_CR_RECOVER,
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'handle_recover' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'token' => array( 'sanitize_callback' => 'sanitize_key' ),
				),
			)
		);

		register_rest_route(
			Niwa_AI_Constants::REST_NS_CART_RECOVERY,
			Niwa_AI_Constants::REST_ROUTE_CR_UNSUBSCRIBE,
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'handle_unsubscribe' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'token' => array( 'sanitize_callback' => 'sanitize_key' ),
				),
			)
		);
	}

	/**
	 * Open pixel. Always returns a 1×1 GIF — a silent no-op on
	 * unknown tokens so Gmail's proxy fetch doesn't spam server logs.
	 *
	 * @param mixed $request
	 */
	public function handle_open( $request ): void {
		$token = $this->read_token( $request );
		if ( '' !== $token ) {
			$row = $this->repository->find_by_unsubscribe_token( $token );
			if ( is_array( $row ) ) {
				$this->repository->increment_opens( (int) ( $row['id'] ?? 0 ) );
				do_action( 'niwa_ai_cart_recovery_mail_opened', (int) ( $row['id'] ?? 0 ), $row );
			}
		}

		$this->respond_gif();
	}

	/**
	 * Click redirector with SSRF guard. Base64url-decoded target must
	 * be http(s) and same-host as `home_url()` — everything else
	 * redirects to home.
	 *
	 * @param mixed $request
	 */
	public function handle_click( $request ): void {
		$token    = $this->read_token( $request );
		$raw_u    = (string) $this->read_param( $request, 'u' );
		$target   = $this->decode_target( $raw_u );
		$fallback = $this->home_url();

		if ( '' === $target || ! $this->is_safe_target( $target ) ) {
			$this->respond_redirect( $fallback );
			return;
		}

		if ( '' !== $token ) {
			$row = $this->repository->find_by_unsubscribe_token( $token );
			if ( is_array( $row ) ) {
				$this->repository->increment_clicks( (int) ( $row['id'] ?? 0 ) );
				do_action( 'niwa_ai_cart_recovery_link_clicked', (int) ( $row['id'] ?? 0 ), $target );
			}
		}

		$this->respond_redirect( $target );
	}

	/**
	 * Restore the abandoned-cart snapshot into the current WooCommerce
	 * session, then send the shopper to the cart. If Woo/cart is not
	 * available, degrade to a plain cart redirect.
	 *
	 * @param mixed $request
	 */
	public function handle_recover( $request ): void {
		$token = $this->read_token( $request );
		if ( '' !== $token ) {
			$row = $this->repository->find_by_unsubscribe_token( $token );
			if ( is_array( $row ) ) {
				$this->restore_cart_from_row( $row );
				do_action( 'niwa_ai_cart_recovery_cart_restored', (int) ( $row['id'] ?? 0 ), $row );
			}
		}

		$this->respond_redirect( $this->cart_url() );
	}

	/**
	 * Unsubscribe handler. Inserts into suppression on any valid
	 * token match; always renders a confirmation page so spam crawlers
	 * that GET this URL early don't reveal whether the token is real.
	 *
	 * @param mixed $request
	 */
	public function handle_unsubscribe( $request ): void {
		$token = $this->read_token( $request );
		$email = '';
		$phone = '';
		if ( '' !== $token ) {
			$row = $this->repository->find_by_unsubscribe_token( $token );
			if ( is_array( $row ) ) {
				$email = (string) ( $row['email'] ?? '' );
				$phone = (string) ( $row['phone'] ?? '' );
				if ( '' !== $email ) {
					$this->repository->add_suppression( $email, 'unsubscribe_link' );
				}
				if ( '' !== $phone ) {
					$this->repository->add_phone_suppression( $phone, 'unsubscribe_link' );
				}
				$id = (int) ( $row['id'] ?? 0 );
				if ( $id > 0 ) {
					$this->repository->update_status( $id, Niwa_AI_Cart_Recovery_Repository::STATUS_EXPIRED );
				}
				do_action( 'niwa_ai_cart_recovery_unsubscribed', $email, $row );
			}
		}

		$this->respond_unsubscribe_page();
	}

	/* ===============================================================
	 * Internal helpers (protected for test overrides)
	 * ============================================================ */

	/**
	 * Is the target URL safe to redirect to? Must be http/https AND
	 * same host as the site's home URL. Same-origin is our whole
	 * click model — we don't proxy to third parties from recovery
	 * mails.
	 */
	protected function is_safe_target( string $target ): bool {
		$parts = parse_url( $target );
		if ( ! is_array( $parts ) ) {
			return false;
		}
		$scheme = strtolower( (string) ( $parts['scheme'] ?? '' ) );
		if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
			return false;
		}
		// Reject URLs with userinfo (`http://attacker.com@yoursite.com/...`
		// parses as host=yoursite.com but browsers navigate to
		// attacker.com first — classic open-redirect bypass).
		if ( ! empty( $parts['user'] ) || ! empty( $parts['pass'] ) ) {
			return false;
		}
		$host = strtolower( (string) ( $parts['host'] ?? '' ) );
		if ( '' === $host ) {
			return false;
		}
		$home_host = strtolower( (string) ( parse_url( $this->home_url(), PHP_URL_HOST ) ?? '' ) );
		if ( '' === $home_host ) {
			return false;
		}
		return $host === $home_host;
	}

	protected function decode_target( string $raw ): string {
		if ( '' === $raw ) {
			return '';
		}
		// base64url → base64.
		$pad     = strlen( $raw ) % 4;
		$padded  = $raw . ( $pad > 0 ? str_repeat( '=', 4 - $pad ) : '' );
		$decoded = base64_decode( strtr( $padded, '-_', '+/' ), true );
		if ( false === $decoded ) {
			return '';
		}
		return $decoded;
	}

	protected function read_token( $request ): string {
		$token = '';
		if ( is_object( $request ) && method_exists( $request, 'get_param' ) ) {
			$token = (string) $request->get_param( 'token' );
		} elseif ( is_array( $request ) && isset( $request['token'] ) ) {
			$token = (string) $request['token'];
		}
		return strtolower( preg_replace( '/[^a-z0-9]/i', '', $token ) ?? '' );
	}

	protected function read_param( $request, string $name ): string {
		if ( is_object( $request ) && method_exists( $request, 'get_param' ) ) {
			return (string) $request->get_param( $name );
		}
		if ( is_array( $request ) && isset( $request[ $name ] ) ) {
			return (string) $request[ $name ];
		}
		return '';
	}

	protected function home_url(): string {
		if ( function_exists( 'home_url' ) ) {
			return (string) home_url();
		}
		return 'https://example.com';
	}

	protected function cart_url(): string {
		if ( function_exists( 'wc_get_cart_url' ) ) {
			$url = wc_get_cart_url();
			if ( is_string( $url ) && '' !== trim( $url ) ) {
				return $url;
			}
		}
		return rtrim( $this->home_url(), '/' ) . '/cart/';
	}

	/**
	 * @param array<string, mixed> $row
	 */
	protected function restore_cart_from_row( array $row ): bool {
		$snapshot_json = (string) ( $row['cart_snapshot_json'] ?? '' );
		if ( '' === $snapshot_json || ! class_exists( 'Niwa_AI_Cart_Recovery_Cart_Snapshot' ) ) {
			return false;
		}

		$snapshot = Niwa_AI_Cart_Recovery_Cart_Snapshot::from_json( $snapshot_json );
		if ( null === $snapshot || $snapshot->is_empty() || ! function_exists( 'WC' ) ) {
			return false;
		}

		if ( function_exists( 'wc_load_cart' ) ) {
			wc_load_cart();
		}

		$wc = WC();
		if ( ! is_object( $wc ) || empty( $wc->cart ) || ! is_object( $wc->cart ) ) {
			return false;
		}

		$cart = $wc->cart;
		if ( method_exists( $cart, 'empty_cart' ) ) {
			$cart->empty_cart();
		}

		$restored = 0;
		foreach ( $snapshot->items as $item ) {
			if ( ! $item instanceof Niwa_AI_Cart_Recovery_Line_Item ) {
				continue;
			}
			$product_id   = (int) $item->product_id;
			$variation_id = (int) $item->variation_id;
			$qty          = max( 1, (int) $item->qty );
			if ( $product_id <= 0 || ! method_exists( $cart, 'add_to_cart' ) ) {
				continue;
			}

			$added = $cart->add_to_cart( $product_id, $qty, $variation_id > 0 ? $variation_id : 0 );
			if ( false === $added && $variation_id > 0 ) {
				// Some Woo setups require adding the concrete variation
				// product directly when the original variation attribute
				// map is no longer available in the lightweight snapshot.
				$added = $cart->add_to_cart( $variation_id, $qty );
			}
			if ( false !== $added ) {
				$restored++;
			}
		}

		if ( $restored > 0 && method_exists( $cart, 'calculate_totals' ) ) {
			$cart->calculate_totals();
		}

		if ( $restored > 0 && is_object( $wc ) && ! empty( $wc->session ) && is_object( $wc->session ) && method_exists( $wc->session, 'set_customer_session_cookie' ) ) {
			$wc->session->set_customer_session_cookie( true );
		}

		return $restored > 0;
	}

	protected function respond_gif(): void {
		if ( ! headers_sent() ) {
			header( 'Content-Type: image/gif' );
			header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
			header( 'Pragma: no-cache' );
			header( 'Content-Length: ' . strlen( self::GIF_1X1 ) );
		}
		echo self::GIF_1X1; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		exit;
	}

	protected function respond_redirect( string $url ): void {
		if ( function_exists( 'wp_safe_redirect' ) ) {
			wp_safe_redirect( $url, 302 );
			exit;
		}
		if ( ! headers_sent() ) {
			header( 'Location: ' . $url, true, 302 );
		}
		exit;
	}

	protected function respond_unsubscribe_page(): void {
		$title   = __( 'You are unsubscribed', 'niwa-ai' );
		$message = __( 'Cart recovery reminders have been disabled. You will no longer receive them by email or WhatsApp.', 'niwa-ai' );
		$locale  = function_exists( 'determine_locale' ) ? (string) determine_locale() : 'en';

		if ( ! headers_sent() ) {
			header( 'Content-Type: text/html; charset=UTF-8' );
			header( 'Cache-Control: no-store' );
		}
		echo '<!DOCTYPE html><html lang="' . esc_attr( str_replace( '_', '-', $locale ) ) . '"><head><meta charset="UTF-8"><title>'
			. esc_html( $title )
			. '</title><meta name="viewport" content="width=device-width,initial-scale=1"></head>'
			. '<body style="margin:0;padding:0;background:#f6f6f6;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#333;">'
			. '<div style="max-width:480px;margin:60px auto;background:#fff;padding:32px;border-radius:8px;text-align:center;">'
			. '<h1 style="font-size:20px;margin:0 0 16px;">' . esc_html( $title ) . '</h1>'
			. '<p style="font-size:15px;line-height:1.55;color:#555;margin:0;">' . esc_html( $message ) . '</p>'
			. '</div></body></html>';
		exit;
	}
}
