"""
bento_meta.objects
==================

This module contains the subclasses of :class:`Entity` which are used
in representing the models contained in the `MDB <https://github.com/CBIIT/bento-mdf>`_.

"""

from __future__ import annotations

import sys

sys.path.append("..")
from copy import deepcopy
from typing import TYPE_CHECKING, Any, ClassVar
from warnings import warn

from bento_meta.entity import Entity

if TYPE_CHECKING:
    import neo4j


def mergespec(
    clsname: str,
    attspec: dict[str, str],
    mapspec: dict[
        str,
        str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None,
    ],
) -> tuple[
    dict[str, str],
    dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None],
]:
    """
    Merge subclass attribute and mapping specification dicts with the base class's.

    Not for human consumption.
    """
    spec = deepcopy(attspec)
    spec.update(Entity.attspec_)
    mo = deepcopy(Entity.mapspec_)
    if "label" in mapspec:
        mo["label"] = mapspec["label"]
    if "key" in mapspec:
        mo["key"] = mapspec["key"]
    if "property" in mapspec:
        mo["property"].update(mapspec["property"])
    if "relationship" in mapspec:
        mo["relationship"].update(mapspec["relationship"])
    mo["relationship"]["_next"]["end_cls"] = {clsname}
    mo["relationship"]["_prev"]["end_cls"] = {clsname}
    return (spec, mo)


class Node(Entity):
    """Subclass that models a data node."""

    attspec_: ClassVar[dict[str, str]] = {
        "handle": "simple",
        "model": "simple",
        "nanoid": "simple",
        "version": "simple",
        "concept": "object",
        "props": "collection",
    }
    mapspec_: ClassVar[
        dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None]
    ] = {
        "label": "node",
        "key": "handle",
        "property": {
            "handle": "handle",
            "model": "model",
            "nanoid": "nanoid",
            "version": "version",
        },
        "relationship": {
            "concept": {"rel": ":has_concept>", "end_cls": "Concept"},
            "props": {"rel": ":has_property>", "end_cls": "Property"},
            "tags": {"rel": ":has_tag>", "end_cls": "Tag"},
        },
    }
    (attspec, _mapspec) = mergespec("Node", attspec_, mapspec_)

    def __init__(self, init: dict | neo4j.graph.Node | Node | None = None) -> None:
        """Initialize a `Node` instance."""
        super().__init__(init=init)

    @property
    def annotations(self) -> dict | None:
        """Return `Term`s if the `Node` is annotated by `Term`s via a `Concept`."""
        if self.concept:
            return self.concept.terms
        return None

    def get_key_prop(self) -> Property | list[Property] | None:
        """
        Return the Property entity with is_key=True for this Node if it exists.

        If multiple key props exist, return a list of them; if none exist, return None.

        Returns:
            Single Property if one key property exists, list of Properties if multiple
            exist, None if no key properties found.
        """
        if not self.props.values():
            warn("No properties found for Node - returning None", stacklevel=2)
            return None
        keys = [p for p in self.props.values() if p.is_key]
        if not keys:
            warn("No key properties found for Node - returning None", stacklevel=2)
            return None
        if len(keys) == 1:
            return keys[0]
        warn("Multiple key properties found for Node - returning all", stacklevel=2)
        return keys


class Property(Entity):
    """Subclass that models a property of a node or relationship (edge)."""

    pvt_attr: ClassVar[list[str]] = [*Entity.pvt_attr, "value_types"]
    attspec_: ClassVar[dict[str, str]] = {
        "handle": "simple",
        "model": "simple",
        "nanoid": "simple",
        "version": "simple",
        "value_domain": "simple",
        "units": "simple",
        "pattern": "simple",
        "item_domain": "simple",
        "is_required": "simple",
        "is_key": "simple",
        "is_nullable": "simple",
        "is_deprecated": "simple",
        "is_strict": "simple",
        "concept": "object",
        "value_set": "object",
        "_parent_handle": "simple",
    }
    mapspec_: ClassVar[
        dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None]
    ] = {
        "label": "property",
        "key": "handle",
        "property": {
            "handle": "handle",
            "model": "model",
            "nanoid": "nanoid",
            "value_domain": "value_domain",
            "pattern": "pattern",
            "units": "units",
            "item_domain": "item_domain",
            "is_required": "is_required",
            "is_key": "is_key",
            "is_nullable": "is_nullable",
            "is_deprecated": "is_deprecated",
            "is_strict": "is_strict",
            "_parent_handle": "_parent_handle",
            "version": "version",
        },
        "relationship": {
            "concept": {"rel": ":has_concept>", "end_cls": "Concept"},
            "value_set": {"rel": ":has_value_set>", "end_cls": "ValueSet"},
            "tags": {"rel": ":has_tag>", "end_cls": "Tag"},
        },
    }
    (attspec, _mapspec) = mergespec("Property", attspec_, mapspec_)
    defaults: ClassVar[dict[str, str]] = {"value_domain": "TBD"}

    def __init__(self, init: dict | neo4j.graph.Node | Property | None = None) -> None:
        """Initialize a `Property` instance."""
        super().__init__(init=init)
        self.value_types = []

    @property
    def annotations(self) -> dict | None:
        """If the `Property` is annotated by `Term`s via a `Concept`, return the `Term`s."""
        if self.concept:
            return self.concept.terms
        return None

    @property
    def terms(self) -> list[Term] | None:
        """Return the `Term` objects of `Property` with a ``value_set`` domain."""
        if self.value_set:
            return self.value_set.terms
        return None

    @property
    def values(self) -> list[str] | None:
        """
        Return Property's term values as a list of str if it has a value_set domain.

        Returns:
            List of term values, or None if no value_set.
        """
        if self.value_set:
            return [self.terms[x].value for x in self.terms]
        return None


