# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Entropy Markets](https://entropy.trade/) support is available at:
#   [Docs](https://docs.entropy.trade/)
#   [Discord](https://discord.gg/67jySBhxrg)
#   [Twitter](https://twitter.com/entropymarkets)
#   [Github](https://github.com/blockworks-foundation)
#   [Email](mailto:hello@blockworks.foundation)


import abc
import logging
import requests
import typing

from urllib.parse import unquote

from .output import output


# # 🥭 Notification
#
# This file contains code to send arbitrary notifications.
#

# # 🥭 NotificationTarget class
#
# This base class is the root of the different notification mechanisms.
#
# Derived classes should override `send_notification()` to implement their own sending logic.
#
# Derived classes should not override `send()` since that is the interface outside classes call and it's used to ensure `NotificationTarget`s don't throw an exception when sending.
#
class NotificationTarget(metaclass=abc.ABCMeta):
    def __init__(self) -> None:
        self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)

    def send(self, item: typing.Any) -> None:
        try:
            self.send_notification(item)
        except Exception as exception:
            self._logger.error(f"Error sending {item} - {self} - {exception}")

    @abc.abstractmethod
    def send_notification(self, item: typing.Any) -> None:
        raise NotImplementedError(
            "NotificationTarget.send() is not implemented on the base type."
        )

    def __str__(self) -> str:
        return "« NotificationTarget »"

    def __repr__(self) -> str:
        return f"{self}"


# # 🥭 TelegramNotificationTarget class
#
# The `TelegramNotificationTarget` sends messages to Telegram.
#
# The format for the telegram notification is:
# 1. The word 'telegram'
# 2. A colon ':'
# 3. The chat ID
# 4. An '@' symbol
# 5. The bot token
#
# For example:
# ```
# telegram:<CHAT-ID>@<BOT-TOKEN>
# ```
#
# The [Telegram instructions to create a bot](https://core.telegram.org/bots#creating-a-new-bot)
# show you how to create the bot token.
class TelegramNotificationTarget(NotificationTarget):
    def __init__(self, address: str) -> None:
        super().__init__()
        chat_id, bot_id = address.split("@", 1)
        self.chat_id = chat_id
        self.bot_id = bot_id

    def send_notification(self, item: typing.Any) -> None:
        payload = {
            "disable_notification": True,
            "chat_id": self.chat_id,
            "text": str(item),
        }
        url = f"https://api.telegram.org/bot{self.bot_id}/sendMessage"
        headers = {"Content-Type": "application/json"}
        requests.post(url, json=payload, headers=headers)

    def __str__(self) -> str:
        return f"« TelegramNotificationTarget Chat ID: {self.chat_id} »"


# # 🥭 DiscordNotificationTarget class
#
# The `DiscordNotificationTarget` sends messages to Discord.
#
class DiscordNotificationTarget(NotificationTarget):
    def __init__(self, address: str) -> None:
        super().__init__()
        self.address = address

    def send_notification(self, item: typing.Any) -> None:
        payload = {"content": str(item)}
        url = self.address
        headers = {"Content-Type": "application/json"}
        requests.post(url, json=payload, headers=headers)

    def __str__(self) -> str:
        return f"« DiscordNotificationTarget Address: {self.address} »"


# # 🥭 MailjetNotificationTarget class
#
# The `MailjetNotificationTarget` sends an email through [Mailjet](https://mailjet.com).
#
# In order to pass everything in to the notifier as a single string (needed to stop
# command-line parameters form getting messy), `MailjetNotificationTarget` requires a
# compound string, separated by colons.
# ```
# mailjet:<MAILJET-API-KEY>:<MAILJET-API-SECRET>:FROM-NAME:FROM-ADDRESS:TO-NAME:TO-ADDRESS
#
# ```
# Individual components are URL-encoded (so, for example, spaces are replaces with %20,
# colons are replaced with %3A).
#
# * `<MAILJET-API-KEY>` and `<MAILJET-API-SECRET>` are from your [Mailjet](https://mailjet.com) account.
# * `FROM-NAME` and `TO-NAME` are just text fields that are used as descriptors in the email messages.
# * `FROM-ADDRESS` is the address the email appears to come from. This must be validated with [Mailjet](https://mailjet.com).
# * `TO-ADDRESS` is the destination address - the email account to which the email is being sent.
#
# Mailjet provides a client library, but really we don't need or want more dependencies. This`
# code just replicates the `curl` way of doing things:
# ```
# curl -s \
# 	-X POST \
# 	--user "$MJ_APIKEY_PUBLIC:$MJ_APIKEY_PRIVATE" \
# 	https://api.mailjet.com/v3.1/send \
# 	-H 'Content-Type: application/json' \
# 	-d '{
#       "SandboxMode":"true",
#       "Messages":[
#         {
#           "From":[
#             {
#               "Email":"pilot@mailjet.com",
#               "Name":"Your Mailjet Pilot"
#             }
#           ],
#           "HTMLPart":"<h3>Dear passenger, welcome to Mailjet!</h3><br />May the delivery force be with you!",
#           "Subject":"Your email flight plan!",
#           "TextPart":"Dear passenger, welcome to Mailjet! May the delivery force be with you!",
#           "To":[
#             {
#               "Email":"passenger@mailjet.com",
#               "Name":"Passenger 1"
#             }
#           ]
#         }
#       ]
# 	}'
# ```
class MailjetNotificationTarget(NotificationTarget):
    def __init__(self, encoded_parameters: str) -> None:
        super().__init__()
        self.address = "https://api.mailjet.com/v3.1/send"
        (
            api_key,
            api_secret,
            subject,
            from_name,
            from_address,
            to_name,
            to_address,
        ) = encoded_parameters.split(":")
        self.api_key: str = unquote(api_key)
        self.api_secret: str = unquote(api_secret)
        self.subject: str = unquote(subject)
        self.from_name: str = unquote(from_name)
        self.from_address: str = unquote(from_address)
        self.to_name: str = unquote(to_name)
        self.to_address: str = unquote(to_address)

    def send_notification(self, item: typing.Any) -> None:
        payload = {
            "Messages": [
                {
                    "From": {"Email": self.from_address, "Name": self.from_name},
                    "Subject": self.subject,
                    "TextPart": str(item),
                    "To": [{"Email": self.to_address, "Name": self.to_name}],
                }
            ]
        }

        url = self.address
        headers = {"Content-Type": "application/json"}
        requests.post(
            url, json=payload, headers=headers, auth=(self.api_key, self.api_secret)
        )

    def __str__(self) -> str:
        return f"« MailjetNotificationTarget To: '{self.to_name}' '{self.to_address}' with subject '{self.subject}' »"


