import logging
import operator
from itertools import chain

from allianceauth.services.modules.discord.models import DiscordUser
from asgiref.sync import sync_to_async
from discord import Embed
from discord.colour import Color
from discord.commands import Option
from discord.ext import commands, tasks
from django.conf import settings
from django.db.models import F
from django.utils import timezone

from opcalendar.app_settings import get_site_url
from opcalendar.models import Event, EventChannel, EventMember, IngameEvents

logger = logging.getLogger(__name__)

SIGNUP_STATUS_MAP = {
    "attending": EventMember.Status.ATTENDING,
    "maybe": EventMember.Status.MAYBE,
    "declined": EventMember.Status.DECLINED,
}

SIGNUP_NUMBER_OPTION_DESCRIPTION = "Event number from the pinned summary list"
SIGNUP_STATUS_OPTION_DESCRIPTION = "Sign-up status"
SIGNUP_STATUS_CHOICES = list(SIGNUP_STATUS_MAP)
SIGNUP_STATUS_DEFAULT = "attending"


def _upcoming_event_qs(event_channel):
    return Event.objects.select_related("operation_type").filter(
        event_visibility__in=event_channel.event_visibility.all(),
        start_time__gte=timezone.now(),
    )


def _upcoming_ingame_event_qs(event_channel):
    return IngameEvents.objects.annotate(start_time=F("event_start_date")).filter(
        owner__event_visibility__in=event_channel.event_visibility.all(),
        start_time__gte=timezone.now(),
    )


def _upcoming_events(event_channel):
    """Returns Event + IngameEvents combined, sorted by start time."""
    events = chain(
        _upcoming_event_qs(event_channel), _upcoming_ingame_event_qs(event_channel)
    )
    return sorted(events, key=operator.attrgetter("start_time"))


MAX_SUMMARY_EVENTS = 20


def _build_summary_embed(event_channel, events):
    embed = Embed(title="Upcoming events")
    embed.colour = Color.green()
    if not events:
        embed.description = "No upcoming events scheduled."
        return embed

    shown_events = events[:MAX_SUMMARY_EVENTS]
    embed.description = (
        "Sign up with `/signup`or directlyt at [AUTH]({}/opcalendar)".format(
            get_site_url()
        )
    )

    numbered_events = list(enumerate(shown_events, start=1))
    for index, event in reversed(numbered_events):
        start_unix = int(event.start_time.timestamp())
        if isinstance(event, Event):
            value = (
                "**Type:** {} {}\n**Formup:** {}\n**FC:** {}\n"
                "**Time:** <t:{}:F> (<t:{}:R>)"
            ).format(
                event.operation_type.name,
                event.operation_type.ticker,
                event.formup_system,
                event.fc,
                start_unix,
                start_unix,
            )
        else:
            value = (
                "**Ingame Event** hosted by {}\n**Time:** <t:{}:F> (<t:{}:R>)".format(
                    event.owner_name,
                    start_unix,
                    start_unix,
                )
            )

        embed.add_field(
            name="{}. {}".format(index, event.title),
            value=value,
            inline=False,
        )

    shown_count = len(shown_events)
    total_count = len(events)
    if total_count > shown_count:
        count_text = "Showing {} of {} upcoming events.".format(
            shown_count, total_count
        )
    else:
        count_text = "Showing {} upcoming event{}.".format(
            shown_count, "" if shown_count == 1 else "s"
        )

    embed.set_footer(text="{} Check out all ops on the opcalendar".format(count_text))

    return embed


@sync_to_async
def _get_enabled_event_channels():
    return list(EventChannel.objects.filter(enabled=True))


@sync_to_async
def _get_upcoming_events(event_channel):
    return _upcoming_events(event_channel)


@sync_to_async
def _render_summary_embed(event_channel, events):
    return _build_summary_embed(event_channel, events)


@sync_to_async
def _save_summary_message_id(event_channel, message_id):
    event_channel.summary_message_id = message_id
    event_channel.save(update_fields=["summary_message_id"])


