using Journal.Core.Dtos; using Journal.Core.Repositories; namespace Journal.Core.Services.Entries; public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileService { private readonly IEntryFileRepository _repo = repo ?? throw new ArgumentNullException(nameof(repo)); public IReadOnlyList ListEntries(string dataDirectory) { return [.. _repo.ListMarkdownFiles(dataDirectory) .Where(path => !EntryFileNaming.IsTemplateFileName(_repo.GetFileName(path))) .Select(path => new EntryListItem( FileName: _repo.GetFileName(path), FilePath: _repo.GetFullPath(path)))]; } public IReadOnlyList ListTemplates(string dataDirectory) { return [.. _repo.ListMarkdownFiles(dataDirectory) .Where(path => EntryFileNaming.IsTemplateFileName(_repo.GetFileName(path))) .Select(path => new EntryListItem( FileName: _repo.GetFileName(path), FilePath: _repo.GetFullPath(path)))]; } public EntryLoadResult LoadEntry(string filePath) { var normalizedPath = _repo.GetFullPath(filePath); if (!_repo.FileExists(normalizedPath)) throw new FileNotFoundException($"Entry file not found: {normalizedPath}"); var rawContent = HtmlSanitizer.StripRichHtml(_repo.ReadFile(normalizedPath)); var fileStem = _repo.GetFileNameWithoutExtension(normalizedPath); var entry = JournalParser.ParseJournalContent(rawContent, fileStem); return new EntryLoadResult( FileName: _repo.GetFileName(normalizedPath), FilePath: normalizedPath, Entry: entry.ToDto()); } public EntryTemplateLoadResult LoadTemplate(string filePath) { var normalizedPath = _repo.GetFullPath(filePath); if (!_repo.FileExists(normalizedPath)) throw new FileNotFoundException($"Template file not found: {normalizedPath}"); var fileName = _repo.GetFileName(normalizedPath); if (!EntryFileNaming.IsTemplateFileName(fileName)) throw new ArgumentException("Template file name must end with .template.md."); var rawContent = HtmlSanitizer.StripRichHtml(_repo.ReadFile(normalizedPath)); return new EntryTemplateLoadResult(fileName, normalizedPath, rawContent); } public EntrySaveResult SaveEntry(EntrySavePayload payload, string defaultDataDirectory) { var targetPath = ResolveTargetPath(payload.FilePath, payload.FileName, defaultDataDirectory); var mode = string.IsNullOrWhiteSpace(payload.Mode) ? "Daily" : payload.Mode.Trim(); var sanitizedContent = HtmlSanitizer.StripRichHtml(payload.Content ?? ""); _repo.EnsureDirectory(targetPath); if (string.Equals(mode, "Overwrite", StringComparison.OrdinalIgnoreCase)) { _repo.WriteFile(targetPath, sanitizedContent); return new EntrySaveResult(targetPath); } if (string.Equals(mode, "Fragment", StringComparison.OrdinalIgnoreCase)) { _repo.AppendFile(targetPath, "\n\n" + sanitizedContent.Trim()); return new EntrySaveResult(targetPath); } string finalContent; if (_repo.FileExists(targetPath)) { var existingContent = _repo.ReadFile(targetPath); var fileStem = _repo.GetFileNameWithoutExtension(targetPath); var existingEntry = JournalParser.ParseJournalContent(existingContent, fileStem); var newEntryData = JournalParser.ParseJournalContent(sanitizedContent, fileStem); existingEntry.MergeWith(newEntryData); finalContent = existingEntry.ToMarkdown(); } else { finalContent = sanitizedContent; } _repo.WriteFile(targetPath, finalContent); return new EntrySaveResult(targetPath); } public EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload, string defaultDataDirectory) { ArgumentNullException.ThrowIfNull(payload); if (string.IsNullOrWhiteSpace(payload.Name)) throw new ArgumentException("Template name is required."); var directory = string.IsNullOrWhiteSpace(payload.DataDirectory) ? defaultDataDirectory : payload.DataDirectory; var targetPath = ResolveTemplatePath(payload.FilePath, payload.Name, directory); var fileName = _repo.GetFileName(targetPath); if (!EntryFileNaming.IsTemplateFileName(fileName)) throw new ArgumentException("Template file name must end with .template.md."); var sanitizedContent = HtmlSanitizer.StripRichHtml(payload.Content ?? ""); _repo.EnsureDirectory(targetPath); _repo.WriteFile(targetPath, sanitizedContent); return new EntrySaveResult(targetPath); } public bool DeleteEntry(string filePath) { var normalizedPath = _repo.GetFullPath(filePath); if (!_repo.FileExists(normalizedPath)) return false; _repo.DeleteFile(normalizedPath); return true; } public bool DeleteTemplate(string filePath) { var normalizedPath = _repo.GetFullPath(filePath); if (!_repo.FileExists(normalizedPath)) return false; var fileName = _repo.GetFileName(normalizedPath); if (!EntryFileNaming.IsTemplateFileName(fileName)) return false; _repo.DeleteFile(normalizedPath); return true; } private string ResolveTargetPath(string? filePath, string? fileName, string defaultDataDirectory) { if (!string.IsNullOrWhiteSpace(filePath)) return _repo.GetFullPath(filePath); var name = !string.IsNullOrWhiteSpace(fileName) ? SanitizeFileName(fileName) : $"{DateTime.Now:yyyy-MM-dd}"; return _repo.GetFullPath(Path.Combine(defaultDataDirectory, $"{name}.md")); } private string ResolveTemplatePath(string? filePath, string templateName, string defaultDataDirectory) { if (!string.IsNullOrWhiteSpace(filePath)) return _repo.GetFullPath(filePath); var name = SanitizeFileName(templateName); return _repo.GetFullPath(Path.Combine(defaultDataDirectory, $"{name}{EntryFileNaming.TemplateSuffix}")); } private static string SanitizeFileName(string name) { var trimmed = name.Trim(); if (trimmed.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) trimmed = trimmed[..^3]; var invalid = Path.GetInvalidFileNameChars(); var sanitized = new string(trimmed.Select(c => Array.IndexOf(invalid, c) >= 0 ? '_' : c).ToArray()); return string.IsNullOrWhiteSpace(sanitized) ? "untitled" : sanitized; } }