using Journal.Core.Models; using Journal.Core.Services.Database; using Microsoft.Data.Sqlite; namespace Journal.Core.Repositories; public sealed class SqliteConversationRepository(IDatabaseSessionService session) : IConversationRepository { private readonly IDatabaseSessionService _session = session; public List GetAll() { var conn = _session.GetConnection(); return ReadAll(conn); } public Conversation? GetById(Guid id) { var conn = _session.GetConnection(); return ReadById(conn, id); } public void Add(Conversation conversation) { ArgumentNullException.ThrowIfNull(conversation); var conn = _session.GetConnection(); Insert(conn, conversation); } public bool Update(Guid id, string? title = null) { var conn = _session.GetConnection(); var existing = ReadById(conn, id); if (existing is null) return false; if (title is not null) { if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Title cannot be empty", nameof(title)); existing.Title = title.Trim(); } existing.UpdatedAt = DateTimeOffset.Now; UpdateRow(conn, existing); return true; } public bool Remove(Guid id) { var conn = _session.GetConnection(); return Delete(conn, id); } public void AddMessage(ConversationMessage message) { ArgumentNullException.ThrowIfNull(message); var conn = _session.GetConnection(); InsertMessage(conn, message); // Touch conversation updated_at using var cmd = conn.CreateCommand(); cmd.CommandText = "UPDATE conversations SET updated_at = @now WHERE guid = @guid;"; cmd.Parameters.AddWithValue("@now", DateTimeOffset.Now.ToString("O")); cmd.Parameters.AddWithValue("@guid", message.ConversationId.ToString("D")); cmd.ExecuteNonQuery(); } public List GetMessages(Guid conversationId) { var conn = _session.GetConnection(); return ReadMessages(conn, conversationId); } // ── Private helpers ────────────────────────────────────────────── private static void Insert(SqliteConnection conn, Conversation c) { using var cmd = conn.CreateCommand(); cmd.CommandText = """ INSERT INTO conversations (guid, title, created_at, updated_at) VALUES (@guid, @title, @createdAt, @updatedAt); """; cmd.Parameters.AddWithValue("@guid", c.Id.ToString("D")); cmd.Parameters.AddWithValue("@title", c.Title); cmd.Parameters.AddWithValue("@createdAt", c.CreatedAt.ToString("O")); cmd.Parameters.AddWithValue("@updatedAt", c.UpdatedAt.ToString("O")); cmd.ExecuteNonQuery(); } private static void UpdateRow(SqliteConnection conn, Conversation c) { using var cmd = conn.CreateCommand(); cmd.CommandText = """ UPDATE conversations SET title = @title, updated_at = @updatedAt WHERE guid = @guid; """; cmd.Parameters.AddWithValue("@guid", c.Id.ToString("D")); cmd.Parameters.AddWithValue("@title", c.Title); cmd.Parameters.AddWithValue("@updatedAt", c.UpdatedAt.ToString("O")); cmd.ExecuteNonQuery(); } private static bool Delete(SqliteConnection conn, Guid id) { using var tx = conn.BeginTransaction(); // Get the row id for cascade delete of messages var rowId = GetRowId(conn, id); if (rowId.HasValue) { using var delMsgs = conn.CreateCommand(); delMsgs.CommandText = "DELETE FROM conversation_messages WHERE conversation_id = @id;"; delMsgs.Parameters.AddWithValue("@id", rowId.Value); delMsgs.ExecuteNonQuery(); } using var cmd = conn.CreateCommand(); cmd.CommandText = "DELETE FROM conversations WHERE guid = @guid;"; cmd.Parameters.AddWithValue("@guid", id.ToString("D")); var rows = cmd.ExecuteNonQuery(); tx.Commit(); return rows > 0; } private static long? GetRowId(SqliteConnection conn, Guid id) { using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT id FROM conversations WHERE guid = @guid;"; cmd.Parameters.AddWithValue("@guid", id.ToString("D")); var result = cmd.ExecuteScalar(); return result is long rowId ? rowId : null; } private static Conversation? ReadById(SqliteConnection conn, Guid id) { using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT guid, title, created_at, updated_at FROM conversations WHERE guid = @guid;"; cmd.Parameters.AddWithValue("@guid", id.ToString("D")); using var reader = cmd.ExecuteReader(); return reader.Read() ? MapConversation(reader) : null; } private static List ReadAll(SqliteConnection conn) { var results = new List(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT guid, title, created_at, updated_at FROM conversations ORDER BY updated_at DESC;"; using var reader = cmd.ExecuteReader(); while (reader.Read()) results.Add(MapConversation(reader)); return results; } private static void InsertMessage(SqliteConnection conn, ConversationMessage m) { var convRowId = GetRowId(conn, m.ConversationId) ?? throw new InvalidOperationException($"Conversation {m.ConversationId} not found"); using var cmd = conn.CreateCommand(); cmd.CommandText = """ INSERT INTO conversation_messages (guid, conversation_id, role, text, created_at) VALUES (@guid, @conversationId, @role, @text, @createdAt); """; cmd.Parameters.AddWithValue("@guid", m.Id.ToString("D")); cmd.Parameters.AddWithValue("@conversationId", convRowId); cmd.Parameters.AddWithValue("@role", m.Role); cmd.Parameters.AddWithValue("@text", m.Text); cmd.Parameters.AddWithValue("@createdAt", m.CreatedAt.ToString("O")); cmd.ExecuteNonQuery(); } private static List ReadMessages(SqliteConnection conn, Guid conversationId) { var rowId = GetRowId(conn, conversationId); if (!rowId.HasValue) return []; var results = new List(); using var cmd = conn.CreateCommand(); cmd.CommandText = """ SELECT guid, role, text, created_at FROM conversation_messages WHERE conversation_id = @conversationId ORDER BY created_at ASC; """; cmd.Parameters.AddWithValue("@conversationId", rowId.Value); using var reader = cmd.ExecuteReader(); while (reader.Read()) results.Add(MapMessage(reader, conversationId)); return results; } private static Conversation MapConversation(SqliteDataReader reader) { var guid = Guid.Parse(reader.GetString(0)); var title = reader.GetString(1); var createdAt = reader.IsDBNull(2) ? DateTimeOffset.MinValue : DateTimeOffset.Parse(reader.GetString(2)); var updatedAt = reader.IsDBNull(3) ? createdAt : DateTimeOffset.Parse(reader.GetString(3)); return new Conversation(guid, title, createdAt, updatedAt); } private static ConversationMessage MapMessage(SqliteDataReader reader, Guid conversationId) { var guid = Guid.Parse(reader.GetString(0)); var role = reader.GetString(1); var text = reader.IsDBNull(2) ? "" : reader.GetString(2); var createdAt = reader.IsDBNull(3) ? DateTimeOffset.MinValue : DateTimeOffset.Parse(reader.GetString(3)); return new ConversationMessage(guid, conversationId, role, text, createdAt); } }