<?php
/**
 * Result class for consistent return values across the plugin.
 *
 * @package    UltimateSecurity
 * @subpackage Support
 * @since      1.0.0
 */

namespace WPUltimateSecurity\UltimateSecurity\Support;

use WP_Error;

/**
 * Result class for consistent return values across the plugin.
 *
 * Replaces mixed return patterns (WP_Error, bool, arrays) with a consistent API.
 *
 * Usage:
 *   return Result::success($data);
 *   return Result::failure('Error message');
 *   return Result::validation(['field' => 'Error for field']);
 *
 * Checking:
 *   if ($result->isSuccess()) { ... }
 *   if ($result->isFailure()) { ... }
 *
 * Chainable operations:
 *   $result->map(fn($data) => transform($data))
 *          ->onSuccess(fn($data) => log_success($data))
 *          ->onFailure(fn($error) => log_error($error));
 *
 * @since 1.0.0
 */
class Result {

    /**
     * Whether the result represents success.
     *
     * @since 1.0.0
     * @var bool
     */
    private bool $success;

    /**
     * The result data on success.
     *
     * @since 1.0.0
     * @var mixed
     */
    private $data;

    /**
     * Error message on failure.
     *
     * @since 1.0.0
     * @var string|null
     */
    private ?string $error_message;

    /**
     * Error code on failure.
     *
     * @since 1.0.0
     * @var string|null
     */
    private ?string $error_code;

    /**
     * Validation errors array.
     *
     * @since 1.0.0
     * @var array<string, string>
     */
    private array $validation_errors;

    /**
     * Additional metadata for the result.
     *
     * @since 1.0.0
     * @var array<string, mixed>
     */
    private array $metadata;

    /**
     * Private constructor to enforce factory method usage.
     *
     * @since 1.0.0
     *
     * @param bool                  $success           Whether the result is successful.
     * @param mixed                 $data              The result data.
     * @param string|null           $error_message     Error message if failed.
     * @param string|null           $error_code        Error code if failed.
     * @param array<string, string> $validation_errors Validation errors if any.
     * @param array<string, mixed>  $metadata          Additional metadata.
     */
    private function __construct(
        bool $success,
        $data = null,
        ?string $error_message = null,
        ?string $error_code = null,
        array $validation_errors = [],
        array $metadata = []
    ) {
        $this->success           = $success;
        $this->data              = $data;
        $this->error_message     = $error_message;
        $this->error_code        = $error_code;
        $this->validation_errors = $validation_errors;
        $this->metadata          = $metadata;
    }

    /**
     * Create a successful result.
     *
     * @since 1.0.0
     *
     * @param mixed $data Optional data to include with the success.
     * @return self
     */
    public static function success( $data = null ): self {
        return new self( true, $data );
    }

    /**
     * Create a failure result.
     *
     * @since 1.0.0
     *
     * @param string $message Error message describing the failure.
     * @param string $code    Optional error code for programmatic handling.
     * @return self
     */
    public static function failure( string $message, string $code = 'error' ): self {
        return new self( false, null, $message, $code );
    }

    /**
     * Create a validation failure result.
     *
     * @since 1.0.0
     *
     * @param array<string, string> $errors Associative array of field => error message.
     * @return self
     */
    public static function validation( array $errors ): self {
        $message = ! empty( $errors ) ? reset( $errors ) : __( 'Validation failed.', 'ultimate-security' );
        return new self( false, null, $message, 'validation_error', $errors );
    }

    /**
     * Create a Result from a WP_Error object.
     *
     * @since 1.0.0
     *
     * @param WP_Error $error The WordPress error object.
     * @return self
     */
    public static function fromWpError( WP_Error $error ): self {
        $code    = $error->get_error_code();
        $message = $error->get_error_message();
        $data    = $error->get_error_data( $code );

        $result = new self( false, null, $message, (string) $code );

        if ( ! empty( $data ) ) {
            $result = $result->withMetadata( 'wp_error_data', $data );
        }

        return $result;
    }

    /**
     * Create a Result from a boolean value.
     *
     * @since 1.0.0
     *
     * @param bool        $value         The boolean value.
     * @param string      $error_message Error message if false.
     * @param string      $error_code    Error code if false.
     * @param mixed       $success_data  Data to include if true.
     * @return self
     */
    public static function fromBool(
        bool $value,
        string $error_message = 'Operation failed.',
        string $error_code = 'error',
        $success_data = null
    ): self {
        if ( $value ) {
            return self::success( $success_data );
        }
        return self::failure( $error_message, $error_code );
    }

    /**
     * Create a Result from a potentially null value.
     *
     * @since 1.0.0
     *
     * @param mixed  $value         The value (null indicates failure).
     * @param string $error_message Error message if null.
     * @param string $error_code    Error code if null.
     * @return self
     */
    public static function fromNullable(
        $value,
        string $error_message = 'Value not found.',
        string $error_code = 'not_found'
    ): self {
        if ( null !== $value ) {
            return self::success( $value );
        }
        return self::failure( $error_message, $error_code );
    }

