using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
using ZB.MOM.WW.OtOpcUa.Server.Observability;
using ZB.MOM.WW.OtOpcUa.Server.Security;

namespace ZB.MOM.WW.OtOpcUa.Server.OpcUa;

/// <summary>
///     Wraps <see cref="ApplicationInstance"/> to bring the OPC UA server online — builds an
///     <see cref="ApplicationConfiguration"/> programmatically (no external XML file), ensures
///     the application certificate exists in the PKI store (auto-generates self-signed on first
///     run), starts the server, then walks each <see cref="DriverNodeManager"/> and invokes
///     <see cref="GenericDriverNodeManager.BuildAddressSpaceAsync"/> against it so the driver's
///     discovery streams into the already-running server's address space.
/// </summary>
public sealed class OpcUaApplicationHost : IAsyncDisposable
{
    private readonly OpcUaServerOptions _options;
    private readonly DriverHost _driverHost;
    private readonly IUserAuthenticator _authenticator;
    private readonly DriverResiliencePipelineBuilder _pipelineBuilder;
    private readonly AuthorizationGate? _authzGate;
    private readonly NodeScopeResolver? _scopeResolver;
    private readonly StaleConfigFlag? _staleConfigFlag;
    private readonly Func<string, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverTier>? _tierLookup;
    private readonly Func<string, string?>? _resilienceConfigLookup;
    private readonly Func<string, ZB.MOM.WW.OtOpcUa.Core.OpcUa.EquipmentNamespaceContent?>? _equipmentContentLookup;

    // Phase 7 Stream G follow-up (task #239). When composed with the VirtualTagEngine +
    // ScriptedAlarmEngine sources these route node reads to the engines instead of the
    // driver. Null = Phase 7 engines not enabled for this deployment (identical to pre-
    // Phase-7 behaviour). Late-bindable via SetPhase7Sources because the engines need
    // the bootstrapped generation id before they can compose, which is only known after
    // the host has been DI-constructed (task #246).
    private ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? _virtualReadable;
    private ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? _scriptedAlarmReadable;

    private readonly ILoggerFactory _loggerFactory;
    private readonly ILogger<OpcUaApplicationHost> _logger;
    private ApplicationInstance? _application;
    private OtOpcUaServer? _server;
    private HealthEndpointsHost? _healthHost;
    private bool _disposed;

    public OpcUaApplicationHost(OpcUaServerOptions options, DriverHost driverHost,
        IUserAuthenticator authenticator, ILoggerFactory loggerFactory, ILogger<OpcUaApplicationHost> logger,
        DriverResiliencePipelineBuilder? pipelineBuilder = null,
        AuthorizationGate? authzGate = null,
        NodeScopeResolver? scopeResolver = null,
        StaleConfigFlag? staleConfigFlag = null,
        Func<string, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverTier>? tierLookup = null,
        Func<string, string?>? resilienceConfigLookup = null,
        Func<string, ZB.MOM.WW.OtOpcUa.Core.OpcUa.EquipmentNamespaceContent?>? equipmentContentLookup = null,
        ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? virtualReadable = null,
        ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? scriptedAlarmReadable = null)
    {
        _options = options;
        _driverHost = driverHost;
        _authenticator = authenticator;
        _pipelineBuilder = pipelineBuilder ?? new DriverResiliencePipelineBuilder();
        _authzGate = authzGate;
        _scopeResolver = scopeResolver;
        _staleConfigFlag = staleConfigFlag;
        _tierLookup = tierLookup;
        _resilienceConfigLookup = resilienceConfigLookup;
        _equipmentContentLookup = equipmentContentLookup;
        _virtualReadable = virtualReadable;
        _scriptedAlarmReadable = scriptedAlarmReadable;
        _loggerFactory = loggerFactory;
        _logger = logger;
    }

    public OtOpcUaServer? Server => _server;

    /// <summary>
    ///     Late-bind the Phase 7 engine-backed <c>IReadable</c> sources. Must be
    ///     called BEFORE <see cref="StartAsync"/> — once the OPC UA server starts, the
    ///     <see cref="OtOpcUaServer"/> ctor captures the field values + per-node
    ///     <see cref="DriverNodeManager"/>s are constructed. Calling this after start has
    ///     no effect on already-materialized node managers.
    /// </summary>
    public void SetPhase7Sources(
        ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? virtualReadable,
        ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable? scriptedAlarmReadable)
    {
        if (_server is not null)
            throw new InvalidOperationException(
                "Phase 7 sources must be set before OpcUaApplicationHost.StartAsync; the OtOpcUaServer + DriverNodeManagers have already captured the previous values.");
        _virtualReadable = virtualReadable;
        _scriptedAlarmReadable = scriptedAlarmReadable;
    }

