using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace Jellyfin.Plugin.RecentlyAddedGuard.Core;
///
/// One restored (or, in dry-run mode, detected) upgrade.
///
/// The corrected item.
/// The display name.
/// The "date added" the upgrade brought along (UTC).
/// The first "date added" from the ledger (UTC).
/// When the correction happened (UTC).
/// true if only detected, not applied.
public sealed record CorrectionRecord(
Guid ItemId,
string Name,
DateTime PreviousDate,
DateTime RestoredDate,
DateTime CorrectedAt,
bool DryRun);
///
/// In-memory ring buffer of the most recent corrections, for the configuration page.
///
public sealed class CorrectionLog
{
///
/// The default number of kept corrections.
///
public const int DefaultCapacity = 100;
private readonly Lock _sync = new();
private readonly List _records;
private readonly int _capacity;
private long _corrections;
private long _dryRunDetections;
///
/// Initializes a new instance of the class.
///
/// The number of kept corrections.
public CorrectionLog(int capacity = DefaultCapacity)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
_capacity = capacity;
_records = new List(capacity);
}
///
/// Gets the number of applied corrections since the server started.
///
public long CorrectionsSinceStart => Interlocked.Read(ref _corrections);
///
/// Gets the number of upgrades detected in dry-run mode since the server started.
///
public long DryRunDetectionsSinceStart => Interlocked.Read(ref _dryRunDetections);
///
/// Records a correction, dropping the oldest one when full. Restoring the same date of the same item again
/// (Jellyfin saved the item with its new date once more, e.g. from a metadata refresh that was already running)
/// replaces the earlier record and is not counted twice.
///
/// The correction.
/// true for a new correction, false for a repeated one.
public bool Add(CorrectionRecord record)
{
ArgumentNullException.ThrowIfNull(record);
lock (_sync)
{
var existing = _records.FindIndex(r => r.ItemId == record.ItemId
&& r.RestoredDate == record.RestoredDate
&& r.DryRun == record.DryRun);
if (existing >= 0)
{
_records.RemoveAt(existing);
_records.Add(record);
return false;
}
if (_records.Count == _capacity)
{
_records.RemoveAt(0);
}
_records.Add(record);
}
if (record.DryRun)
{
Interlocked.Increment(ref _dryRunDetections);
}
else
{
Interlocked.Increment(ref _corrections);
}
return true;
}
///
/// Returns the kept corrections, newest first.
///
/// The corrections.
public IReadOnlyList GetRecent()
{
lock (_sync)
{
return _records.AsEnumerable().Reverse().ToList();
}
}
}