    /**
     * Check if the result represents success.
     *
     * @since 1.0.0
     *
     * @return bool
     */
    public function isSuccess(): bool {
        return $this->success;
    }

    /**
     * Check if the result represents failure.
     *
     * @since 1.0.0
     *
     * @return bool
     */
    public function isFailure(): bool {
        return ! $this->success;
    }

    /**
     * Check if the result has validation errors.
     *
     * @since 1.0.0
     *
     * @return bool
     */
    public function hasValidationErrors(): bool {
        return ! empty( $this->validation_errors );
    }

    /**
     * Get the result data.
     *
     * @since 1.0.0
     *
     * @return mixed The data, or null if failed.
     */
    public function getData() {
        return $this->data;
    }

    /**
     * Get the data or a default value if failed.
     *
     * @since 1.0.0
     *
     * @param mixed $default Default value to return on failure.
     * @return mixed
     */
    public function getDataOr( $default ) {
        return $this->success ? $this->data : $default;
    }

    /**
     * Get the data or throw an exception if failed.
     *
     * @since 1.0.0
     *
     * @return mixed
     * @throws \RuntimeException If the result is a failure.
     */
    public function getDataOrThrow() {
        if ( $this->isFailure() ) {
            $error_message = esc_html($this->error_message);
            throw new \RuntimeException(
                $error_message ?? 'Result is a failure.',
                0
            );
        }
        return $this->data;
    }

    /**
     * Get the error message.
     *
     * @since 1.0.0
     *
     * @return string|null The error message, or null if successful.
     */
    public function getError(): ?string {
        return $this->error_message;
    }

    /**
     * Get the error code.
     *
     * @since 1.0.0
     *
     * @return string|null The error code, or null if successful.
     */
    public function getErrorCode(): ?string {
        return $this->error_code;
    }

    /**
     * Get validation errors.
     *
     * @since 1.0.0
     *
     * @return array<string, string> Associative array of field => error message.
     */
    public function getValidationErrors(): array {
        return $this->validation_errors;
    }

    /**
     * Get a specific validation error.
     *
     * @since 1.0.0
     *
     * @param string $field The field name.
     * @return string|null The error message, or null if no error for that field.
     */
    public function getValidationError( string $field ): ?string {
        return $this->validation_errors[ $field ] ?? null;
    }

    /**
     * Get metadata value.
     *
     * @since 1.0.0
     *
     * @param string $key     The metadata key.
     * @param mixed  $default Default value if key not found.
     * @return mixed
     */
    public function getMetadata( string $key, $default = null ) {
        return $this->metadata[ $key ] ?? $default;
    }

    /**
     * Get all metadata.
     *
     * @since 1.0.0
     *
     * @return array<string, mixed>
     */
    public function getAllMetadata(): array {
        return $this->metadata;
    }

    /**
     * Convert to array format suitable for REST API responses.
     *
     * @since 1.0.0
     *
     * @return array{success: bool, data?: mixed, error?: string, code?: string, validation_errors?: array}
     */
    public function toArray(): array {
        if ( $this->success ) {
            return [
                'success' => true,
                'data'    => $this->data,
            ];
        }

        $response = [
            'success' => false,
            'error'   => $this->error_message,
            'code'    => $this->error_code,
        ];

        if ( ! empty( $this->validation_errors ) ) {
            $response['validation_errors'] = $this->validation_errors;
        }

        if ( ! empty( $this->metadata ) ) {
            $response['metadata'] = $this->metadata;
        }

        return $response;
    }

    /**
     * Convert to WP_Error object.
     *
     * Returns null if the result is successful.
     *
     * @since 1.0.0
     *
     * @return WP_Error|null
     */
    public function toWpError(): ?WP_Error {
        if ( $this->success ) {
            return null;
        }

        $data = [];
        if ( ! empty( $this->validation_errors ) ) {
            $data['validation_errors'] = $this->validation_errors;
        }
        if ( ! empty( $this->metadata ) ) {
            $data = array_merge( $data, $this->metadata );
        }

        return new WP_Error(
            $this->error_code ?? 'error',
            $this->error_message ?? 'An error occurred.',
            ! empty( $data ) ? $data : null
        );
    }

    /**
     * Convert to a REST response.
     *
     * @since 1.0.0
     *
     * @param int $success_status HTTP status code for success (default 200).
     * @param int $error_status   HTTP status code for errors (default 400).
     * @return \WP_REST_Response
     */
    public function toRestResponse( int $success_status = 200, int $error_status = 400 ): \WP_REST_Response {
        $status = $this->success ? $success_status : $error_status;

        // Use 422 for validation errors.
        if ( $this->hasValidationErrors() ) {
            $status = 422;
        }

        return new \WP_REST_Response( $this->toArray(), $status );
    }

