using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Text.Json; using Journal.Core.Dtos; using Journal.Core.Models; using Journal.Core.Services.Ai; using Journal.Core.Services.Config; using Journal.Core.Services.Database; using Journal.Core.Services.Entries; using Journal.Core.Services.Fragments; using Journal.Core.Services.Lists; using Journal.Core.Services.Logging; using Journal.Core.Services.Speech; using Journal.Core.Services.Todos; using Journal.Core.Services.Vault; namespace Journal.Core; public class Entry( IFragmentService fragments, IEntrySearchService entrySearch, IVaultStorageService vaultStorage, IJournalDatabaseService database, IDatabaseSessionService databaseSession, IJournalConfigService config, IAiService ai, ISpeechBridgeService speech, IEntryFileService entryFiles, IListService lists, ITodoService todos, CommandLogger logger) { private readonly IFragmentService _fragments = fragments; private readonly IEntrySearchService _entrySearch = entrySearch; private readonly IVaultStorageService _vaultStorage = vaultStorage; private readonly IJournalDatabaseService _database = database; private readonly IDatabaseSessionService _databaseSession = databaseSession; private readonly IJournalConfigService _config = config; private readonly IAiService _ai = ai; private readonly ISpeechBridgeService _speech = speech; private readonly IEntryFileService _entryFiles = entryFiles; private readonly IListService _lists = lists; private readonly ITodoService _todos = todos; private readonly CommandLogger _logger = logger; private static readonly HashSet VaultSyncActions = new(StringComparer.Ordinal) { "entries.save", "entries.delete", "templates.save", "templates.delete", "fragments.create", "fragments.update", "fragments.delete", "lists.create", "lists.update", "lists.delete", "todos.create", "todos.update", "todos.delete", "todos.items.create", "todos.items.update", "todos.items.delete" }; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; public async Task RunAsync() { string? line; while ((line = Console.ReadLine()) is not null) { var response = await HandleCommandAsync(line); Console.WriteLine(response); } } public async Task HandleCommandAsync(string json) { if (string.IsNullOrWhiteSpace(json)) return Error("Invalid command"); Command? cmd; try { cmd = JsonSerializer.Deserialize(json, JsonOptions); } catch (JsonException) { return Error("Invalid command JSON"); } if (cmd is null || string.IsNullOrWhiteSpace(cmd.Action)) return Error("Invalid command"); var action = cmd.Action.Trim(); var correlationId = string.IsNullOrWhiteSpace(cmd.CorrelationId) ? Guid.NewGuid().ToString("N") : cmd.CorrelationId.Trim(); CommandLogger.LogStart(action, correlationId, cmd.Payload); object? result; try { switch (action) { case "fragments.list": result = _fragments.GetAll(); break; case "fragments.get": if (!Guid.TryParse(cmd.Id, out var getId)) return Error("Invalid or missing id"); result = _fragments.GetById(getId); break; case "fragments.create": var createDto = DeserializePayload(cmd.Payload); if (createDto is null) return Error("Missing or invalid payload"); result = _fragments.Create(createDto); break; case "fragments.update": if (!Guid.TryParse(cmd.Id, out var updateId)) return Error("Invalid or missing id"); var updateDto = DeserializePayload(cmd.Payload); if (updateDto is null) return Error("Missing or invalid payload"); result = _fragments.Update(updateId, updateDto); break; case "fragments.delete": if (!Guid.TryParse(cmd.Id, out var deleteId)) return Error("Invalid or missing id"); result = _fragments.Remove(deleteId); break; case "fragments.search": result = _fragments.Search(cmd.Type, cmd.Tag); break; // ── Lists ──────────────────────────────────────── case "lists.list": result = _lists.GetAll(); break; case "lists.get": if (!Guid.TryParse(cmd.Id, out var getListId)) return Error("Invalid or missing id"); result = _lists.GetById(getListId); break; case "lists.create": var createListDto = DeserializePayload(cmd.Payload); if (createListDto is null) return Error("Missing or invalid payload"); result = _lists.Create(createListDto); break; case "lists.update": if (!Guid.TryParse(cmd.Id, out var updateListId)) return Error("Invalid or missing id"); var updateListDto = DeserializePayload(cmd.Payload); if (updateListDto is null) return Error("Missing or invalid payload"); result = _lists.Update(updateListId, updateListDto); break; case "lists.delete": if (!Guid.TryParse(cmd.Id, out var deleteListId)) return Error("Invalid or missing id"); result = _lists.Remove(deleteListId); break; // ── Todos ──────────────────────────────────────── case "todos.list": result = _todos.GetAllLists(); break; case "todos.get": if (!Guid.TryParse(cmd.Id, out var getTodoListId)) return Error("Invalid or missing id"); result = _todos.GetListById(getTodoListId); break; case "todos.create": var createTodoListDto = DeserializePayload(cmd.Payload); if (createTodoListDto is null) return Error("Missing or invalid payload"); result = _todos.CreateList(createTodoListDto); break; case "todos.update": if (!Guid.TryParse(cmd.Id, out var updateTodoListId)) return Error("Invalid or missing id"); var updateTodoListDto = DeserializePayload(cmd.Payload); if (updateTodoListDto is null) return Error("Missing or invalid payload"); result = _todos.UpdateList(updateTodoListId, updateTodoListDto); break; case "todos.delete": if (!Guid.TryParse(cmd.Id, out var deleteTodoListId)) return Error("Invalid or missing id"); result = _todos.RemoveList(deleteTodoListId); break; case "todos.items.create": var createItemDto = DeserializePayload(cmd.Payload); if (createItemDto is null) return Error("Missing or invalid payload"); result = _todos.CreateItem(createItemDto); break; case "todos.items.update": if (!Guid.TryParse(cmd.Id, out var updateItemId)) return Error("Invalid or missing id"); var updateItemDto = DeserializePayload(cmd.Payload); if (updateItemDto is null) return Error("Missing or invalid payload"); result = _todos.UpdateItem(updateItemId, updateItemDto); break; case "todos.items.delete": if (!Guid.TryParse(cmd.Id, out var deleteItemId)) return Error("Invalid or missing id"); result = _todos.RemoveItem(deleteItemId); break; case "search.entries": var searchPayload = DeserializePayload(cmd.Payload); if (searchPayload is null || string.IsNullOrWhiteSpace(searchPayload.DataDirectory)) return Error("Missing or invalid payload"); var searchRequest = new EntrySearchRequestDto( DataDirectory: searchPayload.DataDirectory, Query: searchPayload.Query, Section: searchPayload.Section, StartDate: searchPayload.StartDate, EndDate: searchPayload.EndDate, Tags: searchPayload.Tags, Types: searchPayload.Types, Checked: searchPayload.Checked, Unchecked: searchPayload.Unchecked); result = await _entrySearch.SearchEntriesAsync(searchRequest); break; case "entries.list": var listPayload = DeserializePayload(cmd.Payload); var listDataDirectory = !string.IsNullOrWhiteSpace(listPayload?.DataDirectory) ? listPayload.DataDirectory : _config.Current.DataDirectory; result = _entryFiles.ListEntries(listDataDirectory); break; case "templates.list": var templateListPayload = DeserializePayload(cmd.Payload); var templateListDirectory = !string.IsNullOrWhiteSpace(templateListPayload?.DataDirectory) ? templateListPayload.DataDirectory : _config.Current.DataDirectory; result = _entryFiles.ListTemplates(templateListDirectory); break; case "entries.load": var loadEntryPayload = DeserializePayload(cmd.Payload); if (loadEntryPayload is null || string.IsNullOrWhiteSpace(loadEntryPayload.FilePath)) return Error("Missing or invalid payload"); result = _entryFiles.LoadEntry(loadEntryPayload.FilePath); break; case "templates.load": var loadTemplatePayload = DeserializePayload(cmd.Payload); if (loadTemplatePayload is null || string.IsNullOrWhiteSpace(loadTemplatePayload.FilePath)) return Error("Missing or invalid payload"); result = _entryFiles.LoadTemplate(loadTemplatePayload.FilePath); break; case "entries.save": var saveEntryPayload = DeserializePayload(cmd.Payload); if (saveEntryPayload is null || string.IsNullOrWhiteSpace(saveEntryPayload.Content)) return Error("Missing or invalid payload"); result = _entryFiles.SaveEntry(saveEntryPayload, _config.Current.DataDirectory); break; case "templates.save": var saveTemplatePayload = DeserializePayload(cmd.Payload); if (saveTemplatePayload is null || string.IsNullOrWhiteSpace(saveTemplatePayload.Name)) return Error("Missing or invalid payload"); result = _entryFiles.SaveTemplate(saveTemplatePayload, _config.Current.DataDirectory); break; case "entries.delete": var deleteEntryPayload = DeserializePayload(cmd.Payload); if (deleteEntryPayload is null || string.IsNullOrWhiteSpace(deleteEntryPayload.FilePath)) return Error("Missing or invalid payload"); result = _entryFiles.DeleteEntry(deleteEntryPayload.FilePath); break; case "templates.delete": var deleteTemplatePayload = DeserializePayload(cmd.Payload); if (deleteTemplatePayload is null || string.IsNullOrWhiteSpace(deleteTemplatePayload.FilePath)) return Error("Missing or invalid payload"); result = _entryFiles.DeleteTemplate(deleteTemplatePayload.FilePath); break; case "config.get": result = _config.Current; break; case "ai.health": result = await _ai.HealthAsync(); break; case "ai.summarize_entry": var summarizeEntryPayload = DeserializePayload(cmd.Payload); if (summarizeEntryPayload is null || string.IsNullOrWhiteSpace(summarizeEntryPayload.Content)) return Error("Missing or invalid payload"); result = await _ai.SummarizeEntryAsync(summarizeEntryPayload.Content, summarizeEntryPayload.FileStem); break; case "ai.summarize_all": var summarizeAllPayload = DeserializePayload(cmd.Payload); if (summarizeAllPayload is null) return Error("Missing or invalid payload"); result = await _ai.SummarizeAllAsync(summarizeAllPayload.Entries ?? []); break; case "ai.chat": var chatPayload = DeserializePayload(cmd.Payload); if (chatPayload is null || string.IsNullOrWhiteSpace(chatPayload.Prompt)) return Error("Missing or invalid payload"); result = await _ai.ChatAsync(chatPayload.Prompt); break; case "ai.embed": var embedPayload = DeserializePayload(cmd.Payload); if (embedPayload is null || string.IsNullOrWhiteSpace(embedPayload.Content)) return Error("Missing or invalid payload"); result = await _ai.EmbedAsync(embedPayload.Content); break; case "speech.devices.list": result = await _speech.ListDevicesAsync(); break; case "speech.transcribe": var speechPayload = DeserializePayload(cmd.Payload); if (speechPayload is null) return Error("Missing or invalid payload"); var audioBase64 = !string.IsNullOrWhiteSpace(speechPayload.AudioBase64) ? speechPayload.AudioBase64 : speechPayload.Audio_Base64; var text = speechPayload.Text; var whisperModel = !string.IsNullOrWhiteSpace(speechPayload.WhisperModel) ? speechPayload.WhisperModel : speechPayload.Whisper_Model; var simulateDelayMs = speechPayload.SimulateDelayMs ?? speechPayload.Simulate_Delay_Ms; if (string.IsNullOrWhiteSpace(audioBase64) && string.IsNullOrWhiteSpace(text)) return Error("Missing or invalid payload"); result = await _speech.TranscribeAsync(new SpeechTranscribeRequestDto( AudioBase64: audioBase64, Engine: speechPayload.Engine, WhisperModel: whisperModel, Text: text, SimulateDelayMs: simulateDelayMs)); break; case "vault.initialize": var initPayload = DeserializePayload(cmd.Payload); if (initPayload is null || string.IsNullOrWhiteSpace(initPayload.Password) || string.IsNullOrWhiteSpace(initPayload.VaultDirectory)) return Error("Missing or invalid payload"); Directory.CreateDirectory(initPayload.VaultDirectory); result = true; break; case "vault.load_all": var loadPayload = DeserializePayload(cmd.Payload); if (loadPayload is null) return Error("Missing or invalid payload"); var loaded = _vaultStorage.LoadAllVaults(loadPayload.Password, loadPayload.VaultDirectory, loadPayload.DataDirectory); if (loaded) _databaseSession.SetPassword(loadPayload.Password, loadPayload.DataDirectory); result = loaded; break; case "vault.save_current_month": var saveCurrentPayload = DeserializePayload(cmd.Payload); if (saveCurrentPayload is null) return Error("Missing or invalid payload"); result = _vaultStorage.SaveCurrentMonthVault( saveCurrentPayload.Password, saveCurrentPayload.VaultDirectory, saveCurrentPayload.DataDirectory, ParseNowOrDefault(saveCurrentPayload.NowUtc)); break; case "vault.rebuild_all": var rebuildPayload = DeserializePayload(cmd.Payload); if (rebuildPayload is null) return Error("Missing or invalid payload"); _databaseSession.CloseConnection(); _vaultStorage.RebuildAllVaults(rebuildPayload.Password, rebuildPayload.VaultDirectory, rebuildPayload.DataDirectory); result = true; break; case "vault.clear_data_directory": var clearPayload = DeserializePayload(cmd.Payload); if (clearPayload is null || string.IsNullOrWhiteSpace(clearPayload.DataDirectory)) return Error("Missing or invalid payload"); if (_databaseSession is IDisposable disposableSession) disposableSession.Dispose(); _vaultStorage.ClearDataDirectory(clearPayload.DataDirectory); result = true; break; case "db.status": var dbStatusPayload = DeserializePayload(cmd.Payload); if (dbStatusPayload is null || string.IsNullOrWhiteSpace(dbStatusPayload.Password)) return Error("Missing or invalid payload"); result = _database.GetStatus(dbStatusPayload.Password, dbStatusPayload.DataDirectory); break; case "db.initialize_schema": var dbInitPayload = DeserializePayload(cmd.Payload); if (dbInitPayload is null) return Error("Missing or invalid payload"); var schemaPath = _database.WriteSchemaBootstrap(dbInitPayload.DataDirectory); result = new { schemaPath }; break; case "db.hydrate_workspace": var dbHydratePayload = DeserializePayload(cmd.Payload); if (dbHydratePayload is null || string.IsNullOrWhiteSpace(dbHydratePayload.Password)) return Error("Missing or invalid payload"); result = _database.HydrateWorkspace(dbHydratePayload.Password, dbHydratePayload.DataDirectory); _databaseSession.SetPassword(dbHydratePayload.Password, dbHydratePayload.DataDirectory); break; default: CommandLogger.LogFailure(action, correlationId, "unknown_action"); return Error($"Unknown action: {action}"); } } catch (JsonException) { CommandLogger.LogFailure(action, correlationId, "invalid_payload_json"); return Error("Missing or invalid payload"); } catch (ValidationException ex) { CommandLogger.LogFailure(action, correlationId, "validation", ex.Message); return Error(ex.Message); } catch (ArgumentException ex) { CommandLogger.LogFailure(action, correlationId, "argument", ex.Message); return Error(ex.Message); } catch (TimeoutException ex) { CommandLogger.LogFailure(action, correlationId, "timeout", ex.Message); return Error(ex.Message); } catch (InvalidOperationException ex) { CommandLogger.LogFailure(action, correlationId, "invalid_operation", ex.Message); return Error(ex.Message); } catch (FileNotFoundException ex) { CommandLogger.LogFailure(action, correlationId, "not_found", ex.Message); return Error(ex.Message); } catch { CommandLogger.LogFailure(action, correlationId, "internal_error"); return Error("Internal error"); } TryAutoSyncVault(action, correlationId); CommandLogger.LogSuccess(action, correlationId); return JsonSerializer.Serialize(new { ok = true, data = result }); } private static string Error(string message) => JsonSerializer.Serialize(new { ok = false, error = message }); private static T? DeserializePayload(JsonElement? payload) { if (payload is null) return default; return payload.Value.Deserialize(JsonOptions); } private static DateTime ParseNowOrDefault(string? nowUtc) { if (string.IsNullOrWhiteSpace(nowUtc)) return DateTime.UtcNow; if (DateTime.TryParse( nowUtc, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed)) { return parsed; } throw new ArgumentException("Invalid nowUtc value. Expected ISO date/time."); } private void TryAutoSyncVault(string action, string correlationId) { if (!VaultSyncActions.Contains(action)) return; if (!_databaseSession.TryGetSession(out var password, out var sessionDataDirectory)) return; try { var config = _config.Current; var dataDirectory = string.IsNullOrWhiteSpace(sessionDataDirectory) ? config.DataDirectory : sessionDataDirectory; _databaseSession.CloseConnection(); _vaultStorage.RebuildAllVaults(password, config.VaultDirectory, dataDirectory); } catch (Exception ex) { CommandLogger.LogFailure(action, correlationId, "vault_auto_sync_failed", ex.Message); } } }