using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;

namespace ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;

/// <summary>
///     Production-side <see cref="IAlarmActorStateStore"/> backed by the
///     <see cref="ScriptedAlarmState"/> table in the central config DB. The actor's
///     3-state enum projects into the table's two persisted dimensions: Acked + an
///     internal "_lastActiveState" recorded via a synthetic mapping (Inactive ⇒ Acked,
///     Active ⇒ Unacked, Acknowledged ⇒ Acked). ActiveState itself is deliberately NOT
///     persisted — re-derives from the evaluator on startup (Phase 7 decision #14).
/// </summary>
public sealed class EfAlarmActorStateStore : IAlarmActorStateStore
{
    private readonly IDbContextFactory<OtOpcUaConfigDbContext> _dbFactory;
    private readonly ILogger<EfAlarmActorStateStore> _logger;

    /// <summary>Initializes a new instance of the EfAlarmActorStateStore.</summary>
    /// <param name="dbFactory">The factory for creating database contexts.</param>
    /// <param name="logger">The logger instance.</param>
    public EfAlarmActorStateStore(
        IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
        ILogger<EfAlarmActorStateStore> logger)
    {
        _dbFactory = dbFactory;
        _logger = logger;
    }

    /// <summary>Loads the alarm state snapshot from the database.</summary>
    /// <param name="alarmId">The identifier of the alarm.</param>
    /// <param name="ct">The cancellation token.</param>
    /// <returns>The alarm state snapshot, or null if not found.</returns>
    public async Task<AlarmActorStateSnapshot?> LoadAsync(string alarmId, CancellationToken ct)
    {
        using var db = await _dbFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
        var row = await db.ScriptedAlarmStates.AsNoTracking()
            .FirstOrDefaultAsync(r => r.ScriptedAlarmId == alarmId, ct)
            .ConfigureAwait(false);
        if (row is null) return null;

        var state = MapAckedToActorState(row.AckedState);
        return new AlarmActorStateSnapshot(
            AlarmId: alarmId,
            State: state,
            LastTransitionUtc: row.UpdatedAtUtc,
            LastAckUser: row.LastAckUser);
    }

    /// <summary>Saves the alarm state snapshot to the database.</summary>
    /// <param name="snapshot">The alarm state snapshot to save.</param>
    /// <param name="ct">The cancellation token.</param>
    public async Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct)
    {
        using var db = await _dbFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
        var row = await db.ScriptedAlarmStates
            .FirstOrDefaultAsync(r => r.ScriptedAlarmId == snapshot.AlarmId, ct)
            .ConfigureAwait(false);

        var ackedState = MapActorStateToAcked(snapshot.State);
        if (row is null)
        {
            db.ScriptedAlarmStates.Add(new ScriptedAlarmState
            {
                ScriptedAlarmId = snapshot.AlarmId,
                EnabledState = "Enabled",
                AckedState = ackedState,
                ConfirmedState = "Confirmed",
                ShelvingState = "Unshelved",
                LastAckUser = snapshot.LastAckUser,
                LastAckUtc = string.Equals(snapshot.State, "Acknowledged", StringComparison.Ordinal)
                    ? snapshot.LastTransitionUtc
                    : null,
                UpdatedAtUtc = snapshot.LastTransitionUtc,
                CommentsJson = "[]",
            });
        }
        else
        {
            row.AckedState = ackedState;
            row.LastAckUser = snapshot.LastAckUser ?? row.LastAckUser;
            if (string.Equals(snapshot.State, "Acknowledged", StringComparison.Ordinal))
                row.LastAckUtc = snapshot.LastTransitionUtc;
            row.UpdatedAtUtc = snapshot.LastTransitionUtc;
        }

        try
        {
            await db.SaveChangesAsync(ct).ConfigureAwait(false);
        }
        catch (DbUpdateConcurrencyException ex)
        {
            // Two actors racing to save the same alarm is benign — the last writer wins on
            // UpdatedAtUtc, and the next transition on either side will write again. Log
            // + drop so a race doesn't crash the dispatcher.
            _logger.LogDebug(ex, "EfAlarmActorStateStore: concurrency conflict for {AlarmId}; dropping save",
                snapshot.AlarmId);
        }
    }

    private static string MapActorStateToAcked(string actorState) => actorState switch
    {
        "Active" => "Unacknowledged",
        "Acknowledged" => "Acknowledged",
        // Inactive maps to Acknowledged — when an alarm clears, nothing is left to ack.
        _ => "Acknowledged",
    };

    private static string MapAckedToActorState(string ackedState)
    {
        // Only Active distinguishes from Acked — Inactive comes from a re-eval, not from
        // the table. Persisted "Unacknowledged" implies the actor was last Active +
        // un-acked; we restore it to Active so a restart doesn't drop pending operator work.
        return string.Equals(ackedState, "Unacknowledged", StringComparison.Ordinal)
            ? "Active"
            : "Acknowledged";
    }
}
