<?php

namespace XF\Install\Controller;

use XF\AddOn\AddOn;
use XF\Db\Exception;
use XF\Entity\User;
use XF\Mvc\ParameterBag;
use XF\Mvc\Reply\AbstractReply;
use XF\Mvc\Reply\Redirect;
use XF\Repository\CollectStatsRepository;
use XF\Repository\IpRepository;
use XF\Repository\OptionRepository;
use XF\Repository\TemplateRepository;
use XF\Service\User\LoginService;
use XF\Util\Arr;
use XF\Util\File;
use XF\Util\Php;

use function is_array;

class Upgrade extends AbstractController
{
	protected function preDispatchController($action, ParameterBag $params)
	{
		if (!$this->getInstallHelper()->isInstalled())
		{
			throw $this->exception($this->redirect('index.php?install/'));
		}

		if (method_exists($this->app, 'setupUpgradeSession'))
		{
			$this->app->setupUpgradeSession();
		}

		if (strtolower($action) !== 'login')
		{
			$visitor = \XF::visitor();
			if (!$visitor->is_admin)
			{
				throw $this->exception($this->rerouteController(self::class, 'login'));
			}
		}
	}

	public function actionIndex()
	{
		Php::resetOpcache();

		$installHelper = $this->getInstallHelper();
		$upgrader = $this->getUpgrader();

		if ($upgrader->getNewestUpgradeVersionId() > \XF::$versionId)
		{
			return $this->error(\XF::phrase('upgrade_found_newer_than_version'));
		}

		$xfAddOn = new AddOn('XF', \XF::app()->addOnManager());
		$xfAddOn->passesHealthCheck($missing, $inconsistent);

		if ($upgrader->isUpgradeComplete() && $this->options()->currentVersionId >= \XF::$versionId)
		{
			return $this->view('XF:Upgrade\Current', 'upgrade_current', [
				'fileErrors' => array_merge($missing, $inconsistent),
				'hasHashes' => $xfAddOn->hasHashes(),
			]);
		}

		$db = $this->app->db();

		$needMigration = $this->app->config('legacyExists');

		$currentVersion = $upgrader->getCurrentVersion();

		$viewParams = [
			'errors' => $installHelper->getRequirementErrors($db),
			'warnings' => $installHelper->getRequirementWarnings($db),
			'currentVersion' => $currentVersion,
			'isSignificantUpgrade' => $upgrader->isSignificantUpgrade(),
			'isCliRecommended' => $upgrader->isCliRecommended(),
			'addOnConflicts' => $upgrader->getAddOnConflicts($currentVersion),
			'fileErrors' => array_merge($missing, $inconsistent),
			'hasHashes' => $xfAddOn->hasHashes(),
			'needsConfigMigration' => $needMigration,
		];
		return $this->view('XF:Upgrade\Start', 'upgrade_start', $viewParams);
	}

	public function actionMigrateConfig()
	{
		$this->assertPostOnly();

		if ($this->app->config('exists') || !$this->app->config('legacyExists'))
		{
			return $this->redirect('index.php?upgrade/');
		}

		$installHelper = $this->getUpgrader();

		$config = $installHelper->migrateLegacyConfigIfNeeded($written);

		$viewParams = [
			'config' => $config,
			'written' => $written,
			'configFile' => $this->app->container('config.file'),
		];
		return $this->view('XF:Upgrade\MigrateConfig', 'upgrade_migrate_config', $viewParams);
	}

