<?php

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Cart\LegacyOrderImport;

use Doctrine\DBAL\DriverManager;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;

final class DbalLegacyOrderSourceConnector implements LegacyOrderSourceConnector
{
    public function connect(LegacyOrderSourceConnectionConfig $config): LegacyOrderSourceConnection
    {
        if (!\extension_loaded('pdo_mysql')) {
            throw new \RuntimeException('L extension PHP pdo_mysql est requise pour lire la base MySQL/MariaDB source.');
        }

        $sshTunnel = null;
        try {
            $localPort = null;
            if ($config->usesSsh()) {
                $localPort = $this->openSshTunnel($config, $sshTunnel);
            }

            $connection = DriverManager::getConnection($config->dbalParameters($localPort));
            $connection->connect();

            return new LegacyOrderSourceConnection($connection, $sshTunnel);
        } catch (\Throwable $exception) {
            if (null !== $sshTunnel && $sshTunnel->isRunning()) {
                $sshTunnel->stop(2);
            }

            throw $exception;
        }
    }

    private function openSshTunnel(LegacyOrderSourceConnectionConfig $config, ?Process &$sshTunnel): int
    {
        if (null === $config->sshKey || !is_file($config->sshKey) || !is_readable($config->sshKey)) {
            throw new \RuntimeException(\sprintf('La cle SSH est absente ou illisible : %s.', $config->sshKey ?? '(aucune)'));
        }

        $sshExecutable = (new ExecutableFinder())->find('ssh');
        if (null === $sshExecutable) {
            throw new \RuntimeException('Le client OpenSSH "ssh" est requis pour ouvrir le tunnel vers la base source.');
        }

        $localPort = $this->reserveLocalPort();
        $sshTunnel = new Process($config->sshCommand($localPort, $sshExecutable));
        $sshTunnel->setTimeout(null);
        $sshTunnel->start();
        $this->waitUntilTunnelIsReady($sshTunnel, $localPort);

        return $localPort;
    }

    private function reserveLocalPort(): int
    {
        $errorCode = 0;
        $errorMessage = '';
        $socket = @stream_socket_server('tcp://127.0.0.1:0', $errorCode, $errorMessage);
        if (false === $socket) {
            throw new \RuntimeException(\sprintf('Impossible de reserver un port local pour le tunnel SSH (%d : %s).', $errorCode, $errorMessage));
        }

        try {
            $address = stream_socket_get_name($socket, false);
        } finally {
            fclose($socket);
        }

        if (false === $address || false === $separator = mb_strrpos($address, ':')) {
            throw new \RuntimeException('Impossible de determiner le port local du tunnel SSH.');
        }

        $port = (int) mb_substr($address, $separator + 1);
        if ($port < 1) {
            throw new \RuntimeException('Le port local reserve pour le tunnel SSH est invalide.');
        }

        return $port;
    }

    private function waitUntilTunnelIsReady(Process $sshTunnel, int $localPort): void
    {
        $deadline = microtime(true) + 10;

        do {
            if (!$sshTunnel->isRunning()) {
                $error = mb_trim($sshTunnel->getErrorOutput());

                throw new \RuntimeException('Le tunnel SSH n a pas pu etre ouvert'.('' === $error ? '.' : ' : '.$error));
            }

            $errorCode = 0;
            $errorMessage = '';
            $socket = @fsockopen(LegacyOrderSourceConnectionConfig::DEFAULT_HOST, $localPort, $errorCode, $errorMessage, 0.2);
            if (false !== $socket) {
                fclose($socket);

                return;
            }

            usleep(50_000);
        } while (microtime(true) < $deadline);

        throw new \RuntimeException('Le tunnel SSH est ouvert mais le port MySQL local ne repond pas apres 10 secondes.');
    }
}
