<?php

namespace App\Services\Ssl;

use Afosto\Acme\Client as AcmeClient;
use App\Models\ClientSslCertificate;
use App\Models\Setting;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\Filesystem;

/**
 * ACME v2 wrapper used only by ACME-style SSL modules (Let's Encrypt etc.).
 * Reseller modülleri (Comodo, Sectigo) bu servisi kullanmaz; kendi CA-spesifik
 * client'larını çağırırlar.
 *
 * `expires_at` parametresi opsiyonel — verilirse ClientSslCertificate'a o
 * yazılır (billing period sonu). Verilmezse CA cert'in NotAfter alanı
 * kullanılır (ACME default 90 gün).
 */
class AcmeService
{
    public function issue(ClientSslCertificate $cert, array $config, ?\DateTimeInterface $billingExpiresAt = null): ClientSslCertificate
    {
        $domains = array_values(array_filter(array_merge(
            [$cert->common_name],
            (array) ($cert->alt_names ?? [])
        )));

        if (empty($domains)) {
            throw new \RuntimeException('ACME issue: no domains supplied');
        }

        $client = $this->makeClient($config);

        try {
            $order = $client->createOrder($domains);
            $authorizations = $client->authorize($order);

            // HTTP-01 challenge tokens → public route can serve them
            $publishedTokens = [];
            foreach ($authorizations as $authorization) {
                $file = $authorization->getFile();
                if (! $file) {
                    continue;
                }
                Cache::put("acme_challenge:{$file->getFilename()}", $file->getContents(), now()->addMinutes(15));
                $publishedTokens[] = $file->getFilename();
            }

            foreach ($authorizations as $authorization) {
                if ($challenge = $authorization->getHttpChallenge()) {
                    $client->validate($challenge);
                }
            }

            $certData = $client->getCertificate($order);

            $cert->update([
                'csr' => $certData->getCsr(),
                'certificate' => $certData->getCertificate(false),
                'certificate_chain' => $certData->getIntermediate(),
                'private_key' => $certData->getPrivateKey(),
                'issuer' => $config['issuer_label'] ?? 'Let\'s Encrypt',
                'installed' => true,
                'installed_at' => now(),
                'expires_at' => $billingExpiresAt ?: $certData->getExpiryDate(),
                'last_error' => null,
                'last_attempt_at' => now(),
            ]);

            foreach ($publishedTokens as $t) {
                Cache::forget("acme_challenge:{$t}");
            }

            // PRC-622 — 'SSL Certificate Issued' client maili
            // (config/communication_events.php eşlemesi). HookManager::action
            // handler hatalarını içeride yutar — issue akışını bozamaz.
            app(\App\Core\Hook\HookBridge::class)->fireAction('SSLCertificateIssued', [
                'ssl_certificate_id' => $cert->id,
                'client_product_id' => $cert->client_product_id,
                'service_id' => $cert->client_product_id,
                'client_id' => $cert->client_id,
                'domain' => $cert->common_name,
            ]);

            return $cert->fresh();
        } catch (\Throwable $e) {
            $cert->update([
                'last_error' => mb_substr($e->getMessage(), 0, 1000),
                'last_attempt_at' => now(),
            ]);
            Log::error('ACME issue failed', [
                'cert_id' => $cert->id,
                'domains' => $domains,
                'error' => $e->getMessage(),
            ]);
            throw $e;
        }
    }

    /**
     * Dönem-ortası yeniden ihraç (mid-period re-issue). Let's Encrypt CA
     * cert'i 90 gün TTL'e sahip; billing period ise 1/2/3 yıl. Ödeme
     * beklemeden fiziksel expire yaklaştığında PEM materyalini tazeler.
     *
     * `expires_at` (billing period sonu) KORUNUR — sadece cert/chain/private_key
     * ve CA TTL tazelenir. `issue()`'yu billing period'u koruyarak yeniden
     * çağırmak idempotenttir: yeni bir 90-günlük cert alınır, görünür ömür
     * (billing) değişmez.
     */
    public function renew(ClientSslCertificate $cert, array $config): ClientSslCertificate
    {
        // Billing period sonunu koru; verilmezse issue() kendi fallback'ini
        // (mevcut expires_at) kullanır.
        return $this->issue($cert, $config, $cert->expires_at);
    }

    /**
     * Depolanan cert PEM'inden fiziksel CA bitiş tarihini (NotAfter) çözer.
     * Bu billing `expires_at`'ten farklıdır — CA'nın gerçek 90-günlük TTL'i.
     * Parse edilemezse null döner (renewal-due mantığı defansif davranır).
     */
    public function caMaterialExpiresAt(ClientSslCertificate $cert): ?\DateTimeInterface
    {
        $pem = $cert->certificate;
        if (! is_string($pem) || $pem === '' || ! function_exists('openssl_x509_parse')) {
            return null;
        }

        $parsed = @openssl_x509_parse($pem);
        if (! is_array($parsed) || empty($parsed['validTo_time_t'])) {
            return null;
        }

        return (new \DateTimeImmutable())->setTimestamp((int) $parsed['validTo_time_t']);
    }

    /**
     * Fiziksel CA materyali eşik gün içinde expire oluyorsa (veya çoktan
     * expire olduysa / parse edilemiyorsa) yeniden ihraç gerekir.
     *
     * NOT: billing period (`expires_at`) hâlâ geçerli certler için çağrılır —
     * bu karar caller'da (command) verilir.
     */
    public function isCaMaterialRenewalDue(ClientSslCertificate $cert, int $thresholdDays = 30): bool
    {
        $caExpiry = $this->caMaterialExpiresAt($cert);
        if ($caExpiry === null) {
            // PEM parse edilemedi → güvenli tarafta kal, tazele.
            return true;
        }

        return $caExpiry <= now()->addDays($thresholdDays);
    }

    /** Public HTTP-01 endpoint payload resolver. */
    public static function resolveChallengeToken(string $token): ?string
    {
        $payload = Cache::get("acme_challenge:{$token}");

        return is_string($payload) && $payload !== '' ? $payload : null;
    }

    private function makeClient(array $config): AcmeClient
    {
        $email = $config['contact_email']
            ?? Setting::getValue('admin_notification_email', null)
            ?? Setting::getValue('company_email', null);

        if (empty($email)) {
            throw new \RuntimeException('ACME contact_email is not configured.');
        }

        $directoryUrl = $config['directory_url']
            ?? 'https://acme-staging-v02.api.letsencrypt.org/directory';

        $disk = Storage::disk('local');
        $driver = $disk->getDriver();
        if (! $driver instanceof Filesystem) {
            throw new \RuntimeException('Local disk is not a Flysystem instance.');
        }

        return new AcmeClient([
            'fs' => $driver,
            'mode' => str_contains($directoryUrl, 'staging')
                ? AcmeClient::MODE_STAGING
                : AcmeClient::MODE_LIVE,
            'basePath' => 'ssl/acme',
            'username' => $email,
        ]);
    }
}
