<?php

namespace XF\Api\Controller;

use XF\Api\ErrorMessage;
use XF\Api\Mvc\Reply\ApiResult;
use XF\Api\Result\ArrayResult;
use XF\Entity\ApiAttachmentKey;
use XF\Entity\ResultInterface;
use XF\Mvc\Controller;
use XF\Mvc\Entity\Entity;
use XF\Mvc\ParameterBag;
use XF\Mvc\Reply\AbstractReply;
use XF\Mvc\Reply\Error;
use XF\Mvc\Reply\Exception;
use XF\Mvc\RouteMatch;
use XF\PreEscaped;

use function count, gettype, intval, is_array;

abstract class AbstractController extends Controller
{
	protected $requestedVersion;

	protected function preDispatchType($action, ParameterBag $params)
	{
		$this->assertCorrectVersion($action);
		// TODO: check IP ban?
		$this->assertUserState($action);
		$this->assertViewingPermissions($action);
		$this->assertBoardActive($action);
		$this->assertNotSecurityLocked($action);

		$this->preDispatchController($action, $params);
	}

	protected function preDispatchController($action, ParameterBag $params)
	{
	}

	public function setupFromMatch(RouteMatch $match)
	{
		$this->setResponseType('api');

		if ($match instanceof \XF\Api\Mvc\RouteMatch)
		{
			$this->requestedVersion = $match->getVersion();
		}
		else
		{
			$this->requestedVersion = \XF::API_VERSION;
		}
	}

	public function applyReplyChanges($action, ParameterBag $params, AbstractReply &$reply)
	{
		$reply->setViewOption('requestedApiVersion', $this->requestedVersion);

		if ($this->responseType && !$reply->getResponseType())
		{
			$reply->setResponseType($this->responseType);
		}
	}

	public function setupFromReply(AbstractReply $reply)
	{
	}

	public function setSectionContext($sectionContext)
	{
	}

	public function setDefaultSectionContext($sectionContext)
	{
	}

	public function setContainerKey($containerKey)
	{
	}

	public function setContentKey($contentKey)
	{
	}

	public function setViewOption($option, $value)
	{
	}

	public function assertApiScope($scope)
	{
		$accessToken = \XF::accessToken();
		if ($accessToken && $accessToken->hasScope($scope))
		{
			return true;
		}

		$key = \XF::apiKey();
		if ($key && $key->hasScope($scope))
		{
			return true;
		}

		throw $this->exception(
			$this->error(\XF::phrase('api_error.missing_scope', ['scope' => $scope]), 403)
		);
	}

	public function assertApiScopeByRequestMethod($scope, array $extraMapping = [])
	{
		$modifier = 'write';
		$method = $this->request->getRequestMethod();

		if (isset($extraMapping[$method]))
		{
			$modifier = $extraMapping[$method];
		}
		else if ($method === 'get')
		{
			$modifier = 'read';
		}

		$this->assertApiScope("$scope:$modifier");
	}

	/**
	 * @template T of \XF\Mvc\Entity\Entity
	 *
	 * @param class-string<T> $identifier
	 * @param mixed $id
	 * @param array|string|null $with
	 * @param string|null $phraseKey
	 *
	 * @return T
	 *
	 * @throws Exception|\LogicException
	 */
	public function assertViewableApiRecord($identifier, $id, $with = null, $phraseKey = null)
	{
		$record = $this->assertRecordExists($identifier, $id, $with, $phraseKey);

		if (!method_exists($record, 'canView'))
		{
			throw new \LogicException("assertViewableApiRecord requires the entity of type $identifier to implement canView()");
		}
		if (\XF::isApiCheckingPermissions() && !$record->canView($error))
		{
			throw $this->exception($this->noPermission($error));
		}

		return $record;
	}

	public function assertValidApiPage($page, $perPage, $total)
	{
		if ($perPage < 1 || $total < 1)
		{
			return;
		}

		$page = max(1, intval($page));
		$maxPage = ceil($total / $perPage);

		if ($page <= $maxPage)
		{
			return; // within the range
		}

		throw $this->exception(
			$this->apiError(
				\XF::phrase('invalid_page_requested'),
				'invalid_page',
				['max' => $maxPage]
			)
		);
	}

	public function assertRequiredApiInput($inputKeys)
	{
		return $this->assertRequiredRequestData($inputKeys, 'input');
	}

	public function assertRequiredApiFile($inputKeys)
	{
		return $this->assertRequiredRequestData($inputKeys, 'files');
	}

	protected function assertRequiredRequestData($inputKeys, $type = 'input')
	{
		if (!is_array($inputKeys))
		{
			$inputKeys = [$inputKeys];
		}

		$missing = [];

		switch ($type)
		{
			case 'files': $method = 'fileExists'; break;
			default: $method = 'exists'; break;
		}

		foreach ($inputKeys AS $key)
		{
			if (!$this->request->$method($key))
			{
				$missing[] = $key;
			}
		}

		if ($missing)
		{
			throw $this->exception($this->requiredInputMissing($missing));
		}
	}

	public function requiredInputMissing($missing)
	{
		if (!is_array($missing))
		{
			$missing = [$missing];
		}

		return $this->apiError(
			\XF::phrase('required_input_missing_x', ['input' => implode(', ', $missing)]),
			'required_input_missing',
			['missing' => $missing]
		);
	}

	public function assertSuperUserKey()
	{
		$key = \XF::apiKey();
		if ($key->key_type !== 'super')
		{
			throw $this->exception($this->noPermission());
		}
	}

