using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Text.Json; using Journal.Core.Dtos; using Journal.Core.Models; using Journal.Core.Services; namespace Journal.Core; public class Entry { private readonly IFragmentService _fragments; private readonly IEntrySearchService _entrySearch; private readonly IVaultStorageService _vaultStorage; private readonly IJournalDatabaseService _database; private readonly IJournalConfigService _config; private readonly IAiService _ai; private readonly ISpeechBridgeService _speech; private readonly IEntryFileService _entryFiles; private readonly CommandLogger _logger; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; public Entry( IFragmentService fragments, IEntrySearchService entrySearch, IVaultStorageService vaultStorage, IJournalDatabaseService database, IJournalConfigService config, IAiService ai, ISpeechBridgeService speech, IEntryFileService entryFiles, CommandLogger logger) { _fragments = fragments; _entrySearch = entrySearch; _vaultStorage = vaultStorage; _database = database; _config = config; _ai = ai; _speech = speech; _entryFiles = entryFiles; _logger = logger; } 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(); _logger.LogStart(action, correlationId, cmd.Payload); object? result; try { switch (action) { case "fragments.list": result = await _fragments.GetAllAsync(); break; case "fragments.get": if (!Guid.TryParse(cmd.Id, out var getId)) return Error("Invalid or missing id"); result = await _fragments.GetByIdAsync(getId); break; case "fragments.create": var createDto = DeserializePayload(cmd.Payload); if (createDto is null) return Error("Missing or invalid payload"); result = await _fragments.CreateAsync(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 = await _fragments.UpdateAsync(updateId, updateDto); break; case "fragments.delete": if (!Guid.TryParse(cmd.Id, out var deleteId)) return Error("Invalid or missing id"); result = await _fragments.RemoveAsync(deleteId); break; case "fragments.search": result = await _fragments.SearchAsync(cmd.Type, cmd.Tag); 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 "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 "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 "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"); result = _vaultStorage.LoadAllVaults(loadPayload.Password, loadPayload.VaultDirectory, loadPayload.DataDirectory); 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"); _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"); _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); break; default: _logger.LogFailure(action, correlationId, "unknown_action"); return Error($"Unknown action: {action}"); } } catch (JsonException) { _logger.LogFailure(action, correlationId, "invalid_payload_json"); return Error("Missing or invalid payload"); } catch (ValidationException ex) { _logger.LogFailure(action, correlationId, "validation", ex.Message); return Error(ex.Message); } catch (ArgumentException ex) { _logger.LogFailure(action, correlationId, "argument", ex.Message); return Error(ex.Message); } catch (TimeoutException ex) { _logger.LogFailure(action, correlationId, "timeout", ex.Message); return Error(ex.Message); } catch (InvalidOperationException ex) { _logger.LogFailure(action, correlationId, "invalid_operation", ex.Message); return Error(ex.Message); } catch (FileNotFoundException ex) { _logger.LogFailure(action, correlationId, "not_found", ex.Message); return Error(ex.Message); } catch { _logger.LogFailure(action, correlationId, "internal_error"); return Error("Internal error"); } _logger.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."); } }