import functools
import logging
import socket
from typing import Union, NoReturn

import discord
import ipinfo
import ipwhois
from discord.ext import commands
from ipinfo.exceptions import RequestQuotaExceededError
from ipwhois import Net
from ipwhois.asn import IPASN
from structured_config import ConfigFile

from tuxbot.cogs.Network.functions.converters import (
    IPConverter,
    IPVersionConverter,
)
from tuxbot.cogs.Network.functions.exceptions import RFC18, InvalidIp
from tuxbot.core.bot import Tux
from tuxbot.core.i18n import (
    Translator,
)
from tuxbot.core.utils.data_manager import cogs_data_path
from tuxbot.core.utils.functions.extra import (
    ContextPlus,
    command_extra,
)
from .config import NetworkConfig

log = logging.getLogger("tuxbot.cogs.Network")
_ = Translator("Network", __file__)


class Network(commands.Cog, name="Network"):
    def __init__(self, bot: Tux):
        self.bot = bot
        self.config: NetworkConfig = ConfigFile(
            str(
                cogs_data_path(self.bot.instance_name, "Network")
                / "config.yaml"
            ),
            NetworkConfig,
        ).config

    async def cog_command_error(self, ctx, error):
        if isinstance(error, (RequestQuotaExceededError, RFC18, InvalidIp)):
            await ctx.send(_(str(error), ctx, self.bot.config))

    # =========================================================================
    # =========================================================================

    async def _get_ip(self, ctx: ContextPlus, ip: str, inet: str = "") -> str:
        inet_text = ""

        if inet == "6":
            inet = socket.AF_INET6
            inet_text = _("in v{v}", ctx, self.bot.config).format(v=inet)
        elif inet == "4":
            inet = socket.AF_INET
            inet_text = _("in v{v}", ctx, self.bot.config).format(v=inet)
        else:
            inet = 0

        try:
            return socket.getaddrinfo(str(ip), None, inet)[1][4][0]
        except socket.gaierror:
            return await ctx.send(
                _(
                    "Impossible to collect information on this ip {version}".format(
                        version=inet_text
                    ),
                    ctx,
                    self.bot.config,
                )
            )

    @staticmethod
    def _get_hostname(ip: str) -> str:
        try:
            return socket.gethostbyaddr(ip)[0]
        except socket.herror:
            return "N/A"

    @staticmethod
    def get_ipwhois_result(ip_address: str) -> Union[NoReturn, dict]:
        try:
            net = Net(ip_address)
            obj = IPASN(net)
            return obj.lookup()
        except ipwhois.exceptions.ASNRegistryError:
            return {}
        except ipwhois.exceptions.IPDefinedError as e:

            def _(x):
                return x

            raise RFC18(
                _(
                    "IP address {ip_address} is already defined as Private-Use"
                    " Networks via RFC 1918."
                )
            ) from e

    async def get_ipinfo_result(
        self, ip_address: str
    ) -> Union[NoReturn, dict]:
        try:
            handler = ipinfo.getHandlerAsync(self.config.ipinfoKey)
            return (await handler.getDetails(ip_address)).all
        except RequestQuotaExceededError:
            return {}

    # =========================================================================
    # =========================================================================

    @command_extra(name="iplocalise", aliases=["localiseip"], deletable=True)
    async def _iplocalise(
        self,
        ctx: ContextPlus,
        ip: IPConverter,
        version: IPVersionConverter = "",
    ):
        tmp = await ctx.send(
            _("*Retrieving information...*", ctx, self.bot.config),
            deletable=False,
        )

        ip_address = await self._get_ip(ctx, str(ip), str(version))
        ip_hostname = self._get_hostname(ip_address)

        ipinfo_result = await self.get_ipinfo_result(ip_address)
        ipwhois_result = await self.bot.loop.run_in_executor(
            None, functools.partial(self.get_ipwhois_result, ip_address)
        )

        e = discord.Embed(
            title=_(
                "Information for ``{ip} ({ip_address})``", ctx, self.bot.config
            ).format(ip=ip, ip_address=ip_address),
            color=0x5858D7,
        )

        if ipinfo_result:
            org = ipinfo_result.get("org", "")
            asn = org.split()[0]

            e.add_field(
                name=_("Belongs to:", ctx, self.bot.config),
                value=f"[{org}](https://bgp.he.net/{asn})",
                inline=True,
            )

            e.add_field(
                name=_("Region:", ctx, self.bot.config),
                value=f"{ipinfo_result.get('city', 'N/A')} - "
                f"{ipinfo_result.get('region', 'N/A')} "
                f"({ipinfo_result.get('country', 'N/A')})",
                inline=False,
            )

            e.set_thumbnail(
                url=f"https://www.countryflags.io/{ipinfo_result['country']}"
                f"/shiny/64.png"
            )
        elif ipwhois_result:
            org = ipwhois_result.get("asn_description", "N/A")
            asn = ipwhois_result.get("asn", "N/A")
            asn_country = ipwhois_result.get("asn_country_code", "N/A")

            e.add_field(
                name=_("Belongs to:", ctx, self.bot.config),
                value=f"{org} ([AS{asn}](https://bgp.he.net/{asn}))",
                inline=True,
            )

            e.add_field(
                name=_("Region:", ctx, self.bot.config),
                value=asn_country,
                inline=False,
            )

            e.set_thumbnail(
                url=f"https://www.countryflags.io/{asn_country}/shiny/64.png"
            )

        if ipwhois_result:
            e.add_field(
                name="RIR :", value=ipwhois_result["asn_registry"], inline=True
            )

        e.set_footer(
            text=_("Hostname: {hostname}", ctx, self.bot.config).format(
                hostname=ip_hostname
            ),
        )

        await tmp.delete()
        await ctx.send(embed=e)