<?php

namespace XF\Cli\Command\AddOn;

use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use XF\Cli\Command\AbstractCommand;
use XF\Cli\Command\AddOnActionTrait;
use XF\Cli\Command\JobRunnerTrait;
use XF\Util\File;
use XF\Util\Hash;
use XF\Util\Json;

use function gettype, in_array, intval, is_array;

class ValidateJson extends AbstractCommand
{
	use AddOnActionTrait;
	use JobRunnerTrait;

	protected function configure()
	{
		$this
			->setName('xf-addon:validate-json')
			->setDescription(
				'Validates the contents of the add-on JSON file to ensure all of the required values exist and are of the correct type.'
			)
			->addArgument(
				'id',
				InputArgument::REQUIRED,
				'Add-on ID'
			);
	}

	protected function execute(InputInterface $input, OutputInterface $output)
	{
		$id = $input->getArgument('id');

		$addOn = $this->checkEditableAddOn($id, $error);
		if (!$addOn)
		{
			$output->writeln('<error>' . $error . '</error>');
			return 1;
		}

		$output->writeln("Attempting to validate addon.json file...");

		/** @var QuestionHelper $helper */
		$helper = $this->getHelper('question');

		$jsonPath = $addOn->getJsonPath();
		if (!file_exists($jsonPath))
		{
			$question = new ConfirmationQuestion("<question>No JSON path can be found, create the default one? (y/n)</question> ");
			if ($helper->ask($input, $output, $question))
			{
				$json = $addOn->prepareJsonFile([
					'title' => $addOn->title,
					'version_id' => intval($addOn->version_id),
					'version_string' => $addOn->version_string,
				]);
				$written = File::writeFile($jsonPath, Json::jsonEncodePretty(
					$addOn->prepareJsonFile($json)
				), false);

				if ($written)
				{
					$addOn->getInstalledAddOn()->fastUpdate('json_hash', Hash::hashTextFile($jsonPath, 'sha256'));
					$output->writeln("The addon.json file was successfully written out to $jsonPath");
				}
				else
				{
					$output->writeln("<error>The addon.json file could not be written out to $jsonPath. Please create the addon.json file manually before continuing.</error>");
					return 1;
				}
			}
			else
			{
				$output->writeln("<error>Please create the addon.json file before continuing.</error>");
				return 1;
			}
		}
		else
		{
			$json = @json_decode(file_get_contents($addOn->getJsonPath()), true);
		}

		if (!is_array($json))
		{
			$output->writeln("<error>The contents of the addon.json file could not be decoded.</error>");
			return 1;
		}

		$requiredKeys = [
			'title',
			['version_id', 'is_int'],
			'version_string',
		];
		$optionalKeys = [
			'legacy_addon_id',
			'description',
			'dev',
			'dev_url',
			'faq_url',
			'support_url',
			['extra_urls', 'is_array'],
			['require', 'is_array'],
			'icon',
		];

		// keys which are uncommon or have a different meaning if unset
		$ignoreKeys = [
			'options',
			'composer_autoload',
		];

		$hasMissingKeys = false;
		$checkedKeys = [];
		$errors = [];
		$warnings = [];

		foreach ($requiredKeys AS $key)
		{
			if (is_array($key))
			{
				[$key, $f] = $key;
			}
			else
			{
				$f = 'is_string';
			}

			$checkedKeys[] = $key;

			if (!isset($json[$key]))
			{
				$errors[] = "Key '{$key}' is missing from your JSON file.";
				$hasMissingKeys = true;
				continue;
			}

			$actualType = gettype($json[$key]);
			if (!$f($json[$key]))
			{
				$type = str_replace('is_', '', $f);
				$errors[] = "Expected type '{$type}' for '{$key}' but is type '{$actualType}'.";
			}
		}

		foreach ($optionalKeys AS $key)
		{
			if (is_array($key))
			{
				[$key, $f] = $key;
			}
			else
			{
				$f = 'is_string';
			}

			$checkedKeys[] = $key;

			if (!isset($json[$key]))
			{
				$warnings[] = "Key '{$key}' is missing from your JSON file.";
				$hasMissingKeys = true;
				continue;
			}

			$actualType = gettype($json[$key]);
			if (!$f($json[$key]))
			{
				$type = str_replace('is_', '', $f);
				$errors[] = "Expected type '{$type}' for '{$key}' but is type '{$actualType}'.";
			}
		}

		$unexpectedKeys = [];
		foreach (array_keys($json) AS $key)
		{
			if (in_array($key, $checkedKeys) || in_array($key, $ignoreKeys))
			{
				continue;
			}
			$unexpectedKeys[] = $key;
		}

		if ($unexpectedKeys)
		{
			$warnings[] = "The following keys were found which were unexpected: '" . implode('\', \'', $unexpectedKeys) . "' these may be safe to ignore or they may represent mistakes.";
		}

		$fixedMissingKeys = false;
		if ($hasMissingKeys)
		{
			$question = new ConfirmationQuestion("<question>Missing keys were detected while validating your addon.json file. Would you like to set them to their default values? (y/n)</question> ");
			if ($helper->ask($input, $output, $question))
			{
				$written = File::writeFile($jsonPath, Json::jsonEncodePretty(
					$addOn->prepareJsonFile($json)
				), false);
				if ($written)
				{
					$fixedMissingKeys = true;
					$addOn->getInstalledAddOn()->fastUpdate('json_hash', Hash::hashTextFile($jsonPath, 'sha256'));
					$output->writeln("The addon.json file was successfully written out to $jsonPath");
				}
			}
		}

		if ($errors)
		{
			$output->writeln(["", "<error>Please rectify the following error/s we found while validating your addon.json file before continuing:</error>"]);
			foreach ($errors AS $error)
			{
				$output->writeln("<info>\t * $error</info>");
			}
			if ($warnings)
			{
				$output->writeln(["", "<warning>Additionally, please review the following warning/s:</warning>"]);
				foreach ($warnings AS $warning)
				{
					$output->writeln("<info>\t * $warning</info>");
				}
			}
			if ($fixedMissingKeys)
			{
				$output->writeln(["", "The addon.json file was updated to repair missing keys, so no action required."]);
				// resolved so we won't error out (this may leave some unresolved errors, notably type errors, but
				// they shouldn't be significant and could get sorted in subsequent runs).
			}
			else
			{
				return 1;
			}
		}

		if ($warnings && !$errors)
		{
			$output->writeln(["", "<warning>Please review the following warning/s we found while validating your addon.json file. These are safe to ignore:</warning>"]);
			foreach ($warnings AS $warning)
			{
				$output->writeln("<info>\t * $warning</info>");
			}
			if ($fixedMissingKeys)
			{
				$output->writeln(["", "The addon.json file was updated to repair missing keys, so no action required."]);
			}
			// We won't block warnings only, consider it successful.
		}

		$output->writeln("JSON file validates successfully!");

		return 0;
	}
}
