using Journal.Core.Models; namespace Journal.Core.Services.Config; public sealed class JournalConfigService : IJournalConfigService { public JournalConfig Current => BuildConfig(); private static JournalConfig BuildConfig() { var projectRoot = ResolveProjectRoot(); var appDirectory = ResolvePath("JOURNAL_APP_DIR", Path.Combine(projectRoot, "journal")); var dataDirectory = ResolvePath("JOURNAL_DATA_DIR", Path.Combine(appDirectory, "data")); var vaultDirectory = ResolvePath("JOURNAL_VAULT_DIR", Path.Combine(appDirectory, "vault")); var logDirectory = ResolvePath("JOURNAL_LOG_DIR", Path.Combine(projectRoot, "logs")); var pidFile = ResolvePath("JOURNAL_PID_FILE", Path.Combine(logDirectory, "nicegui_server.pid")); var serverControlFile = ResolvePath("JOURNAL_SERVER_CONTROL_FILE", Path.Combine(logDirectory, "server_control.action")); var nlpBackend = (Environment.GetEnvironmentVariable("JOURNAL_NLP_BACKEND") ?? "auto").Trim().ToLowerInvariant(); if (nlpBackend is not ("auto" or "spacy" or "fallback")) nlpBackend = "auto"; var aiProvider = (Environment.GetEnvironmentVariable("JOURNAL_AI_PROVIDER") ?? "none").Trim().ToLowerInvariant(); if (aiProvider is not ("none" or "python-sidecar")) aiProvider = "none"; var pythonExecutable = Environment.GetEnvironmentVariable("JOURNAL_PYTHON_EXE"); if (string.IsNullOrWhiteSpace(pythonExecutable)) pythonExecutable = "python"; var defaultAiSidecarPath = Path.Combine(projectRoot, "journal", "ai", "sidecar.py"); var pythonAiSidecarPath = ResolvePath("JOURNAL_AI_SIDECAR_PATH", defaultAiSidecarPath); var aiSidecarTimeoutMs = ParseInt("JOURNAL_AI_TIMEOUT_MS", 45000); return new JournalConfig( ProjectRoot: projectRoot, AppDirectory: appDirectory, DataDirectory: dataDirectory, VaultDirectory: vaultDirectory, LogDirectory: logDirectory, PidFile: pidFile, ServerControlFile: serverControlFile, DatabaseFilename: Environment.GetEnvironmentVariable("JOURNAL_DATABASE_FILENAME") ?? "journal_cache.db", MonthlyVaultFormat: Environment.GetEnvironmentVariable("JOURNAL_MONTHLY_VAULT_FORMAT") ?? "%Y-%m.vault", CloudAiApiKey: Environment.GetEnvironmentVariable("CLOUDAI_API_KEY") ?? "", CloudAiApiUrl: Environment.GetEnvironmentVariable("CLOUDAI_API_URL") ?? "", LlamaCppUrl: Environment.GetEnvironmentVariable("LLAMA_CPP_URL") ?? "http://127.0.0.1:8085/v1/completions", LlamaCppModel: Environment.GetEnvironmentVariable("LLAMA_CPP_MODEL") ?? "qwen/qwen3-4b", LlamaCppTimeout: ParseInt("LLAMA_CPP_TIMEOUT", 6000), EmbeddingApiUrl: Environment.GetEnvironmentVariable("EMBEDDING_API_URL") ?? "http://127.0.0.1:8086/v1/embeddings", EmbeddingModelName: Environment.GetEnvironmentVariable("EMBEDDING_MODEL_NAME") ?? "text-embedding-nomic-embed-text-v2-moe", ModelContextTokens: ParseInt("MODEL_CONTEXT_TOKENS", 131072), ChunkTokenBudget: ParseInt("CHUNK_TOKEN_BUDGET", 120000), MicrophoneDeviceIndex: ParseNullableInt("MICROPHONE_DEVICE_INDEX"), SpeechRecognitionEngine: Environment.GetEnvironmentVariable("SPEECH_RECOGNITION_ENGINE") ?? "whisper", WhisperModelSize: Environment.GetEnvironmentVariable("WHISPER_MODEL_SIZE") ?? "base", NlpBackend: nlpBackend, AiProvider: aiProvider, PythonExecutable: pythonExecutable, PythonAiSidecarPath: pythonAiSidecarPath, AiSidecarTimeoutMs: aiSidecarTimeoutMs); } private static string ResolveProjectRoot() { var envRoot = Environment.GetEnvironmentVariable("JOURNAL_PROJECT_ROOT"); if (!string.IsNullOrWhiteSpace(envRoot)) return Path.GetFullPath(envRoot); var cwd = Directory.GetCurrentDirectory(); if (Directory.Exists(Path.Combine(cwd, "journal"))) return Path.GetFullPath(cwd); var upOne = Path.GetFullPath(Path.Combine(cwd, "..")); if (Directory.Exists(Path.Combine(upOne, "journal"))) return upOne; var upTwo = Path.GetFullPath(Path.Combine(cwd, "..", "..")); if (Directory.Exists(Path.Combine(upTwo, "journal"))) return upTwo; return Path.GetFullPath(cwd); } private static string ResolvePath(string envVar, string defaultPath) { var value = Environment.GetEnvironmentVariable(envVar); var raw = string.IsNullOrWhiteSpace(value) ? defaultPath : value; return Path.GetFullPath(raw); } private static int ParseInt(string envVar, int defaultValue) { var value = Environment.GetEnvironmentVariable(envVar); return int.TryParse(value, out var parsed) ? parsed : defaultValue; } private static int? ParseNullableInt(string envVar) { var value = Environment.GetEnvironmentVariable(envVar); if (string.IsNullOrWhiteSpace(value)) return null; return int.TryParse(value, out var parsed) ? parsed : null; } }