#!/usr/bin/env php
<?php

const UUID_KEY = 'secrets_uuid';
const HOSTS_FILE = 'deploy/deployer-hosts.yml';

if (file_exists($a = __DIR__.'/../../../autoload.php')) {
    require_once $a;
} else {
    require_once __DIR__.'/../vendor/autoload.php';
}

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
use Symfony\Component\Yaml\Yaml;

$application = new Application('secrets', '1.0.0');

$application
    ->register('secrets')
    ->addArgument('action', InputArgument::REQUIRED, 'push (push to 1Password), pull (pull from 1Password) or load (load secrets into docker environment).')
    ->addArgument('host', InputArgument::OPTIONAL, 'a valid host from the yml file')
    ->setCode(/**
     * @param InputInterface $input
     * @param OutputInterface $output
     */ function (InputInterface $input, OutputInterface $output) {

        $hosts = Yaml::parseFile(HOSTS_FILE);

        $action = $input->getArgument('action');

        if (!in_array($action, ['push', 'pull', 'load'])) {
            throw new Exception('Action is invalid. Please specify valid action.');
        }

        $hostKey = $input->getArgument('host');

        if ($hostKey && !isset($hosts[$hostKey])) {
            throw new Exception('Host is invalid. Please specify valid host.');
        }

        if (!$hostKey) {

            $helper = $this->getHelper('question');

            $question = new ChoiceQuestion(
                'Please select a host.',
                array_keys($hosts),
                0
            );

            $question->setErrorMessage('Host %s is invalid.');

            $hostKey = $helper->ask($input, $output, $question);
        }

        $host = $hosts[$hostKey];

        if (in_array($action, ['push', 'pull'])) {
            logToOutput('Making sure 1Password is signed in...');

            $process = runCommand(['op', 'list', 'vaults']);
            if (!$process->isSuccessful()) {

                throw new Exception('You don\'t seem to be signed in. Run > eval $(op signin hutsix)');
            }

            logToOutput('Signed in, continuing...');
        }

        if (!($host[UUID_KEY] ?? null)) {

            if ($action === 'pull') {
                throw new Exception('Can\'t pull from 1Password without a UUID for the specified host.');
            }

            if ($action === 'push') {

                logToOutput('Attempting to push without UUID. Getting vaults to create a new entry.');

                $helper = $this->getHelper('question');

                $vaults = [];

                foreach (getOPVaults() as $vault) {
                    $vaults[$vault['uuid']] = $vault['name'];
                }

                $question = new ChoiceQuestion(
                    'UUID is not set for this host. Would you like to create a new 1Password entry?',
                    $vaults,
                    0
                );

                $question->setErrorMessage('Vault %s is invalid.');

                $vault = $helper->ask($input, $output, $question);

                createOPEntry($hostKey, $vault);

                return;
            }
        }

        $action($hostKey, $host);
    });

$application->setDefaultCommand('secrets', true);


/**
 * @param string $hostKey
 * @param array $host
 * @throws Exception
 */
function push(string $hostKey, array $host)
{
    logToOutput('Getting vault for entry ' . $host[UUID_KEY]);
    $vault = getOPVaultForEntry($host);

    logToOutput('Removing old entry ' . $host[UUID_KEY]);
    removeOPEntry($host);

    createOPEntry($hostKey, $vault);
}

/**
 * @param string $hostKey
 * @param array $host
 */
function pull(string $hostKey, array $host)
{
    $secret = getOPEntry($host);
    $path = getSecretPath($hostKey);
    logToOutput('Writing secret contents to ' . $path);
    file_put_contents($path, $secret);
}

/**
 * @param string $hostKey
 * @param array $host
 */
