<?php

namespace XF\Admin\Controller;

use XF\ControllerPlugin\CodeEditorPlugin;
use XF\ControllerPlugin\StylePlugin;
use XF\Diff;
use XF\Diff3;
use XF\Entity\Style;
use XF\Entity\Template;
use XF\Finder\TemplateFinder;
use XF\Job\TemplateMerge;
use XF\Mvc\ParameterBag;
use XF\Repository\StyleRepository;
use XF\Repository\TemplateHistoryRepository;
use XF\Repository\TemplateRepository;
use XF\Util\Arr;

use function count;

class TemplateController extends AbstractController
{
	protected function preDispatchController($action, ParameterBag $params)
	{
		$this->assertAdminPermission('style');
	}

	public function actionIndex()
	{
		$style = $this->plugin(StylePlugin::class)->getActiveEditStyle();

		$type = $this->filter('type', 'string');
		if (!$type)
		{
			$type = 'public';
		}

		return $this->redirect($this->buildLink('styles/templates', $style, ['type' => $type]));
	}

	protected function templateAddEdit(Template $template)
	{
		if ($template->exists() && !$this->request->exists('style_id'))
		{
			$styleId = $template->style_id;
		}
		else
		{
			$styleId = $this->filter('style_id', 'uint');

			if (empty($template->title) && $prefix = $this->filter('prefix', 'str'))
			{
				$template->set('title', $prefix);
			}
		}

		$style = $this->assertStyleExists($styleId);
		if (!$style->canEdit())
		{
			return $this->error(\XF::phrase('templates_in_this_style_can_not_be_modified'));
		}

		$types = $this->getTemplateRepo()->getTemplateTypes($style);
		if (!isset($types[$template->type]))
		{
			return $this->error(\XF::phrase('templates_in_this_style_can_not_be_modified'));
		}

		if (!$template->exists() && $style->style_id)
		{
			$template->addon_id = '';
		}

		$viewParams = [
			'template' => $template,
			'hasHistory' => $template->exists() ? $template->History->count() : false,
			'style' => $style,
			'styleTree' => $this->getStyleRepo()->getStyleTree(),
			'types' => $types,
			'redirect' => $this->getDynamicRedirect(),
		];
		return $this->view('XF:Template\Edit', 'template_edit', $viewParams);
	}

	public function actionEdit(ParameterBag $params)
	{
		$template = $this->assertTemplateExists($params->template_id);
		return $this->templateAddEdit($template);
	}

	public function actionAdd()
	{
		$template = $this->em()->create(Template::class);
		$template->type = $this->filter('type', 'str', 'public');
		return $this->templateAddEdit($template);
	}

	public function actionCodeEditorModeLoader()
	{
		$language = $this->filter('language', 'str');

		/** @var CodeEditorPlugin $plugin */
		$plugin = $this->plugin(CodeEditorPlugin::class);

		return $plugin->actionModeLoader($language);
	}

	protected function templateSaveProcess(Template $template)
	{
		$form = $this->formAction();

		$input = $this->filter([
			'style_id' => 'uint',
			'type' => 'str',
			'title' => 'str',
			'template' => 'str',
			'addon_id' => 'str',
		]);

		$form->setup(function () use ($template)
		{
			if ($template->style_id > 0)
			{
				// force an update to resolve any out of date issues
				$template->updateVersionId();
				$template->last_edit_date = \XF::$time;
			}
		});

		$form->basicEntitySave($template, $input);

		return $form;
	}

