using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Formatting.Compact;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy;
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
using ZB.MOM.WW.OtOpcUa.Driver.S7;
using ZB.MOM.WW.OtOpcUa.Server;
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
using ZB.MOM.WW.OtOpcUa.Server.Phase7;
using ZB.MOM.WW.OtOpcUa.Server.Security;

var builder = Host.CreateApplicationBuilder(args);

// Per Phase 6.1 Stream C.3: SIEMs (Splunk, Datadog) ingest the JSON file without a
// regex parser. Plain-text rolling file stays on by default for human readability;
// JSON file is opt-in via appsetting `Serilog:WriteJson = true`.
var writeJson = builder.Configuration.GetValue<bool>("Serilog:WriteJson");
var loggerBuilder = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.File("logs/otopcua-.log", rollingInterval: RollingInterval.Day);

if (writeJson)
{
    loggerBuilder = loggerBuilder.WriteTo.File(
        new CompactJsonFormatter(),
        "logs/otopcua-.json.log",
        rollingInterval: RollingInterval.Day);
}

Log.Logger = loggerBuilder.CreateLogger();

builder.Services.AddSerilog();
builder.Services.AddWindowsService(o => o.ServiceName = "OtOpcUa");

var nodeSection = builder.Configuration.GetSection(NodeOptions.SectionName);
var options = new NodeOptions
{
    NodeId = nodeSection.GetValue<string>("NodeId")
             ?? throw new InvalidOperationException("Node:NodeId not configured"),
    ClusterId = nodeSection.GetValue<string>("ClusterId")
             ?? throw new InvalidOperationException("Node:ClusterId not configured"),
    ConfigDbConnectionString = nodeSection.GetValue<string>("ConfigDbConnectionString")
             ?? throw new InvalidOperationException("Node:ConfigDbConnectionString not configured"),
    LocalCachePath = nodeSection.GetValue<string>("LocalCachePath") ?? "config_cache.db",
};

var opcUaSection = builder.Configuration.GetSection(OpcUaServerOptions.SectionName);
var ldapSection = opcUaSection.GetSection("Ldap");
var ldapOptions = new LdapOptions
{
    Enabled = ldapSection.GetValue<bool?>("Enabled") ?? false,
    Server = ldapSection.GetValue<string>("Server") ?? "localhost",
    Port = ldapSection.GetValue<int?>("Port") ?? 3893,
    UseTls = ldapSection.GetValue<bool?>("UseTls") ?? false,
    AllowInsecureLdap = ldapSection.GetValue<bool?>("AllowInsecureLdap") ?? true,
    SearchBase = ldapSection.GetValue<string>("SearchBase") ?? "dc=lmxopcua,dc=local",
    ServiceAccountDn = ldapSection.GetValue<string>("ServiceAccountDn") ?? string.Empty,
    ServiceAccountPassword = ldapSection.GetValue<string>("ServiceAccountPassword") ?? string.Empty,
    GroupToRole = ldapSection.GetSection("GroupToRole").Get<Dictionary<string, string>>() ?? new(StringComparer.OrdinalIgnoreCase),
};

var opcUaOptions = new OpcUaServerOptions
{
    EndpointUrl = opcUaSection.GetValue<string>("EndpointUrl") ?? "opc.tcp://0.0.0.0:4840/OtOpcUa",
    ApplicationName = opcUaSection.GetValue<string>("ApplicationName") ?? "OtOpcUa Server",
    ApplicationUri = opcUaSection.GetValue<string>("ApplicationUri") ?? "urn:OtOpcUa:Server",
    PkiStoreRoot = opcUaSection.GetValue<string>("PkiStoreRoot")
                   ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "OtOpcUa", "pki"),
    AutoAcceptUntrustedClientCertificates = opcUaSection.GetValue<bool?>("AutoAcceptUntrustedClientCertificates") ?? true,
    SecurityProfile = Enum.TryParse<OpcUaSecurityProfile>(opcUaSection.GetValue<string>("SecurityProfile"), true, out var p)
        ? p : OpcUaSecurityProfile.None,
    Ldap = ldapOptions,
    AnonymousRoles = opcUaSection.GetSection("AnonymousRoles").Get<string[]>() ?? [],
};

