using System.Text;

namespace NATS.Server.Raft;

/// <summary>
/// Determines which log entries are eligible for compaction.
/// Go reference: raft.go compactLog / WAL compact.
/// </summary>
public enum CompactionPolicy
{
    /// <summary>No automatic compaction. Only manual compaction via CompactLogAsync is performed.</summary>
    None,

    /// <summary>Compact when the total number of log entries exceeds MaxEntries.</summary>
    ByCount,

    /// <summary>Compact when the estimated total size of log entries exceeds MaxSizeBytes.</summary>
    BySize,

    /// <summary>Compact entries whose creation time is older than MaxAge (based on Timestamp).</summary>
    ByAge,
}

/// <summary>
/// Configuration for log compaction behavior on a RaftNode.
/// Go reference: raft.go compactLog / WAL compact thresholds.
/// </summary>
public sealed class CompactionOptions
{
    /// <summary>The compaction policy to apply. Defaults to <see cref="CompactionPolicy.None"/>.</summary>
    public CompactionPolicy Policy { get; init; } = CompactionPolicy.None;

    /// <summary>
    /// For <see cref="CompactionPolicy.ByCount"/>: keep at most this many entries in the log.
    /// Entries beyond this count (oldest first) are compacted.
    /// Default: 10,000.
    /// </summary>
    public int MaxEntries { get; init; } = 10_000;

    /// <summary>
    /// For <see cref="CompactionPolicy.BySize"/>: compact oldest entries once the total
    /// estimated byte size of all entries exceeds this threshold.
    /// Default: 100 MB.
    /// </summary>
    public long MaxSizeBytes { get; init; } = 100L * 1024 * 1024;

    /// <summary>
    /// For <see cref="CompactionPolicy.ByAge"/>: compact entries whose <see cref="RaftLogEntry.Timestamp"/>
    /// is older than this duration relative to <see cref="DateTime.UtcNow"/>.
    /// Default: 1 hour.
    /// </summary>
    public TimeSpan MaxAge { get; init; } = TimeSpan.FromHours(1);

    /// <summary>
    /// Computes the index up to which the log should be compacted given the current options.
    /// Returns -1 when no compaction should occur.
    ///
    /// Safety guarantee: the returned index never exceeds <paramref name="appliedIndex"/>
    /// so that committed entries are never discarded before they are applied.
    ///
    /// Go reference: raft.go compactLog (threshold checks before truncation).
    /// </summary>
    /// <param name="log">The current RAFT log.</param>
    /// <param name="appliedIndex">The highest index that has been applied to the state machine.
    /// Compaction never proceeds past this point.</param>
    /// <returns>
    /// The log index up to which compaction should occur (inclusive),
    /// or -1 if nothing should be compacted.
    /// </returns>
    public long ComputeCompactionIndex(RaftLog log, long appliedIndex)
    {
        if (Policy == CompactionPolicy.None)
            return -1;

        var entries = log.Entries;
        if (entries.Count == 0)
            return -1;

        long candidateIndex = Policy switch
        {
            CompactionPolicy.ByCount => ComputeByCount(entries),
            CompactionPolicy.BySize  => ComputeBySize(entries),
            CompactionPolicy.ByAge   => ComputeByAge(entries),
            _                        => -1,
        };

        if (candidateIndex < 0)
            return -1;

        // Safety guard: never compact past the applied index.
        return Math.Min(candidateIndex, appliedIndex);
    }

    // ---------------------------------------------------------------------------
    // Private policy implementations
    // ---------------------------------------------------------------------------

    private long ComputeByCount(IReadOnlyList<RaftLogEntry> entries)
    {
        // Keep at most MaxEntries. If entries.Count <= MaxEntries there is nothing to do.
        var excess = entries.Count - MaxEntries;
        if (excess <= 0)
            return -1;

        // Compact the oldest `excess` entries — the entry at position (excess - 1).
        return entries[excess - 1].Index;
    }

    private long ComputeBySize(IReadOnlyList<RaftLogEntry> entries)
    {
        // Estimate total size: each entry contributes (index 8 bytes + term 4 bytes + command UTF-8 bytes).
        long totalBytes = 0;
        foreach (var e in entries)
            totalBytes += 12L + Encoding.UTF8.GetByteCount(e.Command);

        if (totalBytes <= MaxSizeBytes)
            return -1;

        // Walk from the oldest entry, accumulating bytes removed, until we are under threshold.
        long bytesRemoved = 0;
        long overBy = totalBytes - MaxSizeBytes;
        for (var i = 0; i < entries.Count; i++)
        {
            bytesRemoved += 12L + Encoding.UTF8.GetByteCount(entries[i].Command);
            if (bytesRemoved >= overBy)
                return entries[i].Index;
        }

        // Compact everything if somehow we still haven't reached the threshold.
        return entries[^1].Index;
    }

    private long ComputeByAge(IReadOnlyList<RaftLogEntry> entries)
    {
        var cutoff = DateTime.UtcNow - MaxAge;
        long lastOldIndex = -1;

        foreach (var e in entries)
        {
            if (e.Timestamp < cutoff)
                lastOldIndex = e.Index;
            else
                break; // Entries are in index order; once we hit a fresh entry we are done.
        }

        return lastOldIndex;
    }
}
