using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;

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

/// <summary>
///     Phase 7 follow-up (task #243) — maps the generation's <see cref="Script"/> /
///     <see cref="VirtualTag"/> / <see cref="ScriptedAlarm"/> rows into the runtime
///     definitions <see cref="VirtualTagEngine"/> + <see cref="ScriptedAlarmEngine"/>
///     expect, builds the engine instances, and returns the <see cref="IReadable"/>
///     sources plus an <see cref="IAlarmSource"/> for the <c>DriverNodeManager</c>
///     wiring added by task #239.
/// </summary>
/// <remarks>
///     <para>
///         Empty Phase 7 config (no virtual tags + no scripted alarms) is a valid state:
///         <see cref="Compose"/> returns a <see cref="Phase7ComposedSources"/> with null
///         sources so Program.cs can pass them through to <c>OpcUaApplicationHost</c>
///         unchanged — deployments without scripts behave exactly as they did before
///         Phase 7.
///     </para>
///     <para>
///         The caller owns the returned <see cref="Phase7ComposedSources.Disposables"/>
///         and must dispose them on shutdown. Engine cascades + timer ticks run off
///         background threads until then.
///     </para>
/// </remarks>
public static class Phase7EngineComposer
{
    public static Phase7ComposedSources Compose(
        IReadOnlyList<Script> scripts,
        IReadOnlyList<VirtualTag> virtualTags,
        IReadOnlyList<ScriptedAlarm> scriptedAlarms,
        CachedTagUpstreamSource upstream,
        IAlarmStateStore alarmStateStore,
        IAlarmHistorianSink historianSink,
        Serilog.ILogger rootScriptLogger,
        ILoggerFactory loggerFactory)
    {
        ArgumentNullException.ThrowIfNull(scripts);
        ArgumentNullException.ThrowIfNull(virtualTags);
        ArgumentNullException.ThrowIfNull(scriptedAlarms);
        ArgumentNullException.ThrowIfNull(upstream);
        ArgumentNullException.ThrowIfNull(alarmStateStore);
        ArgumentNullException.ThrowIfNull(historianSink);
        ArgumentNullException.ThrowIfNull(rootScriptLogger);
        ArgumentNullException.ThrowIfNull(loggerFactory);

        if (virtualTags.Count == 0 && scriptedAlarms.Count == 0)
            return Phase7ComposedSources.Empty;

        var scriptById = scripts
            .Where(s => s.Enabled())
            .ToDictionary(s => s.ScriptId, StringComparer.Ordinal);

        var scriptLoggerFactory = new ScriptLoggerFactory(rootScriptLogger);
        var disposables = new List<IDisposable>();

        // Engines take Serilog.ILogger — each engine gets its own so rolling-file emissions
        // stay keyed to the right source in the scripts-*.log.
        VirtualTagSource? vtSource = null;
        if (virtualTags.Count > 0)
        {
            var vtDefs = ProjectVirtualTags(virtualTags, scriptById).ToList();
            var vtEngine = new VirtualTagEngine(upstream, scriptLoggerFactory, rootScriptLogger);
            vtEngine.Load(vtDefs);
            vtSource = new VirtualTagSource(vtEngine);
            disposables.Add(vtEngine);
        }

        IReadable? alarmReadable = null;
        if (scriptedAlarms.Count > 0)
        {
            var alarmDefs = ProjectScriptedAlarms(scriptedAlarms, scriptById).ToList();
            var alarmEngine = new ScriptedAlarmEngine(upstream, alarmStateStore, scriptLoggerFactory, rootScriptLogger);
            // Wire alarm emissions to the historian sink (Stream D). Fire-and-forget because
            // the sink's EnqueueAsync is already non-blocking from the producer's view.
            var engineLogger = loggerFactory.CreateLogger("Phase7HistorianRouter");
            alarmEngine.OnEvent += (_, e) => _ = RouteToHistorianAsync(e, historianSink, engineLogger);
            alarmEngine.LoadAsync(alarmDefs, CancellationToken.None).GetAwaiter().GetResult();
            var alarmSource = new ScriptedAlarmSource(alarmEngine);
            // Task #245 — expose each alarm's current Active state as IReadable so OPC UA
            // variable reads on Source=ScriptedAlarm nodes return the live predicate truth
            // instead of BadNotFound. ScriptedAlarmSource stays registered as IAlarmSource
            // for the event stream; the IReadable is a separate adapter over the same engine.
            alarmReadable = new ScriptedAlarmReadable(alarmEngine);
            disposables.Add(alarmEngine);
            disposables.Add(alarmSource);
        }

        return new Phase7ComposedSources(vtSource, alarmReadable, disposables);
    }

