<?php

declare(strict_types=1);

/*
 * eduVPN - End-user friendly VPN.
 *
 * Copyright: 2014-2023, The Commons Conservancy eduVPN Programme
 * SPDX-License-Identifier: AGPL-3.0+
 */

namespace Vpn\Portal;

use RuntimeException;
use SplFileInfo;
use Vpn\Portal\Exception\ConnectionHookException;

/**
 * Run a script on connect/disconnect events.
 */
class ScriptConnectionHook implements ConnectionHookInterface
{
    private string $scriptPath;

    public function __construct(string $scriptPath)
    {
        $this->scriptPath = self::verifyFile($scriptPath);
    }

    public function connect(NodeInfo $nodeInfo, string $userId, string $profileId, string $vpnProto, string $connectionId, string $ipFour, string $ipSix, ?string $originatingIp, ?IpInfo $ipInfo): void
    {
        $envVarList = [
            'EVENT' => 'C',
            'NODE_NUMBER' => (string) $nodeInfo->nodeNumber(),
            'NODE_URL' => $nodeInfo->nodeUrl(),
            'NODE_HOST_NAME' => $nodeInfo->hostName(),
            'USER_ID' => $userId,
            'PROFILE_ID' => $profileId,
            'PROTO' => $vpnProto,
            'CONNECTION_ID' => $connectionId,
            'IP_FOUR' => $ipFour,
            'IP_SIX' => $ipSix,
        ];
        if (null !== $originatingIp) {
            $envVarList['ORIGINATING_IP'] = $originatingIp;
        }
        if (null !== $ipInfo) {
            $envVarList['GEO_IP_CC'] = $ipInfo->countryCode();
            $envVarList['GEO_IP_URI'] = $ipInfo->geoUri();
        }

        $this->exec(
            sprintf(
                '%s %s',
                self::prepareEnvVarList($envVarList),
                $this->scriptPath
            )
        );
    }

    public function disconnect(NodeInfo $nodeInfo, string $userId, string $profileId, string $vpnProto, string $connectionId, string $ipFour, string $ipSix, int $bytesIn, int $bytesOut): void
    {
        $envVarList = [
            'EVENT' => 'D',
            'NODE_NUMBER' => (string) $nodeInfo->nodeNumber(),
            'NODE_URL' => $nodeInfo->nodeUrl(),
            'NODE_HOST_NAME' => $nodeInfo->hostName(),
            'USER_ID' => $userId,
            'PROFILE_ID' => $profileId,
            'PROTO' => $vpnProto,
            'CONNECTION_ID' => $connectionId,
            'IP_FOUR' => $ipFour,
            'IP_SIX' => $ipSix,
            'BYTES_IN' => (string) $bytesIn,
            'BYTES_OUT' => (string) $bytesOut,
        ];

        $this->exec(
            sprintf(
                '%s %s',
                self::prepareEnvVarList($envVarList),
                $this->scriptPath
            )
        );
    }

    private static function verifyFile(string $scriptPath): string
    {
        $fileInfo = new SplFileInfo($scriptPath);
        if (!$fileInfo->isFile()) {
            throw new RuntimeException(sprintf('ScriptConnectionHook: "%s" does not exist', $scriptPath));
        }
        if (!$fileInfo->isExecutable()) {
            throw new RuntimeException(sprintf('ScriptConnectionHook: "%s" is not executable', $scriptPath));
        }

        return $scriptPath;
    }

    /**
     * @param array<string,string> $envVarList
     */
    private static function prepareEnvVarList(array $envVarList): string
    {
        $envList = [];
        foreach ($envVarList as $envKey => $envValue) {
            $envList[] = 'VPN_' . $envKey . '=' . escapeshellarg($envValue);
        }

        return implode(' ', $envList);
    }

    private function exec(string $execCmd): void
    {
        exec(
            sprintf('%s 2>&1', $execCmd),
            $commandOutput,
            $returnValue
        );

        if (0 !== $returnValue) {
            throw new ConnectionHookException(
                sprintf('script "%s" failed with return code: %d', $this->scriptPath, $returnValue)
            );
        }
    }
}