    /**
     * Transform the data on success using a callback.
     *
     * The callback receives the current data and should return the new data.
     * If the result is a failure, returns $this unchanged.
     *
     * @since 1.0.0
     *
     * @param callable $fn Callback function: fn($data) => $newData.
     * @return self A new Result with transformed data, or $this if failed.
     */
    public function map( callable $fn ): self {
        if ( $this->isFailure() ) {
            return $this;
        }

        return new self(
            true,
            $fn( $this->data ),
            null,
            null,
            [],
            $this->metadata
        );
    }

    /**
     * Transform the error on failure using a callback.
     *
     * The callback receives the current error message and should return the new message.
     * If the result is a success, returns $this unchanged.
     *
     * @since 1.0.0
     *
     * @param callable $fn Callback function: fn($errorMessage) => $newMessage.
     * @return self A new Result with transformed error, or $this if successful.
     */
    public function mapError( callable $fn ): self {
        if ( $this->isSuccess() ) {
            return $this;
        }

        return new self(
            false,
            null,
            $fn( $this->error_message ),
            $this->error_code,
            $this->validation_errors,
            $this->metadata
        );
    }

    /**
     * Execute a side effect on success.
     *
     * The callback receives the data but its return value is ignored.
     * Returns $this for chaining.
     *
     * @since 1.0.0
     *
     * @param callable $fn Callback function: fn($data) => void.
     * @return self Returns $this for chaining.
     */
    public function onSuccess( callable $fn ): self {
        if ( $this->isSuccess() ) {
            $fn( $this->data );
        }
        return $this;
    }

    /**
     * Execute a side effect on failure.
     *
     * The callback receives the error message and code but its return value is ignored.
     * Returns $this for chaining.
     *
     * @since 1.0.0
     *
     * @param callable $fn Callback function: fn($errorMessage, $errorCode) => void.
     * @return self Returns $this for chaining.
     */
    public function onFailure( callable $fn ): self {
        if ( $this->isFailure() ) {
            $fn( $this->error_message, $this->error_code );
        }
        return $this;
    }

    /**
     * Chain another Result-returning operation on success.
     *
     * If this Result is successful, applies the callback to the data.
     * The callback must return a Result.
     * If this Result is a failure, returns $this unchanged.
     *
     * @since 1.0.0
     *
     * @param callable $fn Callback function: fn($data) => Result.
     * @return self The Result from the callback, or $this if failed.
     */
    public function flatMap( callable $fn ): self {
        if ( $this->isFailure() ) {
            return $this;
        }

        $result = $fn( $this->data );

        if ( ! $result instanceof self ) {
            throw new \InvalidArgumentException(
                'flatMap callback must return a Result instance.'
            );
        }

        return $result;
    }

    /**
     * Recover from a failure using a callback.
     *
     * If this Result is a failure, applies the callback which can return a new Result.
     * If this Result is a success, returns $this unchanged.
     *
     * @since 1.0.0
     *
     * @param callable $fn Callback function: fn($errorMessage, $errorCode) => Result.
     * @return self The recovered Result, or $this if successful.
     */
    public function recover( callable $fn ): self {
        if ( $this->isSuccess() ) {
            return $this;
        }

        $result = $fn( $this->error_message, $this->error_code );

        if ( ! $result instanceof self ) {
            throw new \InvalidArgumentException(
                'recover callback must return a Result instance.'
            );
        }

        return $result;
    }

    /**
     * Add metadata to the result.
     *
     * Returns a new Result with the added metadata (immutable).
     *
     * @since 1.0.0
     *
     * @param string $key   The metadata key.
     * @param mixed  $value The metadata value.
     * @return self A new Result with the metadata added.
     */
    public function withMetadata( string $key, $value ): self {
        $new_metadata         = $this->metadata;
        $new_metadata[ $key ] = $value;

        return new self(
            $this->success,
            $this->data,
            $this->error_message,
            $this->error_code,
            $this->validation_errors,
            $new_metadata
        );
    }

    /**
     * Combine multiple Results into one.
     *
     * If all Results are successful, returns a success with an array of all data.
     * If any Result is a failure, returns the first failure encountered.
     *
     * @since 1.0.0
     *
     * @param self ...$results The Results to combine.
     * @return self
     */
    public static function all( self ...$results ): self {
        $data = [];

        foreach ( $results as $result ) {
            if ( $result->isFailure() ) {
                return $result;
            }
            $data[] = $result->getData();
        }

        return self::success( $data );
    }

    /**
     * Return the first successful Result, or the last failure.
     *
     * @since 1.0.0
     *
     * @param self ...$results The Results to check.
     * @return self
     */
    public static function any( self ...$results ): self {
        $last_failure = null;

        foreach ( $results as $result ) {
            if ( $result->isSuccess() ) {
                return $result;
            }
            $last_failure = $result;
        }

        return $last_failure ?? self::failure( 'No results provided.', 'no_results' );
    }
}
