<?php

namespace XF\Proxy;

use League\Flysystem\FilesystemException;
use XF\App;
use XF\Entity\ImageProxy;
use XF\Http\Request;
use XF\Http\Response;
use XF\Repository\ImageProxyRepository;
use XF\Repository\LinkProxyRepository;
use XF\Service\ImageProxyService;

use function in_array, is_scalar, strlen, strval;

class Controller
{
	/**
	 * @var App
	 */
	protected $app;

	/**
	 * @var Linker
	 */
	protected $linker;

	/**
	 * @var Request
	 */
	protected $request;

	protected $requestUri;
	protected $referrer;
	protected $eTag;

	/**
	 * If true, image requests will return an error message (and HTTP code) rather than a placeholder image.
	 *
	 * @var bool
	 */
	protected $returnError = false;

	public const ERROR_INVALID_URL = 1;
	public const ERROR_INVALID_HASH = 2;
	public const ERROR_INVALID_REFERRER = 3;
	public const ERROR_DISABLED = 4;
	public const ERROR_FAILED = 5;
	public const ERROR_LOCAL_URL = 6;

	public function __construct(App $app, Linker $linker, ?Request $request = null)
	{
		$this->app = $app;
		$this->linker = $linker;

		if (!$request)
		{
			$request = $app->request();
		}
		$this->request = $request;

		$this->requestUri = $request->getFullRequestUri();
		$this->referrer = $request->getReferrer();
		$this->eTag = $request->getServer('HTTP_IF_NONE_MATCH');
	}

	public function setReferrer($referrer)
	{
		$this->referrer = $referrer;
	}

	public function setReturnError($returnError)
	{
		$this->returnError = (bool) $returnError;
	}

	public function resolveImageProxyRecursion(Request $request, $url)
	{
		$uriParts = explode('?', $request->getFullRequestUri(), 2);
		$subMatchTest = $uriParts[0] . '?';

		$subUrl = $url;
		$subHash = null;

		// Recursion can happen, mostly if people copy proxied image URLs.
		// Try to resolve that here.
		do
		{
			$hasSubMatch = false;

			if (strpos($subUrl, $subMatchTest) === 0)
			{
				$subMatchQs = substr($subUrl, strlen($subMatchTest));
				parse_str($subMatchQs, $subMatchParams);
				if (isset($subMatchParams['image'])
					&& is_scalar($subMatchParams['image'])
					&& isset($subMatchParams['hash'])
					&& is_scalar($subMatchParams['hash'])
				)
				{
					$subMatchUrl = trim(strval($subMatchParams['image']));
					$subMatchHash = trim(strval($subMatchParams['hash']));
					if ($this->linker->verifyHash($subMatchUrl, $subMatchHash))
					{
						$subUrl = $subMatchUrl;
						$subHash = $subMatchHash;
						$hasSubMatch = true;
					}
				}
			}
		}
		while ($hasSubMatch);

		if ($subHash)
		{
			return [$subUrl, $subHash];
		}
		else
		{
			return null;
		}
	}

	public function outputImage($url, $hash)
	{
		if ($this->validateImageRequest($url, $hash, $error))
		{
			$imageProxy = $this->app->service(ImageProxyService::class);
			$image = $imageProxy->getImage($url);
			if (!$image || !$image->isValid())
			{
				$image = null;
			}
		}
		else
		{
			$image = null;
		}

		if (!$image)
		{
			if (!$error)
			{
				$error = self::ERROR_FAILED;
			}

			$proxyRepo = $this->app->repository(ImageProxyRepository::class);
			$image = $proxyRepo->getPlaceholderImage();
		}

		if ($error && $this->returnError)
		{
			return $this->outputImageErrorResponse($url, $error);
		}

		if (!$error)
		{
			$proxyRepo = $this->app->repository(ImageProxyRepository::class);

			$proxyRepo->logImageView($image);
			if ($this->referrer && $this->app->options()->imageLinkProxyReferrer['enabled'])
			{
				$proxyRepo->logImageReferrer($image, $this->referrer);
			}
		}

		$response = $this->app->response();
		$this->applyImageResponseHeaders($response, $image, $error);

		if ($image->isPlaceholder())
		{
			$body = $response->responseFile($image->getPlaceholderPath());
		}
		else
		{
			try
			{
				$stream = $this->app->fs()->readStream($image->getAbstractedImagePath());
				$body = $response->responseStream($stream, $image->file_size);
			}
			catch (FilesystemException $e)
			{
				// the file was pruned mid-request
				$proxyRepo = $this->app->repository(ImageProxyRepository::class);
				$body = $response->responseFile(
					$proxyRepo->getPlaceholderImage()->getPlaceholderPath()
				);
			}
		}

		$response->body($body);

		return $response;
	}

