<?php
/**
 * ConfigValidationException class for configuration validation failures.
 *
 * @package    UltimateSecurity
 * @subpackage Support\Exceptions
 * @since      2.0.0
 */

declare(strict_types=1);

namespace WPUltimateSecurity\UltimateSecurity\Support\Exceptions;

use InvalidArgumentException;

/**
 * Exception thrown when configuration validation fails.
 *
 * Holds the configuration key, attempted value, and validation errors.
 *
 * Usage:
 *   throw new ConfigValidationException('api_key', '', ['API key cannot be empty']);
 *   throw ConfigValidationException::forKey('timeout', -1, ['Timeout must be positive']);
 *
 * Catching:
 *   try {
 *       // configuration code
 *   } catch (ConfigValidationException $e) {
 *       $key = $e->getKey();
 *       $value = $e->getValue();
 *       $errors = $e->getErrors();
 *   }
 *
 * @since 2.0.0
 */
class ConfigValidationException extends InvalidArgumentException {

	/**
	 * The configuration key that failed validation.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	private readonly string $key;

	/**
	 * The value that failed validation.
	 *
	 * @since 2.0.0
	 * @var mixed
	 */
	private readonly mixed $value;

	/**
	 * Validation errors array.
	 *
	 * @since 2.0.0
	 * @var array<int, string>
	 */
	private readonly array $errors;

	/**
	 * Create a new ConfigValidationException.
	 *
	 * @since 2.0.0
	 *
	 * @param string         $key     The configuration key that failed validation.
	 * @param mixed          $value   The value that failed validation.
	 * @param array          $errors  Array of validation error messages.
	 * @param string         $message Optional custom error message.
	 * @param int            $code    Optional error code.
	 * @param \Throwable|null $previous Optional previous exception.
	 */
	public function __construct(
		string $key,
		mixed $value,
		array $errors,
		string $message = '',
		int $code = 0,
		?\Throwable $previous = null
	) {
		$this->key    = $key;
		$this->value  = $value;
		$this->errors = array_values( $errors );

		if ( empty( $message ) ) {
			$message = sprintf(
				/* translators: 1: configuration key, 2: validation errors */
				__( 'Validation failed for config key "%1$s": %2$s', 'ultimate-security' ),
				$key,
				implode( ', ', $errors )
			);
		}

		parent::__construct( $message, $code, $previous );
	}

	/**
	 * Create a ConfigValidationException for a specific key.
	 *
	 * @since 2.0.0
	 *
	 * @param string $key    The configuration key.
	 * @param mixed  $value  The invalid value.
	 * @param array  $errors The validation errors.
	 * @return self
	 */
	public static function forKey( string $key, mixed $value, array $errors ): self {
		return new self( $key, $value, $errors );
	}

	/**
	 * Create a ConfigValidationException for a required configuration.
	 *
	 * @since 2.0.0
	 *
	 * @param string $key The configuration key.
	 * @return self
	 */
	public static function required( string $key ): self {
		return new self(
			$key,
			null,
			[
				sprintf(
					/* translators: %s: configuration key */
					__( 'The configuration "%s" is required.', 'ultimate-security' ),
					$key
				),
			]
		);
	}

	/**
	 * Create a ConfigValidationException for an invalid type.
	 *
	 * @since 2.0.0
	 *
	 * @param string $key           The configuration key.
	 * @param mixed  $value         The invalid value.
	 * @param string $expected_type The expected type.
	 * @return self
	 */
	public static function invalidType( string $key, mixed $value, string $expected_type ): self {
		return new self(
			$key,
			$value,
			[
				sprintf(
					/* translators: 1: configuration key, 2: expected type, 3: actual type */
					__( 'Configuration "%1$s" must be of type %2$s, %3$s given.', 'ultimate-security' ),
					$key,
					$expected_type,
					gettype( $value )
				),
			]
		);
	}

	/**
	 * Create a ConfigValidationException for an out of range value.
	 *
	 * @since 2.0.0
	 *
	 * @param string     $key   The configuration key.
	 * @param mixed      $value The invalid value.
	 * @param int|float  $min   The minimum allowed value.
	 * @param int|float  $max   The maximum allowed value.
	 * @return self
	 */
	public static function outOfRange( string $key, mixed $value, int|float $min, int|float $max ): self {
		return new self(
			$key,
			$value,
			[
				sprintf(
					/* translators: 1: configuration key, 2: minimum value, 3: maximum value */
					__( 'Configuration "%1$s" must be between %2$s and %3$s.', 'ultimate-security' ),
					$key,
					$min,
					$max
				),
			]
		);
	}

	/**
	 * Create a ConfigValidationException for an invalid option.
	 *
	 * @since 2.0.0
	 *
	 * @param string $key             The configuration key.
	 * @param mixed  $value           The invalid value.
	 * @param array  $allowed_options The allowed options.
	 * @return self
	 */
	public static function invalidOption( string $key, mixed $value, array $allowed_options ): self {
		return new self(
			$key,
			$value,
			[
				sprintf(
					/* translators: 1: configuration key, 2: allowed options */
					__( 'Configuration "%1$s" must be one of: %2$s.', 'ultimate-security' ),
					$key,
					implode( ', ', $allowed_options )
				),
			]
		);
	}

	/**
	 * Get the configuration key that failed validation.
	 *
	 * @since 2.0.0
	 *
	 * @return string The configuration key.
	 */
	public function getKey(): string {
		return $this->key;
	}

	/**
	 * Get the value that failed validation.
	 *
	 * @since 2.0.0
	 *
	 * @return mixed The invalid value.
	 */
	public function getValue(): mixed {
		return $this->value;
	}

	/**
	 * Get all validation errors.
	 *
	 * @since 2.0.0
	 *
	 * @return array<int, string> Array of validation error messages.
	 */
	public function getErrors(): array {
		return $this->errors;
	}

	/**
	 * Get the first validation error.
	 *
	 * @since 2.0.0
	 *
	 * @return string|null The first error message, or null if no errors.
	 */
	public function getFirstError(): ?string {
		return $this->errors[0] ?? null;
	}

	/**
	 * Check if there are any validation errors.
	 *
	 * @since 2.0.0
	 *
	 * @return bool True if there are errors, false otherwise.
	 */
	public function hasErrors(): bool {
		return ! empty( $this->errors );
	}

	/**
	 * Get the count of validation errors.
	 *
	 * @since 2.0.0
	 *
	 * @return int The number of validation errors.
	 */
	public function getErrorCount(): int {
		return count( $this->errors );
	}

	/**
	 * Convert to array format suitable for REST API responses.
	 *
	 * @since 2.0.0
	 *
	 * @return array{success: bool, error: string, code: string, key: string, value: mixed, validation_errors: array}
	 */
	public function toArray(): array {
		return [
			'success'           => false,
			'error'             => $this->getMessage(),
			'code'              => 'config_validation_error',
			'key'               => $this->key,
			'value'             => $this->value,
			'validation_errors' => $this->errors,
		];
	}

	/**
	 * Convert to a WP_Error object.
	 *
	 * @since 2.0.0
	 *
	 * @return \WP_Error
	 */
	public function toWpError(): \WP_Error {
		return new \WP_Error(
			'config_validation_error',
			$this->getMessage(),
			[
				'status'            => 422,
				'key'               => $this->key,
				'value'             => $this->value,
				'validation_errors' => $this->errors,
			]
		);
	}
}
