using Boyfriend.Data;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Gateway.Commands;
using Remora.Discord.API.Objects;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Discord.Gateway;
using Remora.Discord.Gateway.Responders;
using Remora.Discord.Interactivity;
using Remora.Rest.Core;
using Remora.Results;

namespace Boyfriend.Services;

/// <summary>
///     Handles executing guild updates (also called "ticks") once per second.
/// </summary>
public class GuildUpdateService : BackgroundService {
    private static readonly (string Name, TimeSpan Duration)[] SongList = {
        ("UNDEAD CORPORATION - The Empress", new TimeSpan(0, 4, 34)),
        ("UNDEAD CORPORATION - Everything will freeze", new TimeSpan(0, 3, 17)),
        ("Splatoon 3 - Rockagilly Blues (Yoko & the Gold Bazookas)", new TimeSpan(0, 3, 37)),
        ("Splatoon 3 - Seep and Destroy", new TimeSpan(0, 2, 42)),
        ("IA - A Tale of Six Trillion Years and a Night", new TimeSpan(0, 3, 40)),
        ("Manuel - Gas Gas Gas", new TimeSpan(0, 3, 17)),
        ("Camellia - Flamewall", new TimeSpan(0, 6, 50))
    };

    private readonly List<Activity> _activityList = new(1) { new Activity("with Remora.Discord", ActivityType.Game) };

    private readonly IDiscordRestChannelAPI             _channelApi;
    private readonly DiscordGatewayClient               _client;
    private readonly GuildDataService                   _dataService;
    private readonly IDiscordRestGuildScheduledEventAPI _eventApi;
    private readonly IDiscordRestGuildAPI               _guildApi;
    private readonly ILogger<GuildUpdateService>        _logger;
    private readonly IDiscordRestUserAPI                _userApi;
    private readonly UtilityService                     _utility;

    private DateTimeOffset _nextSongAt = DateTimeOffset.MinValue;
    private uint           _nextSongIndex;

    public GuildUpdateService(
        IDiscordRestChannelAPI             channelApi, DiscordGatewayClient client, GuildDataService dataService,
        IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, ILogger<GuildUpdateService> logger,
        IDiscordRestUserAPI                userApi, UtilityService utility) {
        _channelApi = channelApi;
        _client = client;
        _dataService = dataService;
        _eventApi = eventApi;
        _guildApi = guildApi;
        _logger = logger;
        _userApi = userApi;
        _utility = utility;
    }

    /// <summary>
    ///     Activates a periodic timer with a 1 second interval and adds guild update tasks on each timer tick.
    ///     Additionally, updates the current presence with songs from <see cref="SongList"/>.
    /// </summary>
    /// <remarks>If update tasks take longer than 1 second, the next timer tick will be skipped.</remarks>
    /// <param name="ct">The cancellation token for this operation.</param>
    protected override async Task ExecuteAsync(CancellationToken ct) {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
        var tasks = new List<Task>();

        while (await timer.WaitForNextTickAsync(ct)) {
            var guildIds = _dataService.GetGuildIds();
            if (guildIds.Count > 0 && DateTimeOffset.UtcNow >= _nextSongAt) {
                var nextSong = SongList[_nextSongIndex];
                _activityList[0] = new Activity(nextSong.Name, ActivityType.Listening);
                _client.SubmitCommand(
                    new UpdatePresence(
                        UserStatus.Online, false, DateTimeOffset.UtcNow, _activityList));
                _nextSongAt = DateTimeOffset.UtcNow.Add(nextSong.Duration);
                _nextSongIndex++;
                if (_nextSongIndex >= SongList.Length) _nextSongIndex = 0;
            }

            tasks.AddRange(guildIds.Select(id => TickGuildAsync(id, ct)));

            await Task.WhenAll(tasks);
            tasks.Clear();
        }
    }