	public function actionSave(ParameterBag $params)
	{
		$this->assertPostOnly();

		if ($params->template_id)
		{
			$template = $this->assertTemplateExists($params->template_id);

			$styleId = $this->filter('style_id', 'uint');
			if ($template->style_id != $styleId)
			{
				$template = $this->finder(TemplateFinder::class)->where([
					'style_id' => $styleId,
					'title' => $template->title,
					'type' => $template->type,
				])->fetchOne();
				if (!$template)
				{
					$template = $this->em()->create(Template::class);
				}
			}
		}
		else
		{
			$template = $this->em()->create(Template::class);
		}

		$this->templateSaveProcess($template)->run();

		$dynamicRedirect = $this->getDynamicRedirect('invalid', false);
		if ($dynamicRedirect == 'invalid' || !preg_match('#(styles|templates)/#', $dynamicRedirect))
		{
			$dynamicRedirect = null;
		}

		if ($this->request->exists('exit'))
		{
			if ($dynamicRedirect)
			{
				$redirect = $dynamicRedirect;
			}
			else
			{
				$redirect = $this->buildLink('styles/templates', $template->Style, ['type' => $template->type]);
			}
			$redirect .= $this->buildLinkHash($template->template_id);
		}
		else
		{
			$redirect = $this->buildLink('templates/edit', $template, ['_xfRedirect' => $dynamicRedirect]);
		}

		return $this->redirect($redirect);
	}

	public function actionHistory(ParameterBag $params)
	{
		$template = $this->assertTemplateExists($params->template_id);

		$oldId = $this->filter('old', 'uint');
		$newId = $this->filter('new', 'uint');

		$list = [];

		if ($template->History)
		{
			$list = $template->History;
			$list = $list->toArray();
			arsort($list);
		}
		$newestHistory = reset($list);

		if ($oldId)
		{
			// doing a comparison
			$oldText = isset($list[$oldId]) ? $list[$oldId]['template'] : '';

			if ($newId)
			{
				$newText = isset($list[$newId]) ? $list[$newId]['template'] : '';
			}
			else
			{
				$newText = $template['template'];
			}

			$diff = new Diff();
			$diffs = $diff->findDifferences($oldText, $newText);
		}
		else
		{
			$diffs = [];
		}

		$viewId = $this->filter('view', 'uint');
		if ($viewId)
		{
			$history = $list[$viewId] ?? false;
		}
		else
		{
			$history = false;
		}

		$viewParams = [
			'template' => $template,
			'list' => $list,
			'oldId' => ($oldId ?: ($newestHistory ? $newestHistory['template_history_id'] : 0)),
			'newId' => $newId,
			'diffs' => $diffs,
			'history' => $history,
		];

		if ($history)
		{
			return $this->view('XF:Template\History\View', 'template_history_view', $viewParams);
		}
		else if ($oldId)
		{
			return $this->view('XF:Template\History\Compare', 'template_history_compare', $viewParams);
		}
		else
		{
			return $this->view('XF:Template\History', 'template_history', $viewParams);
		}
	}

	public function actionDelete(ParameterBag $params)
	{
		$template = $this->assertTemplateExists($params->template_id);
		if (!$template->Style || !$template->Style->canEdit())
		{
			return $this->error(\XF::phrase('templates_in_this_style_can_not_be_modified'));
		}

		if ($this->isPost())
		{
			$template->delete();

			$redirect = $this->getDynamicRedirect('invalid', false);
			if ($redirect == 'invalid' || !preg_match('#(styles|templates)/#', $redirect))
			{
				$redirect = $this->buildLink('styles/templates', $template->Style, ['type' => $template->type]);
			}
			return $this->redirect($redirect);
		}
		else
		{
			$viewParams = [
				'template' => $template,
			];
			return $this->view('XF:Template\Delete', 'template_delete', $viewParams);
		}
	}

	protected function filterSearchConditions()
	{
		return $this->filter([
			'addon_id' => 'str',
			'type' => 'str',
			'title' => 'str',
			'template' => 'str',
			'template_cs' => 'bool',
			'state' => 'array-str',
		]);
	}

