using System.Text.RegularExpressions;
using Ikar.Server.Data;
using Ikar.Server.Data.Entities;
using Ikar.Server.Infrastructure.Auth;
using Ikar.Server.Infrastructure.Hubs;
using Ikar.Server.Infrastructure.Push;
using Ikar.Shared;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;

namespace Ikar.Server.Infrastructure.Bots;

public sealed class BotCreatorService(
    IkarDbContext dbContext,
    BotTokenService botTokenService,
    BotDeletionService botDeletionService,
    PresenceTracker presenceTracker,
    IHubContext<MessengerHub> hubContext,
    PushDispatchQueue pushDispatchQueue)
{
    public const string CreatorUsername = "botfatherbot";
    private const string CreatorDisplayName = "Создатель ботов";
    private static readonly Regex BotUsernameRegex = new("^[A-Za-z0-9_]{5,64}$", RegexOptions.Compiled);
    private readonly PasswordHasher<User> _passwordHasher = new();

    public async Task EnsureCreatorBotAsync(CancellationToken cancellationToken = default)
    {
        var normalizedUsername = CreatorUsername.ToUpperInvariant();
        var user = await dbContext.Users
            .Include(x => x.BotProfile)
                .ThenInclude(x => x!.Commands)
            .SingleOrDefaultAsync(x => x.NormalizedUsername == normalizedUsername, cancellationToken);

        if (user is null)
        {
            user = new User
            {
                Username = CreatorUsername,
                NormalizedUsername = normalizedUsername,
                DisplayName = CreatorDisplayName,
                About = "Создаёт и настраивает ботов Икара через команды в чате.",
                IsBot = true
            };
            user.PasswordHash = _passwordHasher.HashPassword(user, Guid.NewGuid().ToString("N"));
            dbContext.Users.Add(user);
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        if (!user.IsBot)
        {
            user.IsBot = true;
        }

        user.DisplayName = CreatorDisplayName;
        user.About = "Создаёт и настраивает ботов Икара через команды в чате.";
        user.IsBlocked = false;
        user.BlockedAt = null;

        var bot = user.BotProfile;
        if (bot is null)
        {
            bot = new Bot
            {
                UserId = user.Id,
                OwnerUserId = user.Id
            };
            var rawToken = botTokenService.GenerateToken(bot.Id);
            bot.TokenHash = botTokenService.HashToken(rawToken);
            dbContext.Bots.Add(bot);
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        bot.IsEnabled = true;
        bot.UpdatedAt = DateTimeOffset.UtcNow;
        await ReplaceCommandsAsync(bot.Id, CreatorCommands, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
    }

    public async Task<bool> TryHandleIncomingMessageAsync(Guid messageId, CancellationToken cancellationToken = default)
    {
        var message = await dbContext.Messages
            .Include(x => x.Sender)
            .Include(x => x.Chat)
                .ThenInclude(x => x.Members)
                    .ThenInclude(x => x.User)
                        .ThenInclude(x => x.BotProfile)
            .SingleOrDefaultAsync(x => x.Id == messageId, cancellationToken);

        if (message is null ||
            message.Sender.IsBot ||
            message.Chat.Type != ChatType.Direct ||
            string.IsNullOrWhiteSpace(message.Text))
        {
            return false;
        }

        var creator = await LoadCreatorBotAsync(cancellationToken);
        if (creator is null || message.Chat.Members.All(member => member.UserId != creator.UserId))
        {
            return false;
        }

        var replyText = await BuildReplyAsync(message.SenderId, message.Text.Trim(), cancellationToken);
        await SendCreatorReplyAsync(message.Chat, creator.User, replyText, cancellationToken);
        return true;
    }

    private async Task<string> BuildReplyAsync(Guid ownerUserId, string text, CancellationToken cancellationToken)
    {
        var parts = text.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        var command = parts.FirstOrDefault()?.TrimStart('/').ToLowerInvariant();
        var payload = parts.Length > 1 ? parts[1].Trim() : string.Empty;

        return command switch
        {
            "start" or "help" or "помощь" => HelpText,
            "newbot" or "создать_бота" => await CreateBotAsync(ownerUserId, payload, cancellationToken),
            "mybots" or "мои_боты" => await ListBotsAsync(ownerUserId, cancellationToken),
            "token" or "токен" => await RegenerateTokenAsync(ownerUserId, payload, cancellationToken),
            "setcommands" or "команды" => await SetCommandsAsync(ownerUserId, payload, cancellationToken),
            "disablebot" or "выключить_бота" => await SetBotEnabledAsync(ownerUserId, payload, isEnabled: false, cancellationToken),
            "enablebot" or "включить_бота" => await SetBotEnabledAsync(ownerUserId, payload, isEnabled: true, cancellationToken),
            "deletebot" or "удалить_бота" => await DeleteBotAsync(ownerUserId, payload, cancellationToken),
            _ => "Я понимаю команды /создать_бота, /мои_боты, /токен, /команды, /выключить_бота, /включить_бота и /удалить_бота. Напишите /помощь для формата команд."
        };
    }

    private async Task<string> CreateBotAsync(Guid ownerUserId, string payload, CancellationToken cancellationToken)
    {
        var fields = SplitPipePayload(payload);
        if (fields.Count < 2)
        {
            return "Формат: /создать_бота Имя бота | usernamebot | описание";
        }

        var displayName = fields[0];
        var username = NormalizeBotUsername(fields[1]);
        var about = fields.Count >= 3 ? fields[2] : null;

        var validationError = await ValidateBotDefinitionAsync(username, displayName, about, excludingBotId: null, cancellationToken);
        if (validationError is not null)
        {
            return validationError;
        }

        var botUser = new User
        {
            Username = username,
            NormalizedUsername = username.ToUpperInvariant(),
            DisplayName = displayName,
            About = string.IsNullOrWhiteSpace(about) ? null : about.Trim(),
            IsBot = true
        };
        botUser.PasswordHash = _passwordHasher.HashPassword(botUser, Guid.NewGuid().ToString("N"));

        var bot = new Bot
        {
            User = botUser,
            OwnerUserId = ownerUserId
        };

        var rawToken = botTokenService.GenerateToken(bot.Id);
        bot.TokenHash = botTokenService.HashToken(rawToken);
        dbContext.Bots.Add(bot);
        await dbContext.SaveChangesAsync(cancellationToken);

        return $"Бот создан: {displayName} (@{username})\nТокен:\n{rawToken}\n\nСохраните токен сейчас. Позже можно выпустить новый командой /токен {username}.";
    }

    private async Task<string> ListBotsAsync(Guid ownerUserId, CancellationToken cancellationToken)
    {
        var bots = await dbContext.Bots
            .AsNoTracking()
            .Include(x => x.User)
            .Where(x => x.OwnerUserId == ownerUserId && x.User.NormalizedUsername != CreatorUsername.ToUpperInvariant())
            .OrderBy(x => x.User.DisplayName)
            .ToListAsync(cancellationToken);

        if (bots.Count == 0)
        {
            return "У вас пока нет ботов. Создайте первого командой /создать_бота Имя | usernamebot | описание";
        }

        return "Ваши боты:\n" + string.Join(
            "\n",
            bots.Select(bot => $"{bot.User.DisplayName} (@{bot.User.Username}) — {(bot.IsEnabled ? "включён" : "выключен")}"));
    }

    private async Task<string> RegenerateTokenAsync(Guid ownerUserId, string payload, CancellationToken cancellationToken)
    {
        var bot = await LoadOwnedBotByUsernameAsync(ownerUserId, payload, cancellationToken);
        if (bot is null)
        {
            return "Бот не найден. Формат: /токен usernamebot";
        }

        var rawToken = botTokenService.GenerateToken(bot.Id);
        bot.TokenHash = botTokenService.HashToken(rawToken);
        bot.UpdatedAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);
        return $"Новый токен для @{bot.User.Username}:\n{rawToken}";
    }

    private async Task<string> SetCommandsAsync(Guid ownerUserId, string payload, CancellationToken cancellationToken)
    {
        var fields = SplitPipePayload(payload);
        if (fields.Count < 2)
        {
            return "Формат: /команды usernamebot | старт - описание; помощь - описание";
        }

        var bot = await LoadOwnedBotByUsernameAsync(ownerUserId, fields[0], cancellationToken);
        if (bot is null)
        {
            return "Бот не найден.";
        }

        var commands = ParseCommandDefinitions(fields[1]);
        if (commands.Count == 0)
        {
            return "Не нашёл корректных команд. Пример: /команды usernamebot | старт - старт; помощь - помощь";
        }

        await ReplaceCommandsAsync(bot.Id, commands, cancellationToken);
        bot.UpdatedAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);
        return $"Команды @{bot.User.Username} обновлены.";
    }

    private async Task<string> SetBotEnabledAsync(
        Guid ownerUserId,
        string payload,
        bool isEnabled,
        CancellationToken cancellationToken)
    {
        var bot = await LoadOwnedBotByUsernameAsync(ownerUserId, payload, cancellationToken);
        if (bot is null)
        {
            return $"Бот не найден. Формат: /{(isEnabled ? "включить_бота" : "выключить_бота")} usernamebot";
        }

        bot.IsEnabled = isEnabled;
        bot.UpdatedAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);
        return isEnabled ? $"@{bot.User.Username} включён." : $"@{bot.User.Username} выключен.";
    }

    private async Task<string> DeleteBotAsync(Guid ownerUserId, string payload, CancellationToken cancellationToken)
    {
        var bot = await LoadOwnedBotByUsernameAsync(ownerUserId, payload, cancellationToken, includeOwnedData: true);
        if (bot is null)
        {
            return "Бот не найден. Формат: /удалить_бота usernamebot";
        }

        var username = bot.User.Username;
        var deleted = await botDeletionService.DeleteBotAsync(bot.Id, ownerUserId, cancellationToken);
        return deleted is null
            ? "Бот не найден. Формат: /удалить_бота usernamebot"
            : $"@{username} удалён.";
    }

    private async Task<Bot?> LoadCreatorBotAsync(CancellationToken cancellationToken) =>
        await dbContext.Bots
            .Include(x => x.User)
            .SingleOrDefaultAsync(x => x.User.NormalizedUsername == CreatorUsername.ToUpperInvariant(), cancellationToken);

    private async Task<Bot?> LoadOwnedBotByUsernameAsync(
        Guid ownerUserId,
        string rawUsername,
        CancellationToken cancellationToken,
        bool includeOwnedData = false)
    {
        var username = NormalizeBotUsername(rawUsername);
        if (string.IsNullOrWhiteSpace(username) || string.Equals(username, CreatorUsername, StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }

        var query = dbContext.Bots
            .Include(x => x.User)
            .Where(x => x.OwnerUserId == ownerUserId && x.User.NormalizedUsername == username.ToUpperInvariant());

        if (includeOwnedData)
        {
            query = query
                .Include(x => x.User)
                    .ThenInclude(x => x.Sessions)
                .Include(x => x.User)
                    .ThenInclude(x => x.PushDevices);
        }

        return await query.SingleOrDefaultAsync(cancellationToken);
    }

    private async Task<string?> ValidateBotDefinitionAsync(
        string? username,
        string? displayName,
        string? about,
        Guid? excludingBotId,
        CancellationToken cancellationToken)
    {
        var trimmedUsername = username?.Trim() ?? string.Empty;
        var trimmedDisplayName = displayName?.Trim() ?? string.Empty;
        var normalizedUsername = trimmedUsername.ToUpperInvariant();

        if (string.IsNullOrWhiteSpace(trimmedDisplayName))
        {
            return "Укажите имя бота.";
        }

        if (trimmedDisplayName.Length > 120)
        {
            return "Имя бота должно быть не длиннее 120 символов.";
        }

        if (!string.IsNullOrWhiteSpace(about) && about.Trim().Length > 70)
        {
            return "Описание должно быть не длиннее 70 символов.";
        }

        if (!BotUsernameRegex.IsMatch(trimmedUsername) ||
            !trimmedUsername.EndsWith("bot", StringComparison.OrdinalIgnoreCase))
        {
            return "Username бота должен содержать латинские буквы, цифры или подчёркивание и заканчиваться на bot.";
        }

        if (string.Equals(trimmedUsername, CreatorUsername, StringComparison.OrdinalIgnoreCase))
        {
            return "Этот username зарезервирован системным ботом.";
        }

        var usernameTaken = await dbContext.Users
            .AsNoTracking()
            .Include(x => x.BotProfile)
            .AnyAsync(
                x => x.NormalizedUsername == normalizedUsername &&
                     (!excludingBotId.HasValue || x.BotProfile == null || x.BotProfile.Id != excludingBotId.Value),
                cancellationToken);
        return usernameTaken ? "Username уже занят." : null;
    }

    private async Task ReplaceCommandsAsync(Guid botId, IReadOnlyList<BotCommandDto> commands, CancellationToken cancellationToken)
    {
        var existingCommands = await dbContext.BotCommands
            .Where(x => x.BotId == botId)
            .ToListAsync(cancellationToken);
        dbContext.BotCommands.RemoveRange(existingCommands);

        var normalizedCommands = NormalizeCommands(commands);
        for (var index = 0; index < normalizedCommands.Count; index++)
        {
            dbContext.BotCommands.Add(new BotCommand
            {
                BotId = botId,
                Command = normalizedCommands[index].Command,
                Description = normalizedCommands[index].Description,
                SortOrder = index
            });
        }
    }

    private async Task SendCreatorReplyAsync(Chat chat, User creatorUser, string text, CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;
        var reply = new Message
        {
            ChatId = chat.Id,
            Chat = chat,
            SenderId = creatorUser.Id,
            Sender = creatorUser,
            Text = text,
            SentAt = now
        };

        chat.LastActivityAt = now;
        dbContext.Messages.Add(reply);
        await dbContext.SaveChangesAsync(cancellationToken);

        var loadedReply = await dbContext.Messages
            .Include(x => x.Sender)
                .ThenInclude(x => x.Sessions)
            .Include(x => x.Chat)
                .ThenInclude(x => x.Members)
                    .ThenInclude(x => x.User)
                        .ThenInclude(x => x.Sessions)
            .Include(x => x.Attachments)
            .Include(x => x.Reactions)
            .SingleAsync(x => x.Id == reply.Id, cancellationToken);

        var tasks = loadedReply.Chat.Members.Select(async member =>
        {
            var dto = loadedReply.ToDto(presenceTracker, member.UserId);
            await hubContext.Clients.User(member.UserId.ToString())
                .SendAsync("MessageCreated", dto, cancellationToken);
        });
        await Task.WhenAll(tasks);
        await pushDispatchQueue.EnqueueAsync(loadedReply.ChatId, loadedReply.Id);
    }

    private static string NormalizeBotUsername(string value) => value.Trim().TrimStart('@');

    private static List<string> SplitPipePayload(string payload) =>
        payload
            .Split('|', StringSplitOptions.TrimEntries)
            .Where(x => !string.IsNullOrWhiteSpace(x))
            .ToList();

    private static List<BotCommandDto> ParseCommandDefinitions(string value) =>
        value
            .Split([';', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(ParseCommandDefinition)
            .Where(x => x is not null)
            .Cast<BotCommandDto>()
            .ToList();

    private static BotCommandDto? ParseCommandDefinition(string value)
    {
        var parts = value.Split(" - ", 2, StringSplitOptions.TrimEntries);
        if (parts.Length < 2)
        {
            parts = value.Split(':', 2, StringSplitOptions.TrimEntries);
        }

        if (parts.Length < 2)
        {
            return null;
        }

        return new BotCommandDto(parts[0], parts[1]);
    }

    private static List<BotCommandDto> NormalizeCommands(IReadOnlyList<BotCommandDto> commands)
    {
        var normalized = new List<BotCommandDto>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var command in commands)
        {
            var normalizedCommand = (command.Command ?? string.Empty).Trim().TrimStart('/');
            var normalizedDescription = (command.Description ?? string.Empty).Trim();
            if (string.IsNullOrWhiteSpace(normalizedCommand) || string.IsNullOrWhiteSpace(normalizedDescription))
            {
                continue;
            }

            if (!Regex.IsMatch(normalizedCommand, @"^[\p{L}0-9_]{1,32}$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
            {
                continue;
            }

            if (!seen.Add(normalizedCommand))
            {
                continue;
            }

            normalized.Add(new BotCommandDto(normalizedCommand.ToLowerInvariant(), normalizedDescription));
        }

        return normalized;
    }

    private static readonly IReadOnlyList<BotCommandDto> CreatorCommands =
    [
        new("помощь", "показать команды"),
        new("создать_бота", "создать бота"),
        new("мои_боты", "показать моих ботов"),
        new("токен", "выпустить новый токен"),
        new("команды", "настроить команды бота"),
        new("выключить_бота", "выключить бота"),
        new("включить_бота", "включить бота"),
        new("удалить_бота", "удалить бота")
    ];

    private const string HelpText =
        """
        Я создаю и настраиваю ботов Икара.

        /создать_бота Имя бота | usernamebot | описание
        /мои_боты
        /токен usernamebot
        /команды usernamebot | старт - описание; помощь - описание
        /выключить_бота usernamebot
        /включить_бота usernamebot
        /удалить_бота usernamebot

        Username должен быть латиницей и заканчиваться на bot.
        """;
}