    /// <summary>
    ///     Builds the <see cref="ApplicationConfiguration"/>, validates/creates the application
    ///     certificate, constructs + starts the <see cref="OtOpcUaServer"/>, then drives
    ///     <see cref="GenericDriverNodeManager.BuildAddressSpaceAsync"/> per registered driver so
    ///     the address space is populated before the first client connects.
    /// </summary>
    public async Task StartAsync(CancellationToken ct)
    {
        _application = new ApplicationInstance
        {
            ApplicationName = _options.ApplicationName,
            ApplicationType = ApplicationType.Server,
            ApplicationConfiguration = BuildConfiguration(),
        };

        var hasCert = await _application.CheckApplicationInstanceCertificate(silent: true, minimumKeySize: CertificateFactory.DefaultKeySize).ConfigureAwait(false);
        if (!hasCert)
            throw new InvalidOperationException(
                $"OPC UA application certificate could not be validated or created in {_options.PkiStoreRoot}");

        _server = new OtOpcUaServer(_driverHost, _authenticator, _pipelineBuilder, _loggerFactory,
            authzGate: _authzGate, scopeResolver: _scopeResolver,
            tierLookup: _tierLookup, resilienceConfigLookup: _resilienceConfigLookup,
            virtualReadable: _virtualReadable, scriptedAlarmReadable: _scriptedAlarmReadable,
            anonymousRoles: _options.AnonymousRoles);
        await _application.Start(_server).ConfigureAwait(false);

        _logger.LogInformation("OPC UA server started — endpoint={Endpoint} driverCount={Count}",
            _options.EndpointUrl, _server.DriverNodeManagers.Count);

        // Phase 6.1 Stream C: health endpoints on :4841 (loopback by default — see
        // HealthEndpointsHost remarks for the Windows URL-ACL tradeoff).
        if (_options.HealthEndpointsEnabled)
        {
            _healthHost = new HealthEndpointsHost(
                _driverHost,
                _loggerFactory.CreateLogger<HealthEndpointsHost>(),
                usingStaleConfig: _staleConfigFlag is null ? null : () => _staleConfigFlag.IsStale,
                prefix: _options.HealthEndpointsPrefix);
            _healthHost.Start();
        }

        // Drive each driver's discovery through its node manager. The node manager IS the
        // IAddressSpaceBuilder; GenericDriverNodeManager captures alarm-condition sinks into
        // its internal map and wires OnAlarmEvent → sink routing.
        //
        // ADR-001 Option A — when an EquipmentNamespaceContent is supplied for an
        // Equipment-kind driver, run the EquipmentNodeWalker BEFORE the driver's DiscoverAsync
        // so the UNS folder skeleton (Area/Line/Equipment) + Identification sub-folders +
        // the five identifier properties (decision #121) are in place. DiscoverAsync then
        // streams the driver's native shape on top; Tag rows bound to Equipment already
        // materialized via the walker don't get duplicated because the driver's DiscoverAsync
        // output is authoritative for its own native references only.
        foreach (var nodeManager in _server.DriverNodeManagers)
        {
            var driverId = nodeManager.Driver.DriverInstanceId;
            try
            {
                if (_equipmentContentLookup is not null)
                {
                    var content = _equipmentContentLookup(driverId);
                    if (content is not null)
                    {
                        ZB.MOM.WW.OtOpcUa.Core.OpcUa.EquipmentNodeWalker.Walk(nodeManager, content);
                        _logger.LogInformation(
                            "UNS walker populated {Areas} area(s), {Lines} line(s), {Equipment} equipment, {Tags} tag(s) for driver {Driver}",
                            content.Areas.Count, content.Lines.Count, content.Equipment.Count, content.Tags.Count, driverId);
                    }
                }

                var generic = new GenericDriverNodeManager(nodeManager.Driver);
                await generic.BuildAddressSpaceAsync(nodeManager, ct).ConfigureAwait(false);
                _logger.LogInformation("Address space populated for driver {Driver}", driverId);
            }
            catch (Exception ex)
            {
                // Per decision #12: driver exceptions isolate — log and keep the server serving
                // the other drivers' subtrees. Re-building this one takes a Reinitialize call.
                _logger.LogError(ex, "Discovery failed for driver {Driver}; subtree faulted", driverId);
            }
        }
    }

