<?php

declare(strict_types=1);

namespace SecurWP\Modules\Settings;

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

use SecurWP\Includes\Interfaces\SettingsProvider;
use SecurWP\Includes\SettingsStore;

/**
 * Aggregation service that collects, validates, and distributes settings
 * across multiple SettingsProvider instances.
 */
class SettingsService {

	private const SETTINGS_COLUMN = SettingsStore::COL_GLOBAL;

	/** @var SettingsProvider[] */
	private array $providers = [];

	/**
	 * Register a settings provider.
	 *
	 * @param string           $key      Unique provider key.
	 * @param SettingsProvider  $provider Provider instance.
	 */
	public function register_provider( string $key, SettingsProvider $provider ): void {
		$this->providers[ $key ] = $provider;
	}

	/**
	 * Get aggregated settings from all providers + owned settings.
	 *
	 * @return array<string, mixed>
	 */
	public function get_all(): array {
		$settings = $this->get_owned_settings();

		foreach ( $this->providers as $provider ) {
			$settings = array_merge( $settings, $provider->get_settings() );
		}

		return $settings;
	}

	/**
	 * Validate input and distribute to owning providers.
	 *
	 * @param array<string, mixed> $input Raw input from request.
	 * @return true|\WP_Error
	 */
	public function save( array $input ) {
		$owned_schema = SettingsSchema::get_owned_schema();
		$owned_values = [];
		$owned_current = SettingsStore::get( self::SETTINGS_COLUMN );
		$operations = [];

		// Sanitize and collect settings owned by the Settings module.
		foreach ( $owned_schema as $key => $schema ) {
			if ( array_key_exists( $key, $input ) ) {
				$owned_values[ $key ] = SettingsSchema::sanitize( $input[ $key ], $schema );
			}
		}

		if ( ! empty( $owned_values ) ) {
			$operations[] = [
				'id'       => 'owned',
				'current'  => $owned_current,
				'next'     => array_merge( $owned_current, $owned_values ),
				'save'     => static function ( array $payload ): bool {
					return SettingsStore::save( self::SETTINGS_COLUMN, $payload );
				},
			];
		}

		// Route remaining keys to registered providers.
		foreach ( $this->providers as $provider ) {
			$provider_schema = $provider->get_settings_schema();
			$provider_values = [];
			$provider_state  = $provider->get_settings();
			$provider_current = [];

			foreach ( $provider_schema as $key => $schema ) {
				if ( ! empty( $schema['readonly'] ) ) {
					continue;
				}

				$provider_current[ $key ] = SettingsSchema::sanitize( $provider_state[ $key ] ?? ( $schema['default'] ?? null ), $schema );

				if ( array_key_exists( $key, $input ) ) {
					$provider_values[ $key ] = SettingsSchema::sanitize( $input[ $key ], $schema );
				}
			}

			if ( ! empty( $provider_values ) ) {
				$operations[] = [
					'id'       => 'provider_' . spl_object_hash( $provider ),
					'current'  => $provider_current,
					'next'     => array_merge( $provider_current, $provider_values ),
					'save'     => static function ( array $payload ) use ( $provider ): bool {
						return $provider->save_settings( $payload );
					},
				];
			}
		}

		if ( empty( $operations ) ) {
			return true;
		}

		$applied = [];
		foreach ( $operations as $operation ) {
			$saved = $operation['save']( $operation['next'] );
			if ( ! $saved ) {
				$this->rollback_operations( $applied );
				return new \WP_Error(
					'securwp_settings_save_failed',
					'Failed to save settings. Changes were rolled back.',
					[ 'status' => 500 ]
				);
			}

			$applied[] = $operation;
		}

		return true;
	}

	/**
	 * Best-effort rollback for already-applied operations.
	 *
	 * @param array<int, array{id:string,current:array,next:array,save:callable}> $applied
	 */
	private function rollback_operations( array $applied ): void {
		$reversed = array_reverse( $applied );
		foreach ( $reversed as $operation ) {
			try {
				$operation['save']( $operation['current'] );
			} catch ( \Throwable $ignored ) {
				// Best-effort rollback only.
			}
		}
	}

	/**
	 * Get settings owned directly by the Settings module.
	 *
	 * @return array<string, mixed>
	 */
	private function get_owned_settings(): array {
		$schema   = SettingsSchema::get_owned_schema();
		$defaults = [];
		foreach ( $schema as $key => $def ) {
			$defaults[ $key ] = $def['default'];
		}

		return SettingsStore::get( self::SETTINGS_COLUMN, $defaults );
	}
}
