using System.Diagnostics;
using System.Diagnostics.Metrics;

namespace ZB.MOM.WW.OtOpcUa.Commons.Observability;

/// <summary>
/// Central <see cref="Meter"/> + <see cref="ActivitySource"/> definitions for OtOpcUa.
/// All Akka actors, the OPC UA publish path, and the deploy coordinator emit through these
/// pre-created instruments so a single OpenTelemetry / Prometheus binding in <c>Host</c>
/// catches everything. No exporter is required — instruments are no-op until a listener
/// attaches, so tests and dev hosts pay nothing for instrumentation that nobody scrapes.
///
/// Instrument names follow the OpenTelemetry semantic convention pattern
/// <c>otopcua.&lt;subsystem&gt;.&lt;event&gt;</c>. Subsystem is one of: deploy, driver,
/// virtualtag, scriptedalarm, opcua, redundancy.
/// </summary>
public static class OtOpcUaTelemetry
{
    public const string MeterName = "ZB.MOM.WW.OtOpcUa";
    public const string ActivitySourceName = "ZB.MOM.WW.OtOpcUa";

    /// <summary>Singleton <see cref="Meter"/> all counters/histograms hang off.</summary>
    public static readonly Meter Meter = new(MeterName);

    /// <summary>Singleton <see cref="ActivitySource"/> used to start spans wrapping deploy/apply/rebuild.</summary>
    public static readonly ActivitySource ActivitySource = new(ActivitySourceName);

    // ---------------- Deployment / driver-host coordination ----------------

    /// <summary>Incremented every time DriverHostActor finishes applying a deployment (Ack or Reject).</summary>
    public static readonly Counter<long> DeploymentApplied =
        Meter.CreateCounter<long>("otopcua.deploy.applied", unit: "{deployment}",
            description: "Deployments applied by a driver-role node (outcome=ack|reject).");

    /// <summary>Time from DriverHostActor receiving DispatchDeployment to emitting the ack/reject.</summary>
    public static readonly Histogram<double> DeploymentApplyDurationSec =
        Meter.CreateHistogram<double>("otopcua.deploy.apply.duration", unit: "s",
            description: "Driver-role apply latency from DispatchDeployment → Ack/Reject.");

    /// <summary>DriverInstanceActor spawn count (added=new instance; stop=disposed).</summary>
    public static readonly Counter<long> DriverInstanceLifecycle =
        Meter.CreateCounter<long>("otopcua.driver.lifecycle", unit: "{event}",
            description: "DriverInstanceActor lifecycle transitions (event=spawn|stop|fault).");

    // ---------------- VirtualTag / ScriptedAlarm engines ----------------

    public static readonly Counter<long> VirtualTagEval =
        Meter.CreateCounter<long>("otopcua.virtualtag.eval", unit: "{eval}",
            description: "Virtual-tag evaluations attempted (outcome=ok|fail|skip).");

    public static readonly Counter<long> ScriptedAlarmTransition =
        Meter.CreateCounter<long>("otopcua.scriptedalarm.transition", unit: "{transition}",
            description: "Scripted-alarm state transitions (state=active|acknowledged|inactive).");

    // ---------------- OPC UA address-space + redundancy ----------------

    public static readonly Counter<long> OpcUaSinkWrite =
        Meter.CreateCounter<long>("otopcua.opcua.sink.write", unit: "{write}",
            description: "Writes that landed in IOpcUaAddressSpaceSink (kind=value|alarm|rebuild).");

    /// <summary>Address-space apply/materialise passes that swallowed at least one sink failure —
    /// a failed rebuild (kind=rebuild) or per-node materialise failures (kind=nodes). A non-zero rate
    /// means the running server holds a stale or partially-materialised address space despite a
    /// reported-successful deploy (archreview 01/S-1).</summary>
    public static readonly Counter<long> OpcUaApplyFailed =
        Meter.CreateCounter<long>("otopcua.opcua.apply.failed", unit: "{apply}",
            description: "Apply/materialise passes with swallowed sink failures (kind=rebuild|nodes).");

    public static readonly Counter<long> ServiceLevelChange =
        Meter.CreateCounter<long>("otopcua.redundancy.service_level_change", unit: "{change}",
            description: "OPC UA Server.ServiceLevel transitions emitted by the redundancy state.");

    /// <summary>Inbound operations denied by the Primary data-plane gate (archreview 03/S4) —
    /// site=write|ack|alarm-emit, reason=secondary|detached|role-unknown.</summary>
    public static readonly Counter<long> PrimaryGateDenied =
        Meter.CreateCounter<long>("otopcua.redundancy.primary_gate_denied", unit: "{denial}",
            description: "Operations denied by the Primary gate (site=write|ack|alarm-emit, reason=secondary|detached|role-unknown).");

    // ---------------- Convenience helpers ----------------

    /// <summary>
    /// Starts a deploy span tagged with the deployment id. Caller disposes to close. Returns
    /// null when no listener is attached so the call site stays cheap on undecorated builds.
    /// </summary>
    /// <param name="deploymentId">The deployment identifier to tag the span with.</param>
    /// <returns>The started activity, or <c>null</c> when no listener is attached.</returns>
    public static Activity? StartDeployApplySpan(string deploymentId)
    {
        var activity = ActivitySource.StartActivity("otopcua.deploy.apply", ActivityKind.Internal);
        activity?.SetTag("otopcua.deployment_id", deploymentId);
        return activity;
    }

    /// <summary>Span wrapping a full OPC UA address-space rebuild (AddressSpace plan → apply).</summary>
    /// <returns>The started activity, or <c>null</c> when no listener is attached.</returns>
    public static Activity? StartAddressSpaceRebuildSpan()
        => ActivitySource.StartActivity("otopcua.opcua.address_space_rebuild", ActivityKind.Internal);
}
