<?php

namespace PaymentPage\Stripe;

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

use PaymentPage\Settings as API_Settings;

/**
 * After a successful Stripe Connect OAuth, ensure a webhook endpoint exists
 * on the connected account and persist its signing secret (Shortcut #5904 v2).
 *
 * INVARIANT — trigger surface: provision() is invoked ONLY from
 * \PaymentPage\PaymentGateway\Stripe::save_master_credentials_response,
 * which is itself reached only from the OAuth callback in
 * app/RestAPI/PaymentGateway.php::connect_callback. Never wire this into
 * init, admin_init, plugin activation, or any update path — that was the
 * silent-retrofit failure mode the v1 audit (2026-05-17) explicitly ruled
 * out. If you change the call sites, update tests/test-sc-5904-*.php.
 */
class ConnectWebhookProvisioner {

	const FAILURE_TRANSIENT = 'payment_page_stripe_webhook_provision_failure';

	/** Test seam — when set, used in place of constructing a real \Stripe\StripeClient. */
	public static $client_factory = null;

	/**
	 * @param array<string, mixed> $credentials Same shape as PaymentGateway\Stripe::save_master_credentials_response.
	 * @return bool true on success, false when scope-check fails (transient set for admin notice).
	 *              Other errors propagate to the caller's try/catch in PaymentGateway/Stripe.php.
	 */
	public static function provision( array $credentials ): bool {
		if ( ! isset( $credentials['access_token'], $credentials['is_live'] ) ) {
			return false;
		}

		$mode = intval( $credentials['is_live'] ) ? 'live' : 'test';

		if ( self::$client_factory !== null ) {
			$client = ( self::$client_factory )( $credentials['access_token'] );
		} else {
			if ( ! class_exists( '\Stripe\StripeClient' ) ) {
				require_once PAYMENT_PAGE_BASE_PATH . '/lib/stripe/init.php';
			}
			$client = new \Stripe\StripeClient( $credentials['access_token'] );
		}
		$url    = self::expected_url( $mode );

		// Walk pagination; Stripe caps list page size at 100. The first call
		// also doubles as a permission probe — Stripe returns
		// PermissionException / AuthenticationException for restricted keys
		// that lack `rak_webhook_write`. Surface those via transient so the
		// merchant sees an admin notice; OAuth still completes (fail-open).
		$starting_after = null;
		do {
			$params = array( 'limit' => 100 );
			if ( $starting_after !== null ) {
				$params['starting_after'] = $starting_after;
			}

			try {
				$list = $client->webhookEndpoints->all( $params );
			} catch ( \Stripe\Exception\PermissionException $e ) {
				self::flag_failure( 'permission', $e->getMessage() );
				return false;
			} catch ( \Stripe\Exception\AuthenticationException $e ) {
				self::flag_failure( 'authentication', $e->getMessage() );
				return false;
			}

			foreach ( $list->data as $endpoint ) {
				$starting_after = $endpoint->id;

				if ( ! isset( $endpoint->url ) || $endpoint->url !== $url ) {
					continue;
				}

				try {
					$client->webhookEndpoints->delete( $endpoint->id );
				} catch ( \Throwable $e ) {
					error_log( '[payment-page] ConnectWebhookProvisioner: failed to delete pre-existing endpoint ' . $endpoint->id . ' at ' . $url . ': ' . $e->getMessage() );
				}
			}
		} while ( ! empty( $list->has_more ) );

		try {
			$created = $client->webhookEndpoints->create(
				array(
					'url'            => $url,
					// (1.5.1 hotfix) Stripe requires `api_version` when creating a
					// webhook endpoint on a CONNECTED account (this provisioner
					// uses the access_token from the OAuth response as the client
					// key, which IS the connected-account context). 1.5.0 omitted
					// this param, so every connect produced:
					//   `When creating an endpoint on a connected account, you must
					//   specify 'api_version'.`
					// The error was caught by the try/wrap in save_master_credentials_response
					// — OAuth completed, but the dummy/legacy webhook secret remained
					// in settings and merchants never got webhook events delivered.
					// Pin to the same `\Stripe\Util\ApiVersion::CURRENT` PP pinned
					// platform-wide in Phase 2a — keeps event payload shape in lockstep
					// across the SDK + the connected-account webhooks the SDK consumes.
					'api_version'    => \Stripe\Util\ApiVersion::CURRENT,
					'enabled_events' => array(
						'payment_intent.succeeded',
						'setup_intent.succeeded',
					),
					'metadata'       => array(
						'payment_page'     => '1',
						'payment_page_url' => home_url( '/' ),
					),
				)
			);
		} catch ( \Stripe\Exception\PermissionException $e ) {
			self::flag_failure( 'permission', $e->getMessage() );
			return false;
		} catch ( \Stripe\Exception\AuthenticationException $e ) {
			self::flag_failure( 'authentication', $e->getMessage() );
			return false;
		}

		if ( ! empty( $created->secret ) ) {
			self::persist_secret( $mode, $created->secret );
			self::clear_failure();
			return true;
		}

		return false;
	}

	private static function expected_url( string $mode ): string {
		return rest_url( PAYMENT_PAGE_REST_API_PREFIX . '/v1/webhook/stripe-callback/' . $mode );
	}

	private static function persist_secret( string $mode, string $secret ): void {
		API_Settings::instance()->update(
			array(
				'stripe_' . $mode . '_webhook_secret' => $secret,
			)
		);
	}

	private static function flag_failure( string $reason, string $message ): void {
		error_log( '[payment-page] ConnectWebhookProvisioner: ' . $reason . ' failure — ' . $message );

		if ( function_exists( 'set_transient' ) ) {
			set_transient(
				self::FAILURE_TRANSIENT,
				array(
					'reason'  => $reason,
					'message' => $message,
					'at'      => time(),
				),
				DAY_IN_SECONDS
			);
		}
	}

	private static function clear_failure(): void {
		if ( function_exists( 'delete_transient' ) ) {
			delete_transient( self::FAILURE_TRANSIENT );
		}
	}
}
