using System.Security.Cryptography.X509Certificates;
using Akka.Actor;
using Akka.Event;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;

/// <summary>
/// Per-node OPC UA certificate-store actor. Runs on EVERY site
/// node (NOT a singleton) at a well-known name so the Deployment Manager
/// singleton can address it on each node via <see cref="ActorSelection"/>.
///
/// Cert trust is site-local: the trusted-peer PKI store is a directory on each
/// node's file system. A trust/remove decision must therefore reach BOTH site
/// nodes or the two stores diverge across failover. The singleton fans the
/// per-node <see cref="WriteCertToLocalStore"/> / <see cref="RemoveCertFromLocalStore"/>
/// message out; this actor performs the actual file write/delete/enumerate.
///
/// NOTE on the script-trust forbidden-IO rule: that rule applies only to USER
/// scripts. This is a framework/DCL actor doing legitimate PKI store file I/O,
/// exactly as the underlying OPC UA stack does when it persists rejected certs.
/// </summary>
public class CertStoreActor : ReceiveActor
{
    /// <summary>
    /// Well-known actor name. The Deployment Manager singleton addresses each
    /// site node's instance via <c>{member.Address}/user/{WellKnownName}</c>.
    /// </summary>
    public const string WellKnownName = "cert-store";

    private readonly ILoggingAdapter _log = Context.GetLogger();
    private readonly string _trustedStoreDir;
    private readonly string _rejectedStoreDir;

    /// <summary>
    /// Initializes the actor with the deployment-wide OPC UA options used to
    /// resolve the trusted-peer and rejected store directories.
    /// </summary>
    /// <param name="opcUaGlobalOptions">Deployment-wide OPC UA options (store paths).</param>
    public CertStoreActor(OpcUaGlobalOptions opcUaGlobalOptions)
    {
        // Resolve store directories with the SAME fallback logic
        // RealOpcUaClient.ResolveStorePath uses, so a cert trusted here lands in
        // the exact store the OPC UA client validates against. That helper is a
        // private static method on RealOpcUaClient; the tiny logic is replicated
        // here rather than widening its visibility.
        _trustedStoreDir = ResolveStorePath(opcUaGlobalOptions.TrustedPeerStorePath, "trusted");
        _rejectedStoreDir = ResolveStorePath(opcUaGlobalOptions.RejectedCertificateStorePath, "rejected");

        Receive<WriteCertToLocalStore>(HandleWrite);
        Receive<RemoveCertFromLocalStore>(HandleRemove);
        Receive<ListLocalCerts>(_ => HandleList());
        Receive<ExportLocalTrustedCerts>(_ => HandleExport());
    }

    /// <summary>
    /// Replicates <c>RealOpcUaClient.ResolveStorePath</c>: an empty configured
    /// path falls back to <c>%TEMP%/ScadaBridge/pki/&lt;leaf&gt;</c>.
    /// </summary>
    private static string ResolveStorePath(string configured, string fallbackLeaf) =>
        string.IsNullOrWhiteSpace(configured)
            ? Path.Combine(Path.GetTempPath(), "ScadaBridge", "pki", fallbackLeaf)
            : configured;

    private void HandleWrite(WriteCertToLocalStore msg)
    {
        if (!IsSafeThumbprint(msg.Thumbprint))
        {
            _log.Warning("Rejecting write for invalid thumbprint {Thumbprint}", msg.Thumbprint);
            Sender.Tell(new LocalCertOpAck(false, "invalid thumbprint", null));
            return;
        }

        try
        {
            Directory.CreateDirectory(_trustedStoreDir);
            var der = Convert.FromBase64String(msg.DerBase64);
            var path = Path.Combine(_trustedStoreDir, FileNameFor(msg.Thumbprint));
            File.WriteAllBytes(path, der);
            _log.Info("Trusted server certificate {Thumbprint} written to {Path}", msg.Thumbprint, path);
            Sender.Tell(new LocalCertOpAck(true, null, null));
        }
        catch (Exception ex)
        {
            _log.Warning(ex, "Failed to write trusted server certificate {Thumbprint}", msg.Thumbprint);
            Sender.Tell(new LocalCertOpAck(false, ex.Message, null));
        }
    }

    private void HandleRemove(RemoveCertFromLocalStore msg)
    {
        if (!IsSafeThumbprint(msg.Thumbprint))
        {
            _log.Warning("Rejecting remove for invalid thumbprint {Thumbprint}", msg.Thumbprint);
            Sender.Tell(new LocalCertOpAck(false, "invalid thumbprint", null));
            return;
        }

        try
        {
            var path = Path.Combine(_trustedStoreDir, FileNameFor(msg.Thumbprint));
            if (File.Exists(path))
            {
                File.Delete(path);
                _log.Info("Trusted server certificate {Thumbprint} removed from {Path}", msg.Thumbprint, path);
            }
            else
            {
                _log.Info("Trusted server certificate {Thumbprint} not present; remove is a no-op", msg.Thumbprint);
            }
            Sender.Tell(new LocalCertOpAck(true, null, null));
        }
        catch (Exception ex)
        {
            _log.Warning(ex, "Failed to remove trusted server certificate {Thumbprint}", msg.Thumbprint);
            Sender.Tell(new LocalCertOpAck(false, ex.Message, null));
        }
    }

