using FASTER.core;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Recorder;

/// <summary>
///     Crash-safe, FIFO <see cref="IHistorizationOutbox"/> backed by a single FasterLog (append-only
///     persistent log) under <c>&lt;directory&gt;/hlog.log</c>. Maps the outbox onto FasterLog:
///     append → <see cref="FasterLog.EnqueueAsync(byte[], CancellationToken)"/>; peek → forward scan
///     from the logical head; remove → <see cref="FasterLog.TruncateUntil"/> (head advance + reclaim)
///     + commit. In-memory FIFO state (entry id → log start address) is rebuilt from the committed log
///     by a one-pass startup scan, so acked truncations survive an unclean restart.
/// </summary>
/// <remarks>
///     Mirrors the gateway's <c>FasterLogOutboxStore</c> and adds a bounded-capacity drop-oldest
///     policy: when an append would exceed <c>capacity</c>, the head is advanced past the oldest
///     entry (truncate + commit) and <see cref="DroppedCount"/> is incremented. Assumes serialized
///     appends (the recorder actor processes messages sequentially); the lock protects the in-memory
///     index, and FasterLog itself tolerates concurrent enqueue/scan.
/// </remarks>
public sealed class FasterLogHistorizationOutbox : IHistorizationOutbox
{
    private readonly record struct LiveEntry(Guid Id, long Start);

    private readonly ManagedLocalStorageDevice _device;
    private readonly FasterLog _log;
    private readonly Lock _state = new();
    private readonly HistorizationCommitMode _commitMode;
    private readonly int _capacity;

    // Periodic-mode auto-commit machinery (null under PerEntry). The CTS stops the loop, the timer
    // paces it, and the loop Task is retained so Dispose can await it (never leaving an unobserved Task).
    private readonly CancellationTokenSource? _periodicCommitCts;
    private readonly PeriodicTimer? _periodicCommitTimer;
    private readonly Task? _periodicCommitLoop;
    private bool _disposed;

    // FIFO of live (appended-but-not-acked) entries with their FasterLog start addresses, plus an id
    // index for O(1) remove. All three (+ _nextScanAddress, _droppedCount) are read/written under _state.
    private readonly LinkedList<LiveEntry> _live = new();
    private readonly Dictionary<Guid, LinkedListNode<LiveEntry>> _index = new();
    private long _nextScanAddress;   // authoritative logical head; peeks scan from here
    private long _droppedCount;

    /// <summary>
    ///     Opens (or recovers) the FasterLog-backed outbox under <paramref name="directory"/>.
    /// </summary>
    /// <param name="directory">Directory holding the FasterLog segment + commit files.</param>
    /// <param name="commitMode">
    ///     <see cref="HistorizationCommitMode.PerEntry"/> fsyncs before each append returns;
    ///     <see cref="HistorizationCommitMode.Periodic"/> commits on a background timer every
    ///     <paramref name="commitIntervalMs"/> ms.
    /// </param>
    /// <param name="commitIntervalMs">Periodic-mode commit cadence in ms; must be positive when Periodic.</param>
    /// <param name="capacity">
    ///     Maximum un-acked entries before drop-oldest kicks in; <c>0</c> (default) means unbounded.
    /// </param>
    public FasterLogHistorizationOutbox(
        string directory,
        HistorizationCommitMode commitMode = HistorizationCommitMode.PerEntry,
        int commitIntervalMs = 100,
        int capacity = 0)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(directory);
        ArgumentOutOfRangeException.ThrowIfNegative(capacity);
        if (commitMode == HistorizationCommitMode.Periodic)
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(commitIntervalMs);

        Directory.CreateDirectory(directory);
        _commitMode = commitMode;
        _capacity = capacity;
        _device = new ManagedLocalStorageDevice(Path.Combine(directory, "hlog.log"));
        _log = new FasterLog(new FasterLogSettings { LogDevice = _device });
        RecoverState();   // sets _nextScanAddress + rebuilds _live/_index from the committed log

