using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Options;

namespace ZB.MOM.WW.OtOpcUa.Admin.Services;

/// <summary>
///     Metadata for a certificate file found in one of the OPC UA server's PKI stores. The
///     <see cref="FilePath"/> is the absolute path of the DER/CRT file the stack created when it
///     rejected the cert (for <see cref="CertStoreKind.Rejected"/>) or when an operator trusted
///     it (for <see cref="CertStoreKind.Trusted"/>).
/// </summary>
public sealed record CertInfo(
    string Thumbprint,
    string Subject,
    string Issuer,
    DateTime NotBefore,
    DateTime NotAfter,
    string FilePath,
    CertStoreKind Store);

public enum CertStoreKind
{
    Rejected,
    Trusted,
}

/// <summary>
///     Filesystem-backed view over the OPC UA server's PKI store. The Opc.Ua stack uses a
///     Directory-typed store — each cert is a <c>.der</c> file under <c>{root}/{store}/certs/</c>
///     with a filename derived from subject + thumbprint. This service exposes operators for the
///     Admin UI: list rejected, list trusted, trust a rejected cert (move to trusted), remove a
///     rejected cert (delete), untrust a previously trusted cert (delete from trusted).
/// </summary>
/// <remarks>
///     The Admin process is separate from the Server process; this service deliberately has no
///     Opc.Ua dependency — it works on the on-disk layout directly so it can run on the Admin
///     host even when the Server isn't installed locally, as long as the PKI root is reachable
///     (typical deployment has Admin + Server side-by-side on the same machine).
///
///     Trust/untrust requires the Server to re-read its trust list. The Opc.Ua stack re-reads
///     the Directory store on each new incoming connection, so there's no explicit signal
///     needed — the next client handshake picks up the change. Operators should retry the
///     rejected client's connection after trusting.
/// </remarks>
public sealed class CertTrustService
{
    private readonly CertTrustOptions _options;
    private readonly ILogger<CertTrustService> _logger;

    public CertTrustService(IOptions<CertTrustOptions> options, ILogger<CertTrustService> logger)
    {
        _options = options.Value;
        _logger = logger;
    }

    public string PkiStoreRoot => _options.PkiStoreRoot;

    public IReadOnlyList<CertInfo> ListRejected() => ListStore(CertStoreKind.Rejected);
    public IReadOnlyList<CertInfo> ListTrusted() => ListStore(CertStoreKind.Trusted);

    /// <summary>
    ///     Move the cert with <paramref name="thumbprint"/> from the rejected store to the
    ///     trusted store. No-op returns false if the rejected file doesn't exist (already moved
    ///     by another operator, or thumbprint mismatch). Overwrites an existing trusted copy
    ///     silently — idempotent.
    /// </summary>
    public bool TrustRejected(string thumbprint)
    {
        var cert = FindInStore(CertStoreKind.Rejected, thumbprint);
        if (cert is null) return false;

        var trustedDir = CertsDir(CertStoreKind.Trusted);
        Directory.CreateDirectory(trustedDir);
        var destPath = Path.Combine(trustedDir, Path.GetFileName(cert.FilePath));
        File.Move(cert.FilePath, destPath, overwrite: true);
        _logger.LogInformation("Trusted cert {Thumbprint} (subject={Subject}) — moved {From} → {To}",
            cert.Thumbprint, cert.Subject, cert.FilePath, destPath);
        return true;
    }

    public bool DeleteRejected(string thumbprint) => DeleteFromStore(CertStoreKind.Rejected, thumbprint);
    public bool UntrustCert(string thumbprint) => DeleteFromStore(CertStoreKind.Trusted, thumbprint);

    private bool DeleteFromStore(CertStoreKind store, string thumbprint)
    {
        var cert = FindInStore(store, thumbprint);
        if (cert is null) return false;
        File.Delete(cert.FilePath);
        _logger.LogInformation("Deleted cert {Thumbprint} (subject={Subject}) from {Store} store",
            cert.Thumbprint, cert.Subject, store);
        return true;
    }

    private CertInfo? FindInStore(CertStoreKind store, string thumbprint) =>
        ListStore(store).FirstOrDefault(c =>
            string.Equals(c.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase));

    private IReadOnlyList<CertInfo> ListStore(CertStoreKind store)
    {
        var dir = CertsDir(store);
        if (!Directory.Exists(dir)) return [];

        var results = new List<CertInfo>();
        foreach (var path in Directory.EnumerateFiles(dir))
        {
            // Skip CRL sidecars + private-key files — trust operations only concern public certs.
            var ext = Path.GetExtension(path);
            if (!ext.Equals(".der", StringComparison.OrdinalIgnoreCase) &&
                !ext.Equals(".crt", StringComparison.OrdinalIgnoreCase) &&
                !ext.Equals(".cer", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            try
            {
                var cert = X509CertificateLoader.LoadCertificateFromFile(path);
                results.Add(new CertInfo(
                    cert.Thumbprint, cert.Subject, cert.Issuer,
                    cert.NotBefore.ToUniversalTime(), cert.NotAfter.ToUniversalTime(),
                    path, store));
            }
            catch (Exception ex)
            {
                // A malformed file in the store shouldn't take down the page. Surface it in logs
                // but skip — operators see the other certs and can clean the bad file manually.
                _logger.LogWarning(ex, "Failed to parse cert at {Path} — skipping", path);
            }
        }
        return results;
    }

    private string CertsDir(CertStoreKind store) =>
        Path.Combine(_options.PkiStoreRoot, store == CertStoreKind.Rejected ? "rejected" : "trusted", "certs");
}
