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

namespace Ikar.Server.Controllers;

[ApiController]
[AllowAnonymous]
[Route("api/bot/{token}")]
public sealed class BotApiController(
    IkarDbContext dbContext,
    PresenceTracker presenceTracker,
    BotTokenService botTokenService,
    BotUpdateService botUpdateService,
    IHubContext<MessengerHub> hubContext,
    PushDispatchQueue pushDispatchQueue) : ControllerBase
{
    [HttpGet("getMe")]
    public async Task<ActionResult<UserSummaryDto>> GetMe(string token, CancellationToken cancellationToken)
    {
        var bot = await LoadBotByTokenAsync(token, cancellationToken);
        if (bot is null)
        {
            return Unauthorized();
        }

        if (!bot.IsEnabled)
        {
            return StatusCode(StatusCodes.Status423Locked, "Bot is disabled.");
        }

        return Ok(bot.User.ToDto(presenceTracker));
    }

    [HttpGet("getUpdates")]
    public async Task<ActionResult<BotUpdatesResponseDto>> GetUpdates(
        string token,
        [FromQuery] long offset = 0,
        [FromQuery] int limit = 100,
        [FromQuery] int timeoutSeconds = 20,
        CancellationToken cancellationToken = default)
    {
        var bot = await LoadBotByTokenAsync(token, cancellationToken);
        if (bot is null)
        {
            return Unauthorized();
        }

        if (!bot.IsEnabled)
        {
            return StatusCode(StatusCodes.Status423Locked, "Bot is disabled.");
        }

        limit = Math.Clamp(limit, 1, 100);
        timeoutSeconds = Math.Clamp(timeoutSeconds, 0, 50);

        var timeoutAt = DateTimeOffset.UtcNow.AddSeconds(timeoutSeconds);
        while (true)
        {
            var updates = await LoadUpdatesAsync(bot, offset, limit, cancellationToken);
            if (updates.Count > 0 || timeoutSeconds == 0 || DateTimeOffset.UtcNow >= timeoutAt)
            {
                var messages = updates
                    .Select(x => x.Message)
                    .Where(x => x is not null)
                    .Cast<Message>()
                    .ToList();
                if (messages.Count > 0)
                {
                    await botUpdateService.MarkMessagesReadByBotAsync(bot.UserId, messages, cancellationToken);
                }

                var payload = updates
                    .Select(update => new BotUpdateDto(
                        update.Id,
                        BotUpdateKind.Message,
                        update.Message?.ToDto(presenceTracker, bot.UserId)))
                    .ToList();

                var nextOffset = payload.Count == 0 ? offset : payload[^1].Id + 1;
                return Ok(new BotUpdatesResponseDto(nextOffset, payload));
            }

            await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
        }
    }

    [HttpGet("getMyCommands")]
    public async Task<ActionResult<IReadOnlyList<BotCommandDto>>> GetMyCommands(string token, CancellationToken cancellationToken)
    {
        var bot = await LoadBotByTokenAsync(token, cancellationToken);
        if (bot is null)
        {
            return Unauthorized();
        }

        return Ok(bot.Commands
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Command, StringComparer.OrdinalIgnoreCase)
            .Select(x => new BotCommandDto(x.Command, x.Description))
            .ToList());
    }

    [HttpPost("setMyCommands")]
    public async Task<ActionResult<IReadOnlyList<BotCommandDto>>> SetMyCommands(
        string token,
        SetBotCommandsRequest request,
        CancellationToken cancellationToken)
    {
        var bot = await LoadBotByTokenAsync(token, cancellationToken);
        if (bot is null)
        {
            return Unauthorized();
        }

        if (!bot.IsEnabled)
        {
            return StatusCode(StatusCodes.Status423Locked, "Bot is disabled.");
        }

        var commands = NormalizeCommands(request.Commands);
        var existingCommands = await dbContext.BotCommands
            .Where(x => x.BotId == bot.Id)
            .ToListAsync(cancellationToken);
        dbContext.BotCommands.RemoveRange(existingCommands);
        await dbContext.SaveChangesAsync(cancellationToken);

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

        bot.UpdatedAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);
        return Ok(commands);
    }

    [HttpPost("sendMessage")]
    public async Task<ActionResult<MessageDto>> SendMessage(
        string token,
        BotSendMessageRequest request,
        CancellationToken cancellationToken)
    {
        var bot = await LoadBotByTokenAsync(token, cancellationToken);
        if (bot is null)
        {
            return Unauthorized();
        }

        if (!bot.IsEnabled)
        {
            return StatusCode(StatusCodes.Status423Locked, "Bot is disabled.");
        }

        var text = request.Text?.Trim() ?? string.Empty;
        if (string.IsNullOrWhiteSpace(text))
        {
            return ValidationProblem("Message text is required.");
        }

        var chat = await dbContext.Chats
            .Include(x => x.Members)
            .SingleOrDefaultAsync(
                x => x.Id == request.ChatId && x.Members.Any(member => member.UserId == bot.UserId),
                cancellationToken);
        if (chat is null)
        {
            return NotFound("Chat not found.");
        }

        if (!DtoMapper.BuildPermissions(chat.Members.FirstOrDefault(member => member.UserId == bot.UserId), chat.Type).CanPostMessages)
        {
            return StatusCode(StatusCodes.Status403Forbidden, "Bot cannot publish in this chat.");
        }

        var message = new Message
        {
            ChatId = chat.Id,
            Chat = chat,
            SenderId = bot.UserId,
            Sender = bot.User,
            PostedAsChannel = chat.Type == ChatType.Channel,
            AuthorSignature = chat.Type == ChatType.Channel && chat.ChannelSignaturesEnabled
                ? string.IsNullOrWhiteSpace(bot.User.DisplayName) ? bot.User.Username : bot.User.DisplayName
                : null,
            Text = text,
            SentAt = DateTimeOffset.UtcNow
        };

        chat.LastActivityAt = message.SentAt;
        dbContext.Messages.Add(message);
        await dbContext.SaveChangesAsync(cancellationToken);

        await NotifyMessageCreatedAsync(chat, message, cancellationToken);
        await pushDispatchQueue.EnqueueAsync(chat.Id, message.Id);

        return Ok(message.ToDto(presenceTracker, bot.UserId));
    }

    private async Task<Bot?> LoadBotByTokenAsync(string token, CancellationToken cancellationToken)
    {
        var tokenHash = botTokenService.HashToken(token);
        return await dbContext.Bots
            .Include(x => x.User)
                .ThenInclude(x => x.Sessions)
            .Include(x => x.Commands)
            .SingleOrDefaultAsync(x => x.TokenHash == tokenHash, cancellationToken);
    }

    private async Task<List<BotUpdate>> LoadUpdatesAsync(Bot bot, long offset, int limit, CancellationToken cancellationToken)
    {
        return await dbContext.BotUpdates
            .AsNoTracking()
            .Where(x => x.BotId == bot.Id && x.Id >= offset)
            .OrderBy(x => x.Id)
            .Take(limit)
            .Include(x => x.Message)
                .ThenInclude(x => x.Sender)
                    .ThenInclude(x => x.Sessions)
            .Include(x => x.Message)
                .ThenInclude(x => x.Attachments)
            .Include(x => x.Message)
                .ThenInclude(x => x.Reactions)
            .Include(x => x.Message)
                .ThenInclude(x => x.Views)
            .Include(x => x.Message)
                .ThenInclude(x => x.Chat)
                    .ThenInclude(x => x.Members)
            .ToListAsync(cancellationToken);
    }

    private Task NotifyMessageCreatedAsync(Chat chat, Message message, CancellationToken cancellationToken)
    {
        var tasks = chat.Members.Select(async member =>
        {
            var dto = message.ToDto(presenceTracker, member.UserId);
            await hubContext.Clients.User(member.UserId.ToString())
                .SendAsync("MessageCreated", dto, cancellationToken);
        });

        return Task.WhenAll(tasks);
    }

    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 ?? Array.Empty<BotCommandDto>())
        {
            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, "^[a-z0-9_]{1,32}$", RegexOptions.IgnoreCase))
            {
                continue;
            }

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

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

        return normalized;
    }
}