        if (_commitMode == HistorizationCommitMode.Periodic)
        {
            _periodicCommitCts = new CancellationTokenSource();
            _periodicCommitTimer = new PeriodicTimer(TimeSpan.FromMilliseconds(commitIntervalMs));
            // Started after RecoverState so it never races a half-recovered instance.
            _periodicCommitLoop = RunPeriodicCommitLoopAsync(_periodicCommitTimer, _periodicCommitCts.Token);
        }
    }

    /// <inheritdoc />
    public long DroppedCount
    {
        get
        {
            lock (_state)
            {
                return _droppedCount;
            }
        }
    }

    /// <inheritdoc />
    public async ValueTask AppendAsync(HistorizationOutboxEntry entry, CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(entry);

        byte[] payload = HistorizationOutboxEntrySerializer.Serialize(entry);
        long startAddress = await _log.EnqueueAsync(payload, ct).ConfigureAwait(false);
        if (_commitMode == HistorizationCommitMode.PerEntry)
        {
            // PerEntry: durable before returning. Periodic skips this — the background timer (and
            // Dispose) commit on their cadence (accepted throughput/latency trade-off).
            await _log.CommitAsync(ct).ConfigureAwait(false);
        }

        long? truncateTo = null;
        lock (_state)
        {
            LinkedListNode<LiveEntry> node = _live.AddLast(new LiveEntry(entry.Id, startAddress));
            _index[entry.Id] = node;

            // Drop-oldest on overflow. The new head is the start address of whatever entry survives
            // at the front (or the tail if the log emptied); truncate to the furthest such address.
            while (_capacity > 0 && _live.Count > _capacity)
            {
                LinkedListNode<LiveEntry> oldest = _live.First!;
                truncateTo = oldest.Next?.Value.Start ?? _log.TailAddress;
                _index.Remove(oldest.Value.Id);
                _live.RemoveFirst();
                _droppedCount++;
            }

            if (truncateTo is long head)
                _nextScanAddress = head;
        }

        if (truncateTo is long truncateAddr)
        {
            _log.TruncateUntil(truncateAddr);
            await _log.CommitAsync(ct).ConfigureAwait(false);   // make the drop durable
        }
    }

    /// <inheritdoc />
    public ValueTask<IReadOnlyList<HistorizationOutboxEntry>> PeekBatchAsync(int max, CancellationToken ct)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(max);

        var batch = new List<HistorizationOutboxEntry>(Math.Min(max, 64));
        lock (_state)
        {
            using FasterLogScanIterator iter = _log.Scan(_nextScanAddress, _log.TailAddress, recover: false);
            while (batch.Count < max && iter.GetNext(out byte[] bytes, out int len, out _, out _))
            {
                batch.Add(HistorizationOutboxEntrySerializer.Deserialize(bytes.AsSpan(0, len)));
            }
        }

        return ValueTask.FromResult<IReadOnlyList<HistorizationOutboxEntry>>(batch);
    }

    /// <inheritdoc />
    public async ValueTask RemoveAsync(Guid id, CancellationToken ct)
    {
        long truncateTo;
        lock (_state)
        {
            if (!_index.TryGetValue(id, out LinkedListNode<LiveEntry>? node))
                return;   // unknown / already removed -> defensive no-op

            truncateTo = node.Next?.Value.Start ?? _log.TailAddress;
            // FIFO ack: remove the target plus any older entries still ahead of it.
            while (_live.First is { } first)
            {
                bool isTarget = ReferenceEquals(first, node);
                _index.Remove(first.Value.Id);
                _live.RemoveFirst();
                if (isTarget)
                    break;
            }

            _nextScanAddress = truncateTo;
        }

        _log.TruncateUntil(truncateTo);
        await _log.CommitAsync(ct).ConfigureAwait(false);   // make the head advance durable
    }

    /// <inheritdoc />
    public ValueTask<int> CountAsync(CancellationToken ct)
    {
        lock (_state)
        {
            return ValueTask.FromResult(_live.Count);
        }
    }

    // Rebuild the in-memory FIFO index from the committed log after a restart. The FasterLog ctor has
    // already recovered BeginAddress/TailAddress from the on-disk commit metadata, so scanning
    // [BeginAddress, TailAddress) yields exactly the untruncated (un-acked) records in FIFO order, and
    // BeginAddress is the recovered logical head.
    //
    // CTOR-ONLY: called once before the instance is published and before the periodic-commit loop
    // starts. It unconditionally seeds _nextScanAddress/_live/_index, so it must NEVER run post-ctor.
    //
    // Capacity note: if a crash lands between an append's commit and the subsequent drop-oldest
    // truncation commit, recovery scans the still-present oldest record and may transiently rebuild
    // _live with MORE than _capacity entries. This self-corrects on the next AppendAsync — its
    // drop-oldest while-loop runs until _live.Count <= _capacity, so the overflow converges away.
    private void RecoverState()
    {
        _nextScanAddress = _log.BeginAddress;

        using FasterLogScanIterator iter = _log.Scan(_log.BeginAddress, _log.TailAddress, recover: false);
        while (iter.GetNext(out byte[] bytes, out int len, out long currentAddress, out _))
        {
            HistorizationOutboxEntry entry = HistorizationOutboxEntrySerializer.Deserialize(bytes.AsSpan(0, len));
            LinkedListNode<LiveEntry> node = _live.AddLast(new LiveEntry(entry.Id, currentAddress));
            _index[entry.Id] = node;
        }
    }

    // Periodic-mode auto-commit: best-effort _log.Commit every interval until cancelled. Commit
    // failures are swallowed so the loop survives transient errors; the per-remove/per-drop commits
    // and Dispose's final spin-wait commit still bound durability.
    private async Task RunPeriodicCommitLoopAsync(PeriodicTimer timer, CancellationToken cancellationToken)
    {
        try
        {
            while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
            {
                try
                {
                    _log.Commit(spinWait: false);
                }
                catch (FasterException)
                {
                    // Transient/teardown commit failure — keep ticking.
                }
            }
        }
        catch (OperationCanceledException)
        {
            // Expected on Dispose: the CTS cancelled the wait. Normal teardown.
        }
    }

    /// <summary>
    ///     Stops the periodic-commit loop (Periodic mode), flushes a final commit (best-effort), and
    ///     releases the log + device. Idempotent.
    /// </summary>
    public void Dispose()
    {
        if (_disposed)
            return;
        _disposed = true;

        // Stop the periodic loop BEFORE the final commit so we don't race the loop's Commit against
        // the teardown commit / log dispose. Await the loop (it absorbs cancellation) so no Task leaks.
        if (_periodicCommitCts is not null)
        {
            _periodicCommitCts.Cancel();
            try
            {
                _periodicCommitLoop?.GetAwaiter().GetResult();
            }
            catch (OperationCanceledException)
            {
                // Cancellation is the expected stop signal — not an error.
            }
            catch (Exception)
            {
                // The loop faulted on a non-Faster commit error during teardown (e.g. an
                // ObjectDisposedException as the device tears down); swallow — Dispose must not
                // throw. Already-committed enqueues remain durable.
            }

            _periodicCommitTimer?.Dispose();
            _periodicCommitCts.Dispose();
        }

        try
        {
            _log.Commit(spinWait: true);
        }
        catch (FasterException)
        {
            // Best-effort final commit on teardown: already-committed enqueues remain durable.
        }

        _log.Dispose();
        _device.Dispose();
    }
}
