#!/usr/bin/env php
<?php
declare(strict_types=1);

use AsyncAws\CloudFormation\CloudFormationClient;
use AsyncAws\CloudFormation\ValueObject\Output;
use AsyncAws\CloudFormation\ValueObject\StackEvent;
use AsyncAws\Core\Exception\Http\HttpException;
use Bref\Console\LoadingAnimation;
use Bref\Console\OpenUrl;
use Bref\Lambda\InvocationFailed;
use Bref\Lambda\SimpleLambdaClient;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;

if (file_exists(__DIR__ . '/vendor/autoload.php')) {
    require_once __DIR__ . '/vendor/autoload.php';
} elseif (file_exists(__DIR__ . '/../autoload.php')) {
    /** @noinspection PhpIncludeInspection */
    require_once __DIR__ . '/../autoload.php';
} else {
    /** @noinspection PhpIncludeInspection */
    require_once __DIR__ . '/../../autoload.php';
}

$app = new Silly\Application('Deploy serverless PHP applications');

$app->command('init', function (SymfonyStyle $io) {
    $exeFinder = new ExecutableFinder();
    if (! $exeFinder->find('serverless')) {
        $io->error(
            'The `serverless` command is not installed.' . PHP_EOL .
            'Please follow the instructions at https://bref.sh/docs/installation.html'
        );

        return 1;
    }

    if (file_exists('serverless.yml') || file_exists('index.php')) {
        $io->error('The directory already contains a `serverless.yml` and/or `index.php` file.');

        return 1;
    }

    $choice = $io->choice(
        'What kind of lambda do you want to create? (you will be able to add more functions later by editing `serverless.yml`)',
        [
            'PHP function',
            'HTTP application',
            'Console application',
        ],
        'PHP function'
    );
    $templateDirectory = [
        'PHP function' => 'default',
        'HTTP application' => 'http',
        'Console application' => 'console',
    ][$choice];

    $fs = new Filesystem;
    $rootPath = __DIR__ . "/template/$templateDirectory";
    $filesToGitAdd = [];
    foreach (scandir($rootPath, SCANDIR_SORT_NONE) as $file) {
        if (in_array($file, ['.', '..'])) {
            continue;
        }
        $io->writeln("Creating $file");
        $fs->copy("$rootPath/$file", $file);
        $filesToGitAdd[] = $file;
    }

    /*
     * We check if this is a git repository to automatically add files to git.
     */
    if ((new Process(['git', 'rev-parse', '--is-inside-work-tree']))->run() === 0) {
        foreach ($filesToGitAdd as $file) {
            (new Process(['git', 'add', $file]))->run();
        }
        $io->success([
            'Project initialized and ready to test or deploy.',
            'The files created were automatically added to git.',
        ]);
    } else {
        $io->success('Project initialized and ready to test or deploy.');
    }

    return 0;
});

/**
 * Run a CLI command in the remote environment.
 */
$app->command('cli function [--region=] [--profile=] [arguments]*', function (string $function, ?string $region, ?string $profile, array $arguments, SymfonyStyle $io) {
    $lambda = new SimpleLambdaClient(
        $region ?: getenv('AWS_DEFAULT_REGION') ?: 'us-east-1',
        $profile ?: getenv('AWS_PROFILE') ?: 'default',
        15 * 60 // maximum duration on Lambda
    );

    // Because arguments may contain spaces, and are going to be executed remotely
    // as a separate process, we need to escape all arguments.
    $arguments = array_map(static function (string $arg): string {
        return escapeshellarg($arg);
    }, $arguments);

    try {
        $result = $lambda->invoke($function, json_encode(implode(' ', $arguments)));
    } catch (InvocationFailed $e) {
        $io->getErrorStyle()->writeln('<info>' . $e->getInvocationLogs() . '</info>');
        $io->error($e->getMessage());
        return 1;
    }

    $payload = $result->getPayload();
    if (isset($payload['output'])) {
        $io->writeln($payload['output']);
    } else {
        $io->error('The command did not return a valid response.');
        $io->writeln('<info>Logs:</info>');
        $io->write('<comment>' . $result->getLogs() . '</comment>');
        $io->writeln('<info>Lambda result payload:</info>');
        $io->writeln(json_encode($payload, JSON_PRETTY_PRINT));
        return 1;
    }

    return (int) ($payload['exitCode'] ?? 1);
});

