<?php

namespace App\Services;

use Illuminate\Support\Str;

/**
 * The app side of the WireGuard write-bridge. `request()` writes a strictly-validated request to
 * run/wg-request.json (a host watcher runs it); `result()` reads the host-written result for an id
 * the app issued. Validation here is the FIRST gate — the host re-validates every value too.
 */
class WgBridge
{
    /** Actions the UI may request; the host whitelists the same set. */
    public const ACTIONS = ['add-peer', 'remove-peer', 'gate-up', 'gate-down', 'gate-ssh-on', 'gate-ssh-off', 'set-endpoint', 'set-port', 'set-subnet', 'set-dns', 'setup'];

    private function dir(): string
    {
        return storage_path('app/restart-signal');
    }

    /**
     * Write a write-request; returns the request id. Throws InvalidArgumentException on a bad
     * action or arg (defence-in-depth; the host re-validates).
     *
     * @param  array<string,string>  $args
     */
    public function request(string $action, array $args = []): string
    {
        if (! in_array($action, self::ACTIONS, true)) {
            throw new \InvalidArgumentException('unknown action');
        }
        $clean = $this->validateArgs($action, $args);

        $id = Str::random(32);
        $payload = array_merge(['id' => $id, 'action' => $action, 'at' => time()], $clean);

        @mkdir($this->dir(), 0775, true);
        file_put_contents($this->dir().'/wg-request.json', json_encode($payload, JSON_UNESCAPED_SLASHES));

        return $id;
    }

    /**
     * Read the host result for an id THIS app issued. Returns null until the result exists.
     * Includes the QR sidecar SVG when present (add-peer). Never throws.
     *
     * @return array{ok:bool, message:string, config:?string, qr:?string}|null
     */
    public function result(string $id): ?array
    {
        if (preg_match('/^[A-Za-z0-9]{16,64}$/', $id) !== 1) {
            return null;
        }
        $file = $this->dir()."/wg-result-{$id}.json";
        if (! is_file($file)) {
            return null;
        }
        $data = json_decode((string) @file_get_contents($file), true);
        if (! is_array($data) || ($data['id'] ?? null) !== $id) {
            return null;
        }
        $qrFile = $this->dir()."/wg-qr-{$id}.svg";
        $qr = is_file($qrFile) ? (string) @file_get_contents($qrFile) : null;

        return [
            'ok' => (bool) ($data['ok'] ?? false),
            'message' => (string) ($data['message'] ?? ''),
            'config' => isset($data['config']) && is_string($data['config']) ? $data['config'] : null,
            'qr' => $qr,
        ];
    }

    /**
     * @param  array<string,string>  $args
     * @return array<string,string>
     */
    private function validateArgs(string $action, array $args): array
    {
        $name = (string) ($args['name'] ?? '');
        switch ($action) {
            case 'add-peer':
            case 'remove-peer':
                if (preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1) {
                    throw new \InvalidArgumentException('invalid name');
                }

                return ['name' => $name];
            case 'gate-up':
            case 'gate-down':
            case 'gate-ssh-on':
            case 'gate-ssh-off':
                return [];
            case 'set-endpoint':
                $ep = (string) ($args['endpoint'] ?? '');
                if (preg_match('/^[A-Za-z0-9.:_-]{1,128}$/', $ep) !== 1) {
                    throw new \InvalidArgumentException('invalid endpoint');
                }

                return ['endpoint' => $ep];
            case 'set-port':
                $port = (string) ($args['port'] ?? '');
                if (! $this->validPort($port)) {
                    throw new \InvalidArgumentException('invalid port');
                }

                return ['port' => $port];
            case 'set-subnet':
                $subnet = (string) ($args['subnet'] ?? '');
                if (preg_match('#^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$#', $subnet) !== 1) {
                    throw new \InvalidArgumentException('invalid subnet');
                }

                return ['subnet' => $subnet];
            case 'set-dns':
                $dns = (string) ($args['dns'] ?? '');
                // one or more IPv4 addresses, comma/space separated
                if (preg_match('/^\d{1,3}(\.\d{1,3}){3}([,\s]+\d{1,3}(\.\d{1,3}){3})*$/', $dns) !== 1) {
                    throw new \InvalidArgumentException('invalid dns');
                }

                return ['dns' => $dns];
            case 'setup':
                $subnet = (string) ($args['subnet'] ?? '');
                $port = (string) ($args['port'] ?? '');
                $endpoint = (string) ($args['endpoint'] ?? '');
                $name = (string) ($args['name'] ?? '');
                if (preg_match('#^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$#', $subnet) !== 1) {
                    throw new \InvalidArgumentException('invalid subnet');
                }
                if (! $this->validPort($port)) {
                    throw new \InvalidArgumentException('invalid port');
                }
                if ($endpoint !== '' && preg_match('/^[A-Za-z0-9.:_-]{1,128}$/', $endpoint) !== 1) {
                    throw new \InvalidArgumentException('invalid endpoint');
                }
                if (preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1) {
                    throw new \InvalidArgumentException('invalid name');
                }

                return ['subnet' => $subnet, 'port' => $port, 'endpoint' => $endpoint, 'name' => $name];
        }

        return [];
    }

    /** A valid 1–65535 UDP port given as a numeric string. */
    private function validPort(string $port): bool
    {
        return preg_match('/^\d{1,5}$/', $port) === 1 && (int) $port >= 1 && (int) $port <= 65535;
    }
}
