"""Foundational nodes — InputNode and OutputNode.

These define the data-flow boundaries of a pathway:
- InputNode: entry point, validates and applies defaults
- OutputNode: exit point, maps upstream outputs to final shape
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Literal

from pydantic import Field

from core.common import JsonObject
from core.pathways.models.io import NodeInputs, NodeOutputs
from core.pathways.nodes.base import NodeBase
from core.pathways.nodes.helpers import normalize_schema_fields
from core.pathways.nodes.models import InputSignature, OutputSignature

if TYPE_CHECKING:
    from core.common import JSONValue
    from core.pathways.context import Context

# =============================================================================
# INPUT NODE
# =============================================================================


class InputNode(NodeBase):
    """Input declaration node — defines pathway inputs with optional schema.

    This is the entry point for data flowing into a pathway. It:
    - Documents what inputs the pathway expects
    - Optionally validates inputs against a JSON Schema
    - Makes inputs available to downstream nodes as ``{{input.field_name}}``

    Attributes
    ----------
    input_schema : JsonObject
        JSON Schema defining expected inputs (optional but recommended).
    defaults : JsonObject
        Default values for optional inputs.
    description : str
        Human-readable description of the input.
    """

    type: Literal["input"] = "input"
    input_schema: JsonObject = Field(default_factory=dict)
    defaults: JsonObject = Field(default_factory=dict)
    description: str = ""

    async def compute(self, inputs: NodeInputs, _ctx: Context) -> NodeOutputs:
        """Validate and pass through inputs with defaults applied."""
        data = inputs.to_dict()

        # Apply defaults for missing keys
        result: JsonObject = dict(self.defaults)
        result.update(data)

        # Normalize schema to flat {field_name: type_or_schema} for validation
        schema_dict = normalize_schema_fields(self.input_schema)

        if schema_dict:
            errors = self._validate_inputs(result, schema_dict)
            if errors:
                msg = f"Input validation failed: {', '.join(errors)}"
                raise ValueError(msg)

        return NodeOutputs(**result)

    def _validate_inputs(self, data: JsonObject, schema: JsonObject) -> list[str]:
        """Basic validation against schema. Returns list of errors."""
        errors: list[str] = []

        for field_name, raw_schema in schema.items():
            # Handle both formats:
            # - Simple: {"query": "string"} -> raw_schema is a string
            # - Full: {"query": {"type": "string", "required": true}} -> raw_schema is a dict
            field_schema = {"type": raw_schema} if isinstance(raw_schema, str) else raw_schema

            if not isinstance(field_schema, dict):
                continue

            if field_name not in data:
                if field_name not in self.defaults:
                    required = field_schema.get("required", True)
                    if required and "default" not in field_schema:
                        errors.append(f"Missing required input: {field_name}")
                continue

            value = data[field_name]
            expected_type = field_schema.get("type")

            if expected_type and not self._check_type(value, str(expected_type)):
                errors.append(
                    f"Input '{field_name}' expected {expected_type}, got {type(value).__name__}",
                )

        return errors

    @staticmethod
    def _check_type(value: JSONValue, expected: str) -> bool:
        """Check if value matches expected JSON Schema type."""
        type_map: dict[str, type | tuple[type, ...]] = {
            "string": str,
            "number": (int, float),
            "integer": int,
            "boolean": bool,
            "array": list,
            "object": dict,
            "null": type(None),
        }
        expected_types = type_map.get(expected)
        if expected_types is None:
            return True  # Unknown type, allow
        return isinstance(value, expected_types)

    def get_signature(self) -> InputSignature:
        """Return the input signature for documentation."""
        return InputSignature(
            inputs=self.input_schema,
            defaults=self.defaults,
            description=self.description,
        )


# =============================================================================
# OUTPUT NODE
# =============================================================================


class OutputNode(NodeBase):
    """Output declaration node — defines pathway outputs.

    This is the exit point for data flowing out of a pathway. It:
    - Documents what outputs the pathway produces
    - Maps upstream node outputs to final pathway outputs
    - Optionally validates outputs against a JSON Schema

    Attributes
    ----------
    output_schema : JsonObject
        JSON Schema defining output structure (optional).
    mapping : JsonObject
        Maps output field names to upstream values using ``{{node.field}}`` syntax.
    description : str
        Human-readable description of the output.
    """

    type: Literal["output"] = "output"
    output_schema: JsonObject = Field(default_factory=dict)
    mapping: JsonObject = Field(default_factory=dict)
    description: str = ""

    async def compute(self, inputs: NodeInputs, _ctx: Context) -> NodeOutputs:
        """Map upstream outputs to final pathway outputs."""
        data = inputs.to_dict()

        if self.mapping:
            resolved = self._resolve_templates(self.mapping, data)
            if isinstance(resolved, dict):
                return NodeOutputs(**resolved)
        return NodeOutputs(**data)

    def get_signature(self) -> OutputSignature:
        """Return the output signature for documentation."""
        return OutputSignature(
            outputs=self.output_schema,
            mapping=self.mapping,
            description=self.description,
        )


__all__ = [
    "InputNode",
    "OutputNode",
]
