import re
from datetime import datetime
from typing import Any

from aiohttp import ClientResponse
from discord import ButtonStyle, Color, Embed, Emoji, Interaction, Message, NotFound, PartialEmoji, ui
from pydantic import BaseModel, ConfigDict, field_validator
from redbot.core.bot import commands

from ..constants import TYPES, TYPES_LIST, convert_name
from .repository import Repository


class Issue(BaseModel):
    """
    Model for provider issues. Used as an abstraction layer over the GitHub, GitLab, and Forgejo APIs, to simplify Discord-sided integration.
    "Issue" here refers to anything that uses the normal issue numbering scheme, so this includes Pull Requests and (GitHub) Discussions.

    Attributes:
        repository (Repository): The repository the issue belongs to.
        number (int): The issue number.
        author (str): The author of the issue.
        author_avatar (str): The author's avatar URL.
        author_url (str): The author's profile URL.
        link (str): A link to the issue.
        title (str): The title of the issue.
        body (str): The contents of the issue.
        type (str): The type of issue. Can be `issue`, `pull_request`, or `discussion`.
        draft (bool): Whether the issue is a draft. This will usually only apply to pull requests.
        creation_date (datetime): The date the issue was created on.
        milestone (str | None): The milestone the issue is associated with, if applicable.
        milestone_url (str | None): The URL to the milestone, if applicable.
        merge_date (datetime | None): The date the issue was merged, if applicable. This will usually only apply to pull requests.
        response (aiohttp.ClientResponse | None): The raw response from the provider API, if available.
        has_short_embed (bool): Whether the currently present message for this issue has a short embed. Defaults to False, please don't set this manually.
        message (discord.Message | None): The message object for the issue embed, if available.

    Properties:
        color (discord.Color): The color for the embed based on the issue type.
        emoji (discord.PartialEmoji | discord.Emoji): The emoji for the issue based on its type.
        markdown_link (str): A Markdown-formatted link to the issue.
        prefixed_number (str): The issue number prefixed with a `#`, or a `!` if it's a pull request and is from GitLab.
        pretty_title (str): The title with the some extra metadata.
        unix_timestamp (int): The creation date as a Unix timestamp.
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    repository: Repository
    number: int
    author: str
    author_avatar: str
    author_url: str
    link: str
    title: str
    body: str
    type: str
    labels: list[str] = []
    draft: bool = False
    creation_date: datetime
    milestone: str | None = None
    milestone_url: str | None = None
    merge_date: datetime | None = None
    response: ClientResponse | None = None
    _has_short_embed: bool = False
    _message: Message | None = None

    @field_validator("type")
    @classmethod
    def _validate_type(cls, value: Any) -> Any:
        if value not in TYPES_LIST:
            raise ValueError("Issue type '%s' is not supported." % value)
        return value

    @property
    def cleaned_body(self) -> str:
        pattern = r"<!--[\s\S]*?-->"
        return re.sub(pattern, "", self.body)

    @property
    def color(self) -> Color:
        """Get the color for the embed based on the issue type."""
        for t in TYPES:
            if t["name"] == self.type:
                return t["color"]
        return Color.from_str("#000000")  # Default to black if no match found

    @property
    def emoji(self) -> PartialEmoji | Emoji:
        """
        Return the emoji for the issue based on its type.
        This is used for Discord embeds.
        """
        name = ""
        for t in TYPES:
            if t["name"] == self.type:
                name: str = t["name"]
        emoji_name = convert_name(name)
        for emoji in self.repository.cog.application_emojis:
            if emoji.name == emoji_name:
                return emoji
        return PartialEmoji.from_str("❓")

    @property
    def footer_text(self) -> str:
        return f"{self.repository.owner}/{self.repository.name} • {'Merged' if self.merge_date else 'Created'}"

    @property
    def markdown_link(self) -> str:
        return f"[{self.pretty_title}]({self.link})"

    @property
    def markdown_milestone_link(self) -> str:
        if not self.milestone:
            raise ValueError("Issue does not have a milestone.")
        if self.milestone_url:
            return f"[{self.milestone}]({self.milestone_url})"
        return self.milestone

    @property
    def prefixed_number(self) -> str:
        if self.repository.provider.service == "gitlab" and "pull_request" in self.type:
            return f"!{self.number}"
        return f"#{self.number}"

    @property
    def pretty_title(self) -> str:
        """Return the title with the some extra metadata."""
        return f"{str(self.emoji)} {self.title} ({self.prefixed_number})"

    @property
    def timestamp(self) -> datetime:
        return self.merge_date if self.merge_date else self.creation_date

    @property
    def unix_timestamp(self) -> int:
        return int(self.timestamp.timestamp())

    async def send(self, ctx: commands.Context | Message, reply: bool = False, short: bool = False, **kwargs: Any) -> Message:
        """Send an embed for this Issue.

        Args:
            ctx (commands.Context | discord.Message): The context to send the embed in.
            reply (bool): Whether to reply to the message. Defaults to `False`.
            short (bool): Whether to send a short embed. Defaults to `False`.
            **kwargs (Any): Additional keyword arguments to pass to `discord.Messageable.send`.
        """
        if reply is True:
            msg = await ctx.reply(embed=self.short_embed() if short else self.embed(), view=_IssueView(self), mention_author=False, **kwargs)
        else:
            msg = await ctx.channel.send(embed=self.short_embed() if short else self.embed(), view=_IssueView(self), **kwargs)
        self._message = msg
        return msg

    def embed(self) -> Embed:
        """Create an embed for this Issue."""
        embed = self.short_embed()
        body = self.cleaned_body
        if len(body) > 4096:
            body = body[:4093] + "..."
        embed.description = body or "No description provided."

        if len(self.labels) > 0:
            formatted_labels: list[str] = []
            for label in self.labels:
                formatted_labels.extend((f"* {label}",))
            embed.add_field(name=f"Labels [{len(self.labels)}]", value="\n".join(formatted_labels), inline=True)

        if self.milestone:
            embed.add_field(name="Milestone", value=self.markdown_milestone_link, inline=True)

        return embed

    def short_embed(self) -> Embed:
        """Create a short embed for this Issue."""
        embed = Embed(
            url=self.link,
            title=self.pretty_title,
            color=self.color,
        )

        embed.set_author(
            name=self.author,
            url=self.author_url,
            icon_url=self.author_avatar,
        )

        embed.set_footer(text=self.footer_text, icon_url=self.repository.provider.favicon_url)
        embed.timestamp = self.timestamp
        return embed


class _IssueView(ui.View):
    def __init__(self, issue: Issue, timeout: int = 240) -> None:
        super().__init__(timeout=timeout)
        self.issue = issue
        self.timeout = timeout

    async def on_timeout(self) -> None:
        if self.issue._message is not None:  # noqa: SLF001
            try:
                await self.issue._message.edit(view=None)  # noqa: SLF001
            except NotFound:
                pass

    @ui.button(emoji="🗑️", style=ButtonStyle.gray)
    async def delete_message(self, interaction: Interaction, button: ui.Button) -> None:
        await interaction.response.defer()
        if interaction.message is not None:
            try:
                await interaction.message.delete()
            except NotFound:
                pass

    @ui.button(label="Switch View", style=ButtonStyle.primary)
    async def switch_message_embed(self, interaction: Interaction, button: ui.Button) -> None:
        await interaction.response.defer()
        if interaction.message is not None:
            self.issue._has_short_embed = not self.issue._has_short_embed  # noqa: SLF001
            try:
                await interaction.message.edit(embed=self.issue.short_embed() if self.issue._has_short_embed else self.issue.embed())  # noqa: SLF001
            except NotFound:
                pass