	public function assertRegisteredUser()
	{
		$visitor = \XF::visitor();
		if (!$visitor->user_id)
		{
			throw $this->exception($this->noPermission(\XF::phrase('login_required')));
		}
	}

	/**
	 * @throws Exception
	 */
	public function assertSuperAdmin()
	{
		if (\XF::isApiBypassingPermissions())
		{
			return;
		}

		$visitor = \XF::visitor();
		if (!$visitor->is_super_admin)
		{
			throw $this->exception($this->noPermission(\XF::phrase('you_must_be_super_admin_to_access_this_page')));
		}
	}

	/**
	 * @param string $permission
	 * @throws Exception
	 */
	public function assertAdminPermission($permission)
	{
		if (\XF::isApiBypassingPermissions())
		{
			return;
		}

		if (!\XF::visitor()->hasAdminPermission($permission))
		{
			throw $this->exception($this->noPermission());
		}
	}

	public function assertUserState($action)
	{
		if (\XF::isApiBypassingPermissions())
		{
			return;
		}

		$visitor = \XF::visitor();

		if ($visitor->is_banned)
		{
			throw $this->exception(
				$this->error(\XF::phrase('you_have_been_banned'), 403)
			);
		}

		if ($visitor->user_state == 'rejected')
		{
			throw $this->exception(
				$this->error(\XF::phrase('your_account_has_been_rejected'), 403)
			);
		}

		if ($visitor->user_state == 'disabled')
		{
			throw $this->exception(
				$this->error(\XF::phrase('your_account_has_been_disabled'), 403)
			);
		}
	}

	public function assertViewingPermissions($action)
	{
		if (\XF::isApiBypassingPermissions())
		{
			return;
		}

		if (!\XF::visitor()->hasPermission('general', 'view'))
		{
			throw $this->exception($this->noPermission());
		}
	}

	public function assertBoardActive($action)
	{
		$options = $this->options();
		if (!$options->boardActive && !\XF::visitor()->is_admin)
		{
			throw $this->exception($this->message(new PreEscaped($options->boardInactiveMessage), $this->app->config('serviceUnavailableCode')));
		}
	}

	public function assertNotSecurityLocked($action)
	{
		$visitor = \XF::visitor();
		if ($visitor->user_id && $visitor->security_lock && !\XF::isApiBypassingPermissions())
		{
			throw $this->exception($this->noPermission(
				\XF::phrase('your_account_is_currently_security_locked')
			));
		}
	}

	public function getPaginationData($results, $page, $perPage, $total)
	{
		$page = max(1, intval($page));
		$perPage = max(1, intval($perPage));
		$total = max(0, intval($total));
		$maxPage = ceil($total / $perPage);

		$page = min($page, $maxPage);

		return [
			'current_page' => $page,
			'last_page' => $maxPage,
			'per_page' => $perPage,
			'shown' => count($results),
			'total' => $total,
		];
	}

	public function getAttachmentTempHashFromKey(
		$key,
		$expectedContentType,
		array $expectedContext,
		$allowExtraContext = false
	)
	{
		if (!$key)
		{
			return null;
		}

		/** @var ApiAttachmentKey $keyEnt */
		$keyEnt = $this->em()->find(ApiAttachmentKey::class, $key);
		if (!$keyEnt)
		{
			throw $this->exception($this->error(\XF::phrase('api_error.attachment_key_unknown')));
		}

		if ($keyEnt->content_type !== $expectedContentType)
		{
			throw $this->exception($this->apiError(
				\XF::phrase('api_error.attachment_key_type_wrong'),
				'attachment_key_type_wrong',
				['expected' => $expectedContentType]
			));
		}

		if ($keyEnt->user_id !== \XF::visitor()->user_id)
		{
			throw $this->exception($this->apiError(
				\XF::phrase('api_error.attachment_key_user_wrong'),
				'attachment_key_user_wrong',
				['expected' => \XF::visitor()->user_id]
			));
		}

		if (!$keyEnt->hasExpectedContext($expectedContext, $allowExtraContext))
		{
			throw $this->exception($this->apiError(
				\XF::phrase('api_error.attachment_key_context_wrong'),
				'attachment_key_context_wrong',
				['expected' => http_build_query($expectedContext)]
			));
		}

		return $keyEnt->temp_hash;
	}

	public function reroute(RouteMatch $match)
	{
		if ($match instanceof \XF\Api\Mvc\RouteMatch)
		{
			$match->setVersion($this->requestedVersion);
		}

		return parent::reroute($match);
	}

	public function apiError($errorMessage, $errorCode, ?array $params = null, $httpCode = 400)
	{
		$error = new ErrorMessage($errorMessage, $errorCode, $params);

		return new Error($error, $httpCode);
	}

	public function apiResult($result)
	{
		if (is_array($result))
		{
			$result = new ArrayResult($result);
		}
		else if ($result instanceof Entity)
		{
			$result = $result->toApiResult();
		}
		else if (!($result instanceof ResultInterface))
		{
			throw new \LogicException(
				"Must pass \XF\Entity\ResultInterface or array to apiResult; received " . gettype($result)
			);
		}

		return new ApiResult($result);
	}

	public function apiSuccess(array $extra = [])
	{
		return $this->apiBoolResult(true, $extra);
	}

	public function apiBoolResult(bool $success, array $extra = [])
	{
		return $this->apiResult(['success' => $success] + $extra);
	}

	public function checkCsrfIfNeeded($action, ParameterBag $params)
	{
		// never needed in the API as a custom header is required
	}

	public function allowUnauthenticatedRequest($action)
	{
		return false;
	}
}
