using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using OpcUaManager.Models;

namespace OpcUaManager.Services;

/// <summary>
/// OPC UA 클라이언트용 X.509 인증서를 생성하고 PFX로 저장합니다.
/// 원본 Program.cs 의 인증서 체계(pki/ 폴더 구조, X509KeyStorageFlags)를 유지합니다.
/// </summary>
public class CertService
{
    private readonly ILogger<CertService> _logger;
    private readonly string _pkiRoot;

    public CertService(ILogger<CertService> logger, IWebHostEnvironment env)
    {
        _logger  = logger;
        _pkiRoot = Path.Combine(env.ContentRootPath, "pki");
    }

    private void EnsurePkiDirectories()
    {
        Directory.CreateDirectory(Path.Combine(_pkiRoot, "own",      "certs"));
        Directory.CreateDirectory(Path.Combine(_pkiRoot, "trusted",  "certs"));
        Directory.CreateDirectory(Path.Combine(_pkiRoot, "issuers",  "certs"));
        Directory.CreateDirectory(Path.Combine(_pkiRoot, "rejected", "certs"));
    }

    public Task<CertCreateResult> GenerateAsync(CertCreateRequest req)
    {
        try
        {
            EnsurePkiDirectories();

            string appUri = $"urn:{req.ClientHostName}:{req.ApplicationName}";

            // ── 1. RSA 키 생성 ────────────────────────────────────────
            using var rsa = RSA.Create(2048);

            // ── 2. Distinguished Name ──────────────────────────────────
            var dn = new X500DistinguishedName(
                $"CN={req.ApplicationName}, O={req.ClientHostName}, C=KR");

            // ── 3. Certificate Request ─────────────────────────────────
            var certReq = new CertificateRequest(
                dn, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

            // ── 4. Extensions ──────────────────────────────────────────
            certReq.CertificateExtensions.Add(
                new X509BasicConstraintsExtension(false, false, 0, true));

            certReq.CertificateExtensions.Add(
                new X509KeyUsageExtension(
                    X509KeyUsageFlags.DigitalSignature |
                    X509KeyUsageFlags.NonRepudiation   |
                    X509KeyUsageFlags.KeyEncipherment  |
                    X509KeyUsageFlags.DataEncipherment,
                    critical: true));

            certReq.CertificateExtensions.Add(
                new X509EnhancedKeyUsageExtension(
                    new OidCollection
                    {
                        new Oid("1.3.6.1.5.5.7.3.2"),  // clientAuth
                        new Oid("1.3.6.1.5.5.7.3.1")   // serverAuth
                    }, false));

            certReq.CertificateExtensions.Add(
                new X509SubjectKeyIdentifierExtension(certReq.PublicKey, false));

            // ── 5. Subject Alternative Names ──────────────────────────
            var sanBuilder = new SubjectAlternativeNameBuilder();
            sanBuilder.AddUri(new Uri(appUri));

            if (!string.IsNullOrWhiteSpace(req.ServerIp) &&
                System.Net.IPAddress.TryParse(req.ServerIp, out var ip))
                sanBuilder.AddIpAddress(ip);

            if (!string.IsNullOrWhiteSpace(req.ServerHostName))
                sanBuilder.AddDnsName(req.ServerHostName);

            sanBuilder.AddDnsName(req.ClientHostName);
            sanBuilder.AddDnsName("localhost");

            certReq.CertificateExtensions.Add(sanBuilder.Build());

            // ── 6. 자체 서명 인증서 생성 ──────────────────────────────
            var notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
            var notAfter  = notBefore.AddDays(req.ValidDays);

            // FIX: CreateSelfSigned 는 이미 RSA 키가 연결된 X509Certificate2 를 반환함
            // → CopyWithPrivateKey() 를 추가로 호출하면
            //   "The certificate already has an associated private key" 에러 발생
            // → CreateSelfSigned 결과를 직접 Export(Pfx) 하면 개인키 포함됨
            using var certWithKey = certReq.CreateSelfSigned(notBefore, notAfter);

            // ── 7. PFX 내보내기 ───────────────────────────────────────
            string pfxPassword = string.IsNullOrEmpty(req.PfxPassword) ? "" : req.PfxPassword;

            // .NET 9 이상: X509CertificateLoader 권장이지만 net8 에서는 Export 사용
            byte[] pfxBytes = certWithKey.Export(X509ContentType.Pfx, pfxPassword);

            string pfxFileName = $"{req.ApplicationName}.pfx";
            string pfxPath     = Path.Combine(_pkiRoot, "own", "certs", pfxFileName);
            File.WriteAllBytes(pfxPath, pfxBytes);

            // ── 8. Trusted 폴더에 DER(공개키) 복사 ───────────────────
            byte[] derBytes = certWithKey.Export(X509ContentType.Cert);
            string derPath  = Path.Combine(_pkiRoot, "trusted", "certs",
                $"{req.ApplicationName}[{certWithKey.Thumbprint}].der");
            File.WriteAllBytes(derPath, derBytes);

            _logger.LogInformation(
                "인증서 생성 완료 → {PfxPath} | Thumbprint: {Thumbprint}",
                pfxPath, certWithKey.Thumbprint);

            return Task.FromResult(new CertCreateResult
            {
                Success        = true,
                Message        = $"인증서 생성 완료: {pfxFileName}",
                PfxPath        = pfxPath,
                ApplicationUri = appUri,
                Thumbprint     = certWithKey.Thumbprint ?? string.Empty,
                SerialNumber   = certWithKey.SerialNumber ?? string.Empty,
                NotBefore      = notBefore.ToString("yyyy-MM-dd HH:mm:ss UTC"),
                NotAfter       = notAfter.ToString("yyyy-MM-dd HH:mm:ss UTC"),
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "인증서 생성 실패");
            return Task.FromResult(new CertCreateResult
            {
                Success = false,
                Message = $"인증서 생성 실패: {ex.Message}"
            });
        }
    }

    /// <summary>
    /// 기존 PFX를 로드합니다 (원본 코드와 동일한 플래그).
    /// </summary>
    public X509Certificate2 LoadPfx(string pfxPath, string pfxPassword)
    {
        return new X509Certificate2(
            pfxPath,
            pfxPassword,
            X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
    }

    public IEnumerable<string> ListCertificates()
    {
        string dir = Path.Combine(_pkiRoot, "own", "certs");
        if (!Directory.Exists(dir)) return [];
        return Directory.GetFiles(dir, "*.pfx").Select(Path.GetFileName)!;
    }
}