@sync_to_async
def _get_event_channel_for_signup(channel_id):
    return EventChannel.objects.filter(channel_id=channel_id, enabled=True).first()


@sync_to_async
def _get_discord_user(uid):
    return (
        DiscordUser.objects.filter(uid=uid)
        .select_related("user__profile__main_character")
        .exclude(user__profile__main_character__isnull=True)
        .first()
    )


@sync_to_async
def _record_signup(event, character, status):
    EventMember.objects.update_or_create(
        event=event, character=character, defaults={"status": status}
    )


class EventChannelSync(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.sync_summaries.start()

    def cog_unload(self):
        self.sync_summaries.cancel()

    @tasks.loop(seconds=60)
    async def sync_summaries(self):
        for event_channel in await _get_enabled_event_channels():
            channel = self.bot.get_channel(event_channel.channel_id)
            if channel is None:
                logger.error(
                    "Could not find Discord channel %s for event channel %s",
                    event_channel.channel_id,
                    event_channel,
                )
                continue
            try:
                await self._sync_summary(channel, event_channel)
            except Exception:
                logger.exception("Failed to sync summary for %s", event_channel)

    @sync_summaries.before_loop
    async def before_sync_summaries(self):
        await self.bot.wait_until_ready()

    async def _sync_summary(self, channel, event_channel):
        events = await _get_upcoming_events(event_channel)
        embed = await _render_summary_embed(event_channel, events)

        if event_channel.summary_message_id:
            try:
                message = await channel.fetch_message(event_channel.summary_message_id)
                await message.edit(embed=embed)
                return
            except Exception:
                logger.warning(
                    "Pinned summary message %s for channel %s no longer exists, reposting",
                    event_channel.summary_message_id,
                    event_channel,
                )

        message = await channel.send(embed=embed)
        try:
            await message.pin()
        except Exception:
            logger.warning(
                "Could not pin summary message for channel %s", event_channel
            )
        await _save_summary_message_id(event_channel, message.id)

    @commands.slash_command(name="signup", guild_ids=[int(settings.DISCORD_GUILD_ID)])
    async def signup(
        self,
        ctx,
        number: Option(int, SIGNUP_NUMBER_OPTION_DESCRIPTION, required=True),
        status: Option(
            str,
            SIGNUP_STATUS_OPTION_DESCRIPTION,
            choices=SIGNUP_STATUS_CHOICES,
            required=False,
            default=SIGNUP_STATUS_DEFAULT,
        ),
    ):
        """Sign up for an upcoming event by its number in the pinned summary"""
        event_channel = await _get_event_channel_for_signup(ctx.channel.id)
        if event_channel is None:
            await ctx.respond(
                "This channel isn't set up for event sign-ups.", ephemeral=True
            )
            return

        status_key = status.lower()
        member_status = SIGNUP_STATUS_MAP.get(status_key)
        if member_status is None:
            await ctx.respond(
                "Unknown status `{}`. Use one of: {}".format(
                    status, ", ".join(SIGNUP_STATUS_MAP)
                ),
                ephemeral=True,
            )
            return

        events = await _get_upcoming_events(event_channel)
        if number < 1 or number > len(events):
            await ctx.respond(
                "No event numbered `{}` in the current summary. Check the pinned list.".format(
                    number
                ),
                ephemeral=True,
            )
            return

        event = events[number - 1]
        if not isinstance(event, Event):
            await ctx.respond(
                "Sign-ups aren't available for in-game calendar events "
                "(`{}`) - this entry is shown for reference only.".format(event.title),
                ephemeral=True,
            )
            return

        discord_user = await _get_discord_user(ctx.author.id)
        if discord_user is None:
            await ctx.respond(
                "Your Discord account isn't linked to an AllianceAuth character with a main character set.",
                ephemeral=True,
            )
            return

        character = discord_user.user.profile.main_character
        await _record_signup(event, character, member_status)
        await ctx.respond(
            "Marked **{}** as **{}** for **{}**.".format(
                character, status_key, event.title
            )
        )


def setup(bot):
    bot.add_cog(EventChannelSync(bot))