$app->command('invoke function [--region=] [--profile=] [-e|--event=]', function (string $function, ?string $region, ?string $profile, ?string $event, SymfonyStyle $io) {
    $io->warning([
        'The `bref invoke` command is deprecated in favor of the `serverless invoke` command.',
        'Run `serverless invoke --help` to learn how to use it, or read the documentation here: https://bref.sh/docs/runtimes/function.html#cli',
    ]);

    $lambda = new SimpleLambdaClient(
        $region ?: getenv('AWS_DEFAULT_REGION') ?: 'us-east-1',
        $profile ?: getenv('AWS_PROFILE') ?: 'default'
    );

    try {
        $result = $lambda->invoke($function, $event);
    } catch (InvocationFailed $e) {
        $io->getErrorStyle()->writeln('<info>' . $e->getInvocationLogs() . '</info>');
        $io->error($e->getMessage());
        return 1;
    }

    $io->getErrorStyle()->writeln('<info>' . $result->getLogs() . '</info>');

    $io->writeln(json_encode($result->getPayload(), JSON_PRETTY_PRINT));

    return 0;
})->descriptions('Invoke the lambda on the serverless provider', [
    '--event' => 'Event data as JSON, e.g. `--event \'{"name":"matt"}\'`',
]);

$app->command('deployment stack-name [--region=] [--profile=]', function (string $stackName, ?string $region, ?string $profile, SymfonyStyle $io) {
    $region = $region ?: getenv('AWS_DEFAULT_REGION') ?: 'us-east-1';
    $profile = $profile ?: getenv('AWS_PROFILE') ?: 'default';
    $cloudFormation = new CloudFormationClient([
        'region' => $region,
        'profile' => $profile,
    ]);

    try {
        $result = $cloudFormation->describeStacks([
            'StackName' => $stackName,
        ]);
        $stack = null;
        foreach ($result->getStacks() as $currentStack) {
            $stack = $currentStack;
            break;
        }

        if ($stack === null) {
            $io->error(sprintf('The stack %s cannot be found in region %s', $stackName, $region));
            return 1;
        }

    } catch (HttpException $e) {
        $io->error([
            "Error while fetching information about the stack `$stackName` in region `$region`:",
            sprintf('"%s"', $e->getAwsMessage()),
            "In case the stack was not found make sure that `$region` is the correct region.",
        ]);
        return 1;
    }

    $io->section('Events');

    $result = $cloudFormation->describeStackEvents([
        'StackName' => $stackName,
    ]);
    $events = iterator_to_array($result->getStackEvents(true));

    // Last events last
    $events = array_reverse($events);
    // Keep only events from the last 24 hours
    $oneDayAgo = new DateTimeImmutable('-1 day');
    $events = array_filter($events, function (StackEvent $event) use ($oneDayAgo) {
        return $event->getTimestamp() >= $oneDayAgo;
    });

    if (empty($events)) {
        $io->text('No events were found in the last 24 hours.');
    } else {
        $errors = [];
        foreach ($events as $event) {
            /** @var DateTimeInterface $time */
            $time = $event->getTimestamp();

            $status = $event->getResourceStatus();
            $error = false;
            if (strpos($status, 'FAILED') !== false) {
                $error = true;
                $errors[] = $event;
            }

            $io->write(sprintf(
                '<comment>%s</comment> %s %s',
                $time->format('M j G:H'),
                $error ? "<error>$status</error>" : $status,
                $event->getResourceType()
            ));

            if (null !== $event->getResourceStatusReason()) {
                $io->write(" <info>{$event->getResourceStatusReason()}</info>");
            }
            $io->writeln('');
        }
        $io->writeln('');

        if (empty($errors)) {
            $io->writeln('<info>No errors found.</info>');
        } else {
            $io->writeln('<error>Summary of the errors found:</error>');
            foreach ($errors as $event) {
                /** @var DateTimeInterface $time */
                $time = $event->getTimestamp();
                $io->writeln(sprintf(
                    '<comment>%s</comment> <info>%s</info> %s',
                    $time->format('M j G:H'),
                    $event->getResourceType() ?? '',
                    $event->getResourceStatusReason() ?? ''
                ));
            }
        }
    }

    $outputs = $stack->getOutputs();
    if (!empty($outputs)) {
        $io->section('Outputs');
        $io->listing(array_map(function (Output $output): string {
            return sprintf(
                '%s: <info>%s</info>',
                $output->getDescription() ?? $output->getOutputKey(),
                $output->getOutputValue() ?? ''
            );
        }, $outputs));
    }

    return 0;
})->descriptions('Displays the latest deployment logs from CloudFormation. Only the logs from the last 24 hours are displayed. Use these logs to debug why a deployment failed.');

