using System.Text.RegularExpressions;
using Ikar.Server.Data;
using Ikar.Server.Data.Entities;
using Ikar.Server.Infrastructure;
using Ikar.Server.Infrastructure.Auth;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Ikar.Shared;

namespace Ikar.Server.Controllers;

[ApiController]
[Authorize]
[Route("api/[controller]")]
public sealed class BotsController(
    IkarDbContext dbContext,
    BotTokenService botTokenService) : ControllerBase
{
    private static readonly Regex BotUsernameRegex = new("^[A-Za-z0-9_]{5,64}$", RegexOptions.Compiled);
    private readonly PasswordHasher<User> _passwordHasher = new();

    [HttpGet("mine")]
    public async Task<ActionResult<IReadOnlyList<BotSummaryDto>>> GetMine(CancellationToken cancellationToken)
    {
        var ownerUserId = User.GetRequiredUserId();
        var bots = await dbContext.Bots
            .Include(x => x.User)
            .Include(x => x.Commands)
            .AsNoTracking()
            .Where(x => x.OwnerUserId == ownerUserId)
            .OrderBy(x => x.User.DisplayName)
            .ToListAsync(cancellationToken);

        return Ok(bots.Select(x => x.ToDto()).ToList());
    }

    [HttpGet("{botId:guid}")]
    public async Task<ActionResult<BotSummaryDto>> GetBot(Guid botId, CancellationToken cancellationToken)
    {
        var bot = await LoadOwnedBotAsync(botId, User.GetRequiredUserId(), cancellationToken);
        if (bot is null)
        {
            return NotFound();
        }

        return Ok(bot.ToDto());
    }

    [HttpPost]
    public async Task<ActionResult<CreatedBotDto>> CreateBot(
        CreateBotRequest request,
        CancellationToken cancellationToken)
    {
        var ownerUserId = User.GetRequiredUserId();
        var validationError = await ValidateBotDefinitionAsync(
            request.Username,
            request.DisplayName,
            request.About,
            excludingBotId: null,
            cancellationToken);
        if (validationError is not null)
        {
            return ValidationProblem(validationError);
        }

        var botUser = new User
        {
            Username = request.Username.Trim(),
            NormalizedUsername = request.Username.Trim().ToUpperInvariant(),
            DisplayName = request.DisplayName.Trim(),
            About = string.IsNullOrWhiteSpace(request.About) ? null : request.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);

        var created = await LoadOwnedBotAsync(bot.Id, ownerUserId, cancellationToken)
            ?? throw new InvalidOperationException("Newly created bot was not found.");

        return Ok(new CreatedBotDto(created.ToDto(), rawToken));
    }

    [HttpPut("{botId:guid}")]
    public async Task<ActionResult<BotSummaryDto>> UpdateBot(
        Guid botId,
        UpdateBotRequest request,
        CancellationToken cancellationToken)
    {
        var ownerUserId = User.GetRequiredUserId();
        var bot = await LoadOwnedBotAsync(botId, ownerUserId, cancellationToken);
        if (bot is null)
        {
            return NotFound();
        }

        var validationError = await ValidateBotDefinitionAsync(
            bot.User.Username,
            request.DisplayName,
            request.About,
            excludingBotId: botId,
            cancellationToken);
        if (validationError is not null)
        {
            return ValidationProblem(validationError);
        }

        bot.User.DisplayName = request.DisplayName.Trim();
        bot.User.About = string.IsNullOrWhiteSpace(request.About) ? null : request.About.Trim();
        bot.IsEnabled = request.IsEnabled;
        bot.UpdatedAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);

        return Ok(bot.ToDto());
    }

    [HttpPut("{botId:guid}/commands")]
    public async Task<ActionResult<BotSummaryDto>> SetCommands(
        Guid botId,
        SetBotCommandsRequest request,
        CancellationToken cancellationToken)
    {
        var ownerUserId = User.GetRequiredUserId();
        var bot = await LoadOwnedBotAsync(botId, ownerUserId, cancellationToken);
        if (bot is null)
        {
            return NotFound();
        }

        var normalizedCommands = 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 < normalizedCommands.Count; index++)
        {
            dbContext.BotCommands.Add(new BotCommand
            {
                BotId = bot.Id,
                Command = normalizedCommands[index].Command,
                Description = normalizedCommands[index].Description,
                SortOrder = index
            });
        }

        bot.UpdatedAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);
        var reloaded = await LoadOwnedBotAsync(bot.Id, ownerUserId, cancellationToken)
            ?? throw new InvalidOperationException("Updated bot was not found.");
        return Ok(reloaded.ToDto());
    }

    [HttpPost("{botId:guid}/regenerate-token")]
    public async Task<ActionResult<RegenerateBotTokenResponseDto>> RegenerateToken(Guid botId, CancellationToken cancellationToken)
    {
        var ownerUserId = User.GetRequiredUserId();
        var bot = await LoadOwnedBotAsync(botId, ownerUserId, cancellationToken);
        if (bot is null)
        {
            return NotFound();
        }

        var rawToken = botTokenService.GenerateToken(bot.Id);
        bot.TokenHash = botTokenService.HashToken(rawToken);
        bot.UpdatedAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);

        return Ok(new RegenerateBotTokenResponseDto(rawToken));
    }

    [HttpDelete("{botId:guid}")]
    public async Task<IActionResult> DeleteBot(Guid botId, CancellationToken cancellationToken)
    {
        var ownerUserId = User.GetRequiredUserId();
        var bot = await dbContext.Bots
            .Include(x => x.User)
                .ThenInclude(x => x.Sessions)
            .Include(x => x.User)
                .ThenInclude(x => x.PushDevices)
            .SingleOrDefaultAsync(
                x => x.Id == botId && x.OwnerUserId == ownerUserId,
                cancellationToken);
        if (bot is null)
        {
            return NotFound();
        }

        var botCommands = await dbContext.BotCommands
            .Where(x => x.BotId == bot.Id)
            .ToListAsync(cancellationToken);
        var botUpdates = await dbContext.BotUpdates
            .Where(x => x.BotId == bot.Id)
            .ToListAsync(cancellationToken);

        dbContext.BotUpdates.RemoveRange(botUpdates);
        dbContext.BotCommands.RemoveRange(botCommands);
        dbContext.UserSessions.RemoveRange(bot.User.Sessions);
        dbContext.PushDevices.RemoveRange(bot.User.PushDevices);
        bot.User.IsBlocked = true;
        bot.User.BlockedAt = DateTimeOffset.UtcNow;
        dbContext.Bots.Remove(bot);

        await dbContext.SaveChangesAsync(cancellationToken);
        return NoContent();
    }

    private async Task<Bot?> LoadOwnedBotAsync(Guid botId, Guid ownerUserId, CancellationToken cancellationToken)
    {
        return await dbContext.Bots
            .Include(x => x.User)
            .Include(x => x.Commands)
            .SingleOrDefaultAsync(
                x => x.Id == botId && x.OwnerUserId == ownerUserId,
                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 "Display name is required.";
        }

        if (trimmedDisplayName.Length > 120)
        {
            return "Display name must be 120 characters or fewer.";
        }

        if (!string.IsNullOrWhiteSpace(about) && about.Trim().Length > 70)
        {
            return "About must be 70 characters or fewer.";
        }

        if (!BotUsernameRegex.IsMatch(trimmedUsername) ||
            !trimmedUsername.EndsWith("bot", StringComparison.OrdinalIgnoreCase))
        {
            return "Bot username must contain only latin letters, digits or underscore and end with 'bot'.";
        }

        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);
        if (usernameTaken)
        {
            return "Username is already taken.";
        }

        return null;
    }

    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;
    }
}