function load(string $hostKey, array $host)
{
    $path = getSecretPath($hostKey);
    $data = (new Dotenv())->parse(file_get_contents($path));

    $sshHost = sprintf('ssh://%s@%s:%s', $host['user'], $host['hostname'], $host['port']);

    logToOutput('Using docker host '.$sshHost);

    foreach ($data as $key => $value) {

        $parsedKey = $host[UUID_KEY].'-'.$key;

        logToOutput('Setting secret '.$parsedKey);

        //No longer remove old secrets
//        runDockerCommand(['docker', 'secret', 'rm', $parsedKey], $sshHost);

        $process = new Process(['docker', 'secret', 'create', $parsedKey, '-']);
        $process->setInput($value);
        $process->run(null, ['DOCKER_HOST' => $sshHost]);
    }

    logToOutput('Done.');
}

/**
 * @param string $hostKey
 * @return string
 */
function getSecretPath(string $hostKey)
{
    return sprintf('secrets/%s.env', $hostKey);
}

/**
 * @return string
 * @throws Exception
 */
function getProjectVersion(): string
{

    $file = 'VERSION';

    if (!file_exists($file)) {
        throw new Exception('VERSION file does not exist.');
    }

    return trim(file_get_contents($file));
}

/**
 * @return mixed
 * @throws Exception
 */
function getProjectName(): string
{

    $remote = runCommand(['git', 'config', '--get', 'remote.origin.url'])->getOutput();

    if (!$remote) {
        throw new Exception('Remote not defined?');
    }

    [, $path] = explode(':', $remote);

    return trim(str_replace('.git', '', $path));
}

/**
 * @param array $host
 * @return string
 */
function getOPEntry(array $host): string
{

    logToOutput('Getting entry with UUID ' . $host[UUID_KEY]);

    $process = runCommand(['op', 'get', 'document', $host[UUID_KEY]]);
    if (!$process->isSuccessful()) {
        throw new ProcessFailedException($process);
    }
    return $process->getOutput();
}

/**
 * @param array $host
 * @return string
 */
function getOPVaultForEntry(array $host): string
{
    $process = runCommand(['op', 'get', 'item', $host[UUID_KEY]]);

    $vault = json_decode($process->getOutput(), true)['vaultUuid'] ?? null;

    if (!$vault) {
        throw new Exception('Entry not found with UUID ' . $host[UUID_KEY]);
    }

    logToOutput('Vault for entry found: ' . $vault);

    return $vault;
}

/**
 * @param array $host
 */
function removeOPEntry(array $host)
{
    $process = runCommand(['op', 'delete', 'item', $host[UUID_KEY]]);
    logToOutput($process->getOutput());
    logToOutput($process->getErrorOutput());
}

/**
 * @return array
 */
function getOPVaults(): array
{
    $process = runCommand(['op', 'list', 'vaults']);
    return json_decode($process->getOutput(), true);
}


/**
 * @param string $hostKey
 * @param string $vault
 * @throws Exception
 */
function createOPEntry(string $hostKey, string $vault)
{
    $title = sprintf('%s - %s - %s', getProjectName(), getProjectVersion(), $hostKey);

    logToOutput('Creating new entry with title ' . $title);

    $process = runCommand(['op', 'create', 'document', getSecretPath($hostKey), sprintf('--title=%s', $title), sprintf('--vault=%s', $vault)]);

    if (!$process->isSuccessful()) {
        throw new ProcessFailedException($process);
    }

    $json = json_decode($process->getOutput(), true);

    $uuid = $json['uuid'];

    $hosts = Yaml::parseFile(HOSTS_FILE);

    $hosts[$hostKey][UUID_KEY] = $uuid;

    file_put_contents(HOSTS_FILE, Yaml::dump($hosts));
}

/**
 * @param string $log
 */
function logToOutput(string $log)
{
    $log = trim($log);
    echo $log ? $log."\n" : '';
}

/**
 * @param array $command
 * @return Process
 */
function runCommand(array $command): Process
{
    $process = new Process($command);
    $process->run();
    return $process;
}

/**
 * @param array $command
 * @param string $sshHost
 * @return Process
 */
function runDockerCommand(array $command, string $sshHost): Process
{
    $process = new Process($command);
    $process->run(null, ['DOCKER_HOST' => $sshHost]);

    logToOutput($process->getOutput());
    logToOutput($process->getErrorOutput());

    return $process;
}


$application->run();
