"""Validation for AI-generated tree structures.

Ensures generated trees conform to ResolutionFlow's node schema
before they are saved to the database.
"""
from typing import Any


VALID_NODE_TYPES = {"decision", "action", "solution"}

# Required fields per node type
REQUIRED_FIELDS = {
    "decision": {"id", "type", "question", "options", "children"},
    "action": {"id", "type", "title", "description"},
    "solution": {"id", "type", "title", "description"},
}


class TreeValidationError(Exception):
    """Raised when a generated tree fails validation."""

    def __init__(self, errors: list[str]):
        self.errors = errors
        super().__init__(f"Tree validation failed: {'; '.join(errors)}")


def validate_generated_tree(tree: dict[str, Any]) -> list[str]:
    """Validate an AI-generated tree structure.

    Returns a list of error strings. Empty list means valid.
    """
    errors: list[str] = []

    if not isinstance(tree, dict):
        return ["Tree must be a JSON object"]

    # Root must be a decision node
    if tree.get("type") != "decision":
        errors.append("Root node must be type 'decision'")

    # Collect all node IDs and validate structure
    all_ids: set[str] = set()
    all_referenced_ids: set[str] = set()  # option next_node_ids (checked globally below)
    action_next_ids: set[str] = set()    # action next_node_ids (checked globally below)
    node_count = 0
    solution_count = 0

    def _validate_node(node: dict[str, Any], path: str) -> None:
        nonlocal node_count, solution_count

        if not isinstance(node, dict):
            errors.append(f"Node at {path} is not an object")
            return

        node_count += 1
        node_type = node.get("type")
        node_id = node.get("id")

        # Check node ID
        if not node_id:
            errors.append(f"Node at {path} missing 'id'")
        elif node_id in all_ids:
            errors.append(f"Duplicate node ID: '{node_id}'")
        else:
            all_ids.add(node_id)

        # Check node type
        if node_type not in VALID_NODE_TYPES:
            errors.append(
                f"Node '{node_id or path}' has invalid type '{node_type}'. "
                f"Must be one of: {', '.join(sorted(VALID_NODE_TYPES))}"
            )
            return

        # Check required fields
        required = REQUIRED_FIELDS[node_type]
        missing = required - set(node.keys())
        if missing:
            errors.append(
                f"Node '{node_id}' (type={node_type}) missing fields: {', '.join(sorted(missing))}"
            )

        # Type-specific validation
        if node_type == "decision":
            options = node.get("options", [])
            children = node.get("children", [])
            if not isinstance(options, list) or len(options) < 2:
                errors.append(
                    f"Decision node '{node_id}' must have at least 2 options"
                )
            if isinstance(children, list) and len(children) > 0 and len(children) < 2:
                errors.append(
                    f"Decision node '{node_id}' must have at least 2 children (branches)"
                )
            if isinstance(options, list) and len(options) >= 2:
                child_ids = {c.get("id") for c in children if isinstance(c, dict)}
                option_ids: set[str] = set()

                for opt in options:
                    if not isinstance(opt, dict):
                        errors.append(f"Option in node '{node_id}' is not an object")
                        continue
                    opt_id = opt.get("id")
                    if opt_id and opt_id in option_ids:
                        errors.append(
                            f"Duplicate option ID '{opt_id}' in node '{node_id}'"
                        )
                    if opt_id:
                        option_ids.add(opt_id)

                    next_id = opt.get("next_node_id")
                    if next_id:
                        all_referenced_ids.add(next_id)

        elif node_type == "action":
            next_id = node.get("next_node_id")
            if not next_id:
                errors.append(
                    f"Action node '{node_id}' is missing 'next_node_id'. "
                    "Every action node must point to the next node (a sibling in the parent's children)."
                )
            else:
                all_referenced_ids.add(next_id)
                action_next_ids.add(next_id)

        elif node_type == "solution":
            solution_count += 1

        # Recurse into children
        for i, child in enumerate(node.get("children", [])):
            _validate_node(child, f"{path}.children[{i}]")

    _validate_node(tree, "root")

    # Check that all action next_node_ids actually exist in the tree
    for ref_id in action_next_ids:
        if ref_id not in all_ids:
            errors.append(
                f"Action next_node_id '{ref_id}' references a node that does not exist in the tree"
            )

    # Check that all option next_node_ids exist in the tree (allows cross-references)
    for ref_id in all_referenced_ids - action_next_ids:
        if ref_id not in all_ids:
            errors.append(
                f"Option next_node_id '{ref_id}' references a node that does not exist in the tree"
            )

    # Global checks
    if node_count < 5:
        errors.append(
            f"Tree has only {node_count} nodes. Minimum 5 required for a useful tree."
        )
    if node_count > 50:
        errors.append(
            f"Tree has {node_count} nodes. Maximum 50 allowed."
        )
    if solution_count < 2:
        errors.append(
            f"Tree has only {solution_count} solution nodes. "
            "Need at least 2 to cover different resolution paths."
        )

    # Check that all leaf (non-solution) nodes have children or are solutions
    _check_branch_termination(tree, errors)

    return errors


def _check_branch_termination(node: dict[str, Any], errors: list[str]) -> None:
    """Verify every branch eventually reaches a solution node.

    Action nodes continue via next_node_id (validated separately).
    Only decision nodes with no children are dead ends.
    """
    if not isinstance(node, dict):
        return

    node_type = node.get("type")
    node_id = node.get("id", "?")
    children = node.get("children", [])

    if node_type == "solution":
        return  # Solution is a valid terminus

    if node_type == "action":
        # Action nodes continue via next_node_id (a sibling), not children.
        # next_node_id presence is validated in _validate_node.
        # Recurse into children if present (non-standard but tolerate it).
        for child in children:
            _check_branch_termination(child, errors)
        return

    # Decision node: must have children
    if not children:
        errors.append(
            f"Decision node '{node_id}' is a dead end — "
            "it has no children"
        )
        return

    for child in children:
        _check_branch_termination(child, errors)


def count_tree_stats(tree: dict[str, Any]) -> dict[str, int]:
    """Count node types and calculate depth of a tree."""
    stats = {
        "node_count": 0,
        "decision_count": 0,
        "action_count": 0,
        "solution_count": 0,
        "depth": 0,
    }

    def _count(node: dict[str, Any], depth: int) -> None:
        if not isinstance(node, dict):
            return
        stats["node_count"] += 1
        node_type = node.get("type", "")
        if node_type == "decision":
            stats["decision_count"] += 1
        elif node_type == "action":
            stats["action_count"] += 1
        elif node_type == "solution":
            stats["solution_count"] += 1
        stats["depth"] = max(stats["depth"], depth)
        for child in node.get("children", []):
            _count(child, depth + 1)

    _count(tree, 1)
    return stats
