<?php
/**
 * Value object for a Brevo API response.
 *
 * @package RewindBrevoFormBridge
 */

namespace RewindBrevoFormBridge\Api;

defined( 'ABSPATH' ) || exit;

/**
 * Wraps HTTP status, decoded body, Brevo error code, retryability and
 * rate-limit headers. Errors are values, not exceptions.
 *
 * @since 0.1.0
 */
class Response {

	/**
	 * HTTP status. 0 means a transport failure (WP_Error).
	 *
	 * @var int
	 */
	private int $status;

	/**
	 * Raw response body.
	 *
	 * @var string
	 */
	private string $raw_body;

	/**
	 * Decoded JSON body, or null when the body is empty / not JSON.
	 *
	 * @var array|null
	 */
	private ?array $decoded;

	/**
	 * Lower-cased response headers.
	 *
	 * @var array
	 */
	private array $headers;

	/**
	 * Transport error message when status is 0.
	 *
	 * @var string
	 */
	private string $transport_error;

	/**
	 * Build from raw parts.
	 *
	 * @since 0.1.0
	 * @param int    $status          HTTP status, 0 for transport failure.
	 * @param string $raw_body        Raw body string.
	 * @param array  $headers         Lower-cased header map.
	 * @param string $transport_error WP_Error message when status is 0.
	 */
	public function __construct( int $status, string $raw_body = '', array $headers = array(), string $transport_error = '' ) {
		$this->status          = $status;
		$this->raw_body        = $raw_body;
		$this->headers         = $headers;
		$this->transport_error = $transport_error;

		// A 204 (contact updated) legitimately has an empty body. That is a
		// success, not a parse failure — decode only when there is content.
		$decoded       = ( '' !== trim( $raw_body ) ) ? json_decode( $raw_body, true ) : null;
		$this->decoded = is_array( $decoded ) ? $decoded : null;
	}

	/**
	 * Build a Response from a WP_Error transport failure.
	 *
	 * @since 0.1.0
	 * @param \WP_Error $error The transport error.
	 * @return Response
	 */
	public static function from_wp_error( \WP_Error $error ): Response {
		return new self( 0, '', array(), $error->get_error_message() );
	}

	/**
	 * Whether the HTTP status is in the 2xx range.
	 *
	 * @since 0.1.0
	 * @return bool
	 */
	public function is_success(): bool {
		return $this->status >= 200 && $this->status < 300;
	}

	/**
	 * HTTP status code, 0 for transport failures.
	 *
	 * @since 0.1.0
	 * @return int
	 */
	public function status(): int {
		return $this->status;
	}

	/**
	 * Decoded body as an array. Empty array for empty or non-JSON bodies.
	 *
	 * @since 0.1.0
	 * @return array
	 */
	public function body(): array {
		return null !== $this->decoded ? $this->decoded : array();
	}

	/**
	 * The raw, undecoded body.
	 *
	 * @since 0.1.0
	 * @return string
	 */
	public function raw_body(): string {
		return $this->raw_body;
	}

	/**
	 * Brevo's machine error code (the "code" field), if present.
	 *
	 * @since 0.1.0
	 * @return string Empty string when absent.
	 */
	public function error_code(): string {
		$body = $this->body();
		$code = isset( $body['code'] ) ? $body['code'] : '';

		return is_string( $code ) ? $code : '';
	}

	/**
	 * Best available human message: Brevo's "message" field, else the
	 * transport error, else empty.
	 *
	 * @since 0.1.0
	 * @return string
	 */
	public function message(): string {
		$body    = $this->body();
		$message = isset( $body['message'] ) ? $body['message'] : '';

		if ( is_string( $message ) && '' !== $message ) {
			return $message;
		}

		return $this->transport_error;
	}

	/**
	 * Whether this failure is worth retrying.
	 *
	 * Retryable: transport failures (status 0), 429, 425 (Too Early — real
	 * on Brevo contacts endpoints though missing from their central status
	 * list), and all 5xx. Everything else is permanent.
	 *
	 * @since 0.1.0
	 * @return bool
	 */
	public function is_retryable(): bool {
		if ( $this->is_success() ) {
			return false;
		}

		return 0 === $this->status
			|| 429 === $this->status
			|| 425 === $this->status
			|| $this->status >= 500;
	}

	/**
	 * Remaining requests from x-sib-ratelimit-remaining.
	 *
	 * @since 0.1.0
	 * @return int|null Null when the header is absent.
	 */
	public function rate_remaining(): ?int {
		return $this->int_header( 'x-sib-ratelimit-remaining' );
	}

	/**
	 * Seconds until the rate-limit window resets, from x-sib-ratelimit-reset.
	 *
	 * @since 0.1.0
	 * @return int|null Null when the header is absent.
	 */
	public function rate_reset(): ?int {
		return $this->int_header( 'x-sib-ratelimit-reset' );
	}

	/**
	 * Read a header as an integer.
	 *
	 * @param string $name Lower-cased header name.
	 * @return int|null
	 */
	private function int_header( string $name ): ?int {
		if ( ! isset( $this->headers[ $name ] ) || ! is_numeric( $this->headers[ $name ] ) ) {
			return null;
		}

		return (int) $this->headers[ $name ];
	}
}
