using System.Text; using Dapper; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Common; using SIGCM2.Domain.Entities; namespace SIGCM2.Infrastructure.Persistence; public sealed class UsuarioRepository : IUsuarioRepository { private readonly SqlConnectionFactory _connectionFactory; public UsuarioRepository(SqlConnectionFactory connectionFactory) { _connectionFactory = connectionFactory; } public async Task GetByUsernameAsync(string username) { const string sql = """ SELECT Id, Username, PasswordHash, Nombre, Apellido, Email, Rol, PermisosJson, Activo, FechaModificacion, UltimoLogin, MustChangePassword FROM dbo.Usuario WHERE Username = @Username AND Activo = 1 """; await using var connection = _connectionFactory.CreateConnection(); await connection.OpenAsync(); var row = await connection.QuerySingleOrDefaultAsync(sql, new { Username = username }); if (row is null) return null; return MapRow(row); } public async Task GetByIdAsync(int id, CancellationToken ct = default) { const string sql = """ SELECT Id, Username, PasswordHash, Nombre, Apellido, Email, Rol, PermisosJson, Activo, FechaModificacion, UltimoLogin, MustChangePassword FROM dbo.Usuario WHERE Id = @Id """; await using var connection = _connectionFactory.CreateConnection(); await connection.OpenAsync(); var row = await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); if (row is null) return null; return MapRow(row); } public async Task ExistsByUsernameAsync(string username, CancellationToken ct = default) { const string sql = """ SELECT COUNT(1) FROM dbo.Usuario WHERE Username = @Username """; await using var connection = _connectionFactory.CreateConnection(); await connection.OpenAsync(ct); var count = await connection.ExecuteScalarAsync(sql, new { Username = username }); return count > 0; } public async Task AddAsync(Usuario usuario, CancellationToken ct = default) { // DF handles: Activo (1), PermisosJson ('[]'), FechaCreacion (GETDATE()) const string sql = """ INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Email, Rol) OUTPUT INSERTED.Id VALUES (@Username, @PasswordHash, @Nombre, @Apellido, @Email, @Rol) """; await using var connection = _connectionFactory.CreateConnection(); await connection.OpenAsync(ct); var id = await connection.ExecuteScalarAsync(sql, new { usuario.Username, usuario.PasswordHash, usuario.Nombre, usuario.Apellido, usuario.Email, usuario.Rol }); return id; } // UDT-008 ───────────────────────────────────────────────────────────────── public async Task UpdateUltimoLoginAsync(int id, DateTime utcNow, CancellationToken ct = default) { const string sql = """ UPDATE dbo.Usuario SET UltimoLogin = @Utc WHERE Id = @Id """; await using var connection = _connectionFactory.CreateConnection(); await connection.OpenAsync(ct); await connection.ExecuteAsync(sql, new { Utc = utcNow, Id = id }); } public async Task> GetPagedAsync(UsuariosQuery query, CancellationToken ct = default) { // Clamp paging params var page = Math.Max(1, query.Page); var pageSize = Math.Clamp(query.PageSize, 1, 100); var offset = (page - 1) * pageSize; var where = new StringBuilder("WHERE 1=1"); var parameters = new DynamicParameters(); parameters.Add("PageSize", pageSize); parameters.Add("Offset", offset); if (!string.IsNullOrWhiteSpace(query.Rol)) { where.Append(" AND Rol = @Rol"); parameters.Add("Rol", query.Rol); } if (query.Activo.HasValue) { where.Append(" AND Activo = @Activo"); parameters.Add("Activo", query.Activo.Value ? 1 : 0); } if (!string.IsNullOrWhiteSpace(query.Search)) { where.Append(" AND (Username LIKE @Search OR Nombre LIKE @Search OR Apellido LIKE @Search OR Email LIKE @Search)"); parameters.Add("Search", $"%{query.Search}%"); } var sql = $""" SELECT Id, Username, Nombre, Apellido, Email, Rol, Activo, UltimoLogin, FechaModificacion, COUNT(*) OVER() AS TotalCount FROM dbo.Usuario {where} ORDER BY Username OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY """; await using var connection = _connectionFactory.CreateConnection(); await connection.OpenAsync(ct); var rows = await connection.QueryAsync(sql, parameters); var list = rows.ToList(); var total = list.Count > 0 ? list[0].TotalCount : 0; var items = list.Select(r => new UsuarioListItem( r.Id, r.Username, r.Nombre, r.Apellido, r.Email, r.Rol, r.Activo, r.UltimoLogin, r.FechaModificacion )).ToList(); return new PagedResult(items, page, pageSize, total); } public async Task GetDetailAsync(int id, CancellationToken ct = default) => await GetByIdAsync(id, ct); public async Task UpdateAsync(int id, UpdateUsuarioFields fields, DateTime fechaModificacion, CancellationToken ct = default) { const string sql = """ UPDATE dbo.Usuario SET Nombre = @Nombre, Apellido = @Apellido, Email = @Email, Rol = @Rol, Activo = @Activo, FechaModificacion = @FechaModificacion WHERE Id = @Id """; await using var connection = _connectionFactory.CreateConnection(); await connection.OpenAsync(ct); await connection.ExecuteAsync(sql, new { fields.Nombre, fields.Apellido, fields.Email, fields.Rol, fields.Activo, FechaModificacion = fechaModificacion, Id = id }); } public async Task UpdatePasswordAsync(int id, string passwordHash, bool mustChangePassword, CancellationToken ct = default) { const string sql = """ UPDATE dbo.Usuario SET PasswordHash = @PasswordHash, MustChangePassword = @MustChangePassword, FechaModificacion = SYSUTCDATETIME() WHERE Id = @Id """; await using var connection = _connectionFactory.CreateConnection(); await connection.OpenAsync(ct); await connection.ExecuteAsync(sql, new { PasswordHash = passwordHash, MustChangePassword = mustChangePassword ? 1 : 0, Id = id }); } public async Task CountActiveAdminsAsync(CancellationToken ct = default) { const string sql = """ SELECT COUNT(1) FROM dbo.Usuario WITH (NOLOCK) WHERE Activo = 1 AND Rol = 'admin' """; await using var connection = _connectionFactory.CreateConnection(); await connection.OpenAsync(ct); return await connection.ExecuteScalarAsync(sql); } // UDT-009 ───────────────────────────────────────────────────────────────── public async Task UpdatePermisosJsonAsync(int id, string permisosJson, DateTime fechaModificacion, CancellationToken ct = default) { const string sql = """ UPDATE dbo.Usuario SET PermisosJson = @PermisosJson, FechaModificacion = @FechaModificacion WHERE Id = @Id """; await using var connection = _connectionFactory.CreateConnection(); await connection.OpenAsync(ct); await connection.ExecuteAsync(sql, new { PermisosJson = permisosJson, FechaModificacion = fechaModificacion, Id = id }); } // ── mapping ─────────────────────────────────────────────────────────────── private static Usuario MapRow(UsuarioRow row) => new( id: row.Id, username: row.Username, passwordHash: row.PasswordHash, nombre: row.Nombre, apellido: row.Apellido, email: row.Email, rol: row.Rol, permisosJson: row.PermisosJson, activo: row.Activo, fechaModificacion: row.FechaModificacion, ultimoLogin: row.UltimoLogin, mustChangePassword: row.MustChangePassword ); // Flat DTO for Dapper mapping (avoids polluting domain entity with Dapper attributes) private sealed record UsuarioRow( int Id, string Username, string PasswordHash, string Nombre, string Apellido, string? Email, string Rol, string PermisosJson, bool Activo, DateTime? FechaModificacion, DateTime? UltimoLogin, bool MustChangePassword ); private sealed record UsuarioPagedRow( int Id, string Username, string Nombre, string Apellido, string? Email, string Rol, bool Activo, DateTime? UltimoLogin, DateTime? FechaModificacion, int TotalCount ); }