import contextlib
import hashlib
import ipaddress
import re
import socket
import typing
import urllib.parse

import dns.exception
import dns.resolver
import requests
import tldextract


class URLUtils:
    @staticmethod
    def is_valid_url(
        url: str,
    ) -> bool:
        return bool(
            re.match(
                r"^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$",
                url,
            )
        )

    @staticmethod
    def get_domain(
        url: str,
    ) -> str:
        try:
            return urllib.parse.urlparse(url).netloc
        except Exception:
            return ""

    @staticmethod
    def get_ip(
        url: str,
    ) -> typing.Optional[str]:
        try:
            return socket.gethostbyname(URLUtils.get_domain(url))
        except socket.gaierror:
            return None

    @staticmethod
    def is_ip(
        url: str,
    ) -> bool:
        try:
            ipaddress.ip_address(url)
            return True
        except ValueError:
            return False

    @staticmethod
    def url_fingerprint(
        url: str,
    ) -> str:
        return hashlib.sha256(url.encode()).hexdigest()

    @staticmethod
    def get_dns_records(
        domain: str,
    ) -> typing.Dict[
        str,
        typing.List[str],
    ]:
        records: typing.Dict[
            str,
            typing.List[str],
        ] = {}
        with contextlib.suppress(Exception):
            for record_type in (
                "A",
                "AAAA",
                "MX",
                "NS",
                "TXT",
                "CNAME",
            ):
                try:
                    records[record_type] = [
                        str(r)
                        for r in dns.resolver.resolve(
                            domain,
                            record_type,
                        )
                    ]
                except (
                    dns.resolver.NoAnswer,
                    dns.resolver.NXDOMAIN,
                    dns.exception.Timeout,
                ):
                    continue
        return records

    @staticmethod
    def get_http_headers(
        url: str,
    ) -> typing.Dict[
        str,
        str,
    ]:
        try:
            response = requests.head(
                url,
                timeout=5,
                allow_redirects=True,
            )
            return dict(response.headers)
        except requests.RequestException:
            return {}

    @staticmethod
    def extract_urls(
        text: str,
    ) -> typing.List[str]:
        return re.findall(
            r"https?://[^\s<>\"']+",
            text,
        )

    @staticmethod
    def get_redirect_chain(
        url: str,
    ) -> typing.List[str]:
        try:
            response = requests.get(
                url,
                timeout=5,
                allow_redirects=True,
            )
            return [r.url for r in response.history] + [response.url]
        except requests.RequestException:
            return []

    @staticmethod
    def get_tld(
        url: str,
    ) -> str:
        extracted = tldextract.extract(url)
        return (
            f"{extracted.domain}.{extracted.suffix}"
            if extracted.suffix
            else extracted.domain
        )

    @staticmethod
    def is_shortened_url(
        url: str,
    ) -> bool:
        shorteners = {
            "bit.ly",
            "tinyurl.com",
            "goo.gl",
            "t.co",
            "ow.ly",
            "is.gd",
            "buff.ly",
        }
        return URLUtils.get_domain(url) in shorteners

    @staticmethod
    def get_url_path(
        url: str,
    ) -> str:
        try:
            return urllib.parse.urlparse(url).path
        except Exception:
            return ""

    @staticmethod
    def get_query_params(
        url: str,
    ) -> typing.Dict[
        str,
        str,
    ]:
        try:
            return dict(
                urllib.parse.parse_qsl(urllib.parse.urlparse(url).query)
            )
        except Exception:
            return {}

    @staticmethod
    def remove_tracking_params(
        url: str,
    ) -> str:
        try:
            parsed = urllib.parse.urlparse(url)
            query = urllib.parse.parse_qs(parsed.query)
            for key in list(query.keys()):
                if key.startswith("utm_") or key in {
                    "fbclid",
                    "gclid",
                }:
                    del query[key]
            return urllib.parse.urlunparse(
                parsed._replace(
                    query=urllib.parse.urlencode(
                        query,
                        doseq=True,
                    )
                )
            )
        except Exception:
            return url
