using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Server;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
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.Security;

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

/// <summary>
///     <see cref="StandardServer"/> subclass that wires one <see cref="DriverNodeManager"/> per
///     registered driver from <see cref="DriverHost"/>. Anonymous endpoint on
///     <c>opc.tcp://0.0.0.0:4840</c>, no security — PR 16 minimum-viable scope; LDAP + security
///     profiles are deferred to their own PR on top of this.
/// </summary>
public sealed class OtOpcUaServer : StandardServer
{
    private readonly DriverHost _driverHost;
    private readonly IUserAuthenticator _authenticator;
    private readonly DriverResiliencePipelineBuilder _pipelineBuilder;
    private readonly AuthorizationGate? _authzGate;
    private readonly NodeScopeResolver? _scopeResolver;
    private readonly Func<string, DriverTier>? _tierLookup;
    private readonly Func<string, string?>? _resilienceConfigLookup;

    // Phase 7 Stream G follow-up wiring (task #239). Shared across every DriverNodeManager
    // instantiated by this server so virtual-tag reads and scripted-alarm reads from any
    // driver's address-space subtree route to the same engine. When null (no Phase 7
    // engines composed for this deployment) DriverNodeManager falls back to driver-only
    // dispatch — identical to pre-Phase-7 behaviour.
    private readonly IReadable? _virtualReadable;
    private readonly IReadable? _scriptedAlarmReadable;

    /// <summary>
    ///     Roles granted to anonymous sessions. When non-empty, <see cref="OnImpersonateUser"/>
    ///     wraps <c>AnonymousIdentityToken</c> in a <see cref="RoleBasedIdentity"/> carrying
    ///     these roles so <see cref="DriverNodeManager"/>'s write-authz check passes for
    ///     matching classifications. Empty (the default) preserves the pre-existing behaviour
    ///     of rejecting anonymous writes at <c>Operate</c> or higher.
    /// </summary>
    private readonly IReadOnlyList<string> _anonymousRoles;

    private readonly ILoggerFactory _loggerFactory;
    private readonly List<DriverNodeManager> _driverNodeManagers = new();

    public OtOpcUaServer(
        DriverHost driverHost,
        IUserAuthenticator authenticator,
        DriverResiliencePipelineBuilder pipelineBuilder,
        ILoggerFactory loggerFactory,
        AuthorizationGate? authzGate = null,
        NodeScopeResolver? scopeResolver = null,
        Func<string, DriverTier>? tierLookup = null,
        Func<string, string?>? resilienceConfigLookup = null,
        IReadable? virtualReadable = null,
        IReadable? scriptedAlarmReadable = null,
        IReadOnlyList<string>? anonymousRoles = null)
    {
        _driverHost = driverHost;
        _authenticator = authenticator;
        _pipelineBuilder = pipelineBuilder;
        _authzGate = authzGate;
        _scopeResolver = scopeResolver;
        _tierLookup = tierLookup;
        _resilienceConfigLookup = resilienceConfigLookup;
        _virtualReadable = virtualReadable;
        _scriptedAlarmReadable = scriptedAlarmReadable;
        _anonymousRoles = anonymousRoles ?? [];
        _loggerFactory = loggerFactory;
    }

    /// <summary>
    ///     Read-only snapshot of the driver node managers materialized at server start. Used by
    ///     the generic-driver-node-manager-driven discovery flow after the server starts — the
    ///     host walks each entry and invokes
    ///     <c>GenericDriverNodeManager.BuildAddressSpaceAsync(manager)</c> passing the manager
    ///     as its own <see cref="IAddressSpaceBuilder"/>.
    /// </summary>
    public IReadOnlyList<DriverNodeManager> DriverNodeManagers => _driverNodeManagers;

    protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration)
    {
        foreach (var driverId in _driverHost.RegisteredDriverIds)
        {
            var driver = _driverHost.GetDriver(driverId);
            if (driver is null) continue;

            var logger = _loggerFactory.CreateLogger<DriverNodeManager>();
            // Per-driver resilience options: tier comes from lookup (Phase 6.1 Stream B.1
            // DriverTypeRegistry in the prod wire-up) or falls back to Tier A. ResilienceConfig
            // JSON comes from the DriverInstance row via the optional lookup Func; parser
            // layers JSON overrides on top of tier defaults (Phase 6.1 Stream A.2).
            var tier = _tierLookup?.Invoke(driver.DriverType) ?? DriverTier.A;
            var resilienceJson = _resilienceConfigLookup?.Invoke(driver.DriverInstanceId);
            var options = DriverResilienceOptionsParser.ParseOrDefaults(tier, resilienceJson, out var diag);
            if (diag is not null)
                logger.LogWarning("ResilienceConfig parse diagnostic for driver {DriverId}: {Diag}", driver.DriverInstanceId, diag);

            var invoker = new CapabilityInvoker(_pipelineBuilder, driver.DriverInstanceId, () => options, driver.DriverType);
            var manager = new DriverNodeManager(server, configuration, driver, invoker, logger,
                authzGate: _authzGate, scopeResolver: _scopeResolver,
                virtualReadable: _virtualReadable, scriptedAlarmReadable: _scriptedAlarmReadable);
            _driverNodeManagers.Add(manager);
        }

        return new MasterNodeManager(server, configuration, null, _driverNodeManagers.ToArray());
    }

    protected override void OnServerStarted(IServerInternal server)
    {
        base.OnServerStarted(server);
        // Hook UserName / Anonymous token validation here. Anonymous passes through; UserName
        // is validated against the IUserAuthenticator (LDAP in production). Rejected identities
        // throw ServiceResultException which the stack translates to Bad_IdentityTokenInvalid.
        server.SessionManager.ImpersonateUser += OnImpersonateUser;
    }

    private void OnImpersonateUser(Session session, ImpersonateEventArgs args)
    {
        switch (args.NewIdentity)
        {
            case AnonymousIdentityToken:
                args.Identity = _anonymousRoles.Count == 0
                    ? new UserIdentity() // anonymous, no roles — production default
                    : new RoleBasedIdentity("(anonymous)", "Anonymous", _anonymousRoles);
                return;

            case UserNameIdentityToken user:
            {
                var result = _authenticator.AuthenticateAsync(
                    user.UserName, user.DecryptedPassword, CancellationToken.None)
                    .GetAwaiter().GetResult();
                if (!result.Success)
                {
                    throw ServiceResultException.Create(
                        StatusCodes.BadUserAccessDenied,
                        "Invalid username or password ({0})", result.Error ?? "no detail");
                }
                args.Identity = new RoleBasedIdentity(user.UserName, result.DisplayName, result.Roles);
                return;
            }

            default:
                throw ServiceResultException.Create(
                    StatusCodes.BadIdentityTokenInvalid,
                    "Unsupported user identity token type: {0}", args.NewIdentity?.GetType().Name ?? "null");
        }
    }

    /// <summary>
    ///     Tiny UserIdentity carrier that preserves the resolved roles so downstream node
    ///     managers can gate writes by role via <c>session.Identity</c>. Anonymous identity still
    ///     uses the stack's default.
    /// </summary>
    private sealed class RoleBasedIdentity : UserIdentity, IRoleBearer
    {
        public IReadOnlyList<string> Roles { get; }
        public string? Display { get; }

        public RoleBasedIdentity(string userName, string? displayName, IReadOnlyList<string> roles)
            : base(userName, "")
        {
            Display = displayName;
            Roles = roles;
        }
    }

    protected override ServerProperties LoadServerProperties() => new()
    {
        ManufacturerName = "OtOpcUa",
        ProductName = "OtOpcUa.Server",
        ProductUri = "urn:OtOpcUa:Server",
        SoftwareVersion = "2.0.0",
        BuildNumber = "0",
        BuildDate = DateTime.UtcNow,
    };
}
