<?php

namespace FluentBooking\Framework\Http;

use Exception;
use BadMethodCallException;
use FluentBooking\Framework\Support\Arr;
use FluentBooking\Framework\Foundation\App;
use FluentBooking\Framework\Http\Request\File;

/**
 * @method mixed get(string $url, array $params = [])     Send a GET request.
 * @method mixed post(string $url, array $params = [])    Send a POST request.
 * @method mixed put(string $url, array $params = [])     Send a PUT request.
 * @method mixed patch(string $url, array $params = [])   Send a PATCH request.
 * @method mixed delete(string $url, array $params = [])  Send a DELETE request.
 * @method mixed head(string $url, array $params = [])    Send a HEAD request.
 * @method mixed options(string $url, array $params = []) Send an OPTIONS request.
 * @method File download(string|File $url)                Download a remote file.
 * @method mixed upload(string $url, string $path, array $fields = [], string $name = 'file') Upload a file to a remote server.
 */


/**
 * Some examples:
 * 
 * // Using an instance:
 * $client = Client::make('https://example.com');
 * 
 * $response1 = $client->get('/users');
 * $response2 = $client->post('/users', ['body' => ['name' => 'Heera']]);
 * $response3 = $client->put('/users/1', ['body' => ['name' => 'Updated']]);
 * $response4 = $client->patch('/users/1', [
 *     'body' => ['email' => 'updated@example.com']]
 * );
 * $response5 = $client->delete('/users/1');
 * $response6 = $client->head('/users');
 * $response7 = $client->options('/users');
 * 
 * // Using statically (default base URL is empty):
 * $response8 = Client::get('https://example.com/users');
 * $response9 = Client::post('https://example.com/users', [
 *     'body' => ['name' => 'Static']]
 * );
 */


class Client
{
	/**
	 * Base URl for the request.
	 * 
	 * @var string
	 */
	protected $baseUrl = '';
	
	/**
	 * Cookies to send with the request.
	 * 
	 * @var array
	 */
	protected $cookies = [];
	
	/**
	 * Headers to send with the request.
	 * 
	 * @var array
	 */
	protected $headers = [];
	
	/**
	 * Options to set for the request.
	 * 
	 * @var array
	 */
	protected $options = [];

	/**
	 * Request body|Data|params to set in the request.
	 * 
	 * @var array
	 */
	protected $body = [];

	/**
	 * Request query params to pass with the url.
	 * 
	 * @var array
	 */
	protected $query = [];

	/**
	 * Stores args temporarily for then().
	 * 
	 * @var null|array
	 */
	private $args = null;

	/**
	 * Create a new HTTP client.
	 *
	 * @param string $baseUrl
	 * @param array $args
	 */
	public function __construct($baseUrl = '', $args = [])
	{
		$this->baseUrl = rtrim($baseUrl, '/');
		$this->cookies = $args['cookies'] ?? [];
		$this->headers = $args['headers'] ?? [];
		$this->options = $args['options'] ?? [];
	}

	/**
	 * Create a new HTTP client.
	 * 
	 * @param string $baseUrl
	 * @param array $args
	 */
	public static function make($baseUrl = '', $args = [])
	{
		$args['cookies'] = $args['cookies'] ?? [];
		$args['headers'] = $args['headers'] ?? [];
		$args['options'] = $args['options'] ?? [];
		return new static($baseUrl, $args);
	}

	/**
	 * Sets one or more options.
	 * 
	 * @return self
	 */
	public function withOption($key, $value = null)
	{
		$options = is_array($key) ? $key : [$key => $value];

		foreach ($options as $key => $value) {
			$this->options[$key] = $value;
		}

		return $this;
	}

	/**
	 * Sets the blocking option to false (non-blocking).
	 * 
	 * @return self
	 */
	public function async()
	{
		return $this->withOption('blocking', false);
	}

	/**
	 * Sets the sslverify option.
	 * 
	 * @return self
	 */
	public function secure($verify = true)
	{
		return $this->withOption('sslverify', $verify);
	}

	/**
	 * Sets one or more headers.
	 * 
	 * @return self
	 */
	public function withHeader($key, $value = null)
	{
		$headers = is_array($key) ? $key : [$key => $value];

		foreach ($headers as $key => $value) {
			$this->headers[$key] = $value;
		}

		return $this;
	}

	/**
	 * Sets one or more cookies.
	 * 
	 * @return self
	 */
	public function withCookie($key, $value = null)
	{
		$cookies = is_array($key) ? $key : [$key => $value];

		foreach ($cookies as $key => $value) {
			$this->cookies[$key] = $value;
		}

		return $this;
	}

	/**
	 * Sets one or more request body param.
	 * 
	 * @return self
	 */
	public function withData($key, $value = null)
	{
		$data = is_array($key) ? $key : [$key => $value];

		foreach ($data as $key => $value) {
			$this->body[$key] = $value;
		}

		return $this;
	}

	/**
	 * Sets one or more request body param.
	 * 
	 * @return self
	 */
	public function withBody($key, $value = null)
	{
		return $this->withData($key, $value);
	}

	/**
	 * Sets one or more request body param.
	 * 
	 * @return self
	 */
	public function withParam($key, $value = null)
	{
		return $this->withData($key, $value);
	}

	/**
	 * Sets one or more request body param.
	 * 
	 * @return self
	 */
	public function withQuery($key, $value = null)
	{
		$data = is_array($key) ? $key : [$key => $value];
		
		foreach ($data as $key => $value) {
			$this->query[$key] = $value;
		}

		return $this;
	}

