<?php
/**
 * CloudScale shared licence management component.
 *
 * Handles subscription checkout, session polling, OTP recovery, status sync,
 * and card rendering for all CloudScale plugins with a managed subscription.
 *
 * Each plugin instantiates this class with its own storage adapter (closures)
 * so the broker logic lives in one place.
 *
 * Required config keys:
 *   get_state     Closure(): array          Returns normalized licence state array.
 *   set_state     Closure(array): void      Persists normalized licence state.
 *   product       string                    Broker product: 'backup' | 'seo' | 'cyber'.
 *   nonce_name    string                    WP nonce action, e.g. 'csbr_nonce'.
 *   action_prefix string                    AJAX action prefix, e.g. 'csbr_managed'.
 *
 * Optional config keys:
 *   plugin_name      string                 Display label e.g. "Backup & Restore".
 *   on_activate      Closure(array): void   Called when status flips to active.
 *   on_deactivate    Closure(string): void  Called when status flips to cancelled/past_due.
 *   log_fn           Closure(string): void  Plugin-specific logging function.
 *   action_aliases   array<string,string>   ['legacy_action' => 'mapped_suffix'] for back-compat.
 *   paypal_client_id string                 PayPal REST API client ID (enables PayPal checkout).
 *   paypal_secret    string                 PayPal REST API secret.
 *   paypal_mode      string                 'sandbox' (default) or 'live'.
 *   paypal_amount    string                 Order amount, default '10.00'.
 *   paypal_currency  string                 ISO currency code, default 'USD'.
 *   paypal_return_url string                URL PayPal redirects to after approval.
 *   paypal_cancel_url string                URL PayPal redirects to on cancel.
 *
 * Normalized state fields (returned by get_state / passed to set_state):
 *   license_key, status, session_id, email, subscription_code,
 *   usage, limit, boost, reset_date, status_ts, subscribed_at
 *
 * DO NOT EDIT THIS FILE in individual plugin directories.
 * Edit shared-admin-ui/class-cloudscale-licence.php and run sync-admin-css.sh.
 */

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

if ( class_exists( 'CloudScale_Licence' ) ) { return; }

class CloudScale_Licence {

	const BROKER         = 'https://api.cloudscale.consulting';
	const PAYPAL_SANDBOX = 'https://api-m.sandbox.paypal.com';
	const PAYPAL_LIVE    = 'https://api-m.paypal.com';

	private array   $config;
	private Closure $getter;
	private Closure $setter;

	public function __construct( array $config ) {
		$this->config = $config;
		$this->getter = Closure::fromCallable( $config['get_state'] );
		$this->setter = Closure::fromCallable( $config['set_state'] );
	}

	// ── State ─────────────────────────────────────────────────────────────────

	public function get_state(): array {
		return ( $this->getter )();
	}

	public function get( string $field, $default = '' ) {
		return ( $this->getter )()[ $field ] ?? $default;
	}

	public function set( array $delta ): void {
		$state = ( $this->getter )();
		( $this->setter )( array_merge( $state, $delta ) );
	}

	public function clear_session(): void {
		$this->set( [ 'session_id' => '', 'status' => '' ] );
	}

	private function log( string $msg ): void {
		if ( isset( $this->config['log_fn'] ) ) {
			( $this->config['log_fn'] )( $msg );
		}
	}

	private function maybe_cache_subscribers( array $data ): void {
		if ( isset( $data['subscribers'] ) && is_int( $data['subscribers'] ) ) {
			set_transient(
				'cloudscale_subscribers_' . $this->config['product'],
				$data['subscribers'],
				12 * HOUR_IN_SECONDS
			);
		}
	}

	// ── AJAX registration ─────────────────────────────────────────────────────

	public function register_ajax(): void {
		$p     = $this->config['action_prefix'];
		$nonce = $this->config['nonce_name'];

		$map = [
			'checkout'        => [ $this, 'handle_checkout' ],
			'poll'            => [ $this, 'handle_poll' ],
			'save_key'        => [ $this, 'handle_save_key' ],
			'recover'         => [ $this, 'handle_recover' ],
			'recover_verify'  => [ $this, 'handle_recover_verify' ],
			'sync_status'     => [ $this, 'handle_sync_status' ],
			'disconnect'      => [ $this, 'handle_disconnect' ],
			'clear_pending'   => [ $this, 'handle_clear_pending' ],
			'paypal_checkout'      => [ $this, 'handle_paypal_checkout' ],
			'paypal_subscription'  => [ $this, 'handle_paypal_subscription' ],
		];

		foreach ( $map as $suffix => $cb ) {
			$action = "{$p}_{$suffix}";
			add_action( "wp_ajax_{$action}", function () use ( $cb, $nonce ) {
				if ( ! current_user_can( 'manage_options' ) ) {
					wp_send_json_error( 'Forbidden', 403 );
				}
				check_ajax_referer( $nonce, 'nonce' ); // phpcs:ignore WordPress.Security.NonceVerification -- verified here
				$cb();
			} );
		}

		// Legacy action-name aliases (back-compat for existing JS in wild).
		foreach ( $this->config['action_aliases'] ?? [] as $legacy => $suffix ) {
			if ( ! isset( $map[ $suffix ] ) ) { continue; }
			$cb = $map[ $suffix ];
			add_action( "wp_ajax_{$legacy}", function () use ( $cb, $nonce ) {
				if ( ! current_user_can( 'manage_options' ) ) {
					wp_send_json_error( 'Forbidden', 403 );
				}
				check_ajax_referer( $nonce, 'nonce' ); // phpcs:ignore WordPress.Security.NonceVerification -- verified here
				$cb();
			} );
		}
	}

