<?php
/**
 * Settings REST controller (non-secret settings).
 *
 * @package Carticy\Conversions\Rest
 */

declare(strict_types=1);

namespace Carticy\Conversions\Rest;

use Carticy\Conversions\Client\ClientTrackingSettings;
use Carticy\Conversions\Domain\ConversionEventType;
use Carticy\Conversions\Events\EventToggles;
use Carticy\Conversions\Settings\ConsentPostureSettings;
use Carticy\Conversions\Support\DiagnosticLogger;

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

/**
 * Reads and writes the non-secret settings the frontend renders: client
 * (browser) tracking, delivery mode, the event taxonomy, privacy/data, and the
 * runtime enable toggle. Server-side provider SECRETS live in their own
 * controller so they never share a payload with these public values.
 */
final class SettingsController extends AbstractRestController {

	/**
	 * Runtime enable option ('yes'/'no').
	 */
	private const ENABLED_OPTION = 'carticy_conversions_enabled';

	/**
	 * Posture/privacy settings.
	 *
	 * @var ConsentPostureSettings
	 */
	private ConsentPostureSettings $posture;

	/**
	 * Whether the premium server-side layer is available (affects delivery mode).
	 *
	 * @var PremiumServerBridge
	 */
	private PremiumServerBridge $server;

	/**
	 * Per-event-type toggles (Settings · Events).
	 *
	 * @var EventToggles
	 */
	private EventToggles $toggles;

	/**
	 * Constructor.
	 *
	 * @param ConsentPostureSettings $posture Posture/privacy settings.
	 * @param PremiumServerBridge    $server Premium server bridge.
	 * @param EventToggles|null      $toggles Per-event-type toggles.
	 */
	public function __construct( ConsentPostureSettings $posture, PremiumServerBridge $server, ?EventToggles $toggles = null ) {
		$this->posture = $posture;
		$this->server  = $server;
		$this->toggles = $toggles ?? new EventToggles();
	}

	/**
	 * Declared routes.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public function routes(): array {
		return array(
			array(
				'methods'  => 'GET',
				'route'    => '/settings',
				'callback' => array( $this, 'get_settings' ),
			),
			array(
				'methods'  => 'POST',
				'route'    => '/settings',
				'callback' => array( $this, 'update_settings' ),
			),
		);
	}

	/**
	 * Read all non-secret settings + the render schema.
	 *
	 * @return \WP_REST_Response
	 */
	public function get_settings(): \WP_REST_Response {
		$client = ClientTrackingSettings::from_options();

		return $this->ok(
			array(
				'enabled'        => $this->is_enabled(),
				'deliveryMode'   => $this->delivery_mode( $client ),
				'clientTracking' => $client->to_array(),
				'events'         => $this->toggles->to_array(),
				'eventsTaxonomy' => array(
					'supported'        => ConversionEventType::values(),
					// The renewal toggle is only meaningful when a subscriptions
					// engine is present to produce renewal orders.
					'renewalAvailable' => function_exists( 'wcs_order_contains_renewal' ),
				),
				'privacy'        => array(
					'ipMode'        => $this->posture->ip_mode(),
					'retentionDays' => $this->posture->retention_days(),
				),
				// Debug decision tracing (DiagnosticLogger): the Advanced tab
				// renders the toggle; logs land in WooCommerce - Status - Logs
				// under the carticy-conversions source.
				'debugLogging'   => DiagnosticLogger::debug_enabled(),
				'schema'         => $this->schema(),
			)
		);
	}

	/**
	 * Patch the non-secret settings.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function update_settings( \WP_REST_Request $request ) {
		$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 );
		}

		if ( array_key_exists( 'enabled', $body ) ) {
			update_option( self::ENABLED_OPTION, ! empty( $body['enabled'] ) ? 'yes' : 'no' );
		}

		if ( array_key_exists( 'clientTracking', $body ) && is_array( $body['clientTracking'] ) ) {
			// The option is stored in the FLAT shape the normalizer reads from;
			// ClientTrackingSettings::from_options() validates and nests it on
			// read (dropping junk IDs), so storing the normalized nested shape
			// here would be lossy on the next read.
			update_option( ClientTrackingSettings::OPTION_NAME, $this->sanitize_client_tracking( $body['clientTracking'] ) );
		}

		if ( array_key_exists( 'events', $body ) && is_array( $body['events'] ) ) {
			$this->toggles->apply( $body['events'] );
		}

		if ( array_key_exists( 'debugLogging', $body ) ) {
			update_option( DiagnosticLogger::DEBUG_OPTION, ! empty( $body['debugLogging'] ) ? 'yes' : 'no' );
		}

		if ( array_key_exists( 'privacy', $body ) && is_array( $body['privacy'] ) ) {
			$this->posture->apply_privacy(
				(string) ( $body['privacy']['ipMode'] ?? $this->posture->ip_mode() ),
				(int) ( $body['privacy']['retentionDays'] ?? $this->posture->retention_days() )
			);
		}

		return $this->get_settings();
	}

	/**
	 * Allow-list and field-sanitize the flat client-tracking input.
	 *
	 * Validation of the IDs themselves (format, drop-if-junk) happens in
	 * {@see ClientTrackingSettings} on read; here we only keep the known flat
	 * keys and strip control characters.
	 *
	 * @param array<string, mixed> $input Raw flat client-tracking fields.
	 * @return array<string, mixed>
	 */
	private function sanitize_client_tracking( array $input ): array {
		$text_keys = array(
			'ga4_measurement_id',
			'ga4_mode',
			'google_ads_conversion_id',
			'google_ads_conversion_label',
			'google_ads_mode',
			'meta_pixel_id',
			'meta_mode',
			'gtm_container_id',
		);

		$clean = array();
		foreach ( $text_keys as $key ) {
			if ( array_key_exists( $key, $input ) ) {
				$clean[ $key ] = sanitize_text_field( (string) $input[ $key ] );
			}
		}

		if ( array_key_exists( 'allow_duplicate_client_destinations', $input ) ) {
			$clean['allow_duplicate_client_destinations'] = ! empty( $input['allow_duplicate_client_destinations'] );
		}

		return $clean;
	}

	/**
	 * Whether the runtime is enabled.
	 *
	 * @return bool
	 */
	private function is_enabled(): bool {
		return 'yes' === get_option( self::ENABLED_OPTION, 'yes' );
	}

	/**
	 * Derive the effective delivery mode from configured destinations.
	 *
	 * @param ClientTrackingSettings $client Client settings.
	 * @return string client | server | both | off
	 */
	private function delivery_mode( ClientTrackingSettings $client ): string {
		$has_client = $client->has_active_destinations();
		$has_server = $this->server->available();

		if ( $has_client && $has_server ) {
			return 'both';
		}
		if ( $has_server ) {
			return 'server';
		}

		return $has_client ? 'client' : 'off';
	}

	/**
	 * Inline render schema so the frontend renders constraints without
	 * hardcoding them.
	 *
	 * @return array<string, mixed>
	 */
	private function schema(): array {
		return array(
			'enabled'       => array(
				'type'    => 'boolean',
				'default' => true,
			),
			'ipMode'        => array(
				'type'    => 'enum',
				'default' => 'anonymize',
				'options' => array( 'anonymize', 'omit', 'store' ),
			),
			'retentionDays' => array(
				'type'    => 'integer',
				'default' => 180,
				'min'     => 1,
				'max'     => 3650,
			),
		);
	}
}
