<?php

declare(strict_types=1);

namespace Blax\WebRtc\Media\Rust;

/**
 * Keeps one Rust SFU sidecar running per control-socket path — plug-and-play:
 * the first RustMediaEngine call finds the binary (BinaryManager, downloading
 * if permitted) and spawns it detached, flock-guarded so concurrent workers
 * (fork-per-message hosts) race exactly one spawn. `php artisan webrtc:sidecar`
 * runs the same binary in the foreground for supervised/docker deployments —
 * with auto_spawn disabled, that is the only way it starts.
 */
class SidecarSupervisor
{
    /** @param array<string,mixed> $config the `webrtc.sfu` config block */
    public function __construct(
        private readonly BinaryManager $binaries,
        private readonly array $config = [],
    ) {}

    public function socketPath(): string
    {
        return (string) ($this->config['socket'] ?? sys_get_temp_dir().'/blax-webrtc-sfu.sock');
    }

    public function running(): bool
    {
        if (! file_exists($this->socketPath())) {
            return false;
        }

        try {
            (new ControlClient($this->socketPath(), 2.0))->request('ping');

            return true;
        } catch (SidecarException) {
            return false;
        }
    }

    public function ensureRunning(): void
    {
        if ($this->running()) {
            return;
        }

        if (($this->config['auto_spawn'] ?? true) !== true) {
            throw new SidecarException(
                'the SFU sidecar is not running at '.$this->socketPath().
                ' and webrtc.sfu.auto_spawn is disabled — start it with `php artisan webrtc:sidecar`.'
            );
        }

        $lockPath = $this->socketPath().'.lock';
        if (! is_dir($dir = dirname($lockPath))) {
            @mkdir($dir, 0775, true);
        }
        $lock = @fopen($lockPath, 'c');
        if ($lock === false || ! flock($lock, LOCK_EX)) {
            throw new SidecarException("cannot acquire the sidecar spawn lock at {$lockPath}");
        }

        try {
            if ($this->running()) {
                return; // another worker won the race
            }

            $this->spawnDetached();
            $this->awaitSocket();
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
        }
    }

    /** The CLI arguments matching this supervisor's config. @return list<string> */
    public function args(): array
    {
        $args = ['--socket', $this->socketPath()];

        if (($ip = (string) ($this->config['udp_ip'] ?? '')) !== '') {
            array_push($args, '--udp-ip', $ip);
        }
        if (($port = (int) ($this->config['udp_port'] ?? 0)) > 0) {
            array_push($args, '--udp-port', (string) $port);
        }
        if (($ip = (string) ($this->config['public_ip'] ?? '')) !== '') {
            array_push($args, '--public-ip', $ip);
        }

        return $args;
    }

    public function logPath(): string
    {
        return (string) ($this->config['log'] ?? sys_get_temp_dir().'/blax-webrtc-sfu.log');
    }

    /** Graceful stop via the control socket (no-op when not running). */
    public function stop(): void
    {
        if (! $this->running()) {
            return;
        }

        try {
            (new ControlClient($this->socketPath(), 2.0))->request('shutdown');
        } catch (SidecarException) {
            // it went away — that is what we asked for
        }
    }

    private function spawnDetached(): void
    {
        $binary = $this->binaries->ensureInstalled();

        $log = $this->logPath();
        if (! is_dir($dir = dirname($log))) {
            @mkdir($dir, 0775, true);
        }

        // `&` + i/o redirection detaches it from this worker; the shell exits
        // and the sidecar reparents to init (survives the PHP worker).
        $command = sprintf(
            '%s %s >> %s 2>&1 < /dev/null &',
            escapeshellarg($binary),
            implode(' ', array_map('escapeshellarg', $this->args())),
            escapeshellarg($log),
        );

        shell_exec($command);
    }

    private function awaitSocket(): void
    {
        $deadline = microtime(true) + (float) ($this->config['spawn_timeout'] ?? 10.0);

        while (microtime(true) < $deadline) {
            if ($this->running()) {
                return;
            }
            usleep(100_000);
        }

        throw new SidecarException(
            'the SFU sidecar did not come up within the spawn timeout — check '.$this->logPath()
        );
    }
}