	public function applyImageResponseHeaders(Response $response, ImageProxy $image, $error)
	{
		if ($error)
		{
			$response->header('Cache-Control', 'no-cache');
		}
		else
		{
			$nextRefresh = $image->getNextPlannedRefreshDate();
			if ($nextRefresh)
			{
				$maxAge = max(0, $nextRefresh - \XF::$time);
			}
			else
			{
				$maxAge = 86400; // allow daily revalidation if we don't know otherwise
			}

			$response->header('Cache-Control', 'public, max-age=' . $maxAge);
			$response->header('Last-Modified', gmdate('D, d M Y H:i:s', $image->fetch_date) . ' GMT');

			$expectedETag = $image->getETagValue();
			if ($expectedETag)
			{
				$response->header('ETag', '"' . $expectedETag . '"', true);

				if ($this->eTag && $this->eTag === "\"$expectedETag\"")
				{
					$response->httpCode(304);
					$response->removeHeader('Last-Modified');
					return;
				}
			}
		}

		$imageTypes = array_unique(\XF::app()->inlineImageTypes);

		if (in_array($image->mime_type, $imageTypes))
		{
			$response->contentType($image->mime_type);
			$response->setDownloadFileName($image->file_name, true);
		}
		else
		{
			$response->contentType('application/octet-stream');
			$response->setDownloadFileName($image->file_name);
		}

		$response->header('X-Content-Type-Options', 'nosniff');

		if ($error)
		{
			$response->header('X-Proxy-Error', $error);
		}
	}

	protected function outputImageErrorResponse($url, $error)
	{
		$response = $this->app->response();
		$response->httpCode(404);
		$response->contentType('text/plain');
		$response->header('Cache-Control', 'no-cache');
		$response->header('X-Proxy-Error', $error);
		$response->body('error');

		return $response;
	}

	public function validateImageRequest($url, $hash, &$error = null)
	{
		if (!$this->linker->isTypeEnabled('image'))
		{
			$error = self::ERROR_DISABLED;
			return false;
		}

		if (!$this->validateProxyRequestGeneric($url, $hash, $error))
		{
			return false;
		}

		return true;
	}

	public function outputLink($url, $hash, $json = false)
	{
		$response = $this->app->response();
		$response->contentType('application/json', 'utf-8');

		if ($this->validateLinkRequest($url, $hash, $error))
		{
			$proxyRepo = $this->app->repository(LinkProxyRepository::class);

			$link = $proxyRepo->logLinkVisit($url);
			if ($link && $this->referrer && $this->app->options()->imageLinkProxyReferrer['enabled'])
			{
				$proxyRepo->logLinkReferrer($link, $this->referrer);
			}

			$response->body(json_encode(['logged' => true]));
		}
		else
		{
			$response->httpCode(400);

			if (\XF::$debugMode)
			{
				$response->body(json_encode(['invalid' => true, 'code' => $error]));
			}
			else
			{
				$response->body(json_encode(['invalid' => true]));
			}
		}

		return $response;
	}

	public function validateLinkRequest($url, $hash, &$error = null)
	{
		if (!$this->linker->isTypeEnabled('link'))
		{
			$error = self::ERROR_DISABLED;
			return false;
		}

		if (!$this->validateProxyRequestGeneric($url, $hash, $error))
		{
			return false;
		}

		return true;
	}

	protected function validateProxyRequestGeneric($url, $hash, &$error = null)
	{
		$error = null;

		if (!preg_match('#^https?://#i', $url))
		{
			$error = self::ERROR_INVALID_URL;
			return false;
		}

		$urlParts = @parse_url($url);
		if (!$urlParts || empty($urlParts['host']))
		{
			$error = self::ERROR_INVALID_URL;
			return false;
		}

		if ($this->isLocalUrl($urlParts))
		{
			$error = self::ERROR_LOCAL_URL;
			return false;
		}

		if (!$this->linker->verifyHash($url, $hash))
		{
			$error = self::ERROR_INVALID_HASH;
			return false;
		}

		if (!$this->isValidReferrer())
		{
			$error = self::ERROR_INVALID_REFERRER;
			return false;
		}

		return true;
	}

	protected function isValidReferrer()
	{
		if (!$this->referrer)
		{
			return true;
		}

		$referrerParts = @parse_url($this->referrer);
		if (!$referrerParts || empty($referrerParts['host']))
		{
			return true;
		}

		$requestParts = @parse_url($this->requestUri);
		if (!$requestParts || empty($requestParts['host']))
		{
			return true;
		}

		return ($requestParts['host'] === $referrerParts['host']);
	}

	protected function isLocalUrl(array $urlParts): bool
	{
		$boardUrl = $this->app->options()->boardUrl;
		if (!$boardUrl)
		{
			return false;
		}

		$boardParts = @parse_url($boardUrl);
		if (!$boardParts || empty($boardParts['host']))
		{
			return false;
		}

		return (strtolower($urlParts['host']) === strtolower($boardParts['host']));
	}
}
