using Npgsql;

namespace SpaceGame.Api.Auth.Simulation;

public sealed class PostgresAuthRepository(NpgsqlDataSource dataSource) : IAuthRepository
{
    public async Task<UserAccount?> FindUserByEmailAsync(string email, CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            select id, email, password_hash, created_at_utc, roles
            from auth_users
            where email = $1
            """);
        command.Parameters.AddWithValue(email);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        return await reader.ReadAsync(cancellationToken) ? ReadUser(reader) : null;
    }

    public async Task<UserAccount?> FindUserByIdAsync(Guid userId, CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            select id, email, password_hash, created_at_utc, roles
            from auth_users
            where id = $1
            """);
        command.Parameters.AddWithValue(userId);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        return await reader.ReadAsync(cancellationToken) ? ReadUser(reader) : null;
    }

    public async Task<IReadOnlyList<UserAccount>> ListUsersAsync(CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            select id, email, password_hash, created_at_utc, roles
            from auth_users
            order by email asc
            """);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        var users = new List<UserAccount>();
        while (await reader.ReadAsync(cancellationToken))
        {
            users.Add(ReadUser(reader));
        }

        return users;
    }

    public async Task<UserAccount> CreateUserAsync(string email, string passwordHash, IReadOnlyCollection<string> roles, CancellationToken cancellationToken)
    {
        var userId = Guid.NewGuid();
        var createdAtUtc = DateTimeOffset.UtcNow;
        await using var command = dataSource.CreateCommand("""
            insert into auth_users (id, email, password_hash, created_at_utc, roles)
            values ($1, $2, $3, $4, $5)
            """);
        command.Parameters.AddWithValue(userId);
        command.Parameters.AddWithValue(email);
        command.Parameters.AddWithValue(passwordHash);
        command.Parameters.AddWithValue(createdAtUtc);
        command.Parameters.AddWithValue(roles.Select(role => role.Trim().ToLowerInvariant()).Where(role => role.Length > 0).Distinct(StringComparer.Ordinal).ToArray());
        await command.ExecuteNonQueryAsync(cancellationToken);
        return new UserAccount(userId, email, passwordHash, createdAtUtc, roles.Select(role => role.Trim().ToLowerInvariant()).Where(role => role.Length > 0).Distinct(StringComparer.Ordinal).ToArray());
    }

    public async Task<UserAccount> UpsertUserAsync(string email, string passwordHash, IReadOnlyCollection<string> roles, CancellationToken cancellationToken)
    {
        var normalizedRoles = roles.Select(role => role.Trim().ToLowerInvariant()).Where(role => role.Length > 0).Distinct(StringComparer.Ordinal).ToArray();
        var userId = Guid.NewGuid();
        var createdAtUtc = DateTimeOffset.UtcNow;
        await using var command = dataSource.CreateCommand("""
            insert into auth_users (id, email, password_hash, created_at_utc, roles)
            values ($1, $2, $3, $4, $5)
            on conflict (email) do update
            set password_hash = excluded.password_hash,
                roles = excluded.roles
            returning id, email, password_hash, created_at_utc, roles
            """);
        command.Parameters.AddWithValue(userId);
        command.Parameters.AddWithValue(email);
        command.Parameters.AddWithValue(passwordHash);
        command.Parameters.AddWithValue(createdAtUtc);
        command.Parameters.AddWithValue(normalizedRoles);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        await reader.ReadAsync(cancellationToken);
        return ReadUser(reader);
    }

    public async Task StoreRefreshTokenAsync(Guid userId, string tokenHash, DateTimeOffset expiresAtUtc, CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            insert into auth_refresh_tokens (id, user_id, token_hash, created_at_utc, expires_at_utc, revoked_at_utc)
            values ($1, $2, $3, $4, $5, null)
            """);
        command.Parameters.AddWithValue(Guid.NewGuid());
        command.Parameters.AddWithValue(userId);
        command.Parameters.AddWithValue(tokenHash);
        command.Parameters.AddWithValue(DateTimeOffset.UtcNow);
        command.Parameters.AddWithValue(expiresAtUtc);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    public async Task<RefreshTokenRecord?> FindRefreshTokenAsync(string tokenHash, CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            select id, user_id, token_hash, created_at_utc, expires_at_utc, revoked_at_utc
            from auth_refresh_tokens
            where token_hash = $1
            """);
        command.Parameters.AddWithValue(tokenHash);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        if (!await reader.ReadAsync(cancellationToken))
        {
            return null;
        }

        return new RefreshTokenRecord(
          reader.GetGuid(0),
          reader.GetGuid(1),
          reader.GetString(2),
          reader.GetFieldValue<DateTimeOffset>(3),
          reader.GetFieldValue<DateTimeOffset>(4),
          reader.IsDBNull(5) ? null : reader.GetFieldValue<DateTimeOffset>(5));
    }

    public async Task RevokeRefreshTokenAsync(Guid refreshTokenId, CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            update auth_refresh_tokens
            set revoked_at_utc = $2
            where id = $1 and revoked_at_utc is null
            """);
        command.Parameters.AddWithValue(refreshTokenId);
        command.Parameters.AddWithValue(DateTimeOffset.UtcNow);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    public async Task RevokeAllRefreshTokensAsync(Guid userId, CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            update auth_refresh_tokens
            set revoked_at_utc = $2
            where user_id = $1 and revoked_at_utc is null
            """);
        command.Parameters.AddWithValue(userId);
        command.Parameters.AddWithValue(DateTimeOffset.UtcNow);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    public async Task StorePasswordResetTokenAsync(Guid userId, string tokenHash, DateTimeOffset expiresAtUtc, CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            insert into auth_password_reset_tokens (id, user_id, token_hash, created_at_utc, expires_at_utc, consumed_at_utc)
            values ($1, $2, $3, $4, $5, null)
            """);
        command.Parameters.AddWithValue(Guid.NewGuid());
        command.Parameters.AddWithValue(userId);
        command.Parameters.AddWithValue(tokenHash);
        command.Parameters.AddWithValue(DateTimeOffset.UtcNow);
        command.Parameters.AddWithValue(expiresAtUtc);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    public async Task<PasswordResetTokenRecord?> FindPasswordResetTokenAsync(string tokenHash, CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            select id, user_id, token_hash, created_at_utc, expires_at_utc, consumed_at_utc
            from auth_password_reset_tokens
            where token_hash = $1
            """);
        command.Parameters.AddWithValue(tokenHash);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        if (!await reader.ReadAsync(cancellationToken))
        {
            return null;
        }

        return new PasswordResetTokenRecord(
          reader.GetGuid(0),
          reader.GetGuid(1),
          reader.GetString(2),
          reader.GetFieldValue<DateTimeOffset>(3),
          reader.GetFieldValue<DateTimeOffset>(4),
          reader.IsDBNull(5) ? null : reader.GetFieldValue<DateTimeOffset>(5));
    }

    public async Task ConsumePasswordResetTokenAsync(Guid passwordResetTokenId, CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            update auth_password_reset_tokens
            set consumed_at_utc = $2
            where id = $1 and consumed_at_utc is null
            """);
        command.Parameters.AddWithValue(passwordResetTokenId);
        command.Parameters.AddWithValue(DateTimeOffset.UtcNow);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    public async Task UpdatePasswordHashAsync(Guid userId, string passwordHash, CancellationToken cancellationToken)
    {
        await using var command = dataSource.CreateCommand("""
            update auth_users
            set password_hash = $2
            where id = $1
            """);
        command.Parameters.AddWithValue(userId);
        command.Parameters.AddWithValue(passwordHash);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static UserAccount ReadUser(NpgsqlDataReader reader) => new(
      reader.GetGuid(0),
      reader.GetString(1),
      reader.GetString(2),
      reader.GetFieldValue<DateTimeOffset>(3),
      reader.GetFieldValue<string[]>(4));
}
