<?php

declare(strict_types=1);

namespace App\Framework\Deployment\Ssl\Services;

use App\Framework\Deployment\Ssl\Events\SslCertificateObtained;
use App\Framework\Deployment\Ssl\Events\SslCertificateRenewed;
use App\Framework\Deployment\Ssl\Events\SslCertificateRevoked;
use App\Framework\Deployment\Ssl\Exceptions\CertificateObtainFailedException;
use App\Framework\Deployment\Ssl\Exceptions\CertificateRenewalFailedException;
use App\Framework\Deployment\Ssl\ValueObjects\CertificatePath;
use App\Framework\Deployment\Ssl\ValueObjects\CertificateStatus;
use App\Framework\Deployment\Ssl\ValueObjects\DomainName;
use App\Framework\Deployment\Ssl\ValueObjects\SslConfiguration;
use App\Framework\EventBus\EventBus;
use App\Framework\Logging\Logger;
use App\Framework\Process\Process;
use DateTimeImmutable;

/**
 * SSL Certificate Service - Core certificate operations using Let's Encrypt
 */
final readonly class SslCertificateService
{
    public function __construct(
        private Process $process,
        private EventBus $eventBus,
        private Logger $logger
    ) {}

    /**
     * Obtain a new SSL certificate for the configured domain
     */
    public function obtain(SslConfiguration $config): CertificateStatus
    {
        $this->logger->info('Obtaining SSL certificate', [
            'domain' => $config->domain->value,
            'mode' => $config->mode->value,
            'email' => $config->email->value
        ]);

        // Validate prerequisites
        $this->validatePrerequisites($config);

        // Build certbot command
        $command = array_filter($config->getCertbotCommand());

        try {
            // Execute certbot
            $result = $this->process->run($command);

            if (!$result->isSuccessful()) {
                throw new CertificateObtainFailedException(
                    "Failed to obtain certificate: {$result->getErrorOutput()}",
                    (int) $result->getExitCode()
                );
            }

            $this->logger->info('Certificate obtained successfully', [
                'domain' => $config->domain->value,
                'mode' => $config->mode->value
            ]);

            // Get certificate status
            $status = $this->getStatus($config->domain, $config->certbotConfDir->toString());

            // Dispatch event
            $this->eventBus->dispatch(new SslCertificateObtained(
                $config->domain,
                $config->mode,
                $status
            ));

            return $status;

        } catch (\Exception $e) {
            $this->logger->error('Certificate obtain failed', [
                'domain' => $config->domain->value,
                'error' => $e->getMessage()
            ]);

            throw new CertificateObtainFailedException(
                "Certificate obtain failed: {$e->getMessage()}",
                previous: $e
            );
        }
    }

    /**
     * Renew an existing SSL certificate
     */
    public function renew(SslConfiguration $config): CertificateStatus
    {
        $this->logger->info('Renewing SSL certificate', [
            'domain' => $config->domain->value,
            'mode' => $config->mode->value
        ]);

        // Check if certificate exists
        $paths = $config->getCertificatePaths();
        if (!$paths->exists()) {
            throw new CertificateRenewalFailedException(
                "Cannot renew: Certificate does not exist for domain {$config->domain->value}"
            );
        }

        // Build certbot renew command
        $command = [
            'certbot',
            'renew',
            '--config-dir=' . $config->certbotConfDir->toString(),
            '--work-dir=' . $config->certbotConfDir->join('work')->toString(),
            '--logs-dir=' . $config->certbotConfDir->join('logs')->toString(),
            '--cert-name=' . $config->domain->value,
            '--non-interactive',
        ];

        if ($config->mode->isStaging()) {
            $command[] = '--staging';
        }

        try {
            $result = $this->process->run($command);

            if (!$result->isSuccessful()) {
                throw new CertificateRenewalFailedException(
                    "Failed to renew certificate: {$result->getErrorOutput()}",
                    (int) $result->getExitCode()
                );
            }

            $this->logger->info('Certificate renewed successfully', [
                'domain' => $config->domain->value
            ]);

            // Get updated status
            $status = $this->getStatus($config->domain, $config->certbotConfDir->toString());

            // Dispatch event
            $this->eventBus->dispatch(new SslCertificateRenewed(
                $config->domain,
                $status
            ));

            return $status;

        } catch (\Exception $e) {
            $this->logger->error('Certificate renewal failed', [
                'domain' => $config->domain->value,
                'error' => $e->getMessage()
            ]);

            throw new CertificateRenewalFailedException(
                "Certificate renewal failed: {$e->getMessage()}",
                previous: $e
            );
        }
    }

    /**
     * Get the current status of an SSL certificate
     */
    public function getStatus(DomainName $domain, string $baseDir = '/etc/letsencrypt'): CertificateStatus
    {
        $paths = CertificatePath::forDomain($domain->value, $baseDir);

        // Check if certificate exists
        if (!$paths->exists()) {
            return CertificateStatus::notFound();
        }

        // Check if certificate is readable
        if (!$paths->isReadable()) {
            return new CertificateStatus(
                exists: true,
                isValid: false,
                notBefore: null,
                notAfter: null,
                issuer: null,
                subject: null,
                daysUntilExpiry: null,
                isExpiring: false,
                isExpired: false,
                errors: ['Certificate files are not readable']
            );
        }

        // Parse certificate information
        try {
            $certData = openssl_x509_parse(file_get_contents($paths->certPath->toString()));

            if ($certData === false) {
                return new CertificateStatus(
                    exists: true,
                    isValid: false,
                    notBefore: null,
                    notAfter: null,
                    issuer: null,
                    subject: null,
                    daysUntilExpiry: null,
                    isExpiring: false,
                    isExpired: false,
                    errors: ['Failed to parse certificate']
                );
            }

            $notBefore = (new DateTimeImmutable())->setTimestamp($certData['validFrom_time_t']);
            $notAfter = (new DateTimeImmutable())->setTimestamp($certData['validTo_time_t']);
            $issuer = $certData['issuer']['CN'] ?? 'Unknown';
            $subject = $certData['subject']['CN'] ?? 'Unknown';

            return CertificateStatus::fromCertificateData(
                $notBefore,
                $notAfter,
                $issuer,
                $subject
            );

        } catch (\Exception $e) {
            $this->logger->error('Failed to read certificate status', [
                'domain' => $domain->value,
                'error' => $e->getMessage()
            ]);

            return new CertificateStatus(
                exists: true,
                isValid: false,
                notBefore: null,
                notAfter: null,
                issuer: null,
                subject: null,
                daysUntilExpiry: null,
                isExpiring: false,
                isExpired: false,
                errors: [$e->getMessage()]
            );
        }
    }

    /**
     * Test certificate configuration without actually obtaining a certificate
     */
    public function test(SslConfiguration $config): bool
    {
        $this->logger->info('Testing SSL certificate configuration', [
            'domain' => $config->domain->value
        ]);

        // Build certbot dry-run command
        $command = array_filter($config->getCertbotCommand());
        $command[] = '--dry-run';

        try {
            $result = $this->process->run($command);

            if ($result->isSuccessful()) {
                $this->logger->info('SSL configuration test passed', [
                    'domain' => $config->domain->value
                ]);
                return true;
            }

            $this->logger->warning('SSL configuration test failed', [
                'domain' => $config->domain->value,
                'error' => $result->getErrorOutput()
            ]);
            return false;

        } catch (\Exception $e) {
            $this->logger->error('SSL configuration test error', [
                'domain' => $config->domain->value,
                'error' => $e->getMessage()
            ]);
            return false;
        }
    }

    /**
     * Revoke an SSL certificate
     */
    public function revoke(SslConfiguration $config, string $reason = 'unspecified'): bool
    {
        $this->logger->info('Revoking SSL certificate', [
            'domain' => $config->domain->value,
            'reason' => $reason
        ]);

        $paths = $config->getCertificatePaths();

        if (!$paths->exists()) {
            $this->logger->warning('Cannot revoke: Certificate does not exist', [
                'domain' => $config->domain->value
            ]);
            return false;
        }

        $command = [
            'certbot',
            'revoke',
            '--config-dir=' . $config->certbotConfDir->toString(),
            '--work-dir=' . $config->certbotConfDir->join('work')->toString(),
            '--logs-dir=' . $config->certbotConfDir->join('logs')->toString(),
            '--cert-path=' . $paths->certPath->toString(),
            '--reason=' . $reason,
            '--non-interactive',
        ];

        try {
            $result = $this->process->run($command);

            if ($result->isSuccessful()) {
                $this->logger->info('Certificate revoked successfully', [
                    'domain' => $config->domain->value
                ]);

                // Dispatch event
                $this->eventBus->dispatch(new SslCertificateRevoked(
                    $config->domain,
                    $reason
                ));

                return true;
            }

            $this->logger->error('Certificate revocation failed', [
                'domain' => $config->domain->value,
                'error' => $result->getErrorOutput()
            ]);
            return false;

        } catch (\Exception $e) {
            $this->logger->error('Certificate revocation error', [
                'domain' => $config->domain->value,
                'error' => $e->getMessage()
            ]);
            return false;
        }
    }

    /**
     * Validate prerequisites for certificate operations
     */
    private function validatePrerequisites(SslConfiguration $config): void
    {
        // Check if webroot directory exists and is writable
        if (!$config->certbotWwwDir->exists()) {
            throw new CertificateObtainFailedException(
                "Webroot directory does not exist: {$config->certbotWwwDir->toString()}"
            );
        }

        if (!$config->certbotWwwDir->isWritable()) {
            throw new CertificateObtainFailedException(
                "Webroot directory is not writable: {$config->certbotWwwDir->toString()}"
            );
        }

        // Check if config directory exists and is writable
        if (!$config->certbotConfDir->exists()) {
            throw new CertificateObtainFailedException(
                "Config directory does not exist: {$config->certbotConfDir->toString()}"
            );
        }

        if (!$config->certbotConfDir->isWritable()) {
            throw new CertificateObtainFailedException(
                "Config directory is not writable: {$config->certbotConfDir->toString()}"
            );
        }

        // Validate domain accessibility (basic DNS check)
        $this->validateDomainAccessibility($config->domain);
    }

    /**
     * Validate that the domain is accessible
     */
    private function validateDomainAccessibility(DomainName $domain): void
    {
        // Skip validation for wildcard domains
        if ($domain->isWildcard()) {
            return;
        }

        // Check if domain resolves
        $ip = gethostbyname($domain->value);

        if ($ip === $domain->value) {
            $this->logger->warning('Domain does not resolve to an IP address', [
                'domain' => $domain->value
            ]);
            // Don't throw exception, just log warning
            // Let's Encrypt will provide more detailed error if needed
        }
    }
}