$app->command('dashboard [--host=] [--port=] [--profile=] [--stage=]', function (string $host = 'localhost', int $port = 8000, string $profile = null, string $stage = null, SymfonyStyle $io) {
    if ($host === 'localhost') {
        $host = '127.0.0.1';
    }
    if ($profile === null) {
        $profile = getenv('AWS_PROFILE') ?: 'default';
    }

    if (! file_exists('serverless.yml')) {
        $io->error('No `serverless.yml` file found.');

        return 1;
    }

    $exeFinder = new ExecutableFinder();
    if (! $exeFinder->find('docker')) {
        $io->error(
            'The `docker` command is not installed.' . PHP_EOL .
            'Please follow the instructions at https://docs.docker.com/install/'
        );

        return 1;
    }

    if (! $exeFinder->find('serverless')) {
        $io->error(
            'The `serverless` command is not installed.' . PHP_EOL .
            'Please follow the instructions at https://bref.sh/docs/installation.html'
        );

        return 1;
    }

    $serverlessInfo = new Process(['serverless', 'info', '--stage', $stage, '--aws-profile', $profile]);
    $serverlessInfo->start();
    $animation = new LoadingAnimation($io);
    do {
        $animation->tick('Retrieving the stack');
        usleep(100*1000);
    } while ($serverlessInfo->isRunning());
    $animation->clear();

    if (!$serverlessInfo->isSuccessful()) {
        $io->error('The command `serverless info` failed' . PHP_EOL . $serverlessInfo->getOutput());

        return 1;
    }

    $serverlessInfoOutput = $serverlessInfo->getOutput();

    $region = [];
    preg_match('/region: ([a-z0-9-]*)/', $serverlessInfoOutput, $region);
    $region = $region[1];

    $stack = [];
    preg_match('/stack: ([a-zA-Z0-9-]*)/', $serverlessInfoOutput, $stack);
    $stack = $stack[1];

    $io->writeln("Stack: <fg=yellow>$stack ($region)</>");

    $dockerPull = new Process(['docker', 'pull', 'bref/dashboard']);
    $dockerPull->setTimeout(null);
    $dockerPull->start();
    do {
        $animation->tick('Retrieving the latest version of the dashboard');
        usleep(100*1000);
    } while ($dockerPull->isRunning());
    $animation->clear();
    if (! $dockerPull->isSuccessful()) {
        $io->error([
            'The command `docker pull bref/dashboard` failed',
            $dockerPull->getErrorOutput(),
        ]);

        return 1;
    }

    $process = new Process(['docker', 'run', '--rm', '-p', $host . ':' . $port.':8000', '-v', getenv('HOME').'/.aws:/root/.aws:ro', '--env', 'STACKNAME='.$stack, '--env', 'REGION='.$region, '--env', 'AWS_PROFILE='.$profile, 'bref/dashboard']);
    $process->setTimeout(null);
    $process->start();
    do {
        $animation->tick('Starting the dashboard');
        usleep(100*1000);
        $serverOutput = $process->getOutput() . $process->getErrorOutput();
        $hasStarted = (strpos($serverOutput, 'Development Server') !== false);
    } while ($process->isRunning() && !$hasStarted);
    $animation->clear();
    if (!$process->isRunning()) {
        $io->error([
            'The dashboard failed to start',
            $process->getErrorOutput(),
        ]);

        return 1;
    }
    $url = "http://$host:$port";
    $io->writeln("Dashboard started: <fg=green;options=bold,underscore>$url</>");
    OpenUrl::open($url);
    $process->wait(function ($type, $buffer) {
        if (Process::ERR === $type) {
            echo 'ERR > '.$buffer;
        } else {
            echo 'OUT > '.$buffer;
        }
    });

    return $process->getExitCode();
})->descriptions('Start the dashboard');

