using System.Net;
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using Newtonsoft.Json.Linq;

namespace EllieBot.Modules.Utility.LiveChannel;

/// <summary>
/// Service for managing live channels.
/// </summary>
public class LiveChannelService(
    DbService db,
    DiscordSocketClient client,
    IReplacementService repSvc,
    ShardData shardData) : IReadyExecutor, IEService
{
    public const int MAX_LIVECHANNELS = 5;

    private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, LiveChannelConfig>> _liveChannels = new();

    /// <summary>
    /// Initializes data when bot is ready
    /// </summary>
    public async Task OnReadyAsync()
    {
        // Load all existing live channels into memory
        await using var uow = db.GetDbContext();
        var configs = await uow.GetTable<LiveChannelConfig>()
            .AsNoTracking()
            .Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId))
            .ToListAsyncLinqToDB();

        foreach (var config in configs)
        {
            var guildDict = _liveChannels.GetOrAdd(
                config.GuildId,
                _ => new());

            guildDict[config.ChannelId] = config;
        }

        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
        while (await timer.WaitForNextTickAsync())
        {
            try
            {
                // get all live channels from cache
                var channels = new List<LiveChannelConfig>(_liveChannels.Count * 2);

                foreach (var (_, vals) in _liveChannels)
                {
                    foreach (var (_, config) in vals)
                    {
                        channels.Add(config);
                    }
                }

                foreach (var config in channels)
                {
                    var guild = client.GetGuild(config.GuildId);
                    var channel = guild?.GetChannel(config.ChannelId);

                    if (channel is null)
                    {
                        await RemoveLiveChannelAsync(config.GuildId, config.ChannelId);
                        continue;
                    }

                    var repCtx = new ReplacementContext(
                        user: null,
                        guild: guild,
                        client: client
                    );

                    try
                    {
                        var text = await repSvc.ReplaceAsync(config.Template, repCtx);

                        // only update if needed
                        if (channel.Name != text)
                            await channel.ModifyAsync(x => x.Name = text);
                    }
                    catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden
                                                   || ex.DiscordCode == DiscordErrorCode.MissingPermissions
                                                   || ex.HttpCode == HttpStatusCode.NotFound)
                    {
                        await RemoveLiveChannelAsync(config.GuildId, config.ChannelId);
                        Log.Warning(
                            "Channel {ChannelId} in guild {GuildId} is not accessible. Live channel will be removed",
                            config.ChannelId,
                            config.GuildId);
                    }
                    catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.TooManyRequests ||
                                                   ex.DiscordCode == DiscordErrorCode.ChannelWriteRatelimit)
                    {
                        Log.Warning(ex, "LiveChannel hit a ratelimit. Sleeping for 2 minutes: {Message}", ex.Message);
                        await Task.Delay(2.Minutes());
                    }
                    catch (Exception e)
                    {
                        Log.Error(e, "Error in live channel service: {ErrorMessage}", e.Message);
                    }

                    // wait for half a second to reduce the chance of global ratelimits
                    await Task.Delay(500);
                }
            }
            catch (Exception ex)
            {
                Log.Error(ex, "Error in live channel service: {ErrorMessage}", ex.Message);
            }
        }
    }

    /// <summary>
    /// Adds a new live channel configuration to the specified guild.
    /// </summary>
    /// <param name="guildId">ID of the guild</param>
    /// <param name="channel">Channel to set as live</param>
    /// <param name="template">Template text to use for the channel</param>
    /// <returns>True if successfully added, false otherwise</returns>
    public async Task<bool> AddLiveChannelAsync(ulong guildId, IChannel channel, string template)
    {
        var guildDict = _liveChannels.GetOrAdd(
            guildId,
            _ => new());

        if (guildDict.Count >= MAX_LIVECHANNELS)
            return false;

        await using var uow = db.GetDbContext();
        await uow.GetTable<LiveChannelConfig>()
            .InsertOrUpdateAsync(() => new()
            {
                GuildId = guildId,
                ChannelId = channel.Id,
                Template = template
            },
                (_) => new()
                {
                    Template = template
                },
                () => new()
                {
                    GuildId = guildId,
                    ChannelId = channel.Id
                });

        // Add to in-memory cache
        var newConfig = new LiveChannelConfig
        {
            GuildId = guildId,
            ChannelId = channel.Id,
            Template = template
        };

        guildDict[channel.Id] = newConfig;
        return true;
    }

    /// <summary>
    /// Removes a live channel configuration from the specified guild.
    /// </summary>
    /// <param name="guildId">ID of the guild</param>
    /// <param name="channelId">ID of the channel to remove as live</param>
    /// <returns>True if successfully removed, false otherwise</returns>
    public async Task<bool> RemoveLiveChannelAsync(ulong guildId, ulong channelId)
    {
        if (!_liveChannels.TryGetValue(guildId, out var guildDict) ||
            !guildDict.TryRemove(channelId, out _))
            return false;

        await using var uow = db.GetDbContext();
        await uow.GetTable<LiveChannelConfig>()
            .Where(x => x.GuildId == guildId && x.ChannelId == channelId)
            .DeleteAsync();

        return true;
    }

    /// <summary>
    /// Gets all live channels for a guild.
    /// </summary>
    /// <param name="guildId">ID of the guild</param>
    /// <returns>List of live channel configurations</returns>
    public async Task<List<LiveChannelConfig>> GetLiveChannelsAsync(ulong guildId)
    {
        await using var uow = db.GetDbContext();
        return await uow.GetTable<LiveChannelConfig>()
            .AsNoTracking()
            .Where(x => x.GuildId == guildId)
            .ToListAsyncLinqToDB();
    }
}