namespace NATS.Server.Raft;

public sealed class RaftLog
{
    private readonly List<RaftLogEntry> _entries = [];
    private long _baseIndex;

    public IReadOnlyList<RaftLogEntry> Entries => _entries;

    /// <summary>
    /// The base index after compaction. Entries before this index have been removed.
    /// </summary>
    public long BaseIndex => _baseIndex;

    public RaftLogEntry Append(int term, string command)
    {
        var entry = new RaftLogEntry(_baseIndex + _entries.Count + 1, term, command);
        _entries.Add(entry);
        return entry;
    }

    /// <summary>
    /// Appends an entry with an explicit timestamp. Used in tests and by the
    /// <see cref="CompactionPolicy.ByAge"/> policy to set controlled creation times.
    /// </summary>
    public RaftLogEntry AppendWithTimestamp(int term, string command, DateTime timestamp)
    {
        var entry = new RaftLogEntry(_baseIndex + _entries.Count + 1, term, command)
        {
            Timestamp = timestamp,
        };
        _entries.Add(entry);
        return entry;
    }

    public void AppendReplicated(RaftLogEntry entry)
    {
        if (_entries.Any(e => e.Index == entry.Index))
            return;

        _entries.Add(entry);
    }

    public void ReplaceWithSnapshot(RaftSnapshot snapshot)
    {
        _entries.Clear();
        _baseIndex = snapshot.LastIncludedIndex;
    }

    /// <summary>
    /// Removes all log entries with index &lt;= upToIndex and advances the base index accordingly.
    /// This is log compaction: entries covered by a snapshot are discarded.
    /// Go reference: raft.go WAL compact / compactLog.
    /// </summary>
    public void Compact(long upToIndex)
    {
        var removeCount = _entries.Count(e => e.Index <= upToIndex);
        if (removeCount > 0)
        {
            _entries.RemoveRange(0, removeCount);
            _baseIndex = upToIndex;
        }
    }

    public async Task PersistAsync(string path, CancellationToken ct)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        var model = new PersistedLog
        {
            BaseIndex = _baseIndex,
            Entries = [.. _entries],
        };
        await File.WriteAllTextAsync(path, System.Text.Json.JsonSerializer.Serialize(model), ct);
    }

    public static async Task<RaftLog> LoadAsync(string path, CancellationToken ct)
    {
        var log = new RaftLog();
        if (!File.Exists(path))
            return log;

        var json = await File.ReadAllTextAsync(path, ct);
        var model = System.Text.Json.JsonSerializer.Deserialize<PersistedLog>(json) ?? new PersistedLog();
        log._baseIndex = model.BaseIndex;
        log._entries.AddRange(model.Entries);
        return log;
    }

    private sealed class PersistedLog
    {
        public long BaseIndex { get; set; }
        public List<RaftLogEntry> Entries { get; set; } = [];
    }
}

/// <summary>
/// A single RAFT log entry. Timestamp records when the entry was created (UTC) and
/// is used by the <see cref="CompactionPolicy.ByAge"/> compaction policy.
/// </summary>
public sealed record RaftLogEntry(long Index, int Term, string Command)
{
    /// <summary>
    /// UTC creation time. Defaults to <see cref="DateTime.UtcNow"/> at append time.
    /// Stored as a JSON-serializable property so it survives persistence round-trips.
    /// </summary>
    public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}