    /// <summary>
    ///     Runs an update ("tick") for a guild with the provided <paramref name="guildId" />.
    /// </summary>
    /// <remarks>
    ///     This method does the following:
    ///     <list type="bullet">
    ///         <item>Automatically unbans users once their ban period has expired.</item>
    ///         <item>Automatically grants members the guild's <see cref="GuildConfiguration.DefaultRole"/> if one is set.</item>
    ///         <item>Sends reminders about an upcoming scheduled event.</item>
    ///         <item>Automatically starts scheduled events if <see cref="GuildConfiguration.AutoStartEvents"/> is enabled.</item>
    ///         <item>Sends scheduled event start notifications.</item>
    ///         <item>Sends scheduled event completion notifications.</item>
    ///         <item>Sends reminders to members.</item>
    ///     </list>
    ///     This is done here and not in a <see cref="IResponder{TGatewayEvent}" /> for the following reasons:
    ///     <list type="bullet">
    ///         <item>
    ///             Downtime would affect the reliability of notifications and automatic unbans if this logic were to be in a
    ///             <see cref="IResponder{TGatewayEvent}" />.
    ///         </item>
    ///         <item>The Discord API doesn't provide necessary information about scheduled event updates.</item>
    ///     </list>
    /// </remarks>
    /// <param name="guildId">The ID of the guild to update.</param>
    /// <param name="ct">The cancellation token for this operation.</param>
    private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) {
        var data = await _dataService.GetData(guildId, ct);
        Messages.Culture = data.Culture;
        var defaultRoleSnowflake = data.Configuration.DefaultRole.ToDiscordSnowflake();

        foreach (var memberData in data.MemberData.Values) {
            var userId = memberData.Id.ToDiscordSnowflake();

            if (defaultRoleSnowflake.Value is not 0 && !memberData.Roles.Contains(defaultRoleSnowflake))
                _ = _guildApi.AddGuildMemberRoleAsync(
                    guildId, userId, defaultRoleSnowflake, ct: ct);

            if (DateTimeOffset.UtcNow > memberData.BannedUntil) {
                var unbanResult = await _guildApi.RemoveGuildBanAsync(
                    guildId, userId, Messages.PunishmentExpired.EncodeHeader(), ct);
                if (unbanResult.IsSuccess)
                    memberData.BannedUntil = null;
                else
                    _logger.LogWarning(
                        "Error in automatic user unban request.\n{ErrorMessage}", unbanResult.Error.Message);
            }

            var userResult = await _userApi.GetUserAsync(userId, ct);
            if (!userResult.IsDefined(out var user)) continue;

            for (var i = memberData.Reminders.Count - 1; i >= 0; i--) {
                var reminder = memberData.Reminders[i];
                if (DateTimeOffset.UtcNow < reminder.RemindAt) continue;

                var embed = new EmbedBuilder().WithSmallTitle(
                        string.Format(Messages.Reminder, user.GetTag()), user)
                    .WithDescription(
                        string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text)))
                    .WithColour(ColorsList.Magenta)
                    .Build();

                if (!embed.IsDefined(out var built)) continue;

                var messageResult = await _channelApi.CreateMessageAsync(
                    reminder.Channel, Mention.User(user), embeds: new[] { built }, ct: ct);
                if (!messageResult.IsSuccess)
                    _logger.LogWarning(
                        "Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message);

                memberData.Reminders.Remove(reminder);
            }
        }

        var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct);
        if (!eventsResult.IsDefined(out var events)) return;

        if (data.Configuration.EventNotificationChannel is 0) return;

        foreach (var scheduledEvent in events) {
            if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) {
                data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status));
            } else {
                var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value];
                if (storedEvent.Status == scheduledEvent.Status) {
                    if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) {
                        if (data.Configuration.AutoStartEvents
                            && scheduledEvent.Status is not GuildScheduledEventStatus.Active) {
                            var startResult = await _eventApi.ModifyGuildScheduledEventAsync(
                                guildId, scheduledEvent.ID,
                                status: GuildScheduledEventStatus.Active, ct: ct);
                            if (!startResult.IsSuccess)
                                _logger.LogWarning(
                                    "Error in automatic scheduled event start request.\n{ErrorMessage}",
                                    startResult.Error.Message);
                        }
                    } else if (data.Configuration.EventEarlyNotificationOffset != TimeSpan.Zero
                               && !storedEvent.EarlyNotificationSent
                               && DateTimeOffset.UtcNow
                               >= scheduledEvent.ScheduledStartTime - data.Configuration.EventEarlyNotificationOffset) {
                        var earlyResult = await SendScheduledEventUpdatedMessage(scheduledEvent, data, true, ct);
                        if (earlyResult.IsSuccess)
                            storedEvent.EarlyNotificationSent = true;
                        else
                            _logger.LogWarning(
                                "Error in scheduled event early notification sender.\n{ErrorMessage}",
                                earlyResult.Error.Message);
                    }

                    continue;
                }

                storedEvent.Status = scheduledEvent.Status;
            }

            var result = scheduledEvent.Status switch {
                GuildScheduledEventStatus.Scheduled =>
                    await SendScheduledEventCreatedMessage(scheduledEvent, data.Configuration, ct),
                GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed =>
                    await SendScheduledEventUpdatedMessage(scheduledEvent, data, false, ct),
                _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)))
            };

            if (!result.IsSuccess)
                _logger.LogWarning("Error in guild update.\n{ErrorMessage}", result.Error.Message);
        }
    }

    /// <summary>
    ///     Handles sending a notification, mentioning the <see cref="GuildConfiguration.EventNotificationRole" /> if one is
    ///     set,
    ///     when a scheduled event is created
    ///     in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
    /// </summary>
    /// <param name="scheduledEvent">The scheduled event that has just been created.</param>
    /// <param name="config">The configuration of the guild containing the scheduled event.</param>
    /// <param name="ct">The cancellation token for this operation.</param>
    /// <returns>A notification sending result which may or may not have succeeded.</returns>
    private async Task<Result> SendScheduledEventCreatedMessage(
        IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) {
        var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
        if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);

        if (!scheduledEvent.CreatorID.IsDefined(out var creatorId))
            return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.CreatorID)));
        var creatorResult = await _userApi.GetUserAsync(creatorId.Value, ct);
        if (!creatorResult.IsDefined(out var creator)) return Result.FromError(creatorResult);

        string embedDescription;
        var eventDescription = scheduledEvent.Description is { HasValue: true, Value: not null }
            ? scheduledEvent.Description.Value
            : string.Empty;
        switch (scheduledEvent.EntityType) {
            case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice:
                if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId))
                    return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID)));

                embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote(
                    string.Format(
                        Messages.DescriptionLocalEventCreated,
                        Markdown.Timestamp(scheduledEvent.ScheduledStartTime),
                        Mention.Channel(channelId)
                    ))}";
                break;
            case GuildScheduledEventEntityType.External:
                if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata))
                    return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)));
                if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime))
                    return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)));
                if (!metadata.Location.IsDefined(out var location))
                    return Result.FromError(new ArgumentNullError(nameof(metadata.Location)));

                embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote(
                    string.Format(
                        Messages.DescriptionExternalEventCreated,
                        Markdown.Timestamp(scheduledEvent.ScheduledStartTime),
                        Markdown.Timestamp(endTime),
                        Markdown.InlineCode(location)
                    ))}";
                break;
            default:
                return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)));
        }

        var embed = new EmbedBuilder()
            .WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator)
            .WithTitle(scheduledEvent.Name)
            .WithDescription(embedDescription)
            .WithEventCover(scheduledEvent.ID, scheduledEvent.Image)
            .WithUserFooter(currentUser)
            .WithCurrentTimestamp()
            .WithColour(ColorsList.White)
            .Build();
        if (!embed.IsDefined(out var built)) return Result.FromError(embed);

        var roleMention = config.EventNotificationRole is not 0
            ? Mention.Role(config.EventNotificationRole.ToDiscordSnowflake())
            : string.Empty;

        var button = new ButtonComponent(
            ButtonComponentStyle.Primary,
            Messages.EventDetailsButton,
            new PartialEmoji(Name: "📋"),
            CustomIDHelpers.CreateButtonIDWithState(
                "scheduled-event-details", $"{scheduledEvent.GuildID}:{scheduledEvent.ID}")
        );

        return (Result)await _channelApi.CreateMessageAsync(
            config.EventNotificationChannel.ToDiscordSnowflake(), roleMention, embeds: new[] { built },
            components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct);
    }

    /// <summary>
    ///     Handles sending a notification, mentioning the <see cref="GuildConfiguration.EventStartedReceivers" />s,
    ///     when a scheduled event is about to start, has started or completed
    ///     in a guild's <see cref="GuildConfiguration.EventNotificationChannel" /> if one is set.
    /// </summary>
    /// <param name="scheduledEvent">The scheduled event that is about to start, has started or completed.</param>
    /// <param name="data">The data for the guild containing the scheduled event.</param>
    /// <param name="early">Controls whether or not a reminder for the scheduled event should be sent instead of the event started/completed notification</param>
    /// <param name="ct">The cancellation token for this operation</param>
    /// <returns>A reminder/notification sending result which may or may not have succeeded.</returns>
    private async Task<Result> SendScheduledEventUpdatedMessage(
        IGuildScheduledEvent scheduledEvent, GuildData data, bool early, CancellationToken ct = default) {
        var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
        if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);

        var embed = new EmbedBuilder();
        string? content = null;
        if (early)
            embed.WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser)
                .WithColour(ColorsList.Default);
        else
            switch (scheduledEvent.Status) {
                case GuildScheduledEventStatus.Active:
                    data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow;

                    string embedDescription;
                    switch (scheduledEvent.EntityType) {
                        case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice:
                            if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId))
                                return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID)));

                            embedDescription = string.Format(
                                Messages.DescriptionLocalEventStarted,
                                Mention.Channel(channelId)
                            );
                            break;
                        case GuildScheduledEventEntityType.External:
                            if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata))
                                return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)));
                            if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime))
                                return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)));
                            if (!metadata.Location.IsDefined(out var location))
                                return Result.FromError(new ArgumentNullError(nameof(metadata.Location)));

                            embedDescription = string.Format(
                                Messages.DescriptionExternalEventStarted,
                                Markdown.InlineCode(location),
                                Markdown.Timestamp(endTime)
                            );
                            break;
                        default:
                            return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)));
                    }

                    var contentResult = await _utility.GetEventNotificationMentions(
                        scheduledEvent, data.Configuration, ct);
                    if (!contentResult.IsDefined(out content))
                        return Result.FromError(contentResult);

                    embed.WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name))
                        .WithDescription(embedDescription)
                        .WithColour(ColorsList.Green);
                    break;
                case GuildScheduledEventStatus.Completed:
                    embed.WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name))
                        .WithDescription(
                            string.Format(
                                Messages.EventDuration,
                                DateTimeOffset.UtcNow.Subtract(
                                    data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime
                                    ?? scheduledEvent.ScheduledStartTime).ToString()))
                        .WithColour(ColorsList.Black);

                    data.ScheduledEvents.Remove(scheduledEvent.ID.Value);
                    break;
                case GuildScheduledEventStatus.Canceled:
                case GuildScheduledEventStatus.Scheduled:
                default: return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)));
            }

        var result = embed.WithCurrentTimestamp().Build();

        if (!result.IsDefined(out var built)) return Result.FromError(result);

        return (Result)await _channelApi.CreateMessageAsync(
            data.Configuration.EventNotificationChannel.ToDiscordSnowflake(),
            content ?? default(Optional<string>), embeds: new[] { built }, ct: ct);
    }
}