using ControlPlane.Core.Models; using Npgsql; namespace ControlPlane.Api.Services; public class OpcService(NpgsqlDataSource db) { // ── Helpers ────────────────────────────────────────────────────────────── private static OpcRecord ReadOpc(NpgsqlDataReader r) => new( r.GetGuid(0), r.GetString(1), r.GetString(2), r.GetString(3), r.GetString(4), r.GetString(5), r.GetString(6), r.GetString(7), r.GetDateTime(8), r.GetDateTime(9) ); private static OpcNote ReadNote(NpgsqlDataReader r) => new( r.GetGuid(0), r.GetGuid(1), r.GetString(2), r.GetString(3), r.GetDateTime(4) ); private static OpcArtifact ReadArtifact(NpgsqlDataReader r) => new( r.GetGuid(0), r.GetGuid(1), r.GetString(2), r.GetString(3), r.GetString(4), r.GetDateTime(5), r.GetDateTime(6) ); // ── Next OPC number ─────────────────────────────────────────────────────── public async Task NextNumberAsync(CancellationToken ct = default) { await using var cmd = db.CreateCommand( "SELECT number FROM opc ORDER BY CAST(TRIM(SUBSTRING(number FROM 7)) AS INTEGER) DESC LIMIT 1"); var last = await cmd.ExecuteScalarAsync(ct) as string; if (last is null) return "OPC # 0001"; if (int.TryParse(last[6..], out var n)) return $"OPC # {n + 1:D4}"; return "OPC # 0001"; } // ── OPC CRUD ────────────────────────────────────────────────────────────── public async Task> ListAsync( string? typeFilter = null, string? statusFilter = null, CancellationToken ct = default) { var sql = """ SELECT id, number, title, description, type, status, priority, assignee, created_at, updated_at FROM opc WHERE ($1::text IS NULL OR type = $1) AND ($2::text IS NULL OR status = $2) ORDER BY created_at DESC """; await using var cmd = db.CreateCommand(sql); cmd.Parameters.AddWithValue(typeFilter ?? (object)DBNull.Value); cmd.Parameters.AddWithValue(statusFilter ?? (object)DBNull.Value); await using var r = await cmd.ExecuteReaderAsync(ct); var list = new List(); while (await r.ReadAsync(ct)) list.Add(ReadOpc(r)); return list; } public async Task GetAsync(Guid id, CancellationToken ct = default) { await using var cmd = db.CreateCommand( "SELECT id, number, title, description, type, status, priority, assignee, created_at, updated_at FROM opc WHERE id = $1"); cmd.Parameters.AddWithValue(id); await using var r = await cmd.ExecuteReaderAsync(ct); return await r.ReadAsync(ct) ? ReadOpc(r) : null; } public async Task CreateAsync(CreateOpcRequest req, CancellationToken ct = default) { var number = await NextNumberAsync(ct); var sql = """ INSERT INTO opc (number, title, description, type, status, priority, assignee) VALUES ($1, $2, $3, $4, 'New', $5, $6) RETURNING id, number, title, description, type, status, priority, assignee, created_at, updated_at """; await using var cmd = db.CreateCommand(sql); cmd.Parameters.AddWithValue(number); cmd.Parameters.AddWithValue(req.Title); cmd.Parameters.AddWithValue(req.Description); cmd.Parameters.AddWithValue(req.Type); cmd.Parameters.AddWithValue(req.Priority); cmd.Parameters.AddWithValue(req.Assignee); await using var r = await cmd.ExecuteReaderAsync(ct); await r.ReadAsync(ct); return ReadOpc(r); } public async Task UpdateAsync(Guid id, UpdateOpcRequest req, CancellationToken ct = default) { var sql = """ UPDATE opc SET title = COALESCE($2, title), description = COALESCE($3, description), type = COALESCE($4, type), status = COALESCE($5, status), priority = COALESCE($6, priority), assignee = COALESCE($7, assignee), updated_at = NOW() WHERE id = $1 RETURNING id, number, title, description, type, status, priority, assignee, created_at, updated_at """; await using var cmd = db.CreateCommand(sql); cmd.Parameters.AddWithValue(id); cmd.Parameters.AddWithValue(req.Title ?? (object)DBNull.Value); cmd.Parameters.AddWithValue(req.Description ?? (object)DBNull.Value); cmd.Parameters.AddWithValue(req.Type ?? (object)DBNull.Value); cmd.Parameters.AddWithValue(req.Status ?? (object)DBNull.Value); cmd.Parameters.AddWithValue(req.Priority ?? (object)DBNull.Value); cmd.Parameters.AddWithValue(req.Assignee ?? (object)DBNull.Value); await using var r = await cmd.ExecuteReaderAsync(ct); return await r.ReadAsync(ct) ? ReadOpc(r) : null; } public async Task DeleteAsync(Guid id, CancellationToken ct = default) { await using var cmd = db.CreateCommand("DELETE FROM opc WHERE id = $1"); cmd.Parameters.AddWithValue(id); return await cmd.ExecuteNonQueryAsync(ct) > 0; } // ── Notes ────────────────────────────────────────────────────────────────── public async Task> ListNotesAsync(Guid opcId, CancellationToken ct = default) { await using var cmd = db.CreateCommand( "SELECT id, opc_id, author, content, created_at FROM opc_note WHERE opc_id = $1 ORDER BY created_at ASC"); cmd.Parameters.AddWithValue(opcId); await using var r = await cmd.ExecuteReaderAsync(ct); var list = new List(); while (await r.ReadAsync(ct)) list.Add(ReadNote(r)); return list; } public async Task AddNoteAsync(Guid opcId, AddNoteRequest req, CancellationToken ct = default) { var sql = """ INSERT INTO opc_note (opc_id, author, content) VALUES ($1, $2, $3) RETURNING id, opc_id, author, content, created_at """; await using var cmd = db.CreateCommand(sql); cmd.Parameters.AddWithValue(opcId); cmd.Parameters.AddWithValue(req.Author); cmd.Parameters.AddWithValue(req.Content); await using var r = await cmd.ExecuteReaderAsync(ct); await r.ReadAsync(ct); return ReadNote(r); } // ── Artifacts ───────────────────────────────────────────────────────────── public async Task> ListArtifactsAsync(Guid opcId, string? artifactType = null, CancellationToken ct = default) { var sql = """ SELECT id, opc_id, artifact_type, title, content, created_at, updated_at FROM opc_artifact WHERE opc_id = $1 AND ($2::text IS NULL OR artifact_type = $2) ORDER BY created_at ASC """; await using var cmd = db.CreateCommand(sql); cmd.Parameters.AddWithValue(opcId); cmd.Parameters.AddWithValue(artifactType ?? (object)DBNull.Value); await using var r = await cmd.ExecuteReaderAsync(ct); var list = new List(); while (await r.ReadAsync(ct)) list.Add(ReadArtifact(r)); return list; } public async Task UpsertArtifactAsync(Guid opcId, UpsertArtifactRequest req, CancellationToken ct = default) { var sql = """ INSERT INTO opc_artifact (opc_id, artifact_type, title, content) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING RETURNING id, opc_id, artifact_type, title, content, created_at, updated_at """; // Simple insert; for updates use artifact id endpoint await using var cmd = db.CreateCommand(sql); cmd.Parameters.AddWithValue(opcId); cmd.Parameters.AddWithValue(req.ArtifactType); cmd.Parameters.AddWithValue(req.Title); cmd.Parameters.AddWithValue(req.Content); await using var r = await cmd.ExecuteReaderAsync(ct); await r.ReadAsync(ct); return ReadArtifact(r); } public async Task UpdateArtifactAsync(Guid artifactId, UpsertArtifactRequest req, CancellationToken ct = default) { var sql = """ UPDATE opc_artifact SET title = $2, content = $3, updated_at = NOW() WHERE id = $1 RETURNING id, opc_id, artifact_type, title, content, created_at, updated_at """; await using var cmd = db.CreateCommand(sql); cmd.Parameters.AddWithValue(artifactId); cmd.Parameters.AddWithValue(req.Title); cmd.Parameters.AddWithValue(req.Content); await using var r = await cmd.ExecuteReaderAsync(ct); return await r.ReadAsync(ct) ? ReadArtifact(r) : null; } public async Task DeleteArtifactAsync(Guid artifactId, CancellationToken ct = default) { await using var cmd = db.CreateCommand("DELETE FROM opc_artifact WHERE id = $1"); cmd.Parameters.AddWithValue(artifactId); return await cmd.ExecuteNonQueryAsync(ct) > 0; } // ── Pinned commits ──────────────────────────────────────────────────────── private static OpcPinnedCommit ReadPinnedCommit(NpgsqlDataReader r) => new( r.GetGuid(0), r.GetString(1), r.GetString(2), r.GetString(3), r.GetString(4), r.GetDateTime(5), r.GetString(6) ); public async Task> ListPinnedCommitsAsync(Guid opcId, CancellationToken ct = default) { await using var cmd = db.CreateCommand( "SELECT opc_id, hash, short_hash, subject, author, pinned_at, pinned_by FROM opc_pinned_commit WHERE opc_id = $1 ORDER BY pinned_at DESC"); cmd.Parameters.AddWithValue(opcId); await using var r = await cmd.ExecuteReaderAsync(ct); var list = new List(); while (await r.ReadAsync(ct)) list.Add(ReadPinnedCommit(r)); return list; } public async Task PinCommitAsync( Guid opcId, string hash, string shortHash, string subject, string author, string pinnedBy, CancellationToken ct = default) { // Verify the OPC exists await using var existsCmd = db.CreateCommand("SELECT 1 FROM opc WHERE id = $1"); existsCmd.Parameters.AddWithValue(opcId); var exists = await existsCmd.ExecuteScalarAsync(ct); if (exists is null) return null; var sql = """ INSERT INTO opc_pinned_commit (opc_id, hash, short_hash, subject, author, pinned_by) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (opc_id, hash) DO UPDATE SET short_hash = EXCLUDED.short_hash, subject = EXCLUDED.subject, author = EXCLUDED.author, pinned_by = EXCLUDED.pinned_by, pinned_at = NOW() RETURNING opc_id, hash, short_hash, subject, author, pinned_at, pinned_by """; await using var cmd = db.CreateCommand(sql); cmd.Parameters.AddWithValue(opcId); cmd.Parameters.AddWithValue(hash); cmd.Parameters.AddWithValue(shortHash); cmd.Parameters.AddWithValue(subject); cmd.Parameters.AddWithValue(author); cmd.Parameters.AddWithValue(pinnedBy); await using var r = await cmd.ExecuteReaderAsync(ct); return await r.ReadAsync(ct) ? ReadPinnedCommit(r) : null; } public async Task UnpinCommitAsync(Guid opcId, string hash, CancellationToken ct = default) { await using var cmd = db.CreateCommand( "DELETE FROM opc_pinned_commit WHERE opc_id = $1 AND hash = $2"); cmd.Parameters.AddWithValue(opcId); cmd.Parameters.AddWithValue(hash); return await cmd.ExecuteNonQueryAsync(ct) > 0; } }