using Journal.Core.Models; using Journal.Core.Services.Database; using Microsoft.Data.Sqlite; namespace Journal.Core.Repositories; public sealed class SqliteTodoRepository(IDatabaseSessionService session) : ITodoRepository { private readonly IDatabaseSessionService _session = session; // ── Lists ──────────────────────────────────────────────────────── public List GetAllLists() { var conn = _session.GetConnection(); return ReadAllLists(conn); } public TodoList? GetListById(Guid id) { var conn = _session.GetConnection(); return ReadListById(conn, id); } public void AddList(TodoList list) { ArgumentNullException.ThrowIfNull(list); var conn = _session.GetConnection(); InsertList(conn, list); } public bool UpdateList(Guid id, string? label = null) { var conn = _session.GetConnection(); var existing = ReadListById(conn, id); if (existing is null) return false; if (label is not null) { if (string.IsNullOrWhiteSpace(label)) throw new ArgumentException("Label cannot be empty", nameof(label)); existing.Label = label.Trim(); } UpdateListRow(conn, existing); return true; } public bool RemoveList(Guid id) { var conn = _session.GetConnection(); return DeleteList(conn, id); } // ── Items ──────────────────────────────────────────────────────── public List GetItemsByListId(Guid listId) { var conn = _session.GetConnection(); return ReadItemsByListId(conn, listId); } public TodoItem? GetItemById(Guid id) { var conn = _session.GetConnection(); return ReadItemById(conn, id); } public void AddItem(TodoItem item) { ArgumentNullException.ThrowIfNull(item); var conn = _session.GetConnection(); InsertItem(conn, item); } public bool UpdateItem(Guid id, string? text = null, bool? done = null, int? sortOrder = null) { var conn = _session.GetConnection(); var existing = ReadItemById(conn, id); if (existing is null) return false; if (text is not null) { if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Text cannot be empty", nameof(text)); existing.Text = text.Trim(); } if (done.HasValue) existing.Done = done.Value; if (sortOrder.HasValue) existing.SortOrder = sortOrder.Value; UpdateItemRow(conn, existing); return true; } public bool RemoveItem(Guid id) { var conn = _session.GetConnection(); return DeleteItem(conn, id); } // ── Private list helpers ───────────────────────────────────────── private static void InsertList(SqliteConnection conn, TodoList list) { using var cmd = conn.CreateCommand(); cmd.CommandText = """ INSERT INTO todo_lists (guid, label, created_at) VALUES (@guid, @label, @createdAt); """; cmd.Parameters.AddWithValue("@guid", list.Id.ToString("D")); cmd.Parameters.AddWithValue("@label", list.Label); cmd.Parameters.AddWithValue("@createdAt", list.CreatedAt.ToString("O")); cmd.ExecuteNonQuery(); } private static void UpdateListRow(SqliteConnection conn, TodoList list) { using var cmd = conn.CreateCommand(); cmd.CommandText = "UPDATE todo_lists SET label = @label WHERE guid = @guid;"; cmd.Parameters.AddWithValue("@guid", list.Id.ToString("D")); cmd.Parameters.AddWithValue("@label", list.Label); cmd.ExecuteNonQuery(); } private static bool DeleteList(SqliteConnection conn, Guid id) { using var tx = conn.BeginTransaction(); var rowId = GetListRowId(conn, id); if (rowId.HasValue) { using var delItems = conn.CreateCommand(); delItems.CommandText = "DELETE FROM todo_items WHERE list_id = @listId;"; delItems.Parameters.AddWithValue("@listId", rowId.Value); delItems.ExecuteNonQuery(); } using var delList = conn.CreateCommand(); delList.CommandText = "DELETE FROM todo_lists WHERE guid = @guid;"; delList.Parameters.AddWithValue("@guid", id.ToString("D")); var rows = delList.ExecuteNonQuery(); tx.Commit(); return rows > 0; } private static TodoList? ReadListById(SqliteConnection conn, Guid id) { using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT guid, label, created_at FROM todo_lists WHERE guid = @guid;"; cmd.Parameters.AddWithValue("@guid", id.ToString("D")); using var reader = cmd.ExecuteReader(); return reader.Read() ? MapListRow(reader) : null; } private static List ReadAllLists(SqliteConnection conn) { var results = new List(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT guid, label, created_at FROM todo_lists ORDER BY created_at;"; using var reader = cmd.ExecuteReader(); while (reader.Read()) results.Add(MapListRow(reader)); return results; } private static long? GetListRowId(SqliteConnection conn, Guid guid) { using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT id FROM todo_lists WHERE guid = @guid;"; cmd.Parameters.AddWithValue("@guid", guid.ToString("D")); var result = cmd.ExecuteScalar(); return result is long id ? id : null; } private static TodoList MapListRow(SqliteDataReader reader) { var guid = Guid.Parse(reader.GetString(0)); var label = reader.GetString(1); var createdAt = reader.IsDBNull(2) ? DateTimeOffset.MinValue : DateTimeOffset.Parse(reader.GetString(2)); return new TodoList(guid, label, createdAt); } // ── Private item helpers ───────────────────────────────────────── private static void InsertItem(SqliteConnection conn, TodoItem item) { var listRowId = GetListRowId(conn, item.ListId) ?? throw new InvalidOperationException($"Todo list {item.ListId} not found"); using var cmd = conn.CreateCommand(); cmd.CommandText = """ INSERT INTO todo_items (guid, list_id, text, done, sort_order) VALUES (@guid, @listId, @text, @done, @sortOrder); """; cmd.Parameters.AddWithValue("@guid", item.Id.ToString("D")); cmd.Parameters.AddWithValue("@listId", listRowId); cmd.Parameters.AddWithValue("@text", item.Text); cmd.Parameters.AddWithValue("@done", item.Done ? 1 : 0); cmd.Parameters.AddWithValue("@sortOrder", item.SortOrder); cmd.ExecuteNonQuery(); } private static void UpdateItemRow(SqliteConnection conn, TodoItem item) { using var cmd = conn.CreateCommand(); cmd.CommandText = """ UPDATE todo_items SET text = @text, done = @done, sort_order = @sortOrder WHERE guid = @guid; """; cmd.Parameters.AddWithValue("@guid", item.Id.ToString("D")); cmd.Parameters.AddWithValue("@text", item.Text); cmd.Parameters.AddWithValue("@done", item.Done ? 1 : 0); cmd.Parameters.AddWithValue("@sortOrder", item.SortOrder); cmd.ExecuteNonQuery(); } private static bool DeleteItem(SqliteConnection conn, Guid id) { using var cmd = conn.CreateCommand(); cmd.CommandText = "DELETE FROM todo_items WHERE guid = @guid;"; cmd.Parameters.AddWithValue("@guid", id.ToString("D")); return cmd.ExecuteNonQuery() > 0; } private static TodoItem? ReadItemById(SqliteConnection conn, Guid id) { using var cmd = conn.CreateCommand(); cmd.CommandText = """ SELECT ti.guid, tl.guid, ti.text, ti.done, ti.sort_order FROM todo_items ti INNER JOIN todo_lists tl ON tl.id = ti.list_id WHERE ti.guid = @guid; """; cmd.Parameters.AddWithValue("@guid", id.ToString("D")); using var reader = cmd.ExecuteReader(); return reader.Read() ? MapItemRow(reader) : null; } private static List ReadItemsByListId(SqliteConnection conn, Guid listId) { var results = new List(); using var cmd = conn.CreateCommand(); cmd.CommandText = """ SELECT ti.guid, tl.guid, ti.text, ti.done, ti.sort_order FROM todo_items ti INNER JOIN todo_lists tl ON tl.id = ti.list_id WHERE tl.guid = @listGuid ORDER BY ti.sort_order, ti.guid; """; cmd.Parameters.AddWithValue("@listGuid", listId.ToString("D")); using var reader = cmd.ExecuteReader(); while (reader.Read()) results.Add(MapItemRow(reader)); return results; } private static TodoItem MapItemRow(SqliteDataReader reader) { var guid = Guid.Parse(reader.GetString(0)); var listGuid = Guid.Parse(reader.GetString(1)); var text = reader.GetString(2); var done = !reader.IsDBNull(3) && reader.GetInt64(3) != 0; var sortOrder = reader.IsDBNull(4) ? 0 : (int)reader.GetInt64(4); return new TodoItem(guid, listGuid, text, done, sortOrder); } }