<?php

namespace Limb_Chatbot\Includes\Services;

use WP_Error;

/**
 * Abstract base class to handle HTTP responses.
 *
 * @since 1.0.0
 */
abstract class Response_Handler {

	/**
	 * HTTP status code for OK (200).
	 *
	 * @var int
	 * @since 1.0.0
	 */
	const STATUS_CODE_OK = 200;

	/**
	 * HTTP status code for Created (201).
	 *
	 * @var int
	 * @since 1.0.0
	 */
	const STATUS_CODE_CREATED = 201;

	/**
	 * HTTP status code for Accepted (202).
	 *
	 * @var int
	 * @since 1.0.0
	 */
	const STATUS_CODE_ACCEPTED = 202;

	/**
	 * HTTP status code for Bad Request (400).
	 *
	 * @var int
	 * @since 1.0.0
	 */
	const STATUS_CODE_BAD_REQUEST = 400;

	/**
	 * HTTP status code for Not Found (404).
	 *
	 * @var int
	 * @since 1.0.0
	 */
	const STATUS_CODE_NOT_FOUND = 404;

	/**
	 * HTTP status code for Forbidden (403).
	 *
	 * @var int
	 * @since 1.0.0
	 */
	const STATUS_CODE_FORBIDDEN = 403;

	/**
	 * HTTP status code for Conflict (409).
	 *
	 * @var int
	 * @since 1.0.0
	 */
	const STATUS_CODE_CONFLICT = 409;

	/**
	 * Response body.
	 *
	 * @var mixed
	 * @since 1.0.0
	 */
	protected $body;

	/**
	 * Full response data.
	 *
	 * @var mixed
	 * @since 1.0.0
	 */
	protected $response;

	/**
	 * HTTP client instance used for the request.
	 *
	 * @var mixed
	 * @since 1.0.0
	 */
	protected $http_client;

	/**
	 * Optional stream parser callback or callable.
	 *
	 * @var mixed
	 * @since 1.0.0
	 */
	protected $stream_parser = null;

	/**
	 * Optional Endpoint instance related to the response.
	 *
	 * @var Endpoint|null
	 * @since 1.0.0
	 */
	protected ?Endpoint $endpoint = null;

	/**
	 * Optional WP_Error instance if an error occurred.
	 *
	 * @var WP_Error|null
	 * @since 1.0.0
	 */
	protected ?WP_Error $error = null;

	/**
	 * Flag indicating if the response is streamed.
	 *
	 * @var bool
	 * @since 1.0.0
	 */
	protected bool $is_stream = false;

	/**
	 * HTTP status code of the response.
	 *
	 * @var int|null
	 * @since 1.0.0
	 */
	protected ?int $status_code = null;

	/**
	 * Defines the response body content. Must be implemented by subclasses.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	abstract public function define_body(): void;

	/**
	 * Constructor.
	 *
	 * @param mixed $response Response data from HTTP client.
	 * @param mixed $http_client HTTP client instance used for request.
	 * @param Endpoint|null $endpoint Optional Endpoint instance.
	 * @param mixed $stream_parser Optional stream parser callback.
	 * @since 1.0.0
	 */
	public function __construct( $response, $http_client, ?Endpoint $endpoint = null, $stream_parser = null ) {
		$this->http_client   = $http_client;
		$this->stream_parser = $stream_parser;
		$this->is_stream     = $http_client instanceof Event_Stream_Curl;
		$this->endpoint      = $endpoint;
		$this->set_response( $response );
		$this->check_wp_errors();
		$this->define_status_code();
		$this->define_body();
		$this->parse();
	}

	/**
	 * Defines the HTTP status code based on the response.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function define_status_code() {
		if ( is_null( $this->get_error() ) && ! empty( $this->get_response()['response']['code'] ) && is_int( $this->get_response()['response']['code'] ) ) {
			$this->set_status_code( $this->get_response()['response']['code'] );
		}
	}

	/**
	 * Checks if the response is a WP_Error and sets the error property.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function check_wp_errors() {
		if ( is_wp_error( $this->response ) ) {
			$this->set_error( $this->response );
		}
	}

	/**
	 * Parses the response body.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function parse() {
	}

	/**
	 * Gets the WP_Error instance if any error occurred.
	 *
	 * @return WP_Error|null
	 * @since 1.0.0
	 */
	public function get_error(): ?WP_Error {
		return $this->error;
	}

