import re
from typing import Union
from urllib.parse import urlparse

from .exceptions import (
    NdffLibError,  # importing ndff.exceptions will raise a module error in the plugin
)


def last_url_item_is_number(url: str) -> tuple:
    """
    Util function to check if a given url ends with a number.

    To be used for example to check if a url ends with a number.

    :param str url: (ndff) url like "https://accapi.ndff.nl/api/v2/domains/708/bulksets/670"
    :return: tuple with of which first item is boolean, and second item is the last item of the url
    """
    if not is_uri(url):
        return False
    url_items = url.strip().split("/")
    if url_items[-1] == "":
        url_items.pop()
    last_item = url_items.pop()
    if is_numeric(last_item):
        return True, last_item
    else:
        return False, last_item


def is_uri(uri_string: str) -> bool:
    """
    Util function to check if given uri_string parameter is (or can be) an uri or not.

    To be used to test import of NDFF input's in which an uri is required.

    Started of with 'good enough' test from
    https://stackoverflow.com/questions/6718633/python-regular-expression-again-match-url

    :param str uri_string:
    :return: boolean if the argument is a uri string or not
    """

    if "ndff-ecogrid.nl" in str(uri_string):
        # ndff-ecogrid uri's are always very straight uri's, not containing chars like ,;$%() etc.
        # regexp below is based on postgres regexp from NDFF api code (see #145)
        uri_regex = re.compile(
            r"https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,255}\.[a-z]{2,9}\b([-a-zA-Z0-9@:%_\+.~#?&//=()–]*)$",
            re.MULTILINE | re.UNICODE,
        )
    else:
        # good enough test for general uri? https://stackoverflow.com/questions/6718633/python-regular-expression-again-match-url
        uri_regex = re.compile(
            r"((https?):((//)|(\\\\))+[\w:#@%/;$()~_?+-=\\.&]*)",
            re.MULTILINE | re.UNICODE,
        )
    # this one, is not good enough!!
    # uri_regex = re.compile(r"((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*", re.MULTILINE | re.UNICODE)

    # extra check for valid top level domain needed for ndff (Toplevel domain is mandatory!)
    # see: https://ndff.zendesk.com/hc/nl/requests/39562
    domain_and_path_ok = False
    try:
        parse_result = urlparse(f"{uri_string}")
        domain_part = parse_result.netloc
        path_part = parse_result.path
        # smallest domain I can imagine WITH a top-level domain part is a.nl (4 chars) https://en.wikipedia.org/wiki/Single-letter_second-level_domain
        # as we use this for .nl checks, we are not testing for .company etc. etc.
        domain_and_path_ok = (
            len(domain_part.split(".")) >= 2
            and domain_part[-1] not in [".", " "]
            and domain_part[0] not in [".", " "]
            and " " not in path_part
        )

    except NdffLibError:
        # there can be parse exceptions etc etc
        pass

    return (
        domain_and_path_ok
        and uri_regex.match(f"{uri_string}") is not None
        and "\n" not in uri_string
        and uri_string[-1] not in [".", " "]
    )


def is_hex(potential_hex_string: str) -> bool:
    """
    Test if the argument value could be a hexadecimal string

    In Postgis for example geometries are saved as hexadecimal strings (WKB)

    :param str potential_hex_string:
    :return: boolean if the argument is a hex string or not
    """
    try:
        int(potential_hex_string, 16)
        return True
    except ValueError:
        return False


def is_numeric(potential_numeric_value: Union[str, int, float]) -> bool:
    """
    Test if the argument value (either float, int or string) is numeric

    Created here because str.isnumeric() returns False for values like 1.0 or 1e14 so cannot not be used

    :param potential_numeric_value:
    :return: a boolean True incase the value is a number (or can be created from the string), False else
    """
    # first putting it in string here because using this method in the Plugin/Qt environment, the input
    # is potentially a QVariant.None or something like that, which returns 'Null' when casting to str
    if potential_numeric_value is None or str(
        potential_numeric_value
    ).strip().upper() in ("NONE", "FALSE", "TRUE", "NULL", ""):
        return False

    try:
        float(potential_numeric_value)
        return True
    except ValueError:
        return False


def is_empty(potential_empty_value: any) -> bool:
    """
    Test if the argument value is empty

    Created here because test for None fails when value is QVariant NULL

    :param potential_empty_value:
    :return: a boolean True incase the value is empty, False else
    """
    # first putting it in string here because using this method in the Plugin/Qt environment, the input
    # is potentially a QVariant.NULL or something like that, which returns 'Null' when casting to str
    if potential_empty_value is None or str(potential_empty_value).strip().upper() in (
        "NONE",
        "NULL",
        "",
    ):
        return True
    else:
        return False


def ellipsize2string(input: Union[str, int, float], max_len: int) -> str:
    """
    String representations can potentially become very large (polygons for example).
    Their representations then can create havoc on widgets and dialogs.
    This method shortens the string when the length of the string representation is longer then given max_len,
    AND appends '...' to it

    :param input: str | int | float input to 'ellipsize'
    :param max_len: maximum length of the string representation of the input (excluding ellipses)
    :return: a string with ellipses '...' when longer then max_len
    """
    output_string = str(input)
    if len(output_string) >= max_len:
        output_string = output_string[0:max_len] + "..."
    return output_string
