using System.Collections.Immutable;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;

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

/// <summary>
///     Production-side <see cref="IAlarmStateStore"/> backed by the
///     <see cref="ScriptedAlarmState"/> table in the central config DB. This store maps the
///     full Part 9 <see cref="AlarmConditionState"/> — Enabled / Acked / Confirmed / Shelving
///     + the ack/confirm audit trail + operator comments.
/// </summary>
/// <remarks>
///     <para>
///         <b>ActiveState is NOT persisted</b> — the entity has no Active column. On
///         <see cref="LoadAsync"/> it is restored as <see cref="AlarmActiveState.Inactive"/>;
///         the engine re-derives it from the live predicate on startup (Phase 7 decision #14).
///     </para>
///     <para>
///         <b>LastTransitionUtc ↔ UpdatedAtUtc</b>: the table has no dedicated transition
///         column, so <c>LastTransitionUtc</c> is written into the row-write
///         <see cref="ScriptedAlarmState.UpdatedAtUtc"/> on save and read back from it on load.
///     </para>
///     <para>
///         <b>LastActiveUtc / LastClearedUtc are transient</b> — they have no columns and
///         default to <c>null</c> on load (they re-derive from the predicate alongside Active).
///     </para>
///     <para>
///         <see cref="AlarmConditionState.Comments"/> serializes to/from
///         <see cref="ScriptedAlarmState.CommentsJson"/> via System.Text.Json. An empty list
///         round-trips as <c>"[]"</c> (matching the entity default).
///     </para>
/// </remarks>
public sealed class EfAlarmConditionStateStore : IAlarmStateStore
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);

    private readonly IDbContextFactory<OtOpcUaConfigDbContext> _dbFactory;
    private readonly ILogger<EfAlarmConditionStateStore> _logger;

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

    /// <inheritdoc />
    public async Task<AlarmConditionState?> 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);
        return row is null ? null : MapToState(row);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<AlarmConditionState>> LoadAllAsync(CancellationToken ct)
    {
        using var db = await _dbFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
        var rows = await db.ScriptedAlarmStates.AsNoTracking()
            .ToListAsync(ct)
            .ConfigureAwait(false);
        return rows.Select(MapToState).ToArray();
    }

    /// <inheritdoc />
    /// <remarks>
    ///     <b>Concurrency assumption</b>: saves for a given <c>alarmId</c> are serialized by the
    ///     owning host actor (one actor owns the engine per equipment). The check-then-insert
    ///     pattern is therefore safe under that guarantee — two concurrent inserts for the same
    ///     alarm cannot occur in the live
    ///     runtime. The <see cref="DbUpdateConcurrencyException"/> catch handles the edge case of a
    ///     racing concurrent restart during crash recovery.
    /// </remarks>
    public async Task SaveAsync(AlarmConditionState state, CancellationToken ct)
    {
        using var db = await _dbFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
        var row = await db.ScriptedAlarmStates
            .FirstOrDefaultAsync(r => r.ScriptedAlarmId == state.AlarmId, ct)
            .ConfigureAwait(false);

        if (row is null)
        {
            // Required members are set here to satisfy the compiler; ApplyState is the single
            // source of truth for all columns and immediately overwrites these values below.
            row = new ScriptedAlarmState
            {
                ScriptedAlarmId = state.AlarmId,
                EnabledState = MapEnabledToColumn(state.Enabled),
                AckedState = MapAckedToColumn(state.Acked),
                ConfirmedState = MapConfirmedToColumn(state.Confirmed),
                ShelvingState = MapShelvingToColumn(state.Shelving.Kind),
            };
            ApplyState(row, state);
            db.ScriptedAlarmStates.Add(row);
        }
        else
        {
            ApplyState(row, state);
        }

        try
        {
            await db.SaveChangesAsync(ct).ConfigureAwait(false);
        }
        catch (DbUpdateConcurrencyException ex)
        {
            // Two writers racing to save the same alarm is benign — last writer wins on
            // UpdatedAtUtc and the next transition writes again. Log + drop so a race never
            // crashes the engine.
            _logger.LogDebug(ex,
                "EfAlarmConditionStateStore: concurrency conflict for {AlarmId}; dropping save",
                state.AlarmId);
        }
    }

    /// <inheritdoc />
    public async Task RemoveAsync(string alarmId, CancellationToken ct)
    {
        using var db = await _dbFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
        var row = await db.ScriptedAlarmStates
            .FirstOrDefaultAsync(r => r.ScriptedAlarmId == alarmId, ct)
            .ConfigureAwait(false);
        if (row is null) return;

        db.ScriptedAlarmStates.Remove(row);
        await db.SaveChangesAsync(ct).ConfigureAwait(false);
    }

    private static void ApplyState(ScriptedAlarmState row, AlarmConditionState state)
    {
        row.EnabledState = MapEnabledToColumn(state.Enabled);
        row.AckedState = MapAckedToColumn(state.Acked);
        row.ConfirmedState = MapConfirmedToColumn(state.Confirmed);
        row.ShelvingState = MapShelvingToColumn(state.Shelving.Kind);
        row.ShelvingExpiresUtc = state.Shelving.UnshelveAtUtc;
        row.LastAckUser = state.LastAckUser;
        row.LastAckComment = state.LastAckComment;
        row.LastAckUtc = state.LastAckUtc;
        row.LastConfirmUser = state.LastConfirmUser;
        row.LastConfirmComment = state.LastConfirmComment;
        row.LastConfirmUtc = state.LastConfirmUtc;
        row.CommentsJson = SerializeComments(state.Comments);
        // No dedicated transition column — persist LastTransitionUtc into UpdatedAtUtc.
        row.UpdatedAtUtc = state.LastTransitionUtc;
    }

    private static AlarmConditionState MapToState(ScriptedAlarmState row) => new(
        AlarmId: row.ScriptedAlarmId,
        Enabled: string.Equals(row.EnabledState, "Disabled", StringComparison.Ordinal)
            ? AlarmEnabledState.Disabled
            : AlarmEnabledState.Enabled,  // unknown string → Enabled (safe default)
        // Active is not persisted — the engine re-derives it from the predicate at startup.
        Active: AlarmActiveState.Inactive,
        Acked: string.Equals(row.AckedState, "Acknowledged", StringComparison.Ordinal)
            ? AlarmAckedState.Acknowledged
            : AlarmAckedState.Unacknowledged,  // unknown string → Unacknowledged (safe default)
        Confirmed: string.Equals(row.ConfirmedState, "Confirmed", StringComparison.Ordinal)
            ? AlarmConfirmedState.Confirmed
            : AlarmConfirmedState.Unconfirmed,  // unknown string → Unconfirmed (safe default)
        Shelving: new ShelvingState(MapShelvingFromColumn(row.ShelvingState), row.ShelvingExpiresUtc),
        // No transition column — UpdatedAtUtc carries the last transition timestamp.
        LastTransitionUtc: row.UpdatedAtUtc,
        // LastActiveUtc / LastClearedUtc have no columns — they re-derive with Active, so null on load.
        LastActiveUtc: null,
        LastClearedUtc: null,
        LastAckUtc: row.LastAckUtc,
        LastAckUser: row.LastAckUser,
        LastAckComment: row.LastAckComment,
        LastConfirmUtc: row.LastConfirmUtc,
        LastConfirmUser: row.LastConfirmUser,
        LastConfirmComment: row.LastConfirmComment,
        Comments: DeserializeComments(row.CommentsJson));

    private static string MapEnabledToColumn(AlarmEnabledState enabled)
        => enabled == AlarmEnabledState.Enabled ? "Enabled" : "Disabled";

    private static string MapAckedToColumn(AlarmAckedState acked)
        => acked == AlarmAckedState.Acknowledged ? "Acknowledged" : "Unacknowledged";

    private static string MapConfirmedToColumn(AlarmConfirmedState confirmed)
        => confirmed == AlarmConfirmedState.Confirmed ? "Confirmed" : "Unconfirmed";

    private static string MapShelvingToColumn(ShelvingKind kind) => kind switch
    {
        ShelvingKind.OneShot => "OneShotShelved",
        ShelvingKind.Timed => "TimedShelved",
        _ => "Unshelved",
    };

    private static ShelvingKind MapShelvingFromColumn(string column) => column switch
    {
        "OneShotShelved" => ShelvingKind.OneShot,
        "TimedShelved" => ShelvingKind.Timed,
        _ => ShelvingKind.Unshelved,  // unknown string → Unshelved (safe default)
    };

    private static string SerializeComments(ImmutableList<AlarmComment> comments)
    {
        if (comments.IsEmpty) return "[]";
        var dtos = comments.Select(c => new CommentDto
        {
            // AlarmComment.TimestampUtc must be DateTimeKind.Utc for correct ISO-8601 round-trip;
            // the engine always creates AlarmComment instances with Utc kind.
            TimestampUtc = c.TimestampUtc,
            User = c.User,
            Kind = c.Kind,
            Text = c.Text,
        });
        return JsonSerializer.Serialize(dtos, JsonOptions);
    }

    private static ImmutableList<AlarmComment> DeserializeComments(string? json)
    {
        if (string.IsNullOrWhiteSpace(json)) return ImmutableList<AlarmComment>.Empty;
        var dtos = JsonSerializer.Deserialize<List<CommentDto>>(json, JsonOptions);
        if (dtos is null || dtos.Count == 0) return ImmutableList<AlarmComment>.Empty;
        return dtos
            .Select(d => new AlarmComment(d.TimestampUtc, d.User ?? string.Empty, d.Kind ?? string.Empty, d.Text ?? string.Empty))
            .ToImmutableList();
    }

    /// <summary>Stable on-disk shape for a persisted <see cref="AlarmComment"/> in <c>CommentsJson</c>.</summary>
    private sealed class CommentDto
    {
        /// <summary>When the comment was recorded (UTC).</summary>
        public DateTime TimestampUtc { get; set; }

        /// <summary>Identity of the actor that wrote the comment.</summary>
        public string? User { get; set; }

        /// <summary>Human-readable classification of the comment (Acknowledge, Confirm, …).</summary>
        public string? Kind { get; set; }

        /// <summary>Operator-supplied or engine-generated comment text.</summary>
        public string? Text { get; set; }
    }
}
