<?php

namespace App\Services;

/**
 * Reads a TLS endpoint's certificate expiry natively (a PHP SSL stream + openssl_x509_parse) — no
 * SSH, no shell, so `host` is never interpolated into a command. Trust is intentionally NOT verified
 * (verify_peer off): the point is to report expiry even for a self-signed or already-expired cert.
 * Monitoring internal endpoints is a legitimate use, so private/internal hosts are allowed.
 */
class CertService
{
    /**
     * @return array{ok:bool, subject?:string, issuer?:string, expiresAt?:int, daysLeft?:?int, error?:string}
     */
    public function check(string $host, int $port, int $timeout = 5): array
    {
        $ctx = stream_context_create(['ssl' => [
            'capture_peer_cert' => true,
            'verify_peer' => false,
            'verify_peer_name' => false,
            'SNI_enabled' => true,
            'peer_name' => $host,
        ]]);

        $errno = 0;
        $errstr = '';
        // @ — a refused/timed-out/unresolved endpoint is a normal result here, not a PHP warning.
        $client = @stream_socket_client(
            'ssl://'.$host.':'.$port,
            $errno,
            $errstr,
            $timeout,
            STREAM_CLIENT_CONNECT,
            $ctx,
        );

        if (! $client) {
            return ['ok' => false, 'error' => $errstr !== '' ? $errstr : 'connection failed'];
        }

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

        $cert = $params['options']['ssl']['peer_certificate'] ?? null;
        if ($cert === null) {
            return ['ok' => false, 'error' => 'no certificate presented'];
        }

        return ['ok' => true] + $this->certInfo($cert);
    }

    /**
     * Extract subject/issuer/expiry from an openssl cert (resource or PEM). Split out so it is unit-
     * testable against a generated cert without a live network connection.
     *
     * @param  \OpenSSLCertificate|string  $cert
     * @return array{subject:string, issuer:string, expiresAt:?int, daysLeft:?int}
     */
    public function certInfo($cert): array
    {
        $parsed = openssl_x509_parse($cert) ?: [];
        $expiresAt = isset($parsed['validTo_time_t']) ? (int) $parsed['validTo_time_t'] : null;

        return [
            'subject' => (string) ($parsed['subject']['CN'] ?? ($parsed['subject']['O'] ?? '—')),
            'issuer' => (string) ($parsed['issuer']['CN'] ?? ($parsed['issuer']['O'] ?? '—')),
            'expiresAt' => $expiresAt,
            'daysLeft' => $expiresAt !== null ? (int) floor(($expiresAt - time()) / 86400) : null,
        ];
    }

    /** expired · critical (<7d) · warning (<30d) · ok — mapped to the status triad in the UI. */
    public function status(?int $daysLeft): string
    {
        if ($daysLeft === null) {
            return 'unknown';
        }

        return match (true) {
            $daysLeft < 0 => 'expired',
            $daysLeft < 7 => 'critical',
            $daysLeft < 30 => 'warning',
            default => 'ok',
        };
    }
}
