using System.Text.Json; using Journal.Core.Models; namespace Journal.Core.Repositories; public class FileFragmentRepository : IFragmentRepository { private readonly Lock _lock = new(); private readonly string _storagePath; private readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true }; private readonly List _store; public FileFragmentRepository() : this(storagePath: null) { } public FileFragmentRepository(string? storagePath) { _storagePath = ResolveStoragePath(storagePath); _store = LoadStore(_storagePath); } public Task> GetAllAsync() { lock (_lock) { return Task.FromResult(_store.ToList()); } } public Task GetByIdAsync(Guid id) { lock (_lock) { return Task.FromResult(_store.FirstOrDefault(f => f.Id == id)); } } public Task AddAsync(Fragment fragment) { ArgumentNullException.ThrowIfNull(fragment); lock (_lock) { Normalize(fragment); _store.Add(fragment); SaveStoreLocked(); } return Task.CompletedTask; } public Task RemoveAsync(Guid id) { lock (_lock) { var item = _store.FirstOrDefault(f => f.Id == id); if (item is null) return Task.FromResult(false); var removed = _store.Remove(item); if (removed) SaveStoreLocked(); return Task.FromResult(removed); } } public Task UpdateAsync( Guid id, string? type = null, string? description = null, IEnumerable? tags = null, DateTimeOffset? time = null) { lock (_lock) { var item = _store.FirstOrDefault(f => f.Id == id); if (item is null) return Task.FromResult(false); if (type != null) { if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type cannot be empty", nameof(type)); item.Type = type.Trim(); } if (description != null) { if (string.IsNullOrWhiteSpace(description)) throw new ArgumentException("Description cannot be empty", nameof(description)); item.Description = description.Trim(); } if (tags != null) { item.Tags = [.. tags.Where(t => !string.IsNullOrWhiteSpace(t)) .Select(t => t.Trim())]; } if (time.HasValue) item.Time = time.Value; SaveStoreLocked(); return Task.FromResult(true); } } public Task> GetByTagAsync(string tag) { var q = tag?.Trim(); if (string.IsNullOrWhiteSpace(q)) return Task.FromResult(new List()); lock (_lock) { var items = _store .Where(f => f.Tags.Any(t => t.Contains(q, StringComparison.OrdinalIgnoreCase))) .ToList(); return Task.FromResult(items); } } public Task> GetByTypeAsync(string type) { var q = type?.Trim(); if (string.IsNullOrWhiteSpace(q)) return Task.FromResult(new List()); lock (_lock) { var items = _store .Where(f => f.Type.Contains(q, StringComparison.OrdinalIgnoreCase)) .ToList(); return Task.FromResult(items); } } public Task> SearchAsync(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null) { var qType = type?.Trim(); var qTag = tag?.Trim(); lock (_lock) { IEnumerable results = _store; if (!string.IsNullOrWhiteSpace(qType)) results = results.Where(f => f.Type.Contains(qType, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrWhiteSpace(qTag)) results = results.Where(f => f.Tags.Any(t => t.Contains(qTag, StringComparison.OrdinalIgnoreCase))); if (timeAfter.HasValue) results = results.Where(f => f.Time > timeAfter.Value); return Task.FromResult(results.ToList()); } } private static string ResolveStoragePath(string? storagePath) { var configured = storagePath; if (string.IsNullOrWhiteSpace(configured)) configured = Environment.GetEnvironmentVariable("JOURNAL_FRAGMENT_STORE_PATH"); if (string.IsNullOrWhiteSpace(configured)) configured = Path.Combine(Environment.CurrentDirectory, ".journal-sidecar", "fragments.json"); return Path.GetFullPath(configured); } private List LoadStore(string path) { var directory = Path.GetDirectoryName(path); if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory); if (!File.Exists(path)) return []; var json = File.ReadAllText(path); if (string.IsNullOrWhiteSpace(json)) return []; var docs = JsonSerializer.Deserialize>(json, _jsonOptions) ?? []; return [.. docs.Select(d => new Fragment(d.Id, d.Type, d.Description, d.Time, d.Tags))]; } private void SaveStoreLocked() { var directory = Path.GetDirectoryName(_storagePath); if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory); var docs = _store.Select(f => new FragmentDocument { Id = f.Id, Type = f.Type, Description = f.Description, Time = f.Time, Tags = [.. f.Tags] }).ToList(); var json = JsonSerializer.Serialize(docs, _jsonOptions); var tempPath = _storagePath + ".tmp"; File.WriteAllText(tempPath, json); File.Copy(tempPath, _storagePath, overwrite: true); File.Delete(tempPath); } private static void Normalize(Fragment fragment) { fragment.Type = fragment.Type.Trim(); fragment.Description = fragment.Description.Trim(); fragment.Tags = [.. fragment.Tags.Where(t => !string.IsNullOrWhiteSpace(t)) .Select(t => t.Trim())]; } private sealed class FragmentDocument { public Guid Id { get; init; } public string Type { get; init; } = ""; public string Description { get; init; } = ""; public DateTimeOffset Time { get; init; } public List Tags { get; init; } = []; } }