	public function actionSearch()
	{
		$this->setSectionContext('searchTemplates');

		$styleRepo = $this->getStyleRepo();

		if ($this->filter('search', 'uint'))
		{
			$style = $this->assertStyleExists($this->filter('style_id', 'uint'));
			if (!$style->canEdit())
			{
				return $this->error(\XF::phrase('templates_in_this_style_can_not_be_modified'));
			}

			$filterer = $this->setupTemplateFilterer($style);
			$finder = $filterer->apply();

			$filterLinkParams = $filterer->getLinkParams();
			if (count($filterLinkParams) <= 1)
			{
				$type = $filterer->getRawFilters()['type'] ?? 'public';

				// we always get a type filter, so we want to make sure there's something more than that
				return $this->redirect($this->buildLink('styles/templates', $style, ['type' => $type]));
			}

			$linkParams = [
				'search' => 1,
				'style_id' => $style->style_id,
			];
			$linkParams += $filterLinkParams;

			$total = $finder->total();

			if ($this->isPost() && $total > 0)
			{
				return $this->redirect($this->buildLink('templates/search', null, $linkParams));
			}

			$finder->with('Template.AddOn');

			$templates = $finder->fetch();

			$viewParams = [
				'style' => $style,
				'templates' => $templates,
				'total' => $total,
				'linkParams' => $linkParams,
				'filterDisplay' => $filterer->getDisplayValues(),
			];
			return $this->view('XF:Template\SearchResults', 'template_search_results', $viewParams);
		}
		else
		{
			$viewParams = [
				'styleTree' => $styleRepo->getStyleTree(),
				'styleId' => $this->plugin(StylePlugin::class)->getActiveStyleId(),
				'types' => $this->getTemplateRepo()->getTemplateTypes(),
			];
			return $this->view('XF:Template\Search', 'template_search', $viewParams);
		}
	}

	protected function setupTemplateFilterer(Style $style): \XF\Filterer\Template
	{
		$templateRepo = $this->getTemplateRepo();

		/** @var \XF\Filterer\Template $filterer */
		$filterer = $this->app->filterer(\XF\Filterer\Template::class, [
			'style_id' => $style->style_id,
			'template_types' => $templateRepo->getTemplateTypes($style),
		]);
		$filterer->addFilters($this->request, $this->filter('_skipFilter', 'str'));

		return $filterer;
	}

	public function actionRefineSearch()
	{
		$style = $this->assertStyleExists($this->filter('style_id', 'uint'));
		if (!$style->canEdit())
		{
			$style = $this->plugin(StylePlugin::class)->getActiveEditStyle();
		}

		$styleRepo = $this->getStyleRepo();

		$filterer = $this->setupTemplateFilterer($style);

		$viewParams = [
			'style' => $style,
			'styleTree' => $styleRepo->getStyleTree(),
			'conditions' => $filterer->getFiltersForForm(),
			'types' => $this->getTemplateRepo()->getTemplateTypes(),
		];
		return $this->view('XF:Template\RefineSearch', 'template_refine_search', $viewParams);
	}

	public function actionOutdated()
	{
		$this->setSectionContext('outdatedTemplates');

		$outdatedTemplates = $this->getTemplateRepo()->getOutdatedTemplates();
		$outdatedGrouped = Arr::arrayGroup(
			$outdatedTemplates,
			function ($v) { return $v['template']->style_id; }
		);

		$viewParams = [
			'outdatedTemplates' => $outdatedTemplates,
			'outdatedGrouped' => $outdatedGrouped,
			'styleTree' => $this->repository(StyleRepository::class)->getStyleTree(),
			'total' => count($outdatedTemplates),
			'autoMerged' => $this->filter('automerged', 'bool'),
		];
		return $this->view('XF:Template\Outdated', 'template_outdated', $viewParams);
	}

	public function actionCompare(ParameterBag $params)
	{
		$template = $this->assertTemplateForComparison($params->template_id);
		$parentTemplate = $template->ParentTemplate;

		$diff = new Diff();
		$diffs = $diff->findDifferences($parentTemplate->template, $template->template);

		$viewParams = [
			'template' => $template,
			'parentTemplate' => $parentTemplate,
			'diffs' => $diffs,
		];
		return $this->view('XF:Template\Compare', 'template_compare', $viewParams);
	}