class Edge(Entity):
    """Subclass that models a relationship between model nodes."""

    defaults: ClassVar[dict[str, str]] = {
        "multiplicity": "many_to_many",
    }
    attspec_: ClassVar[dict[str, str]] = {
        "handle": "simple",
        "model": "simple",
        "nanoid": "simple",
        "version": "simple",
        "multiplicity": "simple",
        "is_required": "simple",
        "src": "object",
        "dst": "object",
        "concept": "object",
        "props": "collection",
    }
    mapspec_: ClassVar[
        dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None]
    ] = {
        "label": "relationship",
        "key": "handle",
        "property": {
            "handle": "handle",
            "model": "model",
            "nanoid": "nanoid",
            "multiplicity": "multiplicity",
            "is_required": "is_required",
            "version": "version",
        },
        "relationship": {
            "src": {"rel": ":has_src>", "end_cls": "Node"},
            "dst": {"rel": ":has_dst>", "end_cls": "Node"},
            "concept": {"rel": ":has_concept>", "end_cls": "Concept"},
            "props": {"rel": ":has_property>", "end_cls": "Property"},
            "tags": {"rel": ":has_tag>", "end_cls": "Tag"},
        },
    }
    (attspec, _mapspec) = mergespec("Edge", attspec_, mapspec_)

    def __init__(self, init: dict | neo4j.graph.Node | Edge | None = None) -> None:
        """Initialize an `Edge` instance."""
        super().__init__(init=init)

    @property
    def annotations(self) -> dict | None:
        """Return `Term`s if the `Edge` is annotated by `Term`s via a `Concept`."""
        if self.concept:
            return self.concept.terms
        return None

    @property
    def triplet(self) -> tuple[str, str, str] | None:
        """
        Return a 3-tuple that fully qualifies the edge.

        Returns (edge.handle, src.handle, dst.handle).

        src and dst attributes must be set. If not, return None.

        Returns:
            Tuple of (edge.handle, src.handle, dst.handle) or None.
        """
        if self.handle and self.src and self.dst:
            return (self.handle, self.src.handle, self.dst.handle)
        return None


class Term(Entity):
    """Subclass that models a term from a terminology."""

    attspec_: ClassVar[dict[str, str]] = {
        "handle": "simple",
        "value": "simple",
        "nanoid": "simple",
        "origin_id": "simple",
        "origin_version": "simple",
        "origin_definition": "simple",
        "origin_name": "simple",
        "concept": "object",
        "origin": "object",
    }
    mapspec_: ClassVar[
        dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None]
    ] = {
        "label": "term",
        # note using 'value' as the key in terms collections may break silently
        # if the terms are coming from different origins, but use the same
        # value (string representation in data) - i.e., value is not necessarily
        # unique - should be the nanoid of the term.
        "key": "value",
        "property": {
            "handle": "handle",
            "value": "value",
            "nanoid": "nanoid",
            "origin_id": "origin_id",
            "origin_version": "origin_version",
            "origin_definition": "origin_definition",
            "origin_name": "origin_name",
        },
        "relationship": {
            "concept": {"rel": ":represents>", "end_cls": "Concept"},
            "origin": {"rel": ":has_origin>", "end_cls": "Origin"},
            "tags": {"rel": ":has_tag>", "end_cls": "Tag"},
        },
    }
    (attspec, _mapspec) = mergespec("Term", attspec_, mapspec_)

    def __init__(self, init: dict | neo4j.graph.Node | Term | None = None) -> None:
        """Initialize a `Term` instance."""
        super().__init__(init=init)