	public function actionRun()
	{
		$this->assertPostOnly();

		if (!$this->app->config('exists'))
		{
			return $this->error(\XF::phrase('config_file_x_could_not_be_found', [
				'file' => $this->app->container('config.file'),
			]));
		}

		$upgrader = $this->getUpgrader();
		$upgrader->syncUpgradeLogStructure();
		$upgrader->syncJobStructure();

		$lastUpgradeVersion = $upgrader->getLatestUpgradeVersion();

		if ($lastUpgradeVersion['version_id'] === \XF::$versionId && !$lastUpgradeVersion['last_step'])
		{
			return $this->actionRebuild();
		}

		$input = $this->filter([
			'run_version' => 'uint',
			'step' => 'str',
			'position' => 'uint',
			'step_data' => 'json-array',
		]);

		if (!$input['run_version'])
		{
			if ($lastUpgradeVersion['last_step'])
			{
				// pick up from the next step
				$input['run_version'] = $lastUpgradeVersion['version_id'];
				$input['step'] = $lastUpgradeVersion['last_step'] + 1;
			}
			else
			{
				// last upgrade is complete, pick up from the next upgrade
				$input['run_version'] = $upgrader->getNextUpgradeVersionId($lastUpgradeVersion['version_id']);
				$input['step'] = 1;
			}

			// starting a new step (or upgrade script), ignore these
			$input['position'] = 0;
			$input['step_data'] = [];

			if ($input['run_version'])
			{
				if ($input['run_version'] > \XF::$versionId)
				{
					return $this->error(\XF::phrase('upgrade_found_newer_than_version'));
				}

				$upgrade = $upgrader->getUpgrade($input['run_version']);
			}
			else
			{
				$upgrade = false;
			}
		}
		else
		{
			$upgrade = $upgrader->getUpgrade($input['run_version']);
		}

		if (!$upgrade)
		{
			$upgrader->insertUpgradeLog(\XF::$versionId);
			return $this->actionRebuild();
		}

		$upgrader->assertValidUpgradeSteps($upgrade);

		if (!$input['step'])
		{
			$input['step'] = 1;
		}

		if (method_exists($upgrade, 'step' . $input['step']))
		{
			$result = $upgrade->{'step' . $input['step']}($input['position'], $input['step_data'], $this);
		}
		else
		{
			$result = 'complete';
		}

		if ($result instanceof AbstractReply)
		{
			return $result;
		}

		$stepMessage = '';
		$stepData = false;

		if ($result === 'complete')
		{
			$upgrader->insertUpgradeLog($input['run_version']);

			$viewParams = [
				'newRunVersion' => '',
				'newStep' => '',
				'versionName' => $upgrade->getVersionName(),
				'step' => $input['step'],
			];
		}
		else
		{
			if ($result === true || $result === null)
			{
				// step finished
				$upgrader->insertUpgradeLog($input['run_version'], $input['step']);

				$result = $input['step'] + 1;
				$input['position'] = 0;
				$stepData = [];
			}
			else if (is_array($result))
			{
				// step not finished, don't log anything yet
				$input['position'] = $result[0];
				$stepMessage = $result[1];
				if (!empty($result[2]))
				{
					$stepData = $result[2];
				}

				$result = $input['step']; // stay on same step
			}

			$viewParams = [
				'newRunVersion' => $input['run_version'],
				'newStep' => $result,
				'position' => $input['position'],
				'stepMessage' => $stepMessage,
				'stepData' => $stepData,
				'versionName' => $upgrade->getVersionName(),
				'step' => $input['step'],
			];
		}

		return $this->view('XF:Upgrade\Run', 'upgrade_run', $viewParams);
	}

	public function actionRebuild()
	{
		$upgrader = $this->getUpgrader();

		$currentVersion = $upgrader->getCurrentVersion();
		$extraJobs = $upgrader->getExtraUpgradeJobsMap();
		$this->getInstallHelper()->insertRebuildJob(null, $extraJobs, true, $currentVersion);

		return $this->rerouteController(self::class, 'runJob');
	}

	public function actionRunJob()
	{
		$redirect = 'index.php?upgrade/complete';
		$session = $this->app->session();

		if (empty($this->options()->collectServerStats['configured'])
			|| $session->repromptStatsCollection
		)
		{
			$redirect = 'index.php?upgrade/options';
		}

		$output = $this->manualJobRunner('index.php?upgrade/run-job', $redirect);

		if ($output instanceof Redirect)
		{
			// if this still exists, it will have a future date which means the process got interrupted
			if ($this->getInstallHelper()->hasRebuildJobPending())
			{
				return $this->view('XF:Upgrade\RebuildErrors', 'upgrade_rebuild_errors');
			}

			$upgrader = $this->getUpgrader();
			if ($upgrader->isUpgradeComplete())
			{
				// all complete
				$upgrader->completeUpgrade();
			}
		}

		return $output;
	}