    private void HandleList()
    {
        try
        {
            var certs = new List<TrustedCertInfo>();
            certs.AddRange(EnumerateStore(_trustedStoreDir, rejected: false));
            certs.AddRange(EnumerateStore(_rejectedStoreDir, rejected: true));
            Sender.Tell(new LocalCertOpAck(true, null, certs));
        }
        catch (Exception ex)
        {
            _log.Warning(ex, "Failed to list certificates from PKI stores");
            Sender.Tell(new LocalCertOpAck(false, ex.Message, null));
        }
    }

    /// <summary>
    /// Exports the trusted-peer store (thumbprint + raw DER) so the Deployment
    /// Manager singleton can reconcile the trusted set onto a (re)joining node.
    /// Trusted store only — rejected certs are never pushed to peers.
    /// </summary>
    private void HandleExport()
    {
        try
        {
            var certs = new List<ExportedCert>();
            if (Directory.Exists(_trustedStoreDir))
            {
                foreach (var file in Directory.EnumerateFiles(_trustedStoreDir)
                             .Where(f => f.EndsWith(".der", StringComparison.OrdinalIgnoreCase)
                                         || f.EndsWith(".crt", StringComparison.OrdinalIgnoreCase)))
                {
                    try
                    {
                        var der = File.ReadAllBytes(file);
                        // Load only to derive the canonical thumbprint (the store
                        // filename); the DER bytes shipped are the raw file bytes.
                        using var cert = X509CertificateLoader.LoadCertificate(der);
                        certs.Add(new ExportedCert(cert.Thumbprint, der));
                    }
                    catch (Exception ex)
                    {
                        // A malformed file must not abort the whole export.
                        _log.Warning(ex, "Skipping unreadable certificate file {File} during export", file);
                    }
                }
            }

            Sender.Tell(new LocalCertExport(true, null, certs));
        }
        catch (Exception ex)
        {
            _log.Warning(ex, "Failed to export trusted certificates from PKI store");
            Sender.Tell(new LocalCertExport(false, ex.Message, Array.Empty<ExportedCert>()));
        }
    }

    private IEnumerable<TrustedCertInfo> EnumerateStore(string storeDir, bool rejected)
    {
        if (!Directory.Exists(storeDir))
        {
            yield break;
        }

        foreach (var file in Directory.EnumerateFiles(storeDir)
                     .Where(f => f.EndsWith(".der", StringComparison.OrdinalIgnoreCase)
                                 || f.EndsWith(".crt", StringComparison.OrdinalIgnoreCase)))
        {
            TrustedCertInfo? info = null;
            try
            {
                // The TrustedCertInfo projection copies all needed strings/dates
                // out before this scope ends, so disposing the cert is safe and
                // releases the native handle held per loaded certificate.
                using var cert = X509CertificateLoader.LoadCertificate(File.ReadAllBytes(file));
                info = new TrustedCertInfo(
                    cert.Thumbprint,
                    cert.Subject,
                    cert.Issuer,
                    cert.NotBefore.ToUniversalTime(),
                    cert.NotAfter.ToUniversalTime(),
                    rejected);
            }
            catch (Exception ex)
            {
                // A malformed file in the store should not abort the whole listing.
                _log.Warning(ex, "Skipping unreadable certificate file {File}", file);
            }

            if (info is not null)
            {
                yield return info;
            }
        }
    }

    private static string FileNameFor(string thumbprint) => $"{thumbprint}.der";

    /// <summary>
    /// Defense-in-depth path-traversal guard. In production a thumbprint is a
    /// hex SHA1 (always safe), but <c>RemoveServerCertCommand</c> is CLI-exposed
    /// and this actor accepts the string unchecked, so a thumbprint such as
    /// <c>../../etc/foo</c> would otherwise resolve OUTSIDE the trusted-store
    /// directory once combined into a <c>.der</c> file name. Rejects empty,
    /// separator-bearing, or dot-dot thumbprints before any filesystem touch.
    /// </summary>
    private static bool IsSafeThumbprint(string thumbprint) =>
        !string.IsNullOrWhiteSpace(thumbprint)
        && thumbprint.IndexOfAny(Path.GetInvalidFileNameChars()) < 0
        && !thumbprint.Contains("..", StringComparison.Ordinal);
}
