namespace XFEEDToolBox.Core.Services;
public enum ActivitySeverity { Status,Information,Warning,Error }
public sealed record ActivityEntry(DateTimeOffset Time,string Kind,string Message)
{
private static long nextId;
public long Id{get;init;}=Interlocked.Increment(ref nextId);
public DateTimeOffset LastSeen{get;init;}=Time;
public int Occurrences{get;init;}=1;
public ActivitySeverity Severity{get;init;}=ActivitySeverity.Information;
public string Code{get;init;}="";
public bool Resolved{get;init;}
public bool Superseded{get;init;}
public DateTimeOffset? ClosedAt{get;init;}
public string FirstMessage{get;init;}=Message;
public string FirstDiagnostic{get;init;}="";
public string LastDiagnostic{get;init;}="";
public string TimeDisplay=>(ClosedAt??LastSeen).ToLocalTime().ToString("HH:mm:ss");
public string SeverityDisplay=>Resolved?"已恢复":Superseded?"已更新":Severity switch{ActivitySeverity.Status=>"状态",ActivitySeverity.Warning=>"警告",ActivitySeverity.Error=>"错误",_=>"信息"};
public string RepetitionDisplay=>Occurrences>1?$"出现 {Occurrences:N0} 次 · 首次 {Time.ToLocalTime():HH:mm:ss} · 最近 {LastSeen.ToLocalTime():HH:mm:ss}":"";
public bool HasDiagnostic=>FirstDiagnostic.Length>0;
public string DiagnosticDisplay=>$"标识:{Code}\n首次:{Time.ToLocalTime():yyyy-MM-dd HH:mm:ss}\n最近:{LastSeen.ToLocalTime():yyyy-MM-dd HH:mm:ss}\n次数:{Occurrences:N0}\n首次摘要:{FirstMessage}\n\n{FirstDiagnostic}"+
(ClosedAt is {} closed?$"\n本轮结束:{closed.ToLocalTime():yyyy-MM-dd HH:mm:ss}":"")+
(LastDiagnostic.Length>0&&LastDiagnostic!=FirstDiagnostic?$"\n\n最近诊断:\n{LastDiagnostic}":"");
}
/// One visible record per active failure, retaining its first/last observation and diagnostics.
public sealed class ActivityLog(Func? utcNow=null,Action? persist=null)
{
private readonly object sync=new();
private readonly LinkedList rows=new();
private readonly Dictionary Node,string Signature,DateTimeOffset Persisted)> active=new(StringComparer.Ordinal);
private DateTimeOffset Now=>utcNow?.Invoke()??DateTimeOffset.UtcNow;
public IReadOnlyList Snapshot(int limit=200){lock(sync)return rows.Take(Math.Clamp(limit,0,300)).ToArray();}
public void Append(string kind,string message)
{
lock(sync)
{
var entry=new ActivityEntry(Now,kind,message);Add(entry);Persist(entry);
}
}
public void Report(string code,string kind,string message,ActivitySeverity severity,string diagnostic="",string? identity=null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(code);
lock(sync)
{
var now=Now;var signature=identity??message;
if(active.TryGetValue(code,out var previous)&&previous.Signature==signature)
{
var escalated=severity>previous.Node.Value.Severity;
var updated=previous.Node.Value with{LastSeen=now,Occurrences=previous.Node.Value.Occurrences+1,Kind=kind,Message=message,Severity=escalated?severity:previous.Node.Value.Severity,LastDiagnostic=diagnostic.Length>0?diagnostic:previous.Node.Value.LastDiagnostic};
previous.Node.Value=updated;rows.Remove(previous.Node);rows.AddFirst(previous.Node);
var due=now-previous.Persisted>=TimeSpan.FromMinutes(1);
active[code]=(previous.Node,signature,due?now:previous.Persisted);
if(due||escalated)Persist(updated);
return;
}
if(active.Remove(code,out var replaced))
{replaced.Node.Value=replaced.Node.Value with{Superseded=true,ClosedAt=now};Persist(replaced.Node.Value);}
var entry=new ActivityEntry(now,kind,message){Code=code,Severity=severity,FirstDiagnostic=diagnostic,LastDiagnostic=diagnostic};
var node=Add(entry);active[code]=(node,signature,now);Persist(entry);
}
}
public bool Resolve(string code,string kind,string message)
{
lock(sync)
{
if(!active.Remove(code,out var previous))return false;
var entry=previous.Node.Value with{ClosedAt=Now,Resolved=true,Message=message,Kind=kind};
previous.Node.Value=entry;rows.Remove(previous.Node);rows.AddFirst(previous.Node);Persist(entry);return true;
}
}
public void Flush(){lock(sync)foreach(var value in active.Values)Persist(value.Node.Value);}
private LinkedListNode Add(ActivityEntry entry)
{
var node=rows.AddFirst(entry);
while(rows.Count>300)
{
var last=rows.Last!;rows.RemoveLast();
if(last.Value.Code.Length>0)Persist(last.Value);
if(last.Value.Code.Length>0&&active.TryGetValue(last.Value.Code,out var current)&&ReferenceEquals(current.Node,last))active.Remove(last.Value.Code);
}
return node;
}
private void Persist(ActivityEntry entry)
{try{persist?.Invoke(entry);}catch(IOException){}catch(UnauthorizedAccessException){}}
}