    private ApplicationConfiguration BuildConfiguration()
    {
        Directory.CreateDirectory(_options.PkiStoreRoot);

        var cfg = new ApplicationConfiguration
        {
            ApplicationName = _options.ApplicationName,
            ApplicationUri = _options.ApplicationUri,
            ApplicationType = ApplicationType.Server,
            ProductUri = "urn:OtOpcUa:Server",

            SecurityConfiguration = new SecurityConfiguration
            {
                ApplicationCertificate = new CertificateIdentifier
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = Path.Combine(_options.PkiStoreRoot, "own"),
                    SubjectName = "CN=" + _options.ApplicationName,
                },
                TrustedIssuerCertificates = new CertificateTrustList
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = Path.Combine(_options.PkiStoreRoot, "issuers"),
                },
                TrustedPeerCertificates = new CertificateTrustList
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = Path.Combine(_options.PkiStoreRoot, "trusted"),
                },
                RejectedCertificateStore = new CertificateTrustList
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = Path.Combine(_options.PkiStoreRoot, "rejected"),
                },
                AutoAcceptUntrustedCertificates = _options.AutoAcceptUntrustedClientCertificates,
                AddAppCertToTrustedStore = true,
            },

            TransportConfigurations = new TransportConfigurationCollection(),
            TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },

            ServerConfiguration = new ServerConfiguration
            {
                BaseAddresses = new StringCollection { _options.EndpointUrl },
                SecurityPolicies = BuildSecurityPolicies(),
                UserTokenPolicies = BuildUserTokenPolicies(),
                MinRequestThreadCount = 5,
                MaxRequestThreadCount = 100,
                MaxQueuedRequestCount = 200,
            },

            TraceConfiguration = new TraceConfiguration(),
        };

        cfg.Validate(ApplicationType.Server).GetAwaiter().GetResult();

        if (cfg.SecurityConfiguration.AutoAcceptUntrustedCertificates)
        {
            cfg.CertificateValidator.CertificateValidation += (_, e) =>
            {
                if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted)
                    e.Accept = true;
            };
        }

        return cfg;
    }

    private ServerSecurityPolicyCollection BuildSecurityPolicies()
    {
        var policies = new ServerSecurityPolicyCollection
        {
            // Keep the None policy present so legacy clients can discover + browse. Locked-down
            // deployments remove this by setting Ldap.Enabled=true + dropping None here; left in
            // for PR 19 so the PR 17 test harness continues to pass unchanged.
            new ServerSecurityPolicy
            {
                SecurityMode = MessageSecurityMode.None,
                SecurityPolicyUri = SecurityPolicies.None,
            },
        };

        if (_options.SecurityProfile == OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt)
        {
            policies.Add(new ServerSecurityPolicy
            {
                SecurityMode = MessageSecurityMode.SignAndEncrypt,
                SecurityPolicyUri = SecurityPolicies.Basic256Sha256,
            });
        }

        return policies;
    }

    private UserTokenPolicyCollection BuildUserTokenPolicies()
    {
        var tokens = new UserTokenPolicyCollection
        {
            new UserTokenPolicy(UserTokenType.Anonymous)
            {
                PolicyId = "Anonymous",
                SecurityPolicyUri = SecurityPolicies.None,
            },
        };

        if (_options.SecurityProfile == OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt
            && _options.Ldap.Enabled)
        {
            tokens.Add(new UserTokenPolicy(UserTokenType.UserName)
            {
                PolicyId = "UserName",
                // Passwords must ride an encrypted channel — scope this token to Basic256Sha256
                // so the stack rejects any attempt to send UserName over the None endpoint.
                SecurityPolicyUri = SecurityPolicies.Basic256Sha256,
            });
        }

        return tokens;
    }

    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        _disposed = true;
        try
        {
            _server?.Stop();
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "OPC UA server stop threw during dispose");
        }

        if (_healthHost is not null)
        {
            try { await _healthHost.DisposeAsync().ConfigureAwait(false); }
            catch (Exception ex) { _logger.LogWarning(ex, "Health endpoints host dispose threw"); }
        }
        await Task.CompletedTask;
    }
}
