"""Pathway specification model for YAML parsing.

This model represents the raw input format from YAML files, before conversion
to executable Pathway objects. Provides type-safe parsing at the YAML boundary.

Usage:
    from core.pathways.models.pathway_spec import PathwaySpec
    
    # Parse a dict from YAML
    spec = PathwaySpec.model_validate(yaml_data)
    
    # Convert to executable pathway
    pathway = spec.to_pathway()
"""

from __future__ import annotations

import logging
from typing import Any
from uuid import uuid4

from pydantic import BaseModel, Field, model_validator

from core.common import JsonObject
from core.pathways.models.node_spec import parse_node_spec
from core.pathways.pathway import Connection, Pathway, PathwayMode

logger = logging.getLogger(__name__)


class ConnectionSpec(BaseModel):
    """Spec for a connection between nodes."""

    model_config = {"extra": "allow"}

    from_node: str = Field(alias="from", default="")
    to_node: str = Field(alias="to", default="")
    from_output: str = "output"
    to_input: str = "input"
    when: str | None = None

    @model_validator(mode="before")
    @classmethod
    def normalize_aliases(cls, data: Any) -> Any:
        """Handle both 'from'/'to' and 'from_node'/'to_node' formats."""
        if not isinstance(data, dict):
            return data

        # Normalize from/from_node
        if "from_node" in data and "from" not in data:
            data["from"] = data["from_node"]

        # Normalize to/to_node
        if "to_node" in data and "to" not in data:
            data["to"] = data["to_node"]

        return data

    def to_connection(self) -> Any:
        """Convert to Connection model."""
        return Connection(
            from_node=self.from_node,
            to_node=self.to_node,
            from_output=self.from_output,
            to_input=self.to_input,
            when=self.when,
        )


class PathwaySpec(BaseModel):
    """Spec for a pathway from YAML.
    
    Handles multiple input formats:
    - nodes as dict: {node_id: {type: ..., ...}}
    - nodes as list: [{id: ..., type: ..., ...}]
    - connections as strings: ["a -> b", "b → c"]
    - connections as dicts: [{from: a, to: b}]
    """

    model_config = {"extra": "allow"}

    id: str = Field(default_factory=lambda: f"pathway_{uuid4().hex[:8]}")
    name: str | None = None
    description: str | None = None
    mode: str = "dag"

    # Raw node data - can be dict or list format
    nodes: dict[str, JsonObject] | list[JsonObject] = Field(default_factory=dict)

    # Raw connection data - can be string or dict format
    connections: list[str | JsonObject] = Field(default_factory=list)

    # Metadata
    metadata: JsonObject = Field(default_factory=dict)

    def _parse_nodes(self) -> tuple[dict[str, object], list[str]]:
        """Parse nodes from dict or list format.
        
        Returns
        -------
            Tuple of (nodes dict, parse errors list)
        """
        parsed_nodes: dict[str, object] = {}
        parse_errors: list[str] = []

        if isinstance(self.nodes, dict):
            # Dict format: {node_id: {type: ..., config: ...}}
            for node_id, node_config in self.nodes.items():
                if isinstance(node_config, dict):
                    node_config = {**node_config, "id": node_id}
                    try:
                        node = parse_node_spec(node_config)
                        parsed_nodes[node.id] = node
                    except Exception as e:
                        error_msg = f"Failed to parse node '{node_id}': {e}"
                        parse_errors.append(error_msg)
                        logger.warning(error_msg)
                else:
                    parse_errors.append(
                        f"Node '{node_id}' has invalid config "
                        f"(expected dict, got {type(node_config).__name__})"
                    )
        else:
            # List format: [{id: ..., type: ..., config: ...}]
            for i, node_config in enumerate(self.nodes):
                if isinstance(node_config, dict):
                    try:
                        node = parse_node_spec(node_config)
                        parsed_nodes[node.id] = node
                    except Exception as e:
                        node_id = node_config.get("id", f"node_{i}")
                        error_msg = f"Failed to parse node '{node_id}': {e}"
                        parse_errors.append(error_msg)
                        logger.warning(error_msg)

        # Log warnings for empty pathways
        if not parsed_nodes:
            defined_count = len(self.nodes) if isinstance(self.nodes, (dict, list)) else 0
            if defined_count > 0:
                logger.error(
                    "Pathway '%s' has 0 executable nodes despite having %d node definitions. "
                    "Parse errors: %s",
                    self.id,
                    defined_count,
                    parse_errors or ["No specific errors - check node type definitions"],
                )
            else:
                logger.warning("Pathway '%s' has no nodes defined", self.id)

        return parsed_nodes, parse_errors

    def _parse_connections(self) -> list[Any]:
        """Parse connections from various formats."""
        parsed: list[Connection] = []

        for c in self.connections:
            if isinstance(c, str):
                # String format: "a -> b" or "a → b"
                if "→" in c:
                    parts = c.split("→")
                elif "->" in c:
                    parts = c.split("->")
                else:
                    continue
                if len(parts) == 2:
                    parsed.append(
                        Connection(
                            from_node=parts[0].strip(),
                            to_node=parts[1].strip(),
                        )
                    )
            elif isinstance(c, dict):
                # Dict format - use ConnectionSpec for validation
                try:
                    conn_spec = ConnectionSpec.model_validate(c)
                    parsed.append(conn_spec.to_connection())
                except Exception:
                    # Fallback to direct construction
                    parsed.append(
                        Connection(
                            from_node=c.get("from", c.get("from_node", "")),
                            to_node=c.get("to", c.get("to_node", "")),
                            from_output=c.get("from_output", "output"),
                            to_input=c.get("to_input", "input"),
                            when=c.get("when"),
                        )
                    )

        return parsed

    def _parse_mode(self) -> Any:
        """Parse pathway mode from string."""
        valid_modes = [m.value for m in PathwayMode]
        if self.mode in valid_modes:
            return PathwayMode(self.mode)
        return PathwayMode.DAG

    def to_pathway(self) -> Any:
        """Convert spec to executable Pathway.
        
        Returns
        -------
            Pathway instance ready for execution
        """
        # Parse components
        nodes, parse_errors = self._parse_nodes()
        connections = self._parse_connections()
        mode = self._parse_mode()

        # Build metadata with parse diagnostics
        metadata = dict(self.metadata)
        if parse_errors:
            metadata["_parse_errors"] = parse_errors
        metadata["_parsed_node_count"] = len(nodes)
        metadata["_defined_node_count"] = (
            len(self.nodes) if isinstance(self.nodes, (dict, list)) else 0
        )

        return Pathway(
            id=self.id,
            name=self.name,
            description=self.description,
            mode=mode,
            nodes=nodes,
            connections=connections,
            metadata=metadata,
        )


def parse_pathway_spec(data: JsonObject) -> PathwaySpec:
    """Parse a dict into a PathwaySpec.
    
    Args:
        data: Dict from YAML
        
    Returns
    -------
        Typed PathwaySpec instance
    """
    return PathwaySpec.model_validate(data)


__all__ = [
    "ConnectionSpec",
    "PathwaySpec",
    "parse_pathway_spec",
]