$app->command('bref.dev [--profile=] [--stage=]', function (?string $profile, string $stage = null, SymfonyStyle $io) {
    $profile = $profile ?: getenv('AWS_PROFILE') ?: 'default';
    if (! file_exists('serverless.yml')) {
        $io->error('No `serverless.yml` file found.');

        return 1;
    }
    if (! (new ExecutableFinder)->find('serverless')) {
        $io->error(
            'The `serverless` command is not installed.' . PHP_EOL .
            'Please follow the instructions at https://bref.sh/docs/installation.html'
        );

        return 1;
    }

    $serverlessInfo = new Process(['serverless', 'info', '--stage', $stage, '--aws-profile', $profile]);
    $serverlessInfo->start();
    $animation = new LoadingAnimation($io);
    do {
        $animation->tick('Retrieving the API Gateway URL');
        usleep(100*1000);
    } while ($serverlessInfo->isRunning());
    $animation->clear();
    if (!$serverlessInfo->isSuccessful()) {
        $io->error('The command `serverless info` failed' . PHP_EOL . $serverlessInfo->getErrorOutput());

        return 1;
    }

    $serverlessInfoOutput = $serverlessInfo->getOutput();

    // Region
    $regionMatches = [];
    preg_match('/region: ([a-z0-9-]*)/', $serverlessInfoOutput, $regionMatches);
    $region = $regionMatches[1];
    // Stage
    $stageMatches = [];
    preg_match('/stage: ([a-z0-9-]*)/', $serverlessInfoOutput, $stageMatches);
    $stage = $stageMatches[1];
    // API ID
    $apiIdMatches = [];
    preg_match('# - https://([^.]+)\.execute-api\.#', $serverlessInfoOutput, $apiIdMatches);
    $apiId = $apiIdMatches[1];

    $io->writeln("Creating a short URL for <info>https://$apiId.execute-api.$region.amazonaws.com/$stage</info>");
    $client = new GuzzleHttp\Client();
    $response = $client->request('POST', 'https://api.bref.dev/site', [
        'json' => [
            'apiId' => $apiId,
            'region' => $region,
            'stage' => $stage,
        ],
    ]);
    $response = json_decode((string) $response->getBody(), true);
    $shortUrl = $response['url'];
    $io->writeln("Short URL created and active for 7 days: <info>$shortUrl</info>");
    $io->writeln('<comment>Please use this URL for development only and avoid load testing it with a lot of traffic. That helps us provide the service for free. The service is currently in beta and can change at any moment.</comment>');

    return 0;
})->descriptions('Create a short URL on bref.dev');

$app->run();