	public function actionMergeOutdated(ParameterBag $params)
	{
		$this->setSectionContext('outdatedTemplates');

		$template = $this->assertTemplateForComparison($params->template_id);
		$parentTemplate = $template->ParentTemplate;

		if (!$parentTemplate->last_edit_date || $parentTemplate->last_edit_date < $template->last_edit_date)
		{
			return $this->error(\XF::phrase('custom_template_out_of_date_edited_recently_no_merge'));
		}

		$historyRepo = $this->repository(TemplateHistoryRepository::class);
		$previousVersion = $historyRepo->getHistoryForMerge($template, $parentTemplate);

		if (!$previousVersion)
		{
			return $this->error(\XF::phrase('no_previous_version_of_parent_could_be_found'));
		}

		if ($this->isPost())
		{
			$merged = $this->filter('merged', 'array-str,no-trim');
			$final = implode("\n", $merged);

			$template->template = $final;
			$template->last_edit_date = time();

			// even if the template isn't changed we should
			// make the custom template the same version so
			// it is no longer outdated.
			if (!$template->isChanged('template'))
			{
				$template->updateVersionId();
			}

			$template->save();

			return $this->redirect($this->buildLink('templates/outdated'));
		}
		else
		{
			$diff = new Diff3();
			$diffs = $diff->findDifferences($template->template, $previousVersion->template, $parentTemplate->template);

			$viewParams = [
				'template' => $template,
				'parentTemplate' => $parentTemplate,
				'previousVersion' => $previousVersion,
				'diffs' => $diffs,
			];
			return $this->view('XF:Template\MergeOutdated', 'template_merge_outdated', $viewParams);
		}
	}

	public function actionAutoMerge()
	{
		if ($this->isPost())
		{
			$templateRepo = $this->getTemplateRepo();
			$templateIds = array_keys($templateRepo->getOutdatedTemplates());

			$this->app->jobManager()->enqueueUnique('autoMerge', TemplateMerge::class, [
				'templateIds' => $templateIds,
			]);

			return $this->redirect($this->buildLink('templates/outdated', null, ['automerged' => 1]));
		}
		else
		{
			return $this->view('XF:Template\AutoMerge', 'template_auto_merge');
		}
	}

	protected function assertTemplateForComparison($templateId)
	{
		$template = $this->assertTemplateExists($templateId, 'Style');
		if (!$template->style_id)
		{
			throw $this->exception($this->error(\XF::phrase('you_cannot_compare_custom_changes_for_master_template')));
		}

		$parentTemplate = $template->ParentTemplate;
		if (!$parentTemplate)
		{
			throw $this->exception($this->error(\XF::phrase('this_template_does_not_have_parent_version')));
		}

		return $template;
	}

	/**
	 * @param string $id
	 * @param array|string|null $with
	 * @param null|string $phraseKey
	 *
	 * @return Style
	 */
	protected function assertStyleExists($id, $with = null, $phraseKey = null)
	{
		return $this->plugin(StylePlugin::class)->assertStyleExists($id, $with, $phraseKey);
	}

	/**
	 * @param string $id
	 * @param array|string|null $with
	 * @param null|string $phraseKey
	 *
	 * @return Template
	 */
	protected function assertTemplateExists($id, $with = null, $phraseKey = null)
	{
		return $this->assertRecordExists(Template::class, $id, $with, $phraseKey);
	}

	/**
	 * @return StyleRepository
	 */
	protected function getStyleRepo()
	{
		return $this->repository(StyleRepository::class);
	}

	/**
	 * @return TemplateRepository
	 */
	protected function getTemplateRepo()
	{
		return $this->repository(TemplateRepository::class);
	}
}
