using System.Globalization;
using GestionaDenuncias.Shared.Models;
using MySqlConnector;

namespace ApiDenuncias.Services;

public sealed class WorkGroupAdministrationService
{
    private readonly IDenunciaStore _denunciaStore;
    private readonly MySqlConnectionStringProvider _connectionStringProvider;

    public WorkGroupAdministrationService(
        IDenunciaStore denunciaStore,
        MySqlConnectionStringProvider connectionStringProvider)
    {
        _denunciaStore = denunciaStore;
        _connectionStringProvider = connectionStringProvider;
    }

    public async Task<WorkGroupAdministrationDto> GetAsync(
        CancellationToken cancellationToken = default)
    {
        await _denunciaStore.EnsureSchemaAsync(cancellationToken);
        await using var connection = await OpenConnectionAsync(cancellationToken);

        var groups = await LoadGroupsAsync(connection, cancellationToken);
        var users = new Dictionary<long, UserGroupBuilder>();

        const string sql = """
            SELECT
                au.id,
                au.username,
                wg.code AS group_code
            FROM app_users au
            LEFT JOIN app_user_groups aug ON aug.app_user_id = au.id
            LEFT JOIN work_groups wg
                ON wg.id = aug.work_group_id
               AND wg.is_active = 1
            ORDER BY au.username, wg.code;
            """;

        await using var command = new MySqlCommand(sql, connection);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            var userId = reader.GetInt64(reader.GetOrdinal("id"));
            if (!users.TryGetValue(userId, out var user))
            {
                user = new UserGroupBuilder(
                    userId,
                    reader.GetString(reader.GetOrdinal("username")));
                users[userId] = user;
            }

            var groupOrdinal = reader.GetOrdinal("group_code");
            if (!reader.IsDBNull(groupOrdinal))
            {
                user.GroupCodes.Add(reader.GetString(groupOrdinal));
            }
        }

