using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace ytLive.Services;
///
/// TASK 65: where clips live and how many survive.
///
/// Not clip management. There is no gallery, no naming, no per-clip decision. A clip is
/// written to clips\, the oldest is deleted when the next one lands, and the only knobs are the
/// length (10 or 15 s) and how many to keep (3, 4 or 5). Anything richer belongs to a video editor.
///
public sealed class ClipLibrary
{
public const string Extension = ".mp4";
public const string FilePrefix = "clip-";
private readonly Func _folder;
public ClipLibrary(Func folder) => _folder = folder;
/// Bytes the ring should hold for a length and bitrate, with 25% headroom for bitrate
/// movement. This is a memory cap, not a duration — the ring trims whole fragments.
public static long RingBytesFor(int seconds, int bitrateKbps)
=> Math.Max(1, seconds) * (long)bitrateKbps * 1000 / 8 * 5 / 4;
/// Writes a clip and prunes to ; returns the file path, or null when
/// there was nothing to write (an empty snapshot must never become a zero-byte file).
public string? Save(byte[] mp4Bytes, DateTime nowLocal, int keep)
{
if (mp4Bytes.Length == 0) return null;
try
{
var folder = EnsureFolder();
var path = Path.Combine(folder, $"{FilePrefix}{nowLocal:yyyyMMdd-HHmmss}{Extension}");
var unique = path;
for (var n = 2; File.Exists(unique); n++)
unique = Path.Combine(folder, $"{FilePrefix}{nowLocal:yyyyMMdd-HHmmss}-{n}{Extension}");
File.WriteAllBytes(unique, mp4Bytes);
Prune(keep);
return unique;
}
catch (Exception ex)
{
Helpers.AppLog.Write(ex, "Clip save failed");
return null;
}
}
/// Newest first. The replay layer's playlist is this list, so its order matters.
public IReadOnlyList Recent()
{
try
{
var folder = EnsureFolder();
return Directory.EnumerateFiles(folder, FilePrefix + "*" + Extension)
.OrderByDescending(p => p, StringComparer.OrdinalIgnoreCase)
.ToList();
}
catch (Exception ex)
{
Helpers.AppLog.Write(ex, "Clip list failed");
return Array.Empty();
}
}
/// Deletes everything past the retention count. Missing folders are not an error.
public int Prune(int keep)
{
if (keep < 1) keep = 1;
var removed = 0;
foreach (var stale in Recent().Skip(keep))
{
try
{
File.Delete(stale);
removed++;
}
catch (Exception ex)
{
Helpers.AppLog.Write(ex, $"Clip prune failed: {stale}");
}
}
return removed;
}
/// The folder beside the recordings, created on demand.
public string EnsureFolder()
{
var folder = Path.Combine(_folder(), "clips");
Directory.CreateDirectory(folder);
return folder;
}
}