"""Structured taxonomy and classification result models."""

from __future__ import annotations

from enum import Enum
from typing import Any, Iterable, Optional, Sequence, cast

PATH_DELIMITER = " > "
PATH_ESCAPE_TOKEN = "\\>"

from .base import StructureBase, spec_field


class TaxonomyNode(StructureBase):
    """Represent a taxonomy node with optional child categories.

    Attributes
    ----------
    label : str
        Human-readable label for the taxonomy node.
    description : str | None
        Optional description of the node.
    children : list[TaxonomyNode]
        Child nodes in the taxonomy.

    Methods
    -------
    build_path(parent_path)
        Build a computed path using the provided parent path segments.
    computed_path
        Return the computed path for the node.
    is_leaf
        Return True when the taxonomy node has no children.
    child_by_path(path)
        Return the child node matching the provided path.
    path_identifier
        Return the path identifier string for the node.
    keywords
        Return a list of keywords for the node.
    computed_description
        Return the computed description for the node.
    flattened_nodes
        Return a flattened list of all taxonomy nodes.
    """

    label: str = spec_field(
        name="label", description="Human-readable label for the taxonomy node."
    )
    description: str | None = spec_field(
        name="description",
        description="Optional description of the taxonomy node.",
        default=None,
    )
    children: list["TaxonomyNode"] = spec_field(
        name="children",
        description="Child nodes in the taxonomy.",
        default_factory=list,
    )

    @property
    def is_leaf(self) -> bool:
        """Return True when the taxonomy node has no children.

        Returns
        -------
        bool
            True if the node has no children.
        """
        return not self.children

    def build_path(self, parent_path: Iterable[str] | None = None) -> list[str]:
        """Build a computed path using the provided parent path segments.

        Parameters
        ----------
        parent_path : Iterable[str] or None, default=None
            Parent path segments to prepend to the node label.

        Returns
        -------
        list[str]
            Computed path segments for this node.
        """
        if parent_path is None:
            return [self.label]
        return [*parent_path, self.label]

    @property
    def computed_path(self) -> list[str]:
        """Return the computed path for the node.

        Returns
        -------
        list[str]
            Computed path segments for this node.
        """
        return self.build_path()

    def child_by_path(
        self, path: Iterable[str] | str | None
    ) -> Optional["TaxonomyNode"]:
        """Return the child node matching the provided path.

        Parameters
        ----------
        path : Iterable[str] or str or None
            Path segments or a delimited path string to locate.

        Returns
        -------
        TaxonomyNode or None
            Matching child node, if found.
        """
        if path is None:
            return None
        if isinstance(path, str):
            path_segments = split_path_identifier(path)
        else:
            path_segments = list(path)
        last_segment = path_segments[-1] if path_segments else None
        if not last_segment:
            return None
        return next(
            (child for child in self.children if child.label == last_segment),
            None,
        )

    @property
    def path_identifier(self) -> str:
        """Return the path identifier string for this node.

        Returns
        -------
        str
            Delimited path identifier.
        """
        return format_path_identifier(self.computed_path)

    @property
    def keywords(self) -> list[str]:
        """Return a list of keywords for this node.

        Returns
        -------
        list[str]
            Unique list of keywords derived from the node and descendants.
        """
        keywords = [self.label]
        for child in self.children:
            keywords.extend(child.keywords)
        return list(dict.fromkeys(filter(None, keywords)))

    @property
    def computed_description(self) -> str:
        """Return the computed description for this node.

        Returns
        -------
        str
            Node description with optional keyword context.
        """
        keywords = self.keywords
        base = self.description or self.label
        if len(keywords) == 1 and keywords[0] == self.label:
            return base
        if keywords:
            return f"{base}\nKeywords: {', '.join(keywords)}"
        return base

    @property
    def flattened_nodes(self) -> list[TaxonomyNode]:
        """Return a flattened list of all taxonomy nodes.

        Returns
        -------
        list[TaxonomyNode]
            Depth-first list of taxonomy nodes.
        """
        flattened: list[TaxonomyNode] = []
        for node in self.children:
            flattened.append(node)
            flattened.extend(node.flattened_nodes)
        return flattened


class Taxonomy(TaxonomyNode):
    """Represent a taxonomy with metadata and root nodes.

    Attributes
    ----------
    label : str
        Human-readable taxonomy name.
    description : str | None
        Optional description of the taxonomy.
    children : list[TaxonomyNode]
        Root taxonomy nodes.

    Methods
    -------
    root(label, *children)
        Create a taxonomy from root nodes.
    build_path(parent_path)
        Build a computed path using the provided parent path segments.
    computed_path
        Return the computed path for the node.
    is_leaf
        Return True when the taxonomy node has no children.
    child_by_path(path)
        Return the child node matching the provided path.
    path_identifier
        Return the path identifier string for the node.
    keywords
        Return a list of keywords for the node.
    computed_description
        Return the computed description for the node.
    flattened_nodes
        Return a flattened list of all taxonomy nodes.
    """

    def __init__(
        self,
        *,
        label: str,
        description: str | None = None,
        children: list[TaxonomyNode] | None = None,
    ) -> None:
        """Initialize a taxonomy with name and root nodes.

        Parameters
        ----------
        label : str
            Human-readable taxonomy name.
        description : str or None, default=None
            Optional description of the taxonomy.
        children : list[TaxonomyNode] or None, default=None
            Root taxonomy nodes. Defaults to an empty list.
        """
        super().__init__(
            label=label,
            description=description,
            children=children or [],
        )

    @classmethod
    def root(cls, label: str, *children: TaxonomyNode) -> "Taxonomy":
        """Create a taxonomy from root nodes.

        Parameters
        ----------
        label : str
            Human-readable taxonomy name.
        *children : TaxonomyNode
            Root taxonomy nodes.

        Returns
        -------
        Taxonomy
            Taxonomy instance with provided root nodes.
        """
        return cls(label=label, children=list(children))


def split_path_identifier(path: str) -> list[str]:
    """Split a path identifier into label segments.

    Parameters
    ----------
    path : str
        Path identifier to split.

    Returns
    -------
    list[str]
        Label segments extracted from the path identifier.
    """
    segments = path.split(PATH_DELIMITER) if path else []
    return [segment.replace(PATH_ESCAPE_TOKEN, PATH_DELIMITER) for segment in segments]


def format_path_identifier(path_segments: Sequence[str]) -> str:
    """Format path segments into a safe identifier string.

    Parameters
    ----------
    path_segments : Sequence[str]
        Path segments to format.

    Returns
    -------
    str
        Escaped path identifier string.
    """
    escaped_segments = [
        segment.replace(PATH_DELIMITER, PATH_ESCAPE_TOKEN) for segment in path_segments
    ]
    return PATH_DELIMITER.join(escaped_segments)


class ClassificationStopReason(str, Enum):
    """Enumerate stop reasons for taxonomy classification.

    Methods
    -------
    is_terminal
        Return True if the stop reason should halt traversal.
    """

    CONTINUE = "continue"
    STOP = "stop"
    NO_MATCH = "no_match"
    MAX_DEPTH = "max_depth"
    NO_CHILDREN = "no_children"

    @property
    def is_terminal(self) -> bool:
        """Return True if the stop reason should halt traversal.

        Returns
        -------
        bool
            True when traversal should stop.
        """
        return self in {
            ClassificationStopReason.STOP,
            ClassificationStopReason.NO_MATCH,
            ClassificationStopReason.MAX_DEPTH,
            ClassificationStopReason.NO_CHILDREN,
        }


class ClassificationStep(StructureBase):
    """Represent a classification step constrained to taxonomy node enums.

    Attributes
    ----------
    selected_nodes : list[Enum] or None
        Enum values of selected taxonomy nodes for multi-class classification.
    confidence : float or None
        Confidence score between 0 and 1.
    stop_reason : ClassificationStopReason
        Reason for stopping or continuing traversal.
    rationale : str or None
        Optional rationale for the classification decision.

    Methods
    -------
    build_for_enum(enum_cls)
        Build a ClassificationStep subclass with enum-constrained selections.
    as_summary()
        Return a dictionary summary of the classification step.

    Examples
    --------
    Create a multi-class step and summarize the selections:

    >>> NodeEnum = Enum("NodeEnum", {"BILLING": "billing"})
    >>> StepEnum = ClassificationStep.build_for_enum(NodeEnum)
    >>> step = StepEnum(
    ...     selected_nodes=[NodeEnum.BILLING],
    ...     confidence=0.82,
    ...     stop_reason=ClassificationStopReason.STOP,
    ... )
    >>> step.as_summary()["selected_nodes"]
    [<NodeEnum.BILLING: 'billing'>]
    """

    selected_nodes: list[Enum] | None = spec_field(
        "selected_nodes",
        description="Path identifiers of selected taxonomy nodes.",
        default=None,
    )
    confidence: Optional[float] = spec_field(
        "confidence",
        description="Confidence score between 0 and 1.",
        default=None,
    )
    stop_reason: ClassificationStopReason = spec_field(
        "stop_reason",
        description="Reason for stopping or continuing traversal.",
        default=ClassificationStopReason.STOP,
        allow_null=False,
    )
    rationale: Optional[str] = spec_field(
        "rationale",
        description="Optional rationale for the classification decision.",
        default=None,
    )

    @classmethod
    def build_for_enum(cls, enum_cls: type[Enum]) -> type["ClassificationStep"]:
        """Build a ClassificationStep subclass with enum-constrained fields.

        Parameters
        ----------
        enum_cls : type[Enum]
            Enum type to use for node selections.

        Returns
        -------
        type[ClassificationStep]
            Specialized ClassificationStep class bound to the enum.
        """
        namespace: dict[str, Any] = {
            "__annotations__": {
                "selected_nodes": list[enum_cls] | None,
            },
            "selected_nodes": spec_field(
                "selected_nodes",
                description="Path identifiers of selected taxonomy nodes.",
                default=None,
            ),
        }
        return cast(type["ClassificationStep"], type("BoundStep", (cls,), namespace))

    def as_summary(self) -> dict[str, Any]:
        """Return a dictionary summary of the classification step.

        Returns
        -------
        dict[str, Any]
            Summary data for logging or inspection.

        Examples
        --------
        >>> NodeEnum = Enum("NodeEnum", {"ROOT": "root"})
        >>> StepEnum = ClassificationStep.build_for_enum(NodeEnum)
        >>> step = StepEnum(selected_nodes=[NodeEnum.ROOT])
        >>> step.as_summary()["selected_nodes"]
        [<NodeEnum.ROOT: 'root'>]
        """
        selected_nodes = [
            _normalize_enum_value(item) for item in self.selected_nodes or []
        ]
        return {
            "selected_nodes": selected_nodes or None,
            "confidence": self.confidence,
            "stop_reason": self.stop_reason.value,
        }


def _normalize_enum_value(value: Any) -> Any:
    """Normalize enum values into raw primitives.

    Parameters
    ----------
    value : Any
        Value to normalize.

    Returns
    -------
    Any
        Primitive value suitable for summaries.
    """
    if isinstance(value, Enum):
        return value.value
    return value


class ClassificationResult(StructureBase):
    """Represent the final result of taxonomy traversal.

    Attributes
    ----------
    final_nodes : list[TaxonomyNode] or None
        Resolved taxonomy nodes for the final selections across branches.
    confidence : float or None
        Confidence score for the final selection.
    stop_reason : ClassificationStopReason
        Reason the traversal ended.
    steps : list[ClassificationStep]
        Ordered list of classification steps.

    Methods
    -------
    depth
        Return the number of classification steps recorded.
    final_node
        Return the first resolved taxonomy node, if available.
    iter_selected_nodes
        Yield selected identifiers across all steps.
    selected_nodes
        Return the selected identifiers across all steps.
    to_lightweight_summary
        Return a lightweight summary of selected node paths.

    Examples
    --------
    Summarize single and multi-class output:

    >>> node = TaxonomyNode(label="Tax")
    >>> result = ClassificationResult(
    ...     final_nodes=[node],
    ...     confidence=0.91,
    ...     stop_reason=ClassificationStopReason.STOP,
    ... )
    >>> result.final_nodes
    [TaxonomyNode(label='Tax', description=None, children=[])]
    """

    final_nodes: list[TaxonomyNode] | None = spec_field(
        "final_nodes",
        description="Resolved taxonomy nodes for the final selections.",
        default=None,
    )
    confidence: Optional[float] = spec_field(
        "confidence",
        description="Confidence score for the final selection.",
        default=None,
    )
    stop_reason: ClassificationStopReason = spec_field(
        "stop_reason",
        description="Reason the traversal ended.",
        default=ClassificationStopReason.STOP,
    )
    steps: list[ClassificationStep] = spec_field(
        "steps",
        description="Ordered list of classification steps.",
        default_factory=list,
    )

    @property
    def depth(self) -> int:
        """Return the number of classification steps recorded.

        Returns
        -------
        int
            Count of classification steps.
        """
        return len(self.steps)

    @property
    def final_node(self) -> TaxonomyNode | None:
        """Return the first resolved taxonomy node.

        Returns
        -------
        TaxonomyNode or None
            First resolved taxonomy node, if available.
        """
        if not self.final_nodes:
            return None
        return self.final_nodes[0]

    @property
    def selected_nodes(self) -> list[str]:
        """Return the selected identifiers across all steps.

        Returns
        -------
        list[str]
            Selected identifiers in traversal order.
        """
        return list(self.iter_selected_nodes())

    def iter_selected_nodes(self) -> Iterable[str]:
        """Yield selected identifiers across all steps.

        Yields
        ------
        str
            Selected identifier in traversal order.
        """
        for step in self.steps:
            for value in step.selected_nodes or []:
                normalized = _normalize_enum_value(value)
                if normalized:
                    yield normalized

    def to_lightweight_summary(self) -> "ClassificationSummary | None":
        """Return a lightweight summary of selected node paths.

        Returns
        -------
        ClassificationSummary or None
            Summary containing the ``full_paths`` of selected nodes, or None
            when no selections exist.

        Examples
        --------
        >>> result = ClassificationResult(steps=[])
        >>> result.to_lightweight_summary() is None
        True
        """
        full_paths = [
            format_path_identifier(taxonomy_enum_path(node))
            for node in self.selected_nodes
        ]
        if not full_paths:
            return None
        return ClassificationSummary(full_paths=full_paths)


class ClassificationSummary(StructureBase):
    """Represent a lightweight summary of selected taxonomy paths.

    Attributes
    ----------
    full_paths : list[str] or None
        Selected taxonomy paths including parent segments.

    Methods
    -------
    to_json()
        Serialize the summary to a JSON-compatible dictionary.
    from_json(data)
        Construct a summary instance from JSON-compatible data.
    """

    full_paths: list[str] | None = spec_field(
        "full_paths",
        description="Selected taxonomy paths including parent segments.",
        default=None,
    )


def taxonomy_enum_path(value: Enum | str | None) -> list[str]:
    """Return the taxonomy path segments for an enum value.

    Parameters
    ----------
    value : Enum or str or None
        Enum member or path identifier string to split. If None, return an
        empty list.

    Returns
    -------
    list[str]
        Path segments extracted from the taxonomy identifier.

    Examples
    --------
    >>> StepEnum = Enum("StepEnum", {"ROOT_LEAF": "Root > Leaf"})
    >>> taxonomy_enum_path(StepEnum.ROOT_LEAF)
    ['Root', 'Leaf']
    """
    if value is None:
        return []
    normalized_value = _normalize_enum_value(value)
    if not normalized_value:
        return []
    if not isinstance(normalized_value, str):
        normalized_value = str(normalized_value)
    return split_path_identifier(normalized_value)


__all__ = [
    "ClassificationResult",
    "ClassificationSummary",
    "ClassificationStep",
    "ClassificationStopReason",
    "Taxonomy",
    "TaxonomyNode",
    "format_path_identifier",
    "split_path_identifier",
    "taxonomy_enum_path",
]
