from collections import OrderedDict
from typing import List, Optional, Any, Union, Dict, Tuple
import rdflib
from biolinkml.meta import Element, SlotDefinition, ClassDefinition
from cachetools import cached, LRUCache
from rdflib import Namespace, URIRef
from rdflib.namespace import RDF, RDFS, OWL, SKOS

from kgx.config import get_logger
from kgx.prefix_manager import PrefixManager
from kgx.utils.graph_utils import get_category_via_superclass
from kgx.utils.kgx_utils import (
    get_curie_lookup_service,
    contract,
    get_cache,
    get_toolkit,
    sentencecase_to_snakecase,
    sentencecase_to_camelcase,
    get_biolink_ancestors,
)

log = get_logger()

OBAN = Namespace('http://purl.org/oban/')
BIOLINK = Namespace('https://w3id.org/biolink/vocab/')
OIO = Namespace('http://www.geneontology.org/formats/oboInOwl#')
OBO = Namespace('http://purl.obolibrary.org/obo/')

property_mapping: Dict = dict()
reverse_property_mapping: Dict = dict()

# TODO: this should be populated via bmt
is_property_multivalued = {
    'id': False,
    'subject': False,
    'object': False,
    'predicate': False,
    'description': False,
    'synonym': True,
    'in_taxon': False,
    'same_as': True,
    'name': False,
    'has_evidence': False,
    'provided_by': True,
    'category': True,
    'publications': True,
    'type': False,
    'relation': False,
}


top_level_terms = {
    OBO.term('CL_0000000'): 'cell',
    OBO.term('UBERON_0001062'): 'anatomical_entity',
    OBO.term('PATO_0000001'): 'quality',
    OBO.term('NCBITaxon_131567'): 'organism',
    OBO.term('CLO_0000031'): 'cell_line',
    OBO.term('MONDO_0000001'): 'disease',
    OBO.term('CHEBI_23367'): 'molecular_entity',
    OBO.term('CHEBI_23888'): 'drug',
    OBO.term('UPHENO_0001001'): 'phenotypic_feature',
    OBO.term('GO_0008150'): 'biological_process',
    OBO.term('GO_0009987'): 'cellular_process',
    OBO.term('GO_0005575'): 'cellular_component',
    OBO.term('GO_0003674'): 'molecular_function',
    OBO.term('SO_0000704'): 'gene',
    OBO.term('GENO_0000002'): 'variant_locus',
    OBO.term('GENO_0000536'): 'genotype',
    OBO.term('SO_0000110'): 'sequence_feature',
    OBO.term('ECO_0000000'): 'evidence',
    OBO.term('PW_0000001'): 'pathway',
    OBO.term('IAO_0000310'): 'publication',
    OBO.term('SO_0001483'): 'snv',
    OBO.term('GENO_0000871'): 'haplotype',
    OBO.term('SO_0001024'): 'haplotype',
    OBO.term('SO_0000340'): 'chromosome',
    OBO.term('SO_0000104'): 'protein',
    OBO.term('SO_0001500'): 'phenotypic_marker',
    OBO.term('SO_0000001'): 'region',
    OBO.term('HP_0032223'): 'blood_group',
    OBO.term('HP_0031797'): 'clinical_course',
    OBO.term('HP_0040279'): 'frequency',
    OBO.term('HP_0000118'): 'phenotypic_abnormality',
    OBO.term('HP_0032443'): 'past_medical_history',
    OBO.term('HP_0000005'): 'mode_of_inheritance',
    OBO.term('HP_0012823'): 'clinical_modifier',
}