# for ValueSet - updating terms prop should dirty the connected Property
# (from Bento::Meta), signal need to refresh. Engineer so this happens
# here (__setattr__ override), not in Entity
class ValueSet(Entity):
    """
    Subclass that models an enumerated set of Property values.

    Essentially a container for Term instances.
    """

    attspec_: ClassVar[dict[str, str]] = {
        "handle": "simple",
        "nanoid": "simple",
        "url": "simple",
        "path": "simple",
        "prop": "object",
        "origin": "object",
        "terms": "collection",
    }
    mapspec_: ClassVar[
        dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None]
    ] = {
        "label": "value_set",
        "property": {
            "handle": "handle",
            "url": "url",
            "path": "path",
            "nanoid": "nanoid",
        },
        "relationship": {
            "prop": {"rel": "<:has_value_set", "end_cls": "Property"},
            "terms": {"rel": ":has_term>", "end_cls": "Term"},
            "origin": {"rel": ":has_origin>", "end_cls": "Origin"},
            "tags": {"rel": ":has_tag>", "end_cls": "Tag"},
        },
    }
    (attspec, _mapspec) = mergespec("ValueSet", attspec_, mapspec_)

    def __init__(self, init: dict | neo4j.graph.Node | ValueSet | None = None) -> None:
        """Initialize a `ValueSet` instance."""
        super().__init__(init=init)

    def __setattr__(self, name: str, value: Any) -> None:
        super().__setattr__(name, value)
        if name == "dirty" and self.prop and value != 0:
            self.prop.dirty = 1


class Concept(Entity):
    """Subclass that models a semantic concept."""

    attspec_: ClassVar[dict[str, str]] = {"terms": "collection"}
    mapspec_: ClassVar[
        dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None]
    ] = {
        "label": "concept",
        "relationship": {
            "terms": {"rel": "<:represents", "end_cls": "Term"},
            "tags": {"rel": ":has_tag>", "end_cls": "Tag"},
        },
    }
    (attspec, _mapspec) = mergespec("Concept", attspec_, mapspec_)

    def __init__(self, init: dict | neo4j.graph.Node | Concept | None = None) -> None:
        """Initialize a `Concept` instance."""
        super().__init__(init=init)


class Predicate(Entity):
    """Subclass that models a semantic link between concepts."""

    attspec_: ClassVar[dict[str, str]] = {
        "handle": "simple",
        "subject": "object",
        "object": "object",
    }
    mapspec_: ClassVar[
        dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None]
    ] = {
        "label": "predicate",
        "key": "handle",
        "property": {
            "handle": "handle",
        },
        "relationship": {
            "subject": {"rel": ":has_subject>", "end_cls": "Concept"},
            "object": {"rel": ":has_object>", "end_cls": "Concept"},
            "tags": {"rel": ":has_tag>", "end_cls": "Tag"},
        },
    }
    (attspec, _mapspec) = mergespec("Predicate", attspec_, mapspec_)

    def __init__(self, init: dict | neo4j.graph.Node | Predicate | None = None) -> None:
        """Initialize a `Predicate` instance."""
        super().__init__(init=init)


class Origin(Entity):
    """Subclass that models a :class:`Term` 's authoritative source."""

    attspec_: ClassVar[dict[str, str]] = {
        "url": "simple",
        "is_external": "simple",
        "name": "simple",
        "nanoid": "simple",
    }
    mapspec_: ClassVar[
        dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None]
    ] = {
        "label": "origin",
        "key": "name",
        "property": {
            "name": "name",
            "url": "url",
            "is_external": "is_external",
            "nanoid": "nanoid",
        },
        "relationship": {"tags": {"rel": ":has_tag>", "end_cls": "Tag"}},
    }
    (attspec, _mapspec) = mergespec("Origin", attspec_, mapspec_)

    def __init__(self, init: dict | neo4j.graph.Node | Origin | None = None) -> None:
        """Initialize an `Origin` instance."""
        super().__init__(init=init)


class Tag(Entity):
    """Subclass that allows simple key-value tagging of a model at arbitrary points."""

    attspec_: ClassVar[dict[str, str]] = {
        "key": "simple",
        "value": "simple",
        "_parent": "object",
    }
    mapspec_: ClassVar[
        dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None]
    ] = {
        "label": "tag",
        "key": "key",
        "property": {"key": "key", "value": "value"},
        "relationship": {"_parent": {"rel": "<:has_tag",
                                     "end_cls": ["Node", "Edge", "Property", "ValueSet","Concept","Predicate","Transform","TfStep"]}},
    }
    (attspec, _mapspec) = mergespec("Tag", attspec_, mapspec_)

    def __init__(self, init: dict | neo4j.graph.Node | Tag | None = None) -> None:
        """Initialize a `Tag` instance."""
        super().__init__(init=init)


class Model(Entity):
    """Subclass with information regarding data model."""

    attspec_: ClassVar[dict[str, str]] = {
        "handle": "simple",
        "name": "simple",
        "repository": "simple",
        "nanoid": "simple",
        "version": "simple",
        "is_latest_version": "simple",
    }
    mapspec_: ClassVar[
        dict[str, str | dict[str, str] | dict[str, dict[str, str | set[str]]] | None]
    ] = {
        "label": "model",
        "key": "handle",
        "property": {
            "handle": "handle",
            "name": "name",
            "repository": "repository",
            "version": "version",
            "is_latest_version": "is_latest_version",
        },
    }
    (attspec, _mapspec) = mergespec("Model", attspec_, mapspec_)
    defaults: ClassVar[dict[str, bool]] = {"is_latest_version": False}

    def __init__(self, init: dict | neo4j.graph.Node | Model | None = None) -> None:
        """Initialize a `Model` instance."""
        super().__init__(init=init)
