using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Text;

namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Stability;

/// <summary>
///     Ring-buffer of the last N IPC operations, written into a memory-mapped file. On a
///     hard crash the Proxy-side supervisor reads the MMF after the corpse is gone to see
///     what was in flight at the moment the Host died. Single-writer (the Host), multi-reader
///     (the supervisor) — the file format is identical to the Galaxy Tier-C
///     <c>PostMortemMmf</c> so a single reader tool can work both.
/// </summary>
/// <remarks>
///     File layout:
///     <code>
///     [16-byte header: magic(4) | version(4) | capacity(4) | writeIndex(4)]
///     [capacity × 256-byte entries: each is [8-byte utcUnixMs | 8-byte opKind | 240-byte UTF-8 message]]
///     </code>
///     Magic is 'OFPC' (0x4F46_5043) to distinguish a FOCAS file from the Galaxy MMF.
/// </remarks>
public sealed class PostMortemMmf : IDisposable
{
    private const int Magic = 0x4F465043; // 'OFPC'
    private const int Version = 1;
    private const int HeaderBytes = 16;
    public const int EntryBytes = 256;
    private const int MessageOffset = 16;
    private const int MessageCapacity = EntryBytes - MessageOffset;

    public int Capacity { get; }
    public string Path { get; }

    private readonly MemoryMappedFile _mmf;
    private readonly MemoryMappedViewAccessor _accessor;
    private readonly object _writeGate = new();

    public PostMortemMmf(string path, int capacity = 1000)
    {
        if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
        Capacity = capacity;
        Path = path;

        var fileBytes = HeaderBytes + capacity * EntryBytes;
        Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)!);

        var fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
        fs.SetLength(fileBytes);
        _mmf = MemoryMappedFile.CreateFromFile(fs, null, fileBytes,
            MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, leaveOpen: false);
        _accessor = _mmf.CreateViewAccessor(0, fileBytes, MemoryMappedFileAccess.ReadWrite);

        if (_accessor.ReadInt32(0) != Magic)
        {
            _accessor.Write(0, Magic);
            _accessor.Write(4, Version);
            _accessor.Write(8, capacity);
            _accessor.Write(12, 0);
        }
    }

    public void Write(long opKind, string message)
    {
        lock (_writeGate)
        {
            var idx = _accessor.ReadInt32(12);
            var offset = HeaderBytes + idx * EntryBytes;

            _accessor.Write(offset + 0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
            _accessor.Write(offset + 8, opKind);

            var msgBytes = Encoding.UTF8.GetBytes(message ?? string.Empty);
            var copy = Math.Min(msgBytes.Length, MessageCapacity - 1);
            _accessor.WriteArray(offset + MessageOffset, msgBytes, 0, copy);
            _accessor.Write(offset + MessageOffset + copy, (byte)0);

            var next = (idx + 1) % Capacity;
            _accessor.Write(12, next);
        }
    }

    public PostMortemEntry[] ReadAll()
    {
        var magic = _accessor.ReadInt32(0);
        if (magic != Magic) return new PostMortemEntry[0];

        var capacity = _accessor.ReadInt32(8);
        var writeIndex = _accessor.ReadInt32(12);

        var entries = new PostMortemEntry[capacity];
        var count = 0;
        for (var i = 0; i < capacity; i++)
        {
            var slot = (writeIndex + i) % capacity;
            var offset = HeaderBytes + slot * EntryBytes;

            var ts = _accessor.ReadInt64(offset + 0);
            if (ts == 0) continue;

            var op = _accessor.ReadInt64(offset + 8);
            var msgBuf = new byte[MessageCapacity];
            _accessor.ReadArray(offset + MessageOffset, msgBuf, 0, MessageCapacity);
            var nulTerm = Array.IndexOf<byte>(msgBuf, 0);
            var msg = Encoding.UTF8.GetString(msgBuf, 0, nulTerm < 0 ? MessageCapacity : nulTerm);

            entries[count++] = new PostMortemEntry(ts, op, msg);
        }

        Array.Resize(ref entries, count);
        return entries;
    }

    public void Dispose()
    {
        _accessor.Dispose();
        _mmf.Dispose();
    }
}

public readonly struct PostMortemEntry
{
    public long UtcUnixMs { get; }
    public long OpKind { get; }
    public string Message { get; }

    public PostMortemEntry(long utcUnixMs, long opKind, string message)
    {
        UtcUnixMs = utcUnixMs;
        OpKind = opKind;
        Message = message;
    }
}