def infer_category(iri: URIRef, rdfgraph: rdflib.Graph) -> Optional[List]:
    """
    Infer category for a given iri by traversing rdfgraph.

    Parameters
    ----------
    iri: rdflib.term.URIRef
        IRI
    rdfgraph: rdflib.Graph
        A graph to traverse

    Returns
    -------
    Optional[List]
        A list of category corresponding to the given IRI

    """
    closure = list(rdfgraph.transitive_objects(iri, URIRef(RDFS.subClassOf)))
    category = [top_level_terms[x] for x in closure if x in top_level_terms.keys()]
    if category:
        log.debug(
            "Inferred category as {} based on transitive closure over 'subClassOf' relation".format(
                category
            )
        )
    else:
        subj = closure[-1]
        if subj == iri:
            return category
        subject_curie: Optional[str] = contract(subj)
        if subject_curie and '_' in subject_curie:
            fixed_curie = subject_curie.split(':', 1)[1].split('_', 1)[1]
            log.warning("Malformed CURIE {} will be fixed to {}".format(subject_curie, fixed_curie))
            subject_curie = fixed_curie
        cls = get_curie_lookup_service()
        category = list(get_category_via_superclass(cls.ontology_graph, subject_curie))
    return category


@cached(LRUCache(maxsize=1024))
def get_biolink_element(prefix_manager: PrefixManager, predicate: Any) -> Optional[Element]:
    """
    Returns a Biolink Model element for a given predicate.

    Parameters
    ----------
    prefix_manager: PrefixManager
        An instance of prefix manager
    predicate: Any
        The CURIE of a predicate

    Returns
    -------
    Optional[Element]
        The corresponding Biolink Model element

    """
    toolkit = get_toolkit()
    if prefix_manager.is_iri(predicate):
        predicate_curie = prefix_manager.contract(predicate)
    else:
        predicate_curie = predicate
    if prefix_manager.is_curie(predicate_curie):
        reference = prefix_manager.get_reference(predicate_curie)
    else:
        reference = predicate_curie
    element = toolkit.get_element(reference)
    if not element:
        try:
            mapping = toolkit.get_element_by_mapping(predicate)
            if mapping:
                element = toolkit.get_element(mapping)
        except ValueError as e:
            log.error(e)
    return element


def process_predicate(
    prefix_manager: PrefixManager, p: Union[URIRef, str], predicate_mapping: Optional[Dict] = None
) -> Tuple:
    """
    Process a predicate where the method checks if there is a mapping in Biolink Model.

    Parameters
    ----------
    prefix_manager: PrefixManager
        An instance of prefix manager
    p: Union[URIRef, str]
        The predicate
    predicate_mapping: Optional[Dict]
        Predicate mappings

    Returns
    -------
    Tuple[str, str, str, str]
        A tuple that contains the Biolink CURIE (if available), the Biolink slot_uri CURIE (if available),
        the CURIE form of p, the reference of p

    """
    if prefix_manager.is_iri(p):
        predicate = prefix_manager.contract(str(p))
    else:
        predicate = None
    if prefix_manager.is_curie(p):
        property_name = prefix_manager.get_reference(p)
        predicate = p
    else:
        if predicate and prefix_manager.is_curie(predicate):
            property_name = prefix_manager.get_reference(predicate)
        else:
            property_name = p
            predicate = f":{p}"
    element = get_biolink_element(prefix_manager, p)
    canonical_uri = None
    if element:
        if isinstance(element, SlotDefinition):
            # predicate corresponds to a biolink slot
            if element.definition_uri:
                element_uri = prefix_manager.contract(element.definition_uri)
            else:
                element_uri = f"biolink:{sentencecase_to_snakecase(element.name)}"
            if element.slot_uri:
                canonical_uri = element.slot_uri
        elif isinstance(element, ClassDefinition):
            # this will happen only when the IRI is actually
            # a reference to a class
            element_uri = prefix_manager.contract(element.class_uri)
        else:
            element_uri = f"biolink:{sentencecase_to_camelcase(element.name)}"
        if 'biolink:Attribute' in get_biolink_ancestors(element.name):
            element_uri = f"biolink:{sentencecase_to_snakecase(element.name)}"
        if not predicate:
            predicate = element_uri
    else:
        # no mapping to biolink model;
        # look at predicate mappings
        element_uri = None
        if predicate_mapping:
            if p in predicate_mapping:
                property_name = predicate_mapping[p]
                predicate = f":{property_name}"
        # cache[p] = {'element_uri': element_uri, 'canonical_uri': canonical_uri, 'predicate': predicate, 'property_name': property_name}
    return element_uri, canonical_uri, predicate, property_name
