<?php
/**
 * Server-validated onboarding state machine.
 *
 * @package Carticy\Conversions\Rest
 */

declare(strict_types=1);

namespace Carticy\Conversions\Rest;

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

/**
 * Persists and validates the onboarding flow server-side so the frontend can
 * never advance the merchant past a step the backend has not authorized.
 *
 * The flow is intentionally linear: pick platforms, choose delivery, detect
 * existing tags, connect credentials, set consent, then verify real tracking
 * evidence. Reaching `complete` requires a recorded purchase event or an
 * accepted server send; it is never just a frontend flag.
 */
final class OnboardingStateMachine {

	/**
	 * Option storing the current onboarding state.
	 */
	public const OPTION_NAME = 'carticy_conversions_onboarding_state';

	/**
	 * Ordered flow states.
	 */
	public const STATES = array( 'platforms', 'delivery', 'detect', 'connect', 'consent', 'verify', 'complete' );

	/**
	 * Allowed forward/back transitions per state.
	 *
	 * Each state may advance one step or step back one step; `verify` advancing
	 * to `complete` is additionally gated on verification evidence, and
	 * `complete` may reopen to `verify` to re-check.
	 *
	 * @var array<string, array<int, string>>
	 */
	public const TRANSITIONS = array(
		'platforms' => array( 'delivery' ),
		'delivery'  => array( 'detect', 'platforms' ),
		'detect'    => array( 'connect', 'delivery' ),
		'connect'   => array( 'consent', 'detect' ),
		'consent'   => array( 'verify', 'connect' ),
		'verify'    => array( 'complete', 'consent' ),
		'complete'  => array( 'verify' ),
	);

	/**
	 * Read the current onboarding state.
	 *
	 * @return string
	 */
	public function current(): string {
		$state = sanitize_key( (string) get_option( self::OPTION_NAME, 'platforms' ) );

		return in_array( $state, self::STATES, true ) ? $state : 'platforms';
	}

	/**
	 * Allowed next states from a given state.
	 *
	 * @param string $state Source state.
	 * @return array<int, string>
	 */
	public function allowed_next( string $state ): array {
		return self::TRANSITIONS[ $state ] ?? array();
	}

	/**
	 * Build the read contract for the current onboarding position.
	 *
	 * @param bool                             $can_complete Whether verification evidence exists.
	 * @param array<int, array<string, mixed>> $blockers Verification blockers to surface.
	 * @return array<string, mixed>
	 */
	public function snapshot( bool $can_complete, array $blockers = array() ): array {
		$current = $this->current();

		return array(
			'current'     => $current,
			// Alias required by the shared @carticy/admin-ui useServerOnboarding
			// hook contract (GET {path} -> { state, ...context }).
			'state'       => $current,
			'states'      => self::STATES,
			'completed'   => $this->completed_before( $current ),
			'allowedNext' => $this->allowed_next( $current ),
			'canComplete' => $can_complete,
			'blockers'    => array_values( $blockers ),
		);
	}

	/**
	 * Attempt a server-validated transition.
	 *
	 * @param string $to Target state.
	 * @param bool   $can_complete Whether verification evidence permits completion.
	 * @return array<string, mixed> Result discriminated by the `ok`/`error` keys.
	 */
	public function transition( string $to, bool $can_complete ): array {
		$to = sanitize_key( $to );

		if ( ! in_array( $to, self::STATES, true ) ) {
			return array(
				'ok'      => false,
				'error'   => 'unknown_state',
				'allowed' => self::STATES,
			);
		}

		$current = $this->current();
		if ( $current === $to ) {
			// Idempotent no-op: re-asserting the current state is not an error.
			return array(
				'ok'    => true,
				'state' => $to,
			);
		}

		if ( ! in_array( $to, $this->allowed_next( $current ), true ) ) {
			return array(
				'ok'      => false,
				'error'   => 'invalid_transition',
				'from'    => $current,
				'to'      => $to,
				'allowed' => $this->allowed_next( $current ),
			);
		}

		if ( 'complete' === $to && ! $can_complete ) {
			// The green check is earned on the wire, not requested by the client.
			return array(
				'ok'    => false,
				'error' => 'verification_required',
				'from'  => $current,
				'to'    => $to,
			);
		}

		update_option( self::OPTION_NAME, $to );

		return array(
			'ok'    => true,
			'state' => $to,
		);
	}

	/**
	 * States completed before the current position (linear prefix of the flow).
	 *
	 * @param string $current Current state.
	 * @return array<int, string>
	 */
	private function completed_before( string $current ): array {
		$completed = array();
		foreach ( self::STATES as $state ) {
			if ( $state === $current ) {
				break;
			}
			$completed[] = $state;
		}

		return $completed;
	}
}