	// ── Broker helpers ────────────────────────────────────────────────────────

	private function broker_get( string $path, array $params = [] ): array {
		$url  = self::BROKER . $path . ( $params ? ( '?' . http_build_query( $params ) ) : '' );
		$resp = wp_remote_get( $url, [ 'timeout' => 12 ] );
		if ( is_wp_error( $resp ) ) {
			return [ 'ok' => false, 'error' => $resp->get_error_message() ];
		}
		$data = json_decode( wp_remote_retrieve_body( $resp ), true );
		return is_array( $data ) ? $data : [ 'ok' => false, 'error' => 'Invalid broker response.' ];
	}

	private function broker_post( string $path, array $body ): array {
		$resp = wp_remote_post( self::BROKER . $path, [
			'timeout' => 20,
			'headers' => [ 'Content-Type' => 'application/json' ],
			'body'    => wp_json_encode( $body ),
		] );
		if ( is_wp_error( $resp ) ) {
			return [ 'ok' => false, 'error' => $resp->get_error_message() ];
		}
		$data = json_decode( wp_remote_retrieve_body( $resp ), true );
		return is_array( $data ) ? $data : [ 'ok' => false, 'error' => 'Invalid broker response.' ];
	}

	// ── AJAX handlers ─────────────────────────────────────────────────────────