    internal static IEnumerable<VirtualTagDefinition> ProjectVirtualTags(
        IReadOnlyList<VirtualTag> rows, IReadOnlyDictionary<string, Script> scriptById)
    {
        foreach (var row in rows)
        {
            if (!row.Enabled) continue;
            if (!scriptById.TryGetValue(row.ScriptId, out var script))
                throw new InvalidOperationException(
                    $"VirtualTag '{row.VirtualTagId}' references unknown / disabled Script '{row.ScriptId}' in this generation");

            yield return new VirtualTagDefinition(
                Path: row.VirtualTagId,
                DataType: ParseDataType(row.DataType),
                ScriptSource: script.SourceCode,
                ChangeTriggered: row.ChangeTriggered,
                TimerInterval: row.TimerIntervalMs.HasValue
                    ? TimeSpan.FromMilliseconds(row.TimerIntervalMs.Value)
                    : null,
                Historize: row.Historize);
        }
    }

    internal static IEnumerable<ScriptedAlarmDefinition> ProjectScriptedAlarms(
        IReadOnlyList<ScriptedAlarm> rows, IReadOnlyDictionary<string, Script> scriptById)
    {
        foreach (var row in rows)
        {
            if (!row.Enabled) continue;
            if (!scriptById.TryGetValue(row.PredicateScriptId, out var script))
                throw new InvalidOperationException(
                    $"ScriptedAlarm '{row.ScriptedAlarmId}' references unknown / disabled predicate Script '{row.PredicateScriptId}'");

            yield return new ScriptedAlarmDefinition(
                AlarmId: row.ScriptedAlarmId,
                EquipmentPath: row.EquipmentId,
                AlarmName: row.Name,
                Kind: ParseAlarmKind(row.AlarmType),
                Severity: MapSeverity(row.Severity),
                MessageTemplate: row.MessageTemplate,
                PredicateScriptSource: script.SourceCode,
                HistorizeToAveva: row.HistorizeToAveva,
                Retain: row.Retain);
        }
    }

    private static DriverDataType ParseDataType(string raw) =>
        Enum.TryParse<DriverDataType>(raw, ignoreCase: true, out var parsed) ? parsed : DriverDataType.String;

    private static AlarmKind ParseAlarmKind(string raw) => raw switch
    {
        "AlarmCondition" => AlarmKind.AlarmCondition,
        "LimitAlarm" => AlarmKind.LimitAlarm,
        "DiscreteAlarm" => AlarmKind.DiscreteAlarm,
        "OffNormalAlarm" => AlarmKind.OffNormalAlarm,
        _ => throw new InvalidOperationException($"Unknown AlarmType '{raw}' — DB check constraint should have caught this"),
    };

    // OPC UA Part 9 severity bands (1..1000) → AlarmSeverity enum. Matches the same
    // banding the AB CIP ALMA projection + OpcUaClient MapSeverity use.
    private static AlarmSeverity MapSeverity(int s) => s switch
    {
        <= 250 => AlarmSeverity.Low,
        <= 500 => AlarmSeverity.Medium,
        <= 750 => AlarmSeverity.High,
        _ => AlarmSeverity.Critical,
    };

    private static async Task RouteToHistorianAsync(
        ScriptedAlarmEvent e, IAlarmHistorianSink sink, Microsoft.Extensions.Logging.ILogger log)
    {
        try
        {
            var historianEvent = new AlarmHistorianEvent(
                AlarmId: e.AlarmId,
                EquipmentPath: e.EquipmentPath,
                AlarmName: e.AlarmName,
                AlarmTypeName: e.Kind.ToString(),
                Severity: e.Severity,
                EventKind: e.Emission.ToString(),
                Message: e.Message,
                User: e.Condition.LastAckUser ?? "system",
                Comment: e.Condition.LastAckComment,
                TimestampUtc: e.TimestampUtc);
            await sink.EnqueueAsync(historianEvent, CancellationToken.None).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            log.LogWarning(ex, "Historian enqueue failed for alarm {AlarmId}/{Emission}", e.AlarmId, e.Emission);
        }
    }
}

/// <summary>What <see cref="Phase7EngineComposer.Compose"/> returns.</summary>
/// <param name="VirtualReadable">Non-null when virtual tags were composed; pass to <c>OpcUaApplicationHost.virtualReadable</c>.</param>
/// <param name="ScriptedAlarmReadable">Non-null when scripted alarms were composed; pass to <c>OpcUaApplicationHost.scriptedAlarmReadable</c>.</param>
/// <param name="Disposables">Engine + source instances the caller owns. Dispose on shutdown.</param>
public sealed record Phase7ComposedSources(
    IReadable? VirtualReadable,
    IReadable? ScriptedAlarmReadable,
    IReadOnlyList<IDisposable> Disposables)
{
    public static readonly Phase7ComposedSources Empty =
        new(null, null, Array.Empty<IDisposable>());
}

internal static class ScriptEnabledExtensions
{
    // Script has no explicit Enabled column; every row in the generation is a live script.
    public static bool Enabled(this Script _) => true;
}
