<?php

/**
 * This file is part of the CodeIgniter 4 framework.
 *
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CodeIgniter\Commands\Encryption;

use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Config\DotEnv;
use CodeIgniter\Encryption\Encryption;

/**
 * Generates a new encryption key.
 */
class GenerateKey extends BaseCommand
{
	/**
	 * The Command's group.
	 *
	 * @var string
	 */
	protected $group = 'Encryption';

	/**
	 * The Command's name.
	 *
	 * @var string
	 */
	protected $name = 'key:generate';

	/**
	 * The Command's usage.
	 *
	 * @var string
	 */
	protected $usage = 'key:generate [options]';

	/**
	 * The Command's short description.
	 *
	 * @var string
	 */
	protected $description = 'Generates a new encryption key and writes it in an `.env` file.';

	/**
	 * The command's options
	 *
	 * @var array
	 */
	protected $options = [
		'--force'  => 'Force overwrite existing key in `.env` file.',
		'--length' => 'The length of the random string that should be returned in bytes. Defaults to 32.',
		'--prefix' => 'Prefix to prepend to encoded key (either hex2bin or base64). Defaults to hex2bin.',
		'--show'   => 'Shows the generated key in the terminal instead of storing in the `.env` file.',
	];

	/**
	 * Actually execute the command.
	 *
	 * @param array $params
	 *
	 * @return void
	 */
	public function run(array $params)
	{
		$prefix = $params['prefix'] ?? CLI::getOption('prefix');
		if (in_array($prefix, [null, true], true))
		{
			$prefix = 'hex2bin';
		}
		elseif (! in_array($prefix, ['hex2bin', 'base64'], true))
		{
			// @codeCoverageIgnoreStart
			$prefix = CLI::prompt('Please provide a valid prefix to use.', ['hex2bin', 'base64'], 'required');
			// @codeCoverageIgnoreEnd
		}

		$length = $params['length'] ?? CLI::getOption('length');
		if (in_array($length, [null, true], true))
		{
			$length = 32;
		}

		$encodedKey = $this->generateRandomKey($prefix, $length);

		if (array_key_exists('show', $params) || (bool) CLI::getOption('show'))
		{
			CLI::write($encodedKey, 'yellow');
			CLI::newLine();
			return;
		}

		if (! $this->setNewEncryptionKey($encodedKey, $params))
		{
			CLI::write('Error in setting new encryption key to .env file.', 'light_gray', 'red');
			CLI::newLine();
			return;
		}

		// force DotEnv to reload the new env vars
		putenv('encryption.key');
		unset($_ENV['encryption.key'], $_SERVER['encryption.key']);
		$dotenv = new DotEnv(ROOTPATH);
		$dotenv->load();

		CLI::write('Application\'s new encryption key was successfully set.', 'green');
		CLI::newLine();
	}

	/**
	 * Generates a key and encodes it.
	 *
	 * @param string  $prefix
	 * @param integer $length
	 *
	 * @return string
	 */
	protected function generateRandomKey(string $prefix, int $length): string
	{
		$key = Encryption::createKey($length);

		if ($prefix === 'hex2bin')
		{
			return 'hex2bin:' . bin2hex($key);
		}

		return 'base64:' . base64_encode($key);
	}

	/**
	 * Sets the new encryption key in your .env file.
	 *
	 * @param string $key
	 * @param array  $params
	 *
	 * @return boolean
	 */
	protected function setNewEncryptionKey(string $key, array $params): bool
	{
		$currentKey = env('encryption.key', '');

		if (strlen($currentKey) !== 0 && ! $this->confirmOverwrite($params))
		{
			// Not yet testable since it requires keyboard input
			// @codeCoverageIgnoreStart
			return false;
			// @codeCoverageIgnoreEnd
		}

		return $this->writeNewEncryptionKeyToFile($currentKey, $key);
	}

	/**
	 * Checks whether to overwrite existing encryption key.
	 *
	 * @param array $params
	 *
	 * @return boolean
	 */
	protected function confirmOverwrite(array $params): bool
	{
		return (array_key_exists('force', $params) || CLI::getOption('force')) || CLI::prompt('Overwrite existing key?', ['n', 'y']) === 'y';
	}

	/**
	 * Writes the new encryption key to .env file.
	 *
	 * @param string $oldKey
	 * @param string $newKey
	 *
	 * @return boolean
	 */
	protected function writeNewEncryptionKeyToFile(string $oldKey, string $newKey): bool
	{
		$baseEnv = ROOTPATH . 'env';
		$envFile = ROOTPATH . '.env';

		if (! file_exists($envFile))
		{
			if (! file_exists($baseEnv))
			{
				CLI::write('Both default shipped `env` file and custom `.env` are missing.', 'yellow');
				CLI::write('Here\'s your new key instead: ' . CLI::color($newKey, 'yellow'));
				CLI::newLine();
				return false;
			}

			copy($baseEnv, $envFile);
		}

		$ret = file_put_contents($envFile, preg_replace(
			$this->keyPattern($oldKey),
			"\nencryption.key = $newKey",
			file_get_contents($envFile)
		));

		return $ret !== false;
	}

	/**
	 * Get the regex of the current encryption key.
	 *
	 * @param string $oldKey
	 *
	 * @return string
	 */
	protected function keyPattern(string $oldKey): string
	{
		$escaped = preg_quote($oldKey, '/');

		if ($escaped !== '')
		{
			$escaped = "[$escaped]*";
		}

		return "/^[#\s]*encryption.key[=\s]*{$escaped}$/m";
	}
}
