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

/*
 * This file is part of Chevere.
 *
 * (c) Rodolfo Berrios <rodolfo@chevere.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

foreach (['/', '/../../../'] as $path) {
    $autoload = __DIR__ . $path . 'vendor/autoload.php';
    if (stream_resolve_include_path($autoload)) {
        require $autoload;

        break;
    }
}

use function Chevere\Filesystem\directoryForPath;
use function Chevere\Filesystem\fileForPath;
use Chevere\ThrowableHandler\ThrowableHandler;
use function Chevere\Writer\streamFor;
use function Chevere\Writer\streamTemp;
use Chevere\Writer\StreamWriter;
use Chevere\Writer\Writers;
use Chevere\Writer\WritersInstance;
use Chevere\XrServer\Build;
use function Chevere\XrServer\decrypt;
use function Chevere\XrServer\writeToDebugger;
use Clue\React\Sse\BufferedChannel;
use Colors\Color;
use phpseclib3\Crypt\AES;
use phpseclib3\Crypt\EC;
use phpseclib3\Crypt\EC\PrivateKey;
use phpseclib3\Crypt\Random;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Http\Middleware\LimitConcurrentRequestsMiddleware;
use React\Http\Middleware\RequestBodyBufferMiddleware;
use React\Http\Middleware\RequestBodyParserMiddleware;
use React\Http\Middleware\StreamingRequestMiddleware;
use React\Stream\ThroughStream;
use samejack\PHP\ArgvParser;

include __DIR__ . '/meta.php';

new WritersInstance(
    (new Writers())
        ->with(
            new StreamWriter(
                streamFor('php://output', 'w')
            )
        )
        ->withError(
            new StreamWriter(
                streamFor('php://stderr', 'w')
            )
        )
);
set_error_handler(ThrowableHandler::ERROR_AS_EXCEPTION);
register_shutdown_function(ThrowableHandler::SHUTDOWN_ERROR_AS_EXCEPTION);
set_exception_handler(ThrowableHandler::CONSOLE);

$color = new Color();
echo $color(file_get_contents(__DIR__ . '/logo'))->cyan() . "\n";
echo $color(strtr('XR Debug %v (%c) by Rodolfo Berrios', [
    '%v' => XR_SERVER_VERSION,
    '%c' => XR_SERVER_CODENAME,
]))->green() . "\n\n";
$options = (new ArgvParser())->parseConfigs();
if (array_key_exists('h', $options) || array_key_exists('help', $options)) {
    echo implode("\n", [
        '-p Port (default 27420)',
        '-e Enable end-to-end encryption',
        '-k Symmetric key (for -e option)',
        '-v Enable sign verification',
        '-s Private key (for -v option)',
        '-c Cert file for TLS',
        '',
    ]);
    die(0);
}
$host = '0.0.0.0';
$port = $options['p'] ?? '27420';
$cert = $options['c'] ?? null;
$isEncryptionEnabled = $options['e'] ?? false;
$isSignVerificationEnabled = $options['v'] ?? false;
$scheme = isset($cert) ? 'tls' : 'tcp';
$uri = "{$scheme}://{$host}:{$port}";
$context = $scheme === 'tcp'
    ? []
    : [
        'tls' => [
            'local_cert' => $cert,
        ],
    ];
$cipher = null;
if ($isEncryptionEnabled) {
    $symmetricKey = array_key_exists('k', $options) ? $options['k'] : null;
    if ($symmetricKey === null) {
        $symmetricKey = Random::string(32);
        echo $color('INFO: Generated encryption key (empty -k)')->magenta() . "\n";
    } else {
        $symmetricKey = base64_decode($symmetricKey, true);
    }
    $cipher = new AES('gcm');
    $cipher->setKey($symmetricKey);
    $encryptionKeyDisplay = base64_encode($symmetricKey);
    echo <<<PLAIN
    🔐 ENCRYPTION KEY
    {$encryptionKeyDisplay}


    PLAIN;
}
$privateKey = null;
if($isSignVerificationEnabled) {
    $privateKey = array_key_exists('s', $options) ? $options['s'] : null;
    if ($privateKey === null) {
        $privateKey = EC::createKey('ed25519');
        echo $color('INFO: Generated private key (empty -s)')->magenta() . "\n";
    } else {
        $privateKey = EC::load($privateKey);
    }
    $privateKeyDisplay = $privateKey->toString('PKCS8');
    echo <<<PLAIN
    🔏 PRIVATE KEY
    {$privateKeyDisplay}


    PLAIN;
}

try {
    directoryForPath(__DIR__ . '/locks')->removeContents();
} catch (Throwable) {
}
$build = new Build(
    directoryForPath(__DIR__ . '/app/src'),
    XR_SERVER_VERSION,
    XR_SERVER_CODENAME,
    $isEncryptionEnabled,
);
$app = fileForPath(__DIR__ . '/app/build/en.html');
$app->removeIfExists();
$app->create();
$app->put($build->html());
$loop = Loop::get();
$channel = new BufferedChannel();
$handler = function (ServerRequestInterface $request) use (
    $channel,
    $loop,
    $cipher,
    $privateKey
) {
    $path = $request->getUri()->getPath();
    if (in_array($path, ['/lock-patch', '/lock-delete'], true)) {
        if($cipher instanceof AES) {
            $request = $request->withBody(
                streamTemp(
                    decrypt($cipher, $request->getBody()->__toString())
                )
            );
        }
    }
    if (in_array($path, ['/message', '/lock-post', '/locks'], true)) {
        if($privateKey instanceof PrivateKey) {
            $signatureHeader = $request->getHeader('X-Signature');
            if ($signatureHeader === []) {
                return new Response(
                    400,
                    ['Content-Type' => 'text/plain'],
                    'Missing signature'
                );
            }
            $body = $request->getParsedBody() ?? [];
            $serialize = serialize($body);
            $signature = base64_decode($signatureHeader[0]);
            $publicKey = $privateKey->getPublicKey();
            if (!$publicKey->verify($serialize, $signature)) {
                return new Response(
                    400,
                    ['Content-Type' => 'text/plain'],
                    'Invalid signature'
                );
            }
        }
    }
    switch ($path) {
        case '/':
            return new Response(
                '200',
                [
                    'Content-Type' => 'text/html',
                ],
                file_get_contents(__DIR__ . '/app/build/en.html')
            );
        case '/locks':
            $body = $request->getParsedBody() ?? [];
            $lockFile = fileForPath(__DIR__ . '/locks/' . $body['id']);
            $json = '{"lock":false}';
            if ($lockFile->exists()) {
                $json = $lockFile->getContents();
            }

            return new Response(
                '200',
                [
                    'Content-Type' => 'text/json',
                ],
                $json
            );
        case '/lock-post':
            $json = '{"lock":true}';
            $body = $request->getParsedBody() ?? [];
            $lockFile = fileForPath(__DIR__ . '/locks/' . $body['id']);
            $lockFile->removeIfExists();
            $lockFile->create();
            $lockFile->put($json);
            writeToDebugger($request, $channel, 'pause', $cipher);

            return new Response(
                '200',
                [
                    'Content-Type' => 'text/json',
                ],
                $json
            );
        case '/lock-patch':
            $json = '{"stop":true}';
            $body = json_decode($request->getBody()->__toString(), true);
            $lockFile = fileForPath(__DIR__ . '/locks/' . $body['id']);
            $lockFile->removeIfExists();
            $lockFile->create();
            $lockFile->put($json);

            return new Response(
                '200',
                [
                    'Content-Type' => 'text/json',
                ],
                $json
            );
        case '/lock-delete':
            $body = json_decode($request->getBody()->__toString(), true);
            $lockFile = fileForPath(__DIR__ . '/locks/' . $body['id']);
            $lockFile->removeIfExists();

            return new Response(
                '200',
                [
                    'Content-Type' => 'text/json',
                ],
                '{"ok":true}'
            );
        case '/message':
            if ($request->getMethod() !== 'POST') {
                return new Response(405);
            }
            writeToDebugger($request, $channel, 'message', $cipher);

            return new Response(
                '201',
                [
                    'Content-Type' => 'text/json',
                ]
            );
        case '/dump':
            $stream = new ThroughStream();
            $id = $request->getHeaderLine('Last-Event-ID');
            $loop->futureTick(function () use ($channel, $stream, $id) {
                $channel->connect($stream, $id);
            });
            $serverParams = $request->getServerParams();
            $message = '{message: "New dump session started [' . $serverParams['REMOTE_ADDR'] . ']"}';
            $channel->writeMessage($message);
            $stream->on('close', function () use ($stream, $channel, $serverParams) {
                $channel->disconnect($stream);
                $message = '{message: "Dump session ended [' . $serverParams['REMOTE_ADDR'] . ']"}';
                $channel->writeMessage($message);
            });

            return new Response(
                200,
                [
                    'Content-Type' => 'text/event-stream',
                ],
                $stream
            );
        default:
            return new Response(404);
    }
};
$http = new HttpServer(
    $loop,
    new StreamingRequestMiddleware(),
    new LimitConcurrentRequestsMiddleware(100),
    new RequestBodyBufferMiddleware(8 * 1024 * 1024),
    new RequestBodyParserMiddleware(100 * 1024, 1),
    $handler
);
$socket = new \React\Socket\SocketServer(
    $uri,
    $context,
    $loop
);
$http->listen($socket);
$socket->on('error', 'printf');
$scheme = parse_url($socket->getAddress(), PHP_URL_SCHEME);
$httpAddress = strtr(
    $socket->getAddress(),
    [
        'tls' => 'https',
        'tcp' => 'http',
    ]
);
echo <<<PLAIN
    Server listening on {$scheme} {$httpAddress}
    Press Ctrl+C to quit
    --

    PLAIN;
$loop->run();
