<?php

declare(strict_types=1);

namespace Tests\Support\Api;

use RuntimeException;

final class ApiServer
{
    private mixed $process = null;
    private string $stdoutLog;
    private string $stderrLog;

    public function __construct(
        private readonly string $workingDirectory,
        private readonly string $host,
        private readonly int $port,
    ) {
        $logDirectory = app_path('build/logs');
        if (!is_dir($logDirectory)) {
            mkdir($logDirectory, 0777, true);
        }

        $this->stdoutLog = $logDirectory . DIRECTORY_SEPARATOR . 'api-server.out.log';
        $this->stderrLog = $logDirectory . DIRECTORY_SEPARATOR . 'api-server.err.log';
    }

    public function start(): void
    {
        $command = sprintf(
            '%s -S %s index.php',
            escapeshellarg((string)(PHP_BINARY ?: 'php')),
            escapeshellarg($this->host . ':' . $this->port),
        );
        $environment = $this->inheritEnvironment();

        $descriptorSpec = [
            0 => ['pipe', 'r'],
            1 => ['file', $this->stdoutLog, 'a'],
            2 => ['file', $this->stderrLog, 'a'],
        ];

        $this->process = proc_open($command, $descriptorSpec, $pipes, $this->workingDirectory, $environment);
        if (!is_resource($this->process)) {
            throw new RuntimeException('Unable to start the PHP test server.');
        }

        if (isset($pipes[0]) && is_resource($pipes[0])) {
            fclose($pipes[0]);
        }
    }

    public function isRunning(): bool
    {
        if (!is_resource($this->process)) {
            return false;
        }

        $status = proc_get_status($this->process);

        return (bool)($status['running'] ?? false);
    }

    public function stop(): void
    {
        if (!is_resource($this->process)) {
            return;
        }

        proc_terminate($this->process);
        usleep(250000);

        $status = proc_get_status($this->process);
        if (($status['running'] ?? false) && function_exists('posix_kill')) {
            @posix_kill((int)$status['pid'], 9);
        }

        proc_close($this->process);
        $this->process = null;
    }

    public function describeFailure(string $message): string
    {
        $stderr = is_file($this->stderrLog) ? trim((string)file_get_contents($this->stderrLog)) : '';
        if ($stderr !== '') {
            return $message . PHP_EOL . $stderr;
        }

        return $message;
    }

    public static function findAvailablePort(string $host): int
    {
        $socket = @stream_socket_server('tcp://' . $host . ':0', $errorCode, $errorMessage);
        if ($socket === false) {
            throw new RuntimeException(
                sprintf('Unable to reserve an API test port on %s: %s (%d)', $host, $errorMessage, $errorCode)
            );
        }

        $address = stream_socket_get_name($socket, false);
        fclose($socket);

        if (!is_string($address) || $address === '') {
            throw new RuntimeException('Unable to determine the reserved API test port.');
        }

        $lastSeparator = strrpos($address, ':');
        if ($lastSeparator === false) {
            throw new RuntimeException('Unable to parse the reserved API test port.');
        }

        $port = (int)substr($address, $lastSeparator + 1);
        if ($port <= 0) {
            throw new RuntimeException('Unable to parse a valid API test port.');
        }

        return $port;
    }

    /**
     * @return array<string, string>
     */
    private function inheritEnvironment(): array
    {
        $environment = [];

        $sources = [getenv(), $_ENV ?? [], $_SERVER ?? []];
        foreach ($sources as $source) {
            if (!is_array($source)) {
                continue;
            }

            foreach ($source as $key => $value) {
                if (!is_string($key) || $key === '') {
                    continue;
                }

                if (!is_scalar($value) && $value !== null) {
                    continue;
                }

                $environment[$key] = $value === null ? '' : (string)$value;
            }
        }

        return $environment;
    }
}