builder.Services.AddSingleton(options);
builder.Services.AddSingleton(opcUaOptions);
builder.Services.AddSingleton(ldapOptions);
builder.Services.AddSingleton<IUserAuthenticator>(sp => ldapOptions.Enabled
    ? new LdapUserAuthenticator(ldapOptions, sp.GetRequiredService<ILogger<LdapUserAuthenticator>>())
    : new DenyAllUserAuthenticator());
builder.Services.AddSingleton<ILocalConfigCache>(_ => new LiteDbConfigCache(options.LocalCachePath));
builder.Services.AddSingleton<DriverHost>();
builder.Services.AddSingleton<NodeBootstrap>();

// Task #248 — driver-instance bootstrap pipeline. DriverFactoryRegistry is the
// type-name → factory map; each driver project's static Register call pre-loads
// its factory so the bootstrapper can materialise DriverInstance rows from the
// central DB into live IDriver instances.
builder.Services.AddSingleton<DriverFactoryRegistry>(_ =>
{
    var registry = new DriverFactoryRegistry();
    GalaxyProxyDriverFactoryExtensions.Register(registry);
    FocasDriverFactoryExtensions.Register(registry);
    ModbusDriverFactoryExtensions.Register(registry);
    AbCipDriverFactoryExtensions.Register(registry);
    AbLegacyDriverFactoryExtensions.Register(registry);
    S7DriverFactoryExtensions.Register(registry);
    return registry;
});
builder.Services.AddSingleton<DriverInstanceBootstrapper>();

// ADR-001 Option A wiring — the registry is the handoff between OpcUaServerService's
// bootstrap-time population pass + OpcUaApplicationHost's StartAsync walker invocation.
// DriverEquipmentContentRegistry.Get is the equipmentContentLookup delegate that PR #155
// added to OpcUaApplicationHost's ctor seam.
builder.Services.AddSingleton<DriverEquipmentContentRegistry>();
builder.Services.AddScoped<EquipmentNamespaceContentLoader>();

builder.Services.AddSingleton<OpcUaApplicationHost>(sp =>
{
    var registry = sp.GetRequiredService<DriverEquipmentContentRegistry>();
    return new OpcUaApplicationHost(
        sp.GetRequiredService<OpcUaServerOptions>(),
        sp.GetRequiredService<DriverHost>(),
        sp.GetRequiredService<IUserAuthenticator>(),
        sp.GetRequiredService<ILoggerFactory>(),
        sp.GetRequiredService<ILogger<OpcUaApplicationHost>>(),
        equipmentContentLookup: registry.Get);
});
builder.Services.AddHostedService<OpcUaServerService>();

// Central-config DB access for the host-status publisher (LMX follow-up #7). Scoped context
// so per-heartbeat change-tracking stays isolated; publisher opens one scope per tick.
builder.Services.AddDbContext<OtOpcUaConfigDbContext>(opt =>
    opt.UseSqlServer(options.ConfigDbConnectionString));
builder.Services.AddHostedService<HostStatusPublisher>();

// Phase 7 follow-up #246 — historian sink + engine composer. NullAlarmHistorianSink
// is the default until the Galaxy.Host SqliteStoreAndForwardSink writer adapter
// lands (task #248). The composer reads Script/VirtualTag/ScriptedAlarm rows on
// generation bootstrap, builds the engines, and starts the driver-bridge feed.
builder.Services.AddSingleton<IAlarmHistorianSink>(NullAlarmHistorianSink.Instance);
builder.Services.AddSingleton(Log.Logger);  // Serilog root for ScriptLoggerFactory
builder.Services.AddSingleton<Phase7Composer>();

var host = builder.Build();
await host.RunAsync();