# # 🥭 FilteringNotificationTarget class
#
# This class takes a `NotificationTarget` and a filter function, and only calls the
# `NotificationTarget` if the filter function returns `True` for the notification item.
#
class FilteringNotificationTarget(NotificationTarget):
    def __init__(
        self,
        inner_notifier: NotificationTarget,
        filter_func: typing.Callable[[typing.Any], bool],
    ) -> None:
        super().__init__()
        self.inner_notifier: NotificationTarget = inner_notifier
        self.filter_func = filter_func

    def send_notification(self, item: typing.Any) -> None:
        if self.filter_func(item):
            self.inner_notifier.send_notification(item)

    def __str__(self) -> str:
        return f"« FilteringNotificationTarget For: {self.inner_notifier} »"


# # 🥭 ConsoleNotificationTarget class
#
# The `ConsoleNotificationTarget` prints messages on the console.
#
class ConsoleNotificationTarget(NotificationTarget):
    def __init__(self, name: str) -> None:
        super().__init__()
        self.name = name

    def send_notification(self, item: typing.Any) -> None:
        output(self.name, item)

    def __str__(self) -> str:
        return f"« ConsoleNotificationTarget '{self.name}' »"


# # 🥭 CompoundNotificationTarget class
#
# The `CompoundNotificationTarget` acts as a single `NotificationTarget`, sending notifications to all
# inner `NotificationTarget`s.
#
class CompoundNotificationTarget(NotificationTarget):
    def __init__(self, targets: typing.Sequence[NotificationTarget]) -> None:
        super().__init__()
        self.targets: typing.Sequence[NotificationTarget] = targets
        self.in_exception_handler: bool = False

    def send_notification(self, item: typing.Any) -> None:
        for target in self.targets:
            try:
                target.send(item)
            except Exception as exception:
                if not self.in_exception_handler:
                    self.in_exception_handler = True
                    self._logger.error(
                        f"Failed to send notification to: {target} - {exception}"
                    )
            finally:
                self.in_exception_handler = False

    def __str__(self) -> str:
        inner: typing.List[str] = []
        for target in self.targets:
            inner += [f"{target}"]
        inner_text: str = "\n    ".join(inner)
        return f"""« CompoundNotificationTarget with {len(self.targets)} inner targets:
    {inner_text}
»"""


# # 🥭 parse_notification_target() function
#
# `parse_notification_target()` takes a parameter as a string and returns a notification
# target.
#
# This is most likely used when parsing command-line arguments - this function can be used
# in the `type` parameter of an `add_argument()` call.
#
def parse_notification_target(target: str) -> NotificationTarget:
    protocol, destination = target.split(":", 1)

    if protocol == "telegram":
        return TelegramNotificationTarget(destination)
    elif protocol == "discord":
        return DiscordNotificationTarget(destination)
    elif protocol == "mailjet":
        return MailjetNotificationTarget(destination)
    elif protocol == "console":
        return ConsoleNotificationTarget(destination)
    else:
        raise Exception(f"Unknown protocol: {protocol}")


# # 🥭 NotificationHandler class
#
# A bridge between the worlds of notifications and logging. This allows any
# `NotificationTarget` to be plugged in to the `logging` subsystem to receive log messages
# and notify however it chooses.
#
class NotificationHandler(logging.StreamHandler):  # type: ignore
    def __init__(self, target: NotificationTarget) -> None:
        super().__init__()
        self.target = target

    def emit(self, record: logging.LogRecord) -> None:
        # Don't send error logging from solanaweb3
        if record.name == "solanaweb3.rpc.httprpc.HTTPClient":
            return
        message = self.format(record)
        self.target.send_notification(message)

    def __str__(self) -> str:
        return "« NotificationHandler »"