	public function handle_checkout(): void {
		$email    = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by register_ajax
		$site_url = esc_url_raw( wp_unslash( $_POST['site_url'] ?? get_site_url() ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( ! is_email( $email ) ) {
			wp_send_json_error( [ 'message' => 'Valid email address required.' ] );
		}

		$data = $this->broker_post( '/checkout', [
			'email'    => $email,
			'site_url' => $site_url,
			'type'     => 'subscription',
			'product'  => $this->config['product'],
		] );

		if ( ! empty( $data['already_subscribed'] ) ) {
			wp_send_json_success( [ 'already_subscribed' => true, 'manage_url' => $data['manage_url'] ?? '' ] );
		}

		if ( empty( $data['checkout_url'] ) ) {
			$err = $data['error'] ?? ( $data['message'] ?? 'Checkout failed. Please try again.' );
			$this->log( '[CloudScale Licence] Checkout error: ' . $err );
			wp_send_json_error( [ 'message' => $err ] );
		}

		$this->set( [
			'session_id' => sanitize_text_field( $data['session_token'] ?? '' ),
			'email'      => $email,
			'status'     => 'pending',
		] );

		wp_send_json_success( [ 'checkout_url' => $data['checkout_url'] ] );
	}

	public function handle_paypal_checkout(): void {
		if ( ! $this->has_paypal_config() ) {
			wp_send_json_error( [ 'message' => 'PayPal not configured.' ] );
		}
		$email    = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by register_ajax
		$site_url = esc_url_raw( wp_unslash( $_POST['site_url'] ?? get_site_url() ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( ! is_email( $email ) ) {
			wp_send_json_error( [ 'message' => 'Valid email address required.' ] );
		}

		$token = $this->paypal_get_access_token();
		if ( ! $token ) {
			wp_send_json_error( [ 'message' => 'PayPal authentication failed. Please try again.' ] );
		}

		$result = $this->paypal_create_order( $token, $email );
		if ( ! $result['ok'] ) {
			$this->log( '[CloudScale Licence] PayPal order creation failed: ' . $result['error'] );
			wp_send_json_error( [ 'message' => $result['error'] ] );
		}

		$this->set( [
			'session_id' => 'paypal:' . $result['order_id'],
			'email'      => $email,
			'status'     => 'pending',
		] );

		$this->log( '[CloudScale Licence] PayPal order created: ' . $result['order_id'] );
		wp_send_json_success( [ 'checkout_url' => $result['approval_url'] ] );
	}

	public function handle_paypal_subscription(): void {
		if ( ! $this->has_paypal_config() ) {
			wp_send_json_error( [ 'message' => 'PayPal not configured.' ] );
		}
		$subscription_id = sanitize_text_field( wp_unslash( $_POST['subscription_id'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by register_ajax
		if ( ! $subscription_id ) {
			wp_send_json_error( [ 'message' => 'No subscription ID provided.' ] );
		}

		$token = $this->paypal_get_access_token();
		if ( ! $token ) {
			wp_send_json_error( [ 'message' => 'PayPal authentication failed. Please try again.' ] );
		}

		$resp = wp_remote_get( $this->paypal_base_url() . '/v1/billing/subscriptions/' . rawurlencode( $subscription_id ), [
			'timeout' => 15,
			'headers' => [ 'Authorization' => 'Bearer ' . $token ],
		] );

		if ( is_wp_error( $resp ) ) {
			wp_send_json_error( [ 'message' => 'Could not verify subscription with PayPal.' ] );
		}

		$data      = json_decode( wp_remote_retrieve_body( $resp ), true );
		$pp_status = $data['status'] ?? '';
		$plan_id   = $data['plan_id'] ?? '';

		if ( ! in_array( $pp_status, [ 'ACTIVE', 'APPROVED', 'APPROVAL_PENDING' ], true ) ) {
			$this->log( '[CloudScale Licence] PayPal subscription status unexpected: ' . $pp_status );
			wp_send_json_error( [ 'message' => 'Subscription not yet active (status: ' . esc_html( $pp_status ) . '). Please try again in a moment.' ] );
		}

		$expected_plan = $this->config['paypal_plan_id'] ?? '';
		if ( $expected_plan && $plan_id !== $expected_plan ) {
			$this->log( '[CloudScale Licence] PayPal plan mismatch: ' . $plan_id );
			wp_send_json_error( [ 'message' => 'Subscription plan mismatch. Please contact support.' ] );
		}

		$subscriber_email = sanitize_email( $data['subscriber']['email_address'] ?? $this->get( 'email' ) );

		// Ask broker to activate — broker must handle payment_method=paypal.
		$result = $this->broker_post( '/checkout', [
			'email'             => $subscriber_email,
			'site_url'          => get_site_url(),
			'type'              => 'subscription',
			'product'           => $this->config['product'],
			'payment_method'    => 'paypal',
			'payment_ref'       => $subscription_id,
			'subscription_code' => $subscription_id,
		] );

		if ( ! empty( $result['license_key'] ) ) {
			$delta = [
				'license_key'       => sanitize_text_field( $result['license_key'] ),
				'status'            => 'active',
				'session_id'        => '',
				'email'             => $subscriber_email,
				'subscription_code' => $subscription_id,
				'subscribed_at'     => sanitize_text_field( $result['created_at'] ?? '' ),
			];
			$this->set( $delta );
			if ( isset( $this->config['on_activate'] ) ) {
				( $this->config['on_activate'] )( array_merge( $this->get_state(), $delta ) );
			}
			$this->log( '[CloudScale Licence] PayPal subscription activated: ' . $subscription_id );
			wp_send_json_success( [ 'status' => 'active', 'license_key' => $delta['license_key'] ] );
		}

		// Broker not yet updated for PayPal — store subscription ID for tracking.
		$this->set( [
			'session_id'        => 'paypal_sub:' . $subscription_id,
			'status'            => 'pending',
			'subscription_code' => $subscription_id,
			'email'             => $subscriber_email,
		] );
		$this->log( '[CloudScale Licence] PayPal subscription pending broker activation: ' . $subscription_id );
		wp_send_json_success( [ 'status' => 'pending_broker', 'subscription_id' => $subscription_id ] );
	}

	public function handle_poll(): void {
		$session = $this->get( 'session_id' );
		if ( ! $session ) {
			wp_send_json_error( [ 'message' => 'No pending checkout session.' ] );
		}

		// PayPal session: activation is handled by the return-URL hook in the plugin.
		if ( 0 === strpos( $session, 'paypal:' ) ) {
			if ( 'active' === $this->get( 'status' ) && $this->get( 'license_key' ) ) {
				wp_send_json_success( [ 'status' => 'active', 'license_key' => $this->get( 'license_key' ) ] );
			}
			wp_send_json_success( [ 'status' => 'pending' ] );
		}

		$data   = $this->broker_get( '/status', [ 'session_token' => $session, 'product' => $this->config['product'] ] );
		$status = $data['status'] ?? 'pending';
		$this->maybe_cache_subscribers( $data );

		if ( 'active' === $status && ! empty( $data['license_key'] ) ) {
			$delta = [
				'license_key'       => sanitize_text_field( $data['license_key'] ),
				'status'            => 'active',
				'session_id'        => '',
				'usage'             => (int) ( $data['monthly_requests'] ?? 0 ),
				'limit'             => (int) ( $data['monthly_limit'] ?? 200 ),
				'boost'             => (int) ( $data['boost_balance'] ?? 0 ),
				'reset_date'        => sanitize_text_field( $data['reset_date'] ?? '' ),
				'subscription_code' => sanitize_text_field( $data['subscription_code'] ?? '' ),
				'subscribed_at'     => sanitize_text_field( $data['created_at'] ?? '' ),
				'status_ts'         => time(),
			];
			$this->set( $delta );
			if ( isset( $this->config['on_activate'] ) ) {
				( $this->config['on_activate'] )( $delta );
			}
			$this->log( '[CloudScale Licence] Subscription activated.' );
			wp_send_json_success( [ 'status' => 'active', 'license_key' => $delta['license_key'] ] );
		}

		wp_send_json_success( [ 'status' => $status ] );
	}

	public function handle_save_key(): void {
		$key = preg_replace( '/[^a-f0-9]/', '', strtolower( wp_unslash( $_POST['license_key'] ?? '' ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by register_ajax
		if ( strlen( $key ) !== 32 ) {
			wp_send_json_error( [ 'message' => 'Licence key must be 32 hex characters.' ] );
		}

		$data   = $this->broker_get( '/status', [ 'key' => $key, 'product' => $this->config['product'] ] );
		$status = $data['status'] ?? '';
		$this->maybe_cache_subscribers( $data );

		if ( 'active' !== $status ) {
			wp_send_json_error( [ 'message' => $data['error'] ?? "Licence status: {$status}. Please check the key and try again." ] );
		}

		$this->set( [
			'license_key'       => $key,
			'status'            => 'active',
			'session_id'        => '',
			'subscription_code' => sanitize_text_field( $data['subscription_code'] ?? '' ),
			'usage'             => (int) ( $data['monthly_requests'] ?? 0 ),
			'limit'             => (int) ( $data['monthly_limit'] ?? 200 ),
			'boost'             => (int) ( $data['boost_balance'] ?? 0 ),
			'reset_date'        => sanitize_text_field( $data['reset_date'] ?? '' ),
			'status_ts'         => time(),
		] );
		$this->log( '[CloudScale Licence] Licence key saved manually.' );
		wp_send_json_success( [ 'status' => 'active' ] );
	}

	public function handle_recover(): void {
		$email = sanitize_email( wp_unslash( $_POST['email'] ?? $this->get( 'email' ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by register_ajax
		if ( ! is_email( $email ) ) {
			wp_send_json_error( [ 'message' => 'Valid email address required.' ] );
		}

		$data = $this->broker_post( '/recover', [ 'email' => $email, 'product' => $this->config['product'] ] );

		if ( ! empty( $data['error'] ) ) {
			wp_send_json_error( [ 'message' => $data['error'] ] );
		}

		$this->set( [ 'email' => $email ] );
		$this->log( '[CloudScale Licence] Recovery code requested for ' . $email );
		wp_send_json_success( [ 'otp_sent' => true ] );
	}

	public function handle_recover_verify(): void {
		$email = sanitize_email( wp_unslash( $_POST['email'] ?? $this->get( 'email' ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by register_ajax
		$code  = preg_replace( '/[^0-9]/', '', wp_unslash( $_POST['code'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing

		if ( ! is_email( $email ) || strlen( $code ) !== 6 ) {
			wp_send_json_error( [ 'message' => 'Enter the 6-digit code from your email.' ] );
		}

		$data = $this->broker_post( '/recover/verify', [
			'email'   => $email,
			'product' => $this->config['product'],
			'code'    => $code,
		] );

		if ( empty( $data['license_key'] ) ) {
			wp_send_json_error( [ 'message' => $data['error'] ?? 'No active subscription found for that email.' ] );
		}

		$this->set( [
			'license_key'       => sanitize_text_field( $data['license_key'] ),
			'status'            => 'active',
			'session_id'        => '',
			'email'             => $email,
			'subscription_code' => sanitize_text_field( $data['subscription_code'] ?? '' ),
		] );
		$this->log( '[CloudScale Licence] Licence recovered via email OTP.' );
		wp_send_json_success( [ 'message' => 'Licence reconnected.' ] );
	}

	public function handle_sync_status(): void {
		$key = $this->get( 'license_key' );
		if ( ! $key ) {
			wp_send_json_error( [ 'message' => 'No licence key stored.' ] );
		}

		$data   = $this->broker_get( '/status', [ 'key' => $key, 'product' => $this->config['product'] ] );
		$status = $data['status'] ?? '';
		$this->maybe_cache_subscribers( $data );

		// Licence deleted on the proxy side (refunded/removed).
		if ( ! empty( $data['error'] ) && false !== stripos( $data['error'], 'not found' ) ) {
			$this->set( [ 'status' => 'cancelled', 'license_key' => '' ] );
			if ( isset( $this->config['on_deactivate'] ) ) {
				( $this->config['on_deactivate'] )( 'cancelled' );
			}
			wp_send_json_success( [ 'status' => 'cancelled' ] );
		}

		if ( '' !== $status ) {
			$delta = [
				'status'     => sanitize_text_field( $status ),
				'usage'      => (int) ( $data['monthly_requests'] ?? $this->get( 'usage', 0 ) ),
				'limit'      => (int) ( $data['monthly_limit']    ?? $this->get( 'limit', 200 ) ),
				'boost'      => (int) ( $data['boost_balance']    ?? $this->get( 'boost', 0 ) ),
				'reset_date' => sanitize_text_field( $data['reset_date'] ?? $this->get( 'reset_date' ) ),
				'status_ts'  => time(),
			];
			if ( ! empty( $data['subscription_code'] ) ) {
				$delta['subscription_code'] = sanitize_text_field( $data['subscription_code'] );
			}
			$this->set( $delta );

			if ( in_array( $status, [ 'cancelled', 'past_due' ], true ) ) {
				if ( isset( $this->config['on_deactivate'] ) ) {
					( $this->config['on_deactivate'] )( $status );
				}
				$this->log( '[CloudScale Licence] Status synced: ' . $status );
			}
		}

		wp_send_json_success( [ 'status' => $status ] );
	}

	public function handle_disconnect(): void {
		$state = $this->get_state();
		foreach ( array_keys( $state ) as $k ) {
			$state[ $k ] = in_array( $k, [ 'usage', 'limit', 'boost', 'status_ts' ], true ) ? 0 : '';
		}
		$state['limit'] = 200;
		( $this->setter )( $state );
		if ( isset( $this->config['on_deactivate'] ) ) {
			( $this->config['on_deactivate'] )( 'disconnected' );
		}
		$this->log( '[CloudScale Licence] Licence disconnected.' );
		wp_send_json_success();
	}

	public function handle_clear_pending(): void {
		$this->set( [ 'status' => '', 'session_id' => '' ] );
		wp_send_json_success();
	}

	/**
	 * Called by the plugin's admin_init hook when PayPal redirects back after approval.
	 * Captures the order and, if the broker supports PayPal activation, stores the licence.
	 */
	public function handle_paypal_return( string $order_id ): void {
		if ( 'paypal:' . $order_id !== $this->get( 'session_id' ) ) {
			return; // Not our order.
		}

		$token = $this->paypal_get_access_token();
		if ( ! $token ) {
			$this->log( '[CloudScale Licence] PayPal return: access token failed.' );
			return;
		}

		$base = $this->paypal_base_url();
		$resp = wp_remote_post( $base . '/v2/checkout/orders/' . rawurlencode( $order_id ) . '/capture', [
			'timeout' => 20,
			'headers' => [
				'Authorization' => 'Bearer ' . $token,
				'Content-Type'  => 'application/json',
			],
			'body' => '{}',
		] );

		if ( is_wp_error( $resp ) ) {
			$this->log( '[CloudScale Licence] PayPal capture failed: ' . $resp->get_error_message() );
			return;
		}

		$data = json_decode( wp_remote_retrieve_body( $resp ), true );
		if ( 'COMPLETED' !== ( $data['status'] ?? '' ) ) {
			$this->log( '[CloudScale Licence] PayPal capture status: ' . ( $data['status'] ?? 'unknown' ) );
			return;
		}

		$this->log( '[CloudScale Licence] PayPal order captured: ' . $order_id );

		// Ask broker to activate — broker must handle payment_method=paypal.
		$result = $this->broker_post( '/checkout', [
			'email'          => $this->get( 'email' ),
			'site_url'       => get_site_url(),
			'type'           => 'subscription',
			'product'        => $this->config['product'],
			'payment_method' => 'paypal',
			'payment_ref'    => $order_id,
		] );

		if ( ! empty( $result['license_key'] ) ) {
			$delta = [
				'license_key'  => sanitize_text_field( $result['license_key'] ),
				'status'       => 'active',
				'session_id'   => '',
				'subscribed_at' => sanitize_text_field( $result['created_at'] ?? '' ),
			];
			$this->set( $delta );
			if ( isset( $this->config['on_activate'] ) ) {
				( $this->config['on_activate'] )( array_merge( $this->get_state(), $delta ) );
			}
			$this->log( '[CloudScale Licence] PayPal subscription activated.' );
		}
		// else: broker not yet updated for PayPal — session stays pending until polled.
	}

	// ── PayPal helpers ────────────────────────────────────────────────────────

	private function has_paypal_config(): bool {
		return ! empty( $this->config['paypal_client_id'] ) && ! empty( $this->config['paypal_secret'] );
	}

	private function paypal_base_url(): string {
		return ( 'live' === ( $this->config['paypal_mode'] ?? 'sandbox' ) )
			? self::PAYPAL_LIVE
			: self::PAYPAL_SANDBOX;
	}

	private function paypal_get_access_token(): string {
		$resp = wp_remote_post( $this->paypal_base_url() . '/v1/oauth2/token', [
			'timeout' => 15,
			'headers' => [
				'Authorization' => 'Basic ' . base64_encode( ( $this->config['paypal_client_id'] ?? '' ) . ':' . ( $this->config['paypal_secret'] ?? '' ) ),
				'Content-Type'  => 'application/x-www-form-urlencoded',
			],
			'body' => 'grant_type=client_credentials',
		] );

		if ( is_wp_error( $resp ) ) {
			$this->log( '[CloudScale Licence] PayPal token request failed: ' . $resp->get_error_message() );
			return '';
		}

		$data = json_decode( wp_remote_retrieve_body( $resp ), true );
		return $data['access_token'] ?? '';
	}

	private function paypal_create_order( string $token, string $email ): array {
		$base       = $this->paypal_base_url();
		$amount     = $this->config['paypal_amount']     ?? '10.00';
		$currency   = $this->config['paypal_currency']   ?? 'USD';
		$return_url = $this->config['paypal_return_url'] ?? admin_url( 'admin.php?page=cloudscale-backup&cs_paypal_return=1' );
		$cancel_url = $this->config['paypal_cancel_url'] ?? admin_url();

		$resp = wp_remote_post( $base . '/v2/checkout/orders', [
			'timeout' => 20,
			'headers' => [
				'Authorization' => 'Bearer ' . $token,
				'Content-Type'  => 'application/json',
			],
			'body' => wp_json_encode( [
				'intent'         => 'CAPTURE',
				'purchase_units' => [ [
					'amount'      => [ 'currency_code' => $currency, 'value' => $amount ],
					'description' => sprintf( 'CloudScale %s — Monthly', $this->config['plugin_name'] ?? 'Plugin' ),
					'custom_id'   => sanitize_key( $email ) . ':' . $this->config['product'],
				] ],
				'application_context' => [
					'brand_name' => 'CloudScale',
					'return_url' => $return_url,
					'cancel_url' => $cancel_url,
					'user_action' => 'PAY_NOW',
				],
			] ),
		] );

		if ( is_wp_error( $resp ) ) {
			return [ 'ok' => false, 'error' => $resp->get_error_message() ];
		}

		$code = (int) wp_remote_retrieve_response_code( $resp );
		$data = json_decode( wp_remote_retrieve_body( $resp ), true );

		if ( $code < 200 || $code >= 300 || empty( $data['id'] ) ) {
			$err = $data['message'] ?? ( $data['error_description'] ?? "PayPal order creation failed (HTTP {$code})." );
			return [ 'ok' => false, 'error' => $err ];
		}

		$approval_url = '';
		foreach ( $data['links'] ?? [] as $link ) {
			if ( 'approve' === ( $link['rel'] ?? '' ) ) {
				$approval_url = $link['href'];
				break;
			}
		}

		if ( ! $approval_url ) {
			return [ 'ok' => false, 'error' => 'PayPal did not return an approval URL.' ];
		}

		return [ 'ok' => true, 'order_id' => $data['id'], 'approval_url' => $approval_url ];
	}

	// ── Card rendering ────────────────────────────────────────────────────────

	/**
	 * Render the full licence management card.
	 *
	 * @param array $display {
	 *   icon        string  Emoji icon shown in card header.
	 *   heading     string  Card heading text.
	 *   price_text  string  Short pricing description for subscribe state.
	 *   cta_text    string  Primary subscribe button text.
	 *   explain_fn  string  JS function name called by the CTA button.
	 *   card_bg     string  CSS background color for the card.
	 *   card_border string  CSS border color for the card.
	 *   active_extra callable|null  Called inside active state to inject plugin-specific HTML.
	 *   text_domain string  WP i18n text domain.
	 * }
	 */
	public function render_card( array $display = [] ): void {
		$td      = $display['text_domain'] ?? 'cloudscale';
		$state   = $this->get_state();
		$key     = $state['license_key'] ?? '';
		$status  = $state['status'] ?? '';
		$session = $state['session_id'] ?? '';
		$email   = $state['email'] ?? '';
		$usage   = (int) ( $state['usage'] ?? 0 );
		$limit   = (int) ( $state['limit'] ?? 200 );
		$boost   = (int) ( $state['boost'] ?? 0 );

		$prefix     = esc_attr( $this->config['action_prefix'] );
		$manage_url = esc_url( 'https://api.cloudscale.consulting/manage' . ( $key ? ( '?key=' . rawurlencode( $key ) ) : '' ) );

		// Status badge
		$badge_label = '';
		$badge_bg    = '';
		$badge_color = '';
		if ( 'active' === $status && $key ) {
			$badge_label = '&#10003; ' . esc_html__( 'License: Active', $td );
			$badge_bg    = '#dcfce7'; $badge_color = '#15803d';
		} elseif ( 'past_due' === $status ) {
			$badge_label = '&#9888; ' . esc_html__( 'License: Payment Failed', $td );
			$badge_bg    = '#fef9c3'; $badge_color = '#854d0e';
		} elseif ( 'cancelled' === $status ) {
			$badge_label = '&#10005; ' . esc_html__( 'License: Cancelled', $td );
			$badge_bg    = '#fee2e2'; $badge_color = '#dc2626';
		} elseif ( 'pending' === $status && $session ) {
			$badge_label = '&#8230; ' . esc_html__( 'License: Activating', $td );
			$badge_bg    = '#dbeafe'; $badge_color = '#1d4ed8';
		}

		$is_active = ( 'active' === $status && $key );

		$card_id     = esc_attr( $display['card_id'] ?? 'cs-lic-card' );
		$card_bg     = esc_attr( $display['card_bg'] ?? '#f1faf1' );
		$card_border = esc_attr( $display['card_border'] ?? '#a5d6a7' );
		$icon        = $display['icon'] ?? '&#9729;';
		$heading     = $display['heading'] ?? esc_html__( 'Managed subscription', $td );
		$price_text  = $display['price_text'] ?? esc_html__( 'Subscribe to get started.', $td );
		$cta_text    = $display['cta_text'] ?? esc_html__( 'See what\'s included', $td );
		$explain_fn  = esc_js( $display['explain_fn'] ?? 'void 0' );
		?>
		<div style="flex:1;min-width:220px;padding:14px;border:1px solid <?php echo $card_border; ?>;border-radius:8px;background:<?php echo $card_bg; ?>;" id="<?php echo $card_id; ?>">
			<strong style="display:block;color:#1a1a1a;font-size:0.92rem;margin-bottom:8px;"><?php echo $icon; ?> <?php echo esc_html( $heading ); ?></strong>

			<?php if ( $badge_label ) : ?>
			<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;padding:6px 10px;background:<?php echo esc_attr( $badge_bg ); ?>;border-radius:6px;font-size:0.8rem;font-weight:700;color:<?php echo esc_attr( $badge_color ); ?>;">
				<span><?php echo wp_kses( $badge_label, [] ); ?></span>
			</div>
			<?php endif; ?>

			<?php if ( $is_active ) : ?>
			<!-- Active state -->
			<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:8px 12px;margin-bottom:8px;font-size:0.8rem;color:#166534;">
				<div style="display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap;">
					<span style="font-family:monospace;color:#374151;font-size:0.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:130px;" title="<?php echo esc_attr( $key ); ?>">
						&#128273; <?php echo esc_html( substr( $key, 0, 8 ) . '…' . substr( $key, -4 ) ); ?>
					</span>
					<span id="<?php echo $card_id; ?>-stats" style="font-weight:600;color:#15803d;">
						<?php
						$used = $usage + $boost;
						if ( $limit > 0 && $used > 0 ) {
							echo esc_html( $used . '/' . $limit . ' API calls' );
						} else {
							esc_html_e( 'Loading…', $td );
						}
						?>
					</span>
				</div>
				<?php if ( isset( $display['active_extra'] ) && is_callable( $display['active_extra'] ) ) : ?>
				<?php ( $display['active_extra'] )( $state ); ?>
				<?php endif; ?>
			</div>
			<a href="<?php echo $manage_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already esc_url ?>"
			   target="_blank" rel="noopener noreferrer"
			   style="display:block;text-align:center;background:#1565c0;color:#fff;text-decoration:none;padding:9px 14px;border-radius:6px;font-size:0.88rem;font-weight:700;margin-bottom:6px;">
				<?php esc_html_e( 'Manage subscription', $td ); ?>
			</a>
			<button type="button" id="<?php echo $card_id; ?>-cancel"
				style="width:100%;background:#dc2626;border:none;color:#fff;padding:8px 14px;border-radius:6px;font-size:0.85rem;font-weight:700;cursor:pointer;margin-bottom:4px;">
				<?php esc_html_e( 'Cancel subscription', $td ); ?>
			</button>
			<a href="#" id="<?php echo $card_id; ?>-disconnect"
			   style="display:block;text-align:center;font-size:0.75rem;color:#999;margin-top:4px;">
				<?php esc_html_e( 'Disconnect plugin only', $td ); ?>
			</a>

			<?php else : ?>
			<!-- Not subscribed state -->
			<p style="margin:0 0 8px;color:#777;font-size:0.82rem;"><?php echo esc_html( $price_text ); ?></p>
			<?php $use_paypal_btn = ! empty( $this->config['paypal_plan_id'] ) && $this->has_paypal_config(); ?>
			<div id="<?php echo $card_id; ?>-subscribe" style="display:flex;flex-direction:column;gap:6px;">
				<?php if ( $use_paypal_btn ) : ?>
				<div id="<?php echo $card_id; ?>-paypal-btn" style="min-height:44px;"></div>
				<p style="margin:0;font-size:0.78rem;color:#777;text-align:center;"><?php echo esc_html( $this->config['paypal_billing_text'] ?? 'Billed $10 USD/month · Cancel anytime' ); ?></p>
				<?php else : ?>
				<button type="button" class="button button-primary" onclick="<?php echo $explain_fn; ?>()"
					style="background:#1565c0;border-color:#0d47a1;">
					<?php echo esc_html( $cta_text ); ?>
				</button>
				<p style="margin:0;font-size:0.78rem;color:#777;text-align:center;"><?php esc_html_e( "You'll be taken to Paystack to complete payment.", $td ); ?></p>
				<?php endif; ?>
			</div>
			<button type="button" id="<?php echo $card_id; ?>-cancel"
				style="width:100%;background:#dc2626;border:none;color:#fff;padding:8px 14px;border-radius:6px;font-size:0.85rem;font-weight:700;cursor:pointer;margin-top:6px;">
				<?php esc_html_e( 'Cancel existing subscription', $td ); ?>
			</button>
			<button type="button" id="<?php echo $card_id; ?>-recover-toggle"
				style="width:100%;background:#0d9488;border:none;color:#fff;padding:8px 14px;border-radius:6px;font-size:0.85rem;font-weight:600;cursor:pointer;margin-top:6px;white-space:normal;line-height:1.35;">
				<?php esc_html_e( 'Paid but key not found?', $td ); ?><br><?php esc_html_e( 'Recover licence →', $td ); ?>
			</button>
			<div id="<?php echo $card_id; ?>-recover-form" style="display:none;margin-top:8px;">
				<input type="email" id="<?php echo $card_id; ?>-recover-email"
					placeholder="<?php esc_attr_e( 'Email used at checkout', $td ); ?>"
					value="<?php echo esc_attr( $email ); ?>"
					style="width:100%;box-sizing:border-box;padding:6px 10px;border:1px solid #1565c0;border-radius:6px;font-size:0.85rem;margin-bottom:6px;">
				<input type="text" id="<?php echo $card_id; ?>-recover-code"
					inputmode="numeric" maxlength="6" autocomplete="one-time-code"
					placeholder="<?php esc_attr_e( '6-digit code from email', $td ); ?>"
					style="display:none;width:100%;box-sizing:border-box;padding:6px 10px;border:1px solid #1565c0;border-radius:6px;font-size:1.1rem;letter-spacing:6px;text-align:center;font-family:monospace;margin-bottom:6px;">
				<button type="button" id="<?php echo $card_id; ?>-recover-btn"
					style="width:100%;background:#1565c0;color:#fff;border:none;border-radius:6px;padding:7px 16px;font-size:0.85rem;cursor:pointer;">
					<?php esc_html_e( 'Recover Licence', $td ); ?>
				</button>
				<p id="<?php echo $card_id; ?>-recover-msg" style="font-size:0.78rem;margin-top:6px;color:#6b7280;text-align:center;"></p>
			</div>
			<div id="<?php echo $card_id; ?>-key-entry" style="display:none;flex-direction:column;gap:6px;margin-top:6px;">
				<input type="text" id="<?php echo $card_id; ?>-key"
					placeholder="<?php esc_attr_e( 'Paste your 32-char licence key', $td ); ?>"
					style="width:100%;padding:6px 8px;border:1px solid #ddd;border-radius:4px;font-size:0.82rem;font-family:monospace;">
				<button type="button" class="button button-primary" id="<?php echo $card_id; ?>-save-key">
					<?php esc_html_e( 'Connect', $td ); ?>
				</button>
				<p style="margin:2px 0 0;font-size:0.78rem;color:#999;">
					<a href="#" id="<?php echo $card_id; ?>-back-to-subscribe">&#8592; <?php esc_html_e( 'Back', $td ); ?></a>
				</p>
			</div>
			<p style="margin:6px 0 0;font-size:0.78rem;color:#999;text-align:center;">
				<?php esc_html_e( 'Already subscribed?', $td ); ?>
				<a href="#" id="<?php echo $card_id; ?>-enter-key"><?php esc_html_e( 'Enter licence key', $td ); ?></a>
			</p>
			<div id="<?php echo $card_id; ?>-pending" style="display:<?php echo ( 'pending' === $status && $session ) ? 'block' : 'none'; ?>;margin-top:6px;text-align:center;">
				<p style="margin:0 0 6px;font-size:0.85rem;color:#1565c0;">&#9203; <?php esc_html_e( 'Waiting for payment confirmation…', $td ); ?></p>
				<a href="#" id="<?php echo $card_id; ?>-cancel-pending" style="font-size:0.78rem;color:#999;">
					<?php esc_html_e( 'Closed the window? Start over', $td ); ?>
				</a>
			</div>
			<?php endif; ?>

			<div id="<?php echo $card_id; ?>-msg" style="margin-top:6px;font-size:0.82rem;display:none;text-align:center;"></div>
		</div>
		<?php
	}

	/**
	 * Output the JS inline block that initialises cloudscale-licence.js for this instance.
	 * Call this after wp_enqueue_script( 'cloudscale-licence' ).
	 *
	 * @param string $card_id      The card_id used in render_card().
	 * @param string $nonce_value  The nonce value (wp_create_nonce( $nonce_name )).
	 * @param array  $extra        Additional JS config keys (plain associative array).
	 */
	public function render_js_init( string $card_id, string $nonce_value, array $extra = [] ): void {
		$config = array_merge( [
			'cardId'         => $card_id,
			'actionPrefix'   => $this->config['action_prefix'],
			'nonce'          => $nonce_value,
			'manageBase'     => 'https://api.cloudscale.consulting/manage',
			'licKey'         => $this->get( 'license_key' ),
			'usePaypal'      => $this->has_paypal_config(),
			'paypalPlanId'   => $this->config['paypal_plan_id'] ?? '',
			'paypalClientId' => $this->config['paypal_client_id'] ?? '',
		], $extra );
		echo '<script>document.addEventListener("DOMContentLoaded",function(){if(window.CloudScaleLicence){window.CloudScaleLicence.init(' . wp_json_encode( $config ) . ');}});</script>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode is safe
	}
}
