<?php

namespace Aws\S3;

use Aws\CommandInterface;
use Psr\Http\Message\RequestInterface;

/**
 * Used to update the URL used for S3 requests to support:
 * S3 Accelerate, S3 DualStack or Both. It will build to
 * host style paths unless specified, including for S3
 * DualStack.
 *
 * IMPORTANT: this middleware must be added after the "build" step.
 *
 * @internal
 */
class S3EndpointMiddleware {
	const NO_PATTERN = 0;
	const DUALSTACK = 1;
	const ACCELERATE = 2;
	const ACCELERATE_DUALSTACK = 3;
	const PATH_STYLE = 4;
	const HOST_STYLE = 5;
	private static $exclusions = [
		'CreateBucket' => true,
		'DeleteBucket' => true,
		'ListBuckets'  => true,
	];
	/** @var bool */
	private $accelerateByDefault;
	/** @var bool */
	private $dualStackByDefault;
	/** @var bool */
	private $pathStyleByDefault;
	/** @var string */
	private $region;
	/** @var callable */
	private $nextHandler;

	public function __construct(
		callable $nextHandler,
		$region,
		array $options
	) {
		$this->pathStyleByDefault  = isset( $options['path_style'] )
			? (bool) $options['path_style'] : false;
		$this->dualStackByDefault  = isset( $options['dual_stack'] )
			? (bool) $options['dual_stack'] : false;
		$this->accelerateByDefault = isset( $options['accelerate'] )
			? (bool) $options['accelerate'] : false;
		$this->region              = (string) $region;
		$this->nextHandler         = $nextHandler;
	}

	/**
	 * Create a middleware wrapper function
	 *
	 * @param string $region
	 * @param array $options
	 *
	 * @return callable
	 */
	public static function wrap( $region, array $options ) {
		return function ( callable $handler ) use ( $region, $options ) {
			return new self( $handler, $region, $options );
		};
	}

	public function __invoke( CommandInterface $command, RequestInterface $request ) {
		switch ( $this->endpointPatternDecider( $command, $request ) ) {
			case self::HOST_STYLE:
				$request = $this->applyHostStyleEndpoint( $command, $request );
				break;
			case self::NO_PATTERN:
			case self::PATH_STYLE:
				break;
			case self::DUALSTACK:
				$request = $this->applyDualStackEndpoint( $command, $request );
				break;
			case self::ACCELERATE:
				$request = $this->applyAccelerateEndpoint(
					$command,
					$request,
					's3-accelerate'
				);
				break;
			case self::ACCELERATE_DUALSTACK:
				$request = $this->applyAccelerateEndpoint(
					$command,
					$request,
					's3-accelerate.dualstack'
				);
				break;
		}

		$nextHandler = $this->nextHandler;

		return $nextHandler( $command, $request );
	}

	private function endpointPatternDecider(
		CommandInterface $command,
		RequestInterface $request
	) {
		$accelerate = isset( $command['@use_accelerate_endpoint'] )
			? $command['@use_accelerate_endpoint'] : $this->accelerateByDefault;
		$dualStack  = isset( $command['@use_dual_stack_endpoint'] )
			? $command['@use_dual_stack_endpoint'] : $this->dualStackByDefault;
		$pathStyle  = isset( $command['@use_path_style_endpoint'] )
			? $command['@use_path_style_endpoint'] : $this->pathStyleByDefault;

		if ( $accelerate && $dualStack ) {
			// When try to enable both for operations excluded from s3-accelerate,
			// only dualstack endpoints will be enabled.
			return $this->canAccelerate( $command )
				? self::ACCELERATE_DUALSTACK
				: self::DUALSTACK;
		}

		if ( $accelerate && $this->canAccelerate( $command ) ) {
			return self::ACCELERATE;
		}

		if ( $dualStack ) {
			return self::DUALSTACK;
		}

		if ( ! $pathStyle
		     && self::isRequestHostStyleCompatible( $command, $request )
		) {
			return self::HOST_STYLE;
		}

		return self::PATH_STYLE;
	}

	private function canAccelerate( CommandInterface $command ) {
		return empty( self::$exclusions[ $command->getName() ] )
		       && S3Client::isBucketDnsCompatible( $command['Bucket'] );
	}

	private static function isRequestHostStyleCompatible(
		CommandInterface $command,
		RequestInterface $request
	) {
		return S3Client::isBucketDnsCompatible( $command['Bucket'] )
		       && (
			       $request->getUri()->getScheme() === 'http'
			       || strpos( $command['Bucket'], '.' ) === false
		       );
	}

	private function applyHostStyleEndpoint(
		CommandInterface $command,
		RequestInterface $request
	) {
		$uri     = $request->getUri();
		$request = $request->withUri(
			$uri->withHost( $this->getBucketStyleHost(
				$command,
				$uri->getHost()
			) )
			    ->withPath( $this->getBucketlessPath(
				    $uri->getPath(),
				    $command
			    ) )
		);

		return $request;
	}

	private function getBucketStyleHost( CommandInterface $command, $host ) {
		// For operations on the base host (e.g. ListBuckets)
		if ( ! isset( $command['Bucket'] ) ) {
			return $host;
		}

		return "{$command['Bucket']}.{$host}";
	}

	private function getBucketlessPath( $path, CommandInterface $command ) {
		$pattern = '/^\\/' . preg_quote( $command['Bucket'], '/' ) . '/';

		return preg_replace( $pattern, '', $path ) ?: '/';
	}

	private function applyDualStackEndpoint(
		CommandInterface $command,
		RequestInterface $request
	) {
		$request = $request->withUri(
			$request->getUri()
			        ->withHost( $this->getDualStackHost() )
		);
		if ( empty( $command['@use_path_style_endpoint'] )
		     && ! $this->pathStyleByDefault
		     && self::isRequestHostStyleCompatible( $command, $request )
		) {
			$request = $this->applyHostStyleEndpoint( $command, $request );
		}

		return $request;
	}

	private function getDualStackHost() {
		return "s3.dualstack.{$this->region}.amazonaws.com";
	}

	private function applyAccelerateEndpoint(
		CommandInterface $command,
		RequestInterface $request,
		$pattern
	) {
		$request = $request->withUri(
			$request->getUri()
			        ->withHost( $this->getAccelerateHost( $command, $pattern ) )
			        ->withPath( $this->getBucketlessPath(
				        $request->getUri()->getPath(),
				        $command
			        ) )
		);

		return $request;
	}

	private function getAccelerateHost( CommandInterface $command, $pattern ) {
		return "{$command['Bucket']}.{$pattern}.amazonaws.com";
	}
}