        return new WorkGroupAdministrationDto(
            groups,
            users.Values
                .Select(user => new UserWorkGroupDto(
                    user.UserId,
                    user.Username,
                    user.GroupCodes.OrderBy(code => code, StringComparer.Ordinal).ToArray()))
                .ToArray());
    }

    public async Task<CurrentUserWorkGroupsDto> GetUserGroupsAsync(
        string username,
        CancellationToken cancellationToken = default)
    {
        await _denunciaStore.EnsureSchemaAsync(cancellationToken);

        var normalizedUsername = username?.Trim();
        if (string.IsNullOrWhiteSpace(normalizedUsername))
        {
            throw new InvalidOperationException("No hay usuario autenticado.");
        }

        await using var connection = await OpenConnectionAsync(cancellationToken);
        const string sql = """
            SELECT wg.code
            FROM app_users au
            INNER JOIN app_user_groups aug ON aug.app_user_id = au.id
            INNER JOIN work_groups wg
                ON wg.id = aug.work_group_id
               AND wg.is_active = 1
            WHERE LOWER(au.username) = LOWER(@username)
            ORDER BY wg.code;
            """;

        var groupCodes = new List<string>();
        await using var command = new MySqlCommand(sql, connection);
        command.Parameters.AddWithValue("@username", normalizedUsername);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            groupCodes.Add(reader.GetString(reader.GetOrdinal("code")));
        }

        return new CurrentUserWorkGroupsDto(normalizedUsername, groupCodes);
    }

    public async Task<WorkGroupAdministrationDto> UpdateUserGroupsAsync(
        string username,
        IEnumerable<string> groupCodes,
        string changedByUsername,
        CancellationToken cancellationToken = default)
    {
        await _denunciaStore.EnsureSchemaAsync(cancellationToken);

        var normalizedUsername = username?.Trim();
        if (string.IsNullOrWhiteSpace(normalizedUsername))
        {
            throw new InvalidOperationException("Debes indicar el usuario que se va a configurar.");
        }

        var normalizedCodes = groupCodes
            .Where(code => !string.IsNullOrWhiteSpace(code))
            .Select(code => code.Trim())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        if (normalizedCodes.Length == 0)
        {
            throw new InvalidOperationException("Cada usuario debe pertenecer al menos a un grupo de trabajo.");
        }

        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            var validGroups = await LoadGroupIdsAsync(
                connection,
                (MySqlTransaction)transaction,
                normalizedCodes,
                cancellationToken);

            if (validGroups.Count != normalizedCodes.Length)
            {
                throw new InvalidOperationException("Se ha indicado un grupo de trabajo que no existe o no esta activo.");
            }

            var userId = await EnsureUserAsync(
                connection,
                (MySqlTransaction)transaction,
                normalizedUsername,
                cancellationToken);
            var existingGroupIds = await LoadUserGroupIdsAsync(
                connection,
                (MySqlTransaction)transaction,
                userId,
                cancellationToken);
            var desiredGroupIds = validGroups.Values.ToHashSet();

            foreach (var removedGroupId in existingGroupIds.Except(desiredGroupIds))
            {
                await DeleteMembershipAsync(
                    connection,
                    (MySqlTransaction)transaction,
                    userId,
                    removedGroupId,
                    cancellationToken);
                await AddHistoryAsync(
                    connection,
                    (MySqlTransaction)transaction,
                    userId,
                    removedGroupId,
                    "removed",
                    changedByUsername,
                    cancellationToken);
            }

            foreach (var addedGroupId in desiredGroupIds.Except(existingGroupIds))
            {
                await AddMembershipAsync(
                    connection,
                    (MySqlTransaction)transaction,
                    userId,
                    addedGroupId,
                    changedByUsername,
                    cancellationToken);
                await AddHistoryAsync(
                    connection,
                    (MySqlTransaction)transaction,
                    userId,
                    addedGroupId,
                    "added",
                    changedByUsername,
                    cancellationToken);
            }

            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }

        return await GetAsync(cancellationToken);
    }

    private static async Task<List<WorkGroupDto>> LoadGroupsAsync(
        MySqlConnection connection,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT code, name
            FROM work_groups
            WHERE is_active = 1
            ORDER BY code;
            """;

        var groups = new List<WorkGroupDto>();
        await using var command = new MySqlCommand(sql, connection);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            groups.Add(new WorkGroupDto(
                reader.GetString(reader.GetOrdinal("code")),
                reader.GetString(reader.GetOrdinal("name"))));
        }

        return groups;
    }

    private static async Task<Dictionary<string, long>> LoadGroupIdsAsync(
        MySqlConnection connection,
        MySqlTransaction transaction,
        IReadOnlyList<string> codes,
        CancellationToken cancellationToken)
    {
        var result = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
        await using var command = new MySqlCommand { Connection = connection, Transaction = transaction };
        var parameters = new List<string>(codes.Count);
        for (var index = 0; index < codes.Count; index++)
        {
            var parameterName = $"@code{index}";
            parameters.Add(parameterName);
            command.Parameters.AddWithValue(parameterName, codes[index]);
        }

        command.CommandText = $"""
            SELECT id, code
            FROM work_groups
            WHERE is_active = 1
              AND code IN ({string.Join(", ", parameters)});
            """;

        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            result[reader.GetString(reader.GetOrdinal("code"))] =
                reader.GetInt64(reader.GetOrdinal("id"));
        }

        return result;
    }

    private static async Task<long> EnsureUserAsync(
        MySqlConnection connection,
        MySqlTransaction transaction,
        string username,
        CancellationToken cancellationToken)
    {
        const string insertSql = """
            INSERT INTO app_users (username)
            VALUES (@username)
            ON DUPLICATE KEY UPDATE
                updated_at_utc = CURRENT_TIMESTAMP(6);
            """;

        await using (var insert = new MySqlCommand(insertSql, connection, transaction))
        {
            insert.Parameters.AddWithValue("@username", username);
            await insert.ExecuteNonQueryAsync(cancellationToken);
        }

        const string selectSql = """
            SELECT id
            FROM app_users
            WHERE username = @username
            LIMIT 1;
            """;

        await using var select = new MySqlCommand(selectSql, connection, transaction);
        select.Parameters.AddWithValue("@username", username);
        var result = await select.ExecuteScalarAsync(cancellationToken);
        return Convert.ToInt64(result, CultureInfo.InvariantCulture);
    }

    private static async Task<HashSet<long>> LoadUserGroupIdsAsync(
        MySqlConnection connection,
        MySqlTransaction transaction,
        long userId,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT work_group_id
            FROM app_user_groups
            WHERE app_user_id = @userId;
            """;

        var result = new HashSet<long>();
        await using var command = new MySqlCommand(sql, connection, transaction);
        command.Parameters.AddWithValue("@userId", userId);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            result.Add(reader.GetInt64(0));
        }

        return result;
    }

    private static async Task DeleteMembershipAsync(
        MySqlConnection connection,
        MySqlTransaction transaction,
        long userId,
        long groupId,
        CancellationToken cancellationToken)
    {
        const string sql = """
            DELETE FROM app_user_groups
            WHERE app_user_id = @userId
              AND work_group_id = @groupId;
            """;

        await using var command = new MySqlCommand(sql, connection, transaction);
        command.Parameters.AddWithValue("@userId", userId);
        command.Parameters.AddWithValue("@groupId", groupId);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static async Task AddMembershipAsync(
        MySqlConnection connection,
        MySqlTransaction transaction,
        long userId,
        long groupId,
        string changedByUsername,
        CancellationToken cancellationToken)
    {
        const string sql = """
            INSERT INTO app_user_groups (
                app_user_id,
                work_group_id,
                assigned_by_username
            ) VALUES (
                @userId,
                @groupId,
                @changedByUsername
            )
            ON DUPLICATE KEY UPDATE
                assigned_by_username = VALUES(assigned_by_username);
            """;

        await using var command = new MySqlCommand(sql, connection, transaction);
        command.Parameters.AddWithValue("@userId", userId);
        command.Parameters.AddWithValue("@groupId", groupId);
        command.Parameters.AddWithValue("@changedByUsername", changedByUsername?.Trim() ?? string.Empty);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private static async Task AddHistoryAsync(
        MySqlConnection connection,
        MySqlTransaction transaction,
        long userId,
        long groupId,
        string action,
        string changedByUsername,
        CancellationToken cancellationToken)
    {
        const string sql = """
            INSERT INTO app_user_group_history (
                app_user_id,
                work_group_id,
                action,
                changed_by_username
            ) VALUES (
                @userId,
                @groupId,
                @action,
                @changedByUsername
            );
            """;

        await using var command = new MySqlCommand(sql, connection, transaction);
        command.Parameters.AddWithValue("@userId", userId);
        command.Parameters.AddWithValue("@groupId", groupId);
        command.Parameters.AddWithValue("@action", action);
        command.Parameters.AddWithValue("@changedByUsername", changedByUsername?.Trim() ?? string.Empty);
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
    {
        var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
        var connection = new MySqlConnection(connectionString);
        await connection.OpenAsync(cancellationToken);
        await using var command = new MySqlCommand("SET time_zone = '+00:00';", connection);
        await command.ExecuteNonQueryAsync(cancellationToken);
        return connection;
    }

    private sealed record UserGroupBuilder(long UserId, string Username)
    {
        public HashSet<string> GroupCodes { get; } = new(StringComparer.OrdinalIgnoreCase);
    }
}
