<?php
/**
 * Server-side provider settings REST controller (premium secrets).
 *
 * @package Carticy\Conversions\Rest
 */

declare(strict_types=1);

namespace Carticy\Conversions\Rest;

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

/**
 * Reads and writes server-side provider credentials through the premium bridge
 * without naming premium classes, so the same REST surface can ship in both
 * artifacts. Free builds return `available:false`; premium builds redact stored
 * secrets on read and treat blank secret fields as "keep existing".
 */
final class ServerProvidersController extends AbstractRestController {

	/**
	 * Mask shown in place of a stored secret.
	 */
	private const SECRET_MASK = '••••••••';

	/**
	 * Premium server bridge.
	 *
	 * @var PremiumServerBridge
	 */
	private PremiumServerBridge $server;

	/**
	 * Constructor.
	 *
	 * @param PremiumServerBridge $server Premium server bridge.
	 */
	public function __construct( PremiumServerBridge $server ) {
		$this->server = $server;
	}

	/**
	 * Declared routes.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public function routes(): array {
		return array(
			array(
				'methods'  => 'GET',
				'route'    => '/server-providers',
				'callback' => array( $this, 'get_providers' ),
			),
			array(
				'methods'  => 'POST',
				'route'    => '/server-providers',
				'callback' => array( $this, 'update_providers' ),
			),
		);
	}

	/**
	 * Read server provider config with secrets redacted.
	 *
	 * @return \WP_REST_Response
	 */
	public function get_providers(): \WP_REST_Response {
		if ( ! $this->server->available() ) {
			return $this->ok( array( 'available' => false ) );
		}

		$data = $this->server->read();

		return $this->ok(
			array(
				'available' => true,
				'providers' => array(
					'ga4'  => array(
						'enabled'        => ! empty( $data['ga4']['enabled'] ),
						'measurement_id' => (string) ( $data['ga4']['measurement_id'] ?? '' ),
						'api_secret'     => $this->secret_field( ! empty( $data['ga4']['api_secret_set'] ) ),
						'debug_mode'     => ! empty( $data['ga4']['debug_mode'] ),
					),
					'meta' => array(
						'enabled'         => ! empty( $data['meta']['enabled'] ),
						'pixel_id'        => (string) ( $data['meta']['pixel_id'] ?? '' ),
						'access_token'    => $this->secret_field( ! empty( $data['meta']['access_token_set'] ) ),
						'api_version'     => (string) ( $data['meta']['api_version'] ?? 'v21.0' ),
						'test_event_code' => (string) ( $data['meta']['test_event_code'] ?? '' ),
					),
				),
			)
		);
	}

	/**
	 * Write server provider credentials (write-only secrets), then kick the queue.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function update_providers( \WP_REST_Request $request ) {
		if ( ! $this->server->available() ) {
			return $this->fail( 'carticy_pro_required', __( 'Server-side delivery is a Pro feature.', 'carticy-conversions-for-woocommerce' ), 403 );
		}

		$body = $this->body( $request );
		if ( array() === $body ) {
			return $this->fail( 'carticy_invalid_body', __( 'Request body must be a non-empty JSON object.', 'carticy-conversions-for-woocommerce' ), 400 );
		}

		// The bridge writer preserves blank/absent secrets and sanitizes through
		// the premium settings object, then schedules an immediate sweep.
		$providers = is_array( $body['providers'] ?? null ) ? $body['providers'] : $body;
		$this->server->save( $this->strip_unchanged_secrets( $providers ) );
		$this->server->kick_queue();

		return $this->get_providers();
	}

	/**
	 * Drop secret fields that are not a genuine new value.
	 *
	 * The read contract returns a secret as `{set, masked}`; a frontend that
	 * echoes the GET shape back - the redaction object, or the literal mask
	 * string - must NOT overwrite the stored secret with the mask. Only a
	 * non-empty string that differs from the mask is treated as a new secret;
	 * anything else is removed so the bridge writer preserves the stored value.
	 *
	 * @param array<string, mixed> $providers Provider patch.
	 * @return array<string, mixed>
	 */
	private function strip_unchanged_secrets( array $providers ): array {
		$secret_fields = array(
			'ga4'  => 'api_secret',
			'meta' => 'access_token',
		);

		foreach ( $secret_fields as $provider => $field ) {
			if ( ! isset( $providers[ $provider ] ) || ! is_array( $providers[ $provider ] ) ) {
				continue;
			}
			if ( ! array_key_exists( $field, $providers[ $provider ] ) ) {
				continue;
			}

			$value = $providers[ $provider ][ $field ];
			if ( ! is_string( $value ) || '' === $value || self::SECRET_MASK === $value ) {
				unset( $providers[ $provider ][ $field ] );
			}
		}

		return $providers;
	}

	/**
	 * Build the redacted secret field contract.
	 *
	 * @param bool $is_set Whether a secret is stored.
	 * @return array<string, mixed>
	 */
	private function secret_field( bool $is_set ): array {
		return array(
			'set'    => $is_set,
			'masked' => $is_set ? self::SECRET_MASK : '',
		);
	}
}
