<?php

namespace App\Services\Traefik;

use App\Services\Secrets\SecretVault;
use App\Services\Ssh\RemoteShell;
use App\Support\ProvisioningSettings;
use Illuminate\Support\Facades\Http;

/**
 * Writes the Traefik file-provider YAML onto the host that actually serves the
 * traffic (DNS points at it), via SSH — not the worker's local filesystem — so
 * that host's Traefik picks up the route. Checks HTTP-01 cert issuance by
 * probing the fqdn. Not unit-tested (live I/O).
 */
class SshTraefikWriter implements TraefikWriter
{
    public function __construct(private RemoteShell $shell) {}

    public function write(string $trafficHost, string $subdomain, array $hostnames, string $backend): void
    {
        // The file is rewritten wholesale rather than appended to, so the
        // hostnames handed in are exactly the hostnames served afterwards —
        // that is what makes withdrawing a domain a write rather than a
        // separate deletion somebody has to remember.
        $yaml = $this->render($subdomain, $hostnames, $backend);

        $this->shell->connectWithKey($trafficHost, 'root', $this->privateKey());
        $this->shell->putFile($this->path($subdomain), $yaml); // putFile throws on failure
    }

    public function remove(string $trafficHost, string $subdomain): void
    {
        $this->shell->connectWithKey($trafficHost, 'root', $this->privateKey());
        $this->shell->run('rm -f '.escapeshellarg($this->path($subdomain)));
    }

    /**
     * The identity CluPilot deploys to every host: the one stored in the
     * console if there is one, else CLUPILOT_SSH_PRIVATE_KEY(_PATH). Read
     * HERE, at the point of use — see SecretVault's docblock (rule 3).
     */
    private function privateKey(): string
    {
        return (string) app(SecretVault::class)->get('ssh.private_key');
    }

    public function certReachable(string $fqdn): bool
    {
        // While DNS propagates / Traefik warms up, the client throws
        // (ConnectionException, TLS) — that's "not ready yet", not a hard error,
        // so the step keeps polling instead of burning the retry budget.
        try {
            return Http::timeout(5)->connectTimeout(5)->get("https://{$fqdn}/status.php")->successful();
        } catch (\Throwable) {
            return false;
        }
    }

    private function path(string $subdomain): string
    {
        return rtrim(ProvisioningSettings::traefikDynamicPath(), '/')."/{$subdomain}.yml";
    }

    /**
     * One router, one rule, every hostname in it.
     *
     * Traefik's `Host()` matcher takes several names in one call, and a single
     * rule keeps the certResolver, the service and the entryPoint stated once —
     * a second router would have to repeat all three and could drift from the
     * first.
     *
     * @param  array<int, string>  $hostnames
     */
    private function render(string $subdomain, array $hostnames, string $backend): string
    {
        $names = implode(', ', array_map(fn (string $host) => "`{$host}`", $hostnames));

        return implode("\n", [
            'http:',
            '  routers:',
            "    {$subdomain}:",
            "      rule: \"Host({$names})\"",
            "      service: \"{$subdomain}\"",
            '      entryPoints: ["websecure"]',
            '      tls:',
            '        certResolver: "letsencrypt"',
            '  services:',
            "    {$subdomain}:",
            '      loadBalancer:',
            '        servers:',
            "          - url: \"http://{$backend}:80\"",
            '',
        ]);
    }
}