	public function actionOptions()
	{
		$session = $this->app->session();

		if ($this->isPost())
		{
			$options = Arr::arrayFilterKeys(
				$this->filter('options', 'array'),
				['collectServerStats'],
				true
			);

			$optionRepo = $this->repository(OptionRepository::class);
			$optionRepo->updateOptions($options);

			$session->remove('repromptStatsCollection');
			$session->remove('repromptStatsCollectionReasons');

			return $this->redirect('index.php?upgrade/complete');
		}
		else
		{
			$collectStatsRepo = $this->repository(CollectStatsRepository::class);

			$viewParams = [
				'isEnabled' => $collectStatsRepo->isEnabled(),
				'isReprompt' => $session->repromptStatsCollection,
				'repromptReasons' => $session->repromptStatsCollectionReasons,
			];
			return $this->view('XF:Upgrade\Options', 'upgrade_options', $viewParams);
		}
	}

	public function actionComplete()
	{
		if ($this->getInstallHelper()->hasRebuildJobPending())
		{
			return $this->view('XF:Upgrade\RebuildErrors', 'upgrade_rebuild_errors');
		}

		if ($this->options()->currentVersionId == \XF::$versionId)
		{
			$schemaErrors = $this->getUpgrader()->getDefaultSchemaErrors();
			if ($schemaErrors)
			{
				$viewParams = [
					'errors' => $schemaErrors,
				];
				return $this->view('XF:Upgrade\Errors', 'upgrade_errors', $viewParams);
			}

			$upgrader = $this->getUpgrader();
			$upgrader->renameLegacyConfigIfNeeded();

			$viewParams = [
				'outdatedTemplates' => $this->app->repository(TemplateRepository::class)->countOutdatedTemplates(),
			];

			return $this->view('XF:Upgrade\Complete', 'upgrade_complete', $viewParams);
		}
		else
		{
			return $this->error(\XF::phrase('uh_oh_upgrade_did_not_complete'));
		}
	}

	public function actionLogin()
	{
		$error = null;

		if ($this->isPost())
		{
			$input = $this->filter([
				'login' => 'str',
				'password' => 'str',
			]);

			$ip = $this->request->getIp();
			$user = null;
			$error = null;

			try
			{
				$loginService = $this->service(LoginService::class, $input['login'], $ip);
				if ($loginService->isLoginLimited($limitType))
				{
					return $this->error(\XF::phrase('your_account_has_temporarily_been_locked_due_to_failed_login_attempts'));
				}

				$loginService->setAllowPasswordUpgrade(false);

				$user = $loginService->validate($input['password'], $error);
			}
			catch (Exception $e)
			{
			}

			if (!$user)
			{
				return $this->notFound($error ?: \XF::phrase('requested_user_not_found'));
			}

			$this->completeLogin($user);

			$visitor = \XF::visitor();
			if (!$visitor->is_admin)
			{
				return $this->error(\XF::phrase('your_account_does_not_have_admin_privileges'));
			}

			return $this->redirect('index.php?upgrade/');
		}

		$viewParams = [
			'error' => $error,
		];
		return $this->view('XF:Upgrade\Login', 'upgrade_login', $viewParams);
	}

	protected function completeLogin(User $user)
	{
		$this->session()->changeUser($user);
		\XF::setVisitor($user);

		$ip = $this->request->getIp();

		// Avoid an exception for *very* legacy upgrades
		try
		{
			$this->repository(IpRepository::class)->logIp(
				$user->user_id,
				$ip,
				'user',
				$user->user_id,
				'login_upgrade'
			);
		}
		catch (Exception $e)
		{
		}

		if (!File::installLockExists())
		{
			File::writeInstallLock();
		}
	}
}
