using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
using ZB.MOM.WW.OtOpcUa.Server.Phase7;

namespace ZB.MOM.WW.OtOpcUa.Server;

/// <summary>
///     BackgroundService that owns the OPC UA server lifecycle (decision #30, replacing TopShelf).
///     Bootstraps config, starts the <see cref="DriverHost"/>, starts the OPC UA server via
///     <see cref="OpcUaApplicationHost"/>, drives each driver's discovery into the address space,
///     runs until stopped.
/// </summary>
public sealed class OpcUaServerService(
    NodeBootstrap bootstrap,
    DriverHost driverHost,
    OpcUaApplicationHost applicationHost,
    DriverEquipmentContentRegistry equipmentContentRegistry,
    DriverInstanceBootstrapper driverBootstrapper,
    Phase7Composer phase7Composer,
    IServiceScopeFactory scopeFactory,
    ILogger<OpcUaServerService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("OtOpcUa.Server starting");

        var result = await bootstrap.LoadCurrentGenerationAsync(stoppingToken);
        logger.LogInformation("Bootstrap complete: source={Source} generation={Gen}", result.Source, result.GenerationId);

        // ADR-001 Option A — populate per-driver Equipment namespace snapshots into the
        // registry before StartAsync walks the address space. The walker on the OPC UA side
        // reads synchronously from the registry; pre-loading here means the hot path stays
        // non-blocking + each driver pays at most one Config-DB query at bootstrap time.
        // Skipped when no generation is Published yet — the fleet boots into a UNS-less
        // address space until the first publish, then the registry fills on next restart.
        if (result.GenerationId is { } gen)
        {
            // Task #248 — register IDriver instances from the published DriverInstance
            // rows BEFORE the equipment-content load + Phase 7 compose, so the rest of
            // the pipeline sees a populated DriverHost. Without this step Phase 7's
            // CachedTagUpstreamSource has no upstream feed + virtual-tag scripts read
            // BadNodeIdUnknown for every tag path (gap surfaced by task #240 smoke).
            await driverBootstrapper.RegisterDriversFromGenerationAsync(gen, stoppingToken);

            await PopulateEquipmentContentAsync(gen, stoppingToken);

            // Phase 7 follow-up #246 — load Script + VirtualTag + ScriptedAlarm rows,
            // compose VirtualTagEngine + ScriptedAlarmEngine, start the driver-bridge
            // feed. SetPhase7Sources MUST run before applicationHost.StartAsync because
            // OtOpcUaServer + DriverNodeManager construction captures the field values
            // — late binding after server start is rejected with InvalidOperationException.
            // No-op when the generation has no virtual tags or scripted alarms.
            var phase7 = await phase7Composer.PrepareAsync(gen, stoppingToken);
            applicationHost.SetPhase7Sources(phase7.VirtualReadable, phase7.ScriptedAlarmReadable);
        }

        await applicationHost.StartAsync(stoppingToken);

        logger.LogInformation("OtOpcUa.Server running. Hosted drivers: {Count}", driverHost.RegisteredDriverIds.Count);

        try
        {
            await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken);
        }
        catch (OperationCanceledException)
        {
            logger.LogInformation("OtOpcUa.Server stopping");
        }
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        await base.StopAsync(cancellationToken);
        // Dispose Phase 7 first so the bridge stops feeding the cache + the engines
        // stop firing alarm/historian events before the OPC UA server tears down its
        // node managers. Otherwise an in-flight cascade could try to push through a
        // disposed source and surface as a noisy shutdown warning.
        await phase7Composer.DisposeAsync();
        await applicationHost.DisposeAsync();
        await driverHost.DisposeAsync();
    }

    /// <summary>
    ///     Pre-load an <c>EquipmentNamespaceContent</c> snapshot for each registered driver at
    ///     the bootstrapped generation. Null results (driver has no Equipment rows —
    ///     Modbus/AB CIP/TwinCAT/FOCAS today per decisions #116–#121) are skipped: the walker
    ///     wire-in sees Get(driverId) return null + falls back to DiscoverAsync-owns-it.
    ///     Opens one scope so the scoped <c>OtOpcUaConfigDbContext</c> is shared across all
    ///     per-driver queries rather than paying scope-setup overhead per driver.
    /// </summary>
    private async Task PopulateEquipmentContentAsync(long generationId, CancellationToken ct)
    {
        using var scope = scopeFactory.CreateScope();
        var loader = scope.ServiceProvider.GetRequiredService<EquipmentNamespaceContentLoader>();

        var loaded = 0;
        foreach (var driverId in driverHost.RegisteredDriverIds)
        {
            var content = await loader.LoadAsync(driverId, generationId, ct).ConfigureAwait(false);
            if (content is null) continue;
            equipmentContentRegistry.Set(driverId, content);
            loaded++;
        }
        logger.LogInformation(
            "Equipment namespace snapshots loaded for {Count}/{Total} driver(s) at generation {Gen}",
            loaded, driverHost.RegisteredDriverIds.Count, generationId);
    }
}