	/**
	 * Sets the error instance.
	 *
	 * @param WP_Error $error Error instance.
	 * @return void
	 * @since 1.0.0
	 */
	public function set_error( $error ): void {
		$this->error = $error;
	}

	/**
	 * Gets the raw response.
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	public function get_response() {
		return $this->response;
	}

	/**
	 * Sets the raw response.
	 *
	 * @param mixed $response Response data.
	 * @return void
	 * @since 1.0.0
	 */
	public function set_response( $response ): void {
		$this->response = $response;
	}

	/**
	 * Gets the response body.
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	public function get_body() {
		return $this->body;
	}

	/**
	 * Gets the associated Endpoint instance.
	 *
	 * @return Endpoint|null
	 * @since 1.0.0
	 */
	public function get_endpoint(): ?Endpoint {
		return $this->endpoint;
	}

	/**
	 * Checks if the response Content-Type header is application/json.
	 *
	 * @return bool True if Content-Type is application/json, false otherwise.
	 * @since 1.0.0
	 */
	public function is_application_json() {
		if ( $this->http_client instanceof \WP_Http ) {
			$headers = wp_remote_retrieve_headers( $this->response );
		}

		return ! empty( $headers['content-type'] ) && ( str_starts_with( $headers['content-type'], 'application/json' ) );
	}

	/**
	 * Gets the HTTP status code.
	 *
	 * @return int|null HTTP status code or null if not set.
	 * @since 1.0.0
	 */
	public function get_status_code(): ?int {
		return $this->status_code;
	}

	/**
	 * Sets the HTTP status code.
	 *
	 * @param int|null $status_code HTTP status code.
	 * @return void
	 * @since 1.0.0
	 */
	public function set_status_code( ?int $status_code ): void {
		$this->status_code = $status_code;
	}

	/**
	 * Checks if status code is HTTP 202 Accepted.
	 *
	 * @return bool True if status code is 202, false otherwise.
	 * @since 1.0.0
	 */
	public function status_code_is_accepted(): bool {
		return $this->status_code === self::STATUS_CODE_ACCEPTED;
	}

	/**
	 * Checks if status code is HTTP 200 OK.
	 *
	 * @return bool True if status code is 200, false otherwise.
	 * @since 1.0.0
	 */
	public function status_code_is_ok(): bool {
		return $this->status_code === self::STATUS_CODE_OK;
	}

	/**
	 * Checks if status code is HTTP 400 Bad Request.
	 *
	 * @return bool True if status code is 400, false otherwise.
	 * @since 1.0.0
	 */
	public function status_code_is_bad_request(): bool {
		return $this->status_code === self::STATUS_CODE_BAD_REQUEST;
	}

	/**
	 * Checks if status code is HTTP 404 Not Found.
	 *
	 * @return bool True if status code is 404, false otherwise.
	 * @since 1.0.0
	 */
	public function status_code_is_not_found(): bool {
		return $this->status_code === self::STATUS_CODE_NOT_FOUND;
	}

	/**
	 * Checks if status code is HTTP 409 Conflict.
	 *
	 * @return bool True if status code is 409, false otherwise.
	 * @since 1.0.0
	 */
	public function status_code_is_conflict(): bool {
		return $this->status_code === self::STATUS_CODE_CONFLICT;
	}

	/**
	 * Checks if status code is HTTP 201 Created.
	 *
	 * @return bool True if status code is 201, false otherwise.
	 * @since 1.0.0
	 */
	public function status_code_is_created(): bool {
		return $this->status_code === self::STATUS_CODE_CREATED;
	}

	/**
	 * Checks if status code is HTTP 403 Forbidden.
	 *
	 * @return bool True if status code is 403, false otherwise.
	 * @since 1.0.0
	 */
	public function status_code_is_forbidden(): bool {
		return $this->status_code === self::STATUS_CODE_FORBIDDEN;
	}

	/**
	 * Checks if status code starts with 2xx.
	 *
	 * @return bool True if status code starts with '2', false otherwise.
	 * @since 1.0.0
	 */
	public function status_starts_with_2(): bool {
		return str_starts_with( (string) $this->get_status_code(), '2' );
	}

	/**
	 * Checks if there is an error.
	 *
	 * @return bool True if an error exists, false otherwise.
	 * @since 1.0.0
	 */
	public function has_error(): bool {
		return ! empty( $this->error );
	}
}