import builtins
import datetime
import re
from dataclasses import field
from typing import Union, Optional, Tuple
from urllib.parse import urlparse

from rdflib import Literal, BNode, URIRef
from rdflib.namespace import NamespaceManager, is_ncname
from rdflib.term import Identifier as rdflib_Identifier

from biolinkml.utils.namespaces import Namespaces
from biolinkml.utils.strictness import is_strict


# ===========================
# Fields for use in dataclass
# ===========================


def empty_list():
    """ Return a field with a list factory """
    return field(default_factory=list)


def empty_dict():
    """ Return a field with a dictionary factory """
    return field(default_factory=dict)


def empty_set():
    """" Return a field with a set factory """
    return field(default_factory=set)


def bnode():
    """ Return a field with a BNode factory """
    return field(default_factory=lambda: BNode().n3())


# List of builtins for mangling names
builtinnames = dir(builtins)


class NCName(str):
    """ Wrapper for NCName class """
    def __init__(self, v: str) -> None:
        if is_strict() and not self.is_valid(v):
            raise ValueError(f"{v}: invalid prefix")
        self.nsm: Optional[Namespaces] = None
        super().__init__()

    @classmethod
    def is_valid(cls, v: str) -> bool:
        return is_ncname(v) if v else True      # Empty is default name


class Identifier(str):
    """ A datatype that can be a URI, CURIE or a BNode """
    def __init__(self, v: Union[str, URIRef, BNode, "URIorCURIE", "Identifier"]) -> None:
        v = str(v)
        if is_strict() and not self.is_valid(v):
            raise ValueError(f"{v}: is not a valid Identifier")
        super().__init__()

    @classmethod
    def is_valid(cls, v: Union[str, URIRef, BNode, "URIorCURIE", "Identifier"]) -> bool:
        if v.startswith('_:'):
            pfx, ln = v.split(':')
            return len(ln) == 1 and bool(Curie.term_name.match(ln))
        else:
            return URIorCURIE.is_valid(v)

    def as_identifier(self, nsm: Optional[Namespaces]) -> Optional[rdflib_Identifier]:
        if self.startswith('_:'):
            return BNode(self)
        elif URIorCURIE.is_absolute(self):
            return URIRef(self)
        elif nsm:
            return URIorCURIE(self).as_uri(nsm)
        return None


class URIorCURIE(Identifier):
    """ A datatype that can either be a URI or a CURIE """
    def __init__(self, v: Union[str, URIRef, "Curie", "URIorCURIE"], *args, **kwargs) -> None:
        if is_strict() and not URIorCURIE.is_valid(v):
            raise ValueError(f"{v} is not a valid URI or CURIE")
        super().__init__(v, *args, **kwargs)

    @classmethod
    def is_valid(cls, v: Union[str, URIRef, "Curie", "URIorCURIE"]) -> bool:
        v = str(v)
        if ':' in v and '://' not in v:
            return URIorCURIE.is_curie(v)
        else:
            return URI.is_valid(v)

    @staticmethod
    def is_absolute(v: str) -> bool:
        return bool(urlparse(v).netloc)

    @staticmethod
    def is_curie(v: str, nsm: Optional[Namespaces] = None) -> bool:
        if ':' in v and '://' not in v:
            ns, ln = v.split(':', 1)
            return len(ns) == 0 or (NCName.is_valid(ns) and
                                    (nsm is None or any(ns == nsns for nsns, _ in nsm.namespaces())))
        return False

    def as_uri(self, nsm: Namespaces) -> Optional[URIRef]:
        if self.is_absolute(self):
            return URIRef(self)
        return Curie(self).as_uri(nsm)


class URI(URIorCURIE):
    """ A relative absolute URI
    """
    def __init__(self, v: str, *args, **kwargs) -> None:
        if is_strict() and not URI.is_valid(v):
            raise ValueError(f"{v}: is not a valid URI")
        super().__init__(v, *args, **kwargs)

    @classmethod
    def is_valid(cls, v: str) -> bool:
        # TODO: need to get a bit more rigorous here...
        return not URIorCURIE.is_curie(v) and bool(urlparse(v))


class Curie(URIorCURIE):

    """ Wrapper for an element that MUST be represented as a CURIE """
    def __init__(self, v: str, *args, **kwargs) -> None:
        if is_strict() and not self.is_valid(v):
            raise ValueError(f"{v} is not a valid CURIE")
        super().__init__(v, *args, **kwargs)

    # TODO: see whether we can leverage the rdflib RDFA termorcurie package
    term_name = re.compile("^[A-Za-z]([A-Za-z0-9._-]|/)*$")

    @classmethod
    def ns_ln(cls, v: str) -> Optional[Tuple[str, str]]:
        # See if this is indeed a valid CURIE, ie, it can be split by a colon
        curie_split = v.split(':', 1)
        if len(curie_split) == 1:
            # there is no ':' character in the string, ie, it is not a valid CURIE
            return None
        else:
            prefix = curie_split[0].lower()
            if not NCName.is_valid(prefix):
                return None
            reference = curie_split[1]
            if not cls.term_name.match(reference):
                return None
        return prefix, reference

    @classmethod
    def is_valid(cls, v: str) -> bool:
        return cls.ns_ln(v) is not None

    # This code was extracted from the termorcurie package of the rdfa
    def as_uri(self, nsm: Namespaces) -> Optional[URIRef]:
        """ Return the URI for the CURIE if a mapping is available, otherwise return None """
        try:
            return nsm.uri_for(self)
        except ValueError:
            pass
        return None


class Bool:
    """ Wrapper for boolean datatype """
    bool_true = re.compile(r'([Tt]rue)|(1)$')
    bool_false = re.compile(r'([Ff]alse)|(0)$')

    def __new__(cls, v: Union[str, bool, "Bool"]) -> bool:
        if isinstance(v, bool):
            return v
        if cls.bool_true.match(str(v)):
            return True
        if cls.bool_false.match(str(v)):
            return False
        if is_strict():
            raise ValueError(f"{v}: Must be a boolean value")
        return bool(v)

    @classmethod
    def is_valid(cls, v: str) -> bool:
        """ Determine whether the string v is a valid instance of bool """
        return isinstance(v, bool) or cls.bool_true.match(str(v)) or cls.bool_false.match(str(v))


class XSDTime(str):
    """ Wrapper for time datatype """
    def __new__(cls, value: Union[str, datetime.time, datetime.datetime, Literal]) -> str:
        if is_strict() and not cls.is_valid(value):
            raise ValueError(f"{value} is not a valid time")
        if isinstance(value, Literal):
            value = value.value
        try:
            if not isinstance(value, datetime.time):
                value = datetime.time.fromisoformat(value)
            return datetime.time.fromisoformat(str(value)).isoformat()
        except TypeError as e:
            pass
        except ValueError as e:
            pass
        if not is_strict():
            return str(value)
        raise e

    @classmethod
    def is_valid(cls, value: Union[str, datetime.time, datetime.datetime, Literal]) -> bool:
        if isinstance(value, Literal):
            value = value.value
        if isinstance(value, (datetime.time, datetime.datetime)):
            value = value.isoformat()
        try:
            datetime.time.fromisoformat(str(value))
        except ValueError:
            return False
        return True


class XSDDate(str):
    """ Wrapper for date datatype """
    def __new__(cls, value: Union[str, datetime.date, Literal]) -> str:
        if is_strict() and not cls.is_valid(value):
            raise ValueError(f"{value} is not a valid date")
        if isinstance(value, Literal):
            value = value.value
        try:
            if not isinstance(value, datetime.date):
                value = datetime.date.fromisoformat(str(value))
            return value.isoformat()
        except TypeError as e:
            pass
        except ValueError as e:
            pass
        if not is_strict():
            return str(value)
        raise e

    @classmethod
    def is_valid(cls, value: Union[str, datetime.date, Literal]) -> bool:
        if isinstance(value, Literal):
            value = value.value
        if isinstance(value, datetime.date):
            value = value.isoformat()
        try:
            datetime.date.fromisoformat(str(value))
        except ValueError:
            return False
        return True


class XSDDateTime(str):
    """ Wrapper for date time dataclass """
    def __new__(cls, value: Union[str, datetime.datetime, Literal]) -> str:
        if is_strict() and not cls.is_valid(value):
            raise ValueError(f"{value} is not a valid datetime")
        if isinstance(value, Literal):
            value = value.value
        try:
            if not isinstance(value, datetime.datetime):
                value = datetime.datetime.fromisoformat(value)      # Note that this handles non 'T' format as well
            return value.isoformat()
        except TypeError as e:
            pass
        except ValueError as e:
            pass
        if not is_strict():
            return str(value)
        raise e

    @classmethod
    def is_valid(cls, value: Union[str, datetime.datetime, Literal]) -> bool:
        if isinstance(value, Literal):
            value = value.value
        if isinstance(value, datetime.datetime):
            value = value.isoformat()
        try:
            datetime.datetime.fromisoformat(value)
        except ValueError:
            return False
        return True


class NodeIdentifier(Identifier):
    """ A RDFLib Identifier that represents a URI, CURIE or BNode in a model """

    def __new__(cls, v: Union[str, URIRef, URIorCURIE, "NodeIdentifier", BNode]) -> "NodeIdentifier":
        if hasattr(v, 'id'):        # Allows passing instances of identified objects
            v = v.id
        if isinstance(v, NodeIdentifier):
            if is_strict() and not issubclass(type(v), cls):
                raise ValueError(f"Invalid identifier type for {cls.__name__}: {v} ({type(v).__name__})")
            else:
                return super().__new__(cls, v)
        elif type(v) in (str, URIRef, URIorCURIE, BNode) or not is_strict():
            return super().__new__(cls, str(v))
        else:
            raise ValueError(f"Unknown identifier for {cls.__name__}: {v}")

    def __post_init__(self):
        pass


class ElementIdentifier(NodeIdentifier):
    """ A URIorCURIE that represents an element in a model """

    def __new__(cls, v: Union[str, URIRef, URIorCURIE, NodeIdentifier, "ElementIdentifier"]) -> "ElementIdentifier":
        if hasattr(v, 'id'):        # Allows passing instances of identified objects
            v = v.id
        if is_strict() and (type(v) is BNode or str(v).startswith('_:')):
            raise ValueError(f"Invalid identifier type for {cls.__name__}: {v} ({type(v).__name__})")
        return super().__new__(cls, v)

    def __post_init__(self):
        pass

