<?php

namespace App\Services\Proxy;

use Illuminate\Support\Carbon;

/**
 * Was ein Besucher wirklich bekommt, wenn er diesen Namen aufruft.
 *
 * Gemessen, nicht aus der Konfiguration gelesen. Eine Zeile im Caddyfile sagt,
 * was gewollt ist; ein TLS-Handschlag sagt, was passiert. Genau zwischen diesen
 * beiden lag der Fall, der diese Klasse veranlasst hat: DNS richtig gesetzt,
 * `FILES_HOST` richtig gesetzt, Proxy auf derselben Maschine — und trotzdem
 * `tlsv1 alert internal error`, weil kein Zertifikat für den Namen existierte.
 * Keine der drei Einstellungen hätte das verraten.
 *
 * Deshalb wird von außen nach innen gefragt und nicht umgekehrt.
 */
class CertificateInspector
{
    /**
     * @return array{ok: bool, expires_at: ?Carbon, issuer: ?string, error: ?string}
     */
    public function inspect(string $hostname, int $timeoutSeconds = 8): array
    {
        // `verify_peer` bleibt AN. Ein Zertifikat, das die Prüfung nicht
        // besteht, ist für diese Frage kein Zertifikat — der Browser des
        // Kunden sieht es genauso, und eine Anzeige, die es trotzdem als
        // gültig führte, wäre die Attrappe, vor der R19 warnt.
        $context = stream_context_create([
            'ssl' => [
                'capture_peer_cert' => true,
                'SNI_enabled' => true,
                'peer_name' => $hostname,
                'verify_peer' => true,
                'verify_peer_name' => true,
            ],
        ]);

        $client = @stream_socket_client(
            'ssl://'.$hostname.':443',
            $errorNumber,
            $errorMessage,
            $timeoutSeconds,
            STREAM_CLIENT_CONNECT,
            $context,
        );

        if ($client === false) {
            return [
                'ok' => false,
                'expires_at' => null,
                'issuer' => null,
                // Die Meldung von PHP ist lang und trägt Dateipfade. Was zählt,
                // ist die erste Zeile — sie unterscheidet „kein Zertifikat" von
                // „Name löst nicht auf", und das sind zwei ganz verschiedene
                // Aufgaben für den Betreiber.
                'error' => $this->firstLine($errorMessage ?: 'kein Verbindungsaufbau'),
            ];
        }

        $params = stream_context_get_params($client);
        fclose($client);

        $certificate = $params['options']['ssl']['peer_certificate'] ?? null;

        if ($certificate === null) {
            return ['ok' => false, 'expires_at' => null, 'issuer' => null, 'error' => 'kein Zertifikat vorgelegt'];
        }

        $parsed = openssl_x509_parse($certificate);

        if ($parsed === false || ! isset($parsed['validTo_time_t'])) {
            return ['ok' => false, 'expires_at' => null, 'issuer' => null, 'error' => 'Zertifikat nicht lesbar'];
        }

        return [
            'ok' => true,
            'expires_at' => Carbon::createFromTimestampUTC($parsed['validTo_time_t']),
            'issuer' => $parsed['issuer']['O'] ?? ($parsed['issuer']['CN'] ?? null),
            'error' => null,
        ];
    }

    private function firstLine(string $message): string
    {
        $line = trim((string) preg_split('/\R/', $message)[0]);

        // Der PHP-Vorspann („stream_socket_client(): ") sagt nichts über die
        // Sache und frisst die halbe Spalte.
        $line = (string) preg_replace('/^stream_socket_client\(\):\s*/', '', $line);

        return mb_substr($line, 0, 190);
    }
}