	/**
	 * Build the request arguments.
	 * 
	 * @param  array  $params
	 * @param  string $method 
	 * @return array
	 */
	protected function buildRequestArgs($params, $method)
	{
		$defaultParams = [
	        'body' => [],
	        'cookies' => [],
	        'headers' => [],
	    ];

	    $params = wp_parse_args($params[0] ?? [], $defaultParams);

	    $options = array_merge($this->options, $params['options'] ?? []);

	    $params = [
	        'method' => strtoupper($method),
	        'body' => array_merge($this->body, $params['body']),
	        'cookies' => array_merge($this->cookies, $params['cookies']),
	        'headers' => array_merge($this->headers, $params['headers']),
	    ];

	    
	    foreach($options as $key => $value) {
	        $params[$key] = $value;
	    }

	    return $params;
	}

	/**
	 * Send the request.
	 * 
	 * @param  string $url
	 * @param  array  $args
	 * @return \FluentBooking\Framework\Http\Response
	 */
	protected function request($url, $args = [])
	{
		$parsedUrl = parse_url($url);
		$delimiter = isset($parsedUrl['query']) ? '&' : '?';
		$url .= $this->query ? $delimiter . http_build_query($this->query) : '';

		$response = wp_remote_request($url, $args);

		if (is_wp_error($response)) {
			throw new Exception($response->get_error_message(), 500);
		}
		
		$this->cookies = array_merge(
			$this->cookies,
			wp_remote_retrieve_cookies($response)
		);

		return $this->makeResponse($response);
	}

	/**
	 * Download a remote file.
	 * 
	 * @param  string $url
	 * @return \FluentBooking\Framework\Http\Request\File
	 * @throws \Exception
	 */
	public function downloadFile($url)
	{
		if (!function_exists('download_url')) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		$parsed = parse_url($url);

		if (!isset($parsed['scheme'])) {
			$url = trim($this->baseUrl, '/') . '/' . trim($url, '/');
		}

		if (is_wp_error($file = download_url($url))) {
			throw new Exception($file->get_error_message(), 500);
		}

		add_action('shutdown', function () use ($file) {
			@unlink($file);
		});

		return new File(
			$file,
			basename($url),
			mime_content_type($file) ?: 'application/octet-stream',
			filesize($file),
			UPLOAD_ERR_OK
		);
	}

	/**
	 * Upload  a file to a remote server.
	 * 
	 * @param  string $url
	 * @param  string $path
	 * @param  array  $fields
	 * @param  string $name
	 * @return \FluentBooking\Framework\Http\Response
	 */
	public function uploadFile($url, $path, $fields = [], $name = 'file')
	{
	    $path = $path instanceof File ? $path->getPathname() : $path;

	    if (!file_exists($path)) {
	        throw new Exception('File does not exist.', 500);
	    }

	    $boundary = wp_generate_password(24, false);

	    $headers = [
	        'Accept'       => '*/*',
	        'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
	    ];

	    $fileName = basename($path);
	    $content = file_get_contents($path);
	    $mime = mime_content_type($path);

	    $body = '';

	    foreach ($fields as $key => $value) {
	        $body .= "--" . $boundary . "\r\n";
	        $body .= 'Content-Disposition: form-data; name="' . $key . '"' . "\r\n\r\n";
	        $body .= $value . "\r\n";
	    }

	    $body .= "--" . $boundary . "\r\n";
	    $body .= 'Content-Disposition: form-data; name="'.$name.'"; filename="' . $fileName . '"' . "\r\n";
	    $body .= 'Content-Type: ' . $mime . "\r\n\r\n";
	    $body .= $content . "\r\n";
	    $body .= "--" . $boundary . "--\r\n";

	    $response = wp_remote_post($url, [
	        'headers' => $headers,
	        'body'    => $body,
	        'timeout' => 60,
	    ]);

	    return $this->makeResponse($response);
	}

	/**
	 * Build a response object from an anonymous class.
	 * 
	 * @param  array $response
	 * @return \FluentBooking\Framework\Http\Response
	 */
	protected function makeResponse($response)
	{
		return new Response($response);
	}

	protected function checkIfValidHttpMethod($method)
	{
		$validHttpMethods = [
			'get', 'post', 'put', 'delete', 'patch', 'options', 'head'
		];

		if (!in_array(strtolower($method), $validHttpMethods)) {
			throw new BadMethodCallException("Method $method does not exist.");
		}
	}

	/**
	 * Handles the dynamic calls.
	 * 
	 * @param  string $method
	 * @param  array  $args
	 * @return mixed
	 */
	public function __call($method, $args)
	{
		if ($method === 'download') {
			return $this->downloadFile(...$args);
		}
		
		// Handles dynamic method calls like:
		// get, post and so on.
		$url = array_shift($args);

		$parsed = parse_url($url);

		if (!isset($parsed['scheme'])) {
			$url = trim($this->baseUrl, '/') . '/' . trim($url, '/');
		}
		
		$this->checkIfValidHttpMethod($method);

		return $this->request(
			$url, $this->buildRequestArgs($args, $method)
		);
	}

	/**
	 * Handle the static dynamic calls.
	 * 
	 * @param  string $method
	 * @param  array  $args
	 * @return self
	 */
	public static function __callStatic($method, $args)
	{
		return static::make()->$method(...$args);
	}
}
