"""
Spec Alignment Testing - Verify Pydantic schemas match YAML specifications.

The core principle: YAML is the Single Source of Truth (SST).
Pydantic schemas must match what the YAML defines, not the other way around.

This module provides tools to:
1. Compare YAML definitions against Pydantic schema implementations
2. Find missing implementations (YAML has it, Pydantic doesn't)
3. Find orphan implementations (Pydantic has it, YAML doesn't)
4. Verify field types and constraints match

Usage:
    from lightwave.schema.testing import SpecAlignmentChecker

    checker = SpecAlignmentChecker(
        yaml_path="definitions/websockets.yaml",
        pydantic_module="lightwave.schema.pydantic.websockets",
    )
    report = checker.check_all()
    assert report.is_aligned, report.summary()
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import yaml
from pydantic import BaseModel


@dataclass
class AlignmentIssue:
    """A single alignment issue between spec and implementation."""

    category: str  # "missing", "orphan", "type_mismatch", "constraint_mismatch"
    item_type: str  # "channel", "message_type", "field", etc.
    item_name: str
    message: str
    spec_value: Any = None
    impl_value: Any = None


@dataclass
class AlignmentReport:
    """Report of spec alignment check."""

    spec_path: str
    module_name: str
    issues: list[AlignmentIssue] = field(default_factory=list)
    stats: dict[str, int] = field(default_factory=dict)

    @property
    def is_aligned(self) -> bool:
        return len(self.issues) == 0

    @property
    def missing_count(self) -> int:
        return sum(1 for i in self.issues if i.category == "missing")

    @property
    def orphan_count(self) -> int:
        return sum(1 for i in self.issues if i.category == "orphan")

    def add_issue(self, issue: AlignmentIssue) -> None:
        self.issues.append(issue)

    def summary(self) -> str:
        lines = [
            "Spec Alignment Report",
            f"  YAML: {self.spec_path}",
            f"  Module: {self.module_name}",
            f"  Aligned: {self.is_aligned}",
            f"  Issues: {len(self.issues)}",
        ]

        if self.issues:
            lines.append("\nIssues:")
            for issue in self.issues:
                lines.append(f"  [{issue.category}] {issue.item_type}:{issue.item_name} - {issue.message}")

        if self.stats:
            lines.append("\nStats:")
            for key, value in self.stats.items():
                lines.append(f"  {key}: {value}")

        return "\n".join(lines)


class SpecAlignmentChecker:
    """
    Check alignment between YAML spec and Pydantic implementations.

    Usage:
        checker = SpecAlignmentChecker(
            yaml_path="definitions/websockets.yaml",
            pydantic_module="lightwave.schema.pydantic.websockets",
        )
        report = checker.check_all()

        # Or check specific aspects
        checker.check_message_types()
        checker.check_channels()
    """

    def __init__(
        self,
        yaml_path: str | Path,
        pydantic_module: str,
        type_field: str = "type",
    ):
        """
        Initialize the checker.

        Args:
            yaml_path: Path to the YAML spec file
            pydantic_module: Fully qualified module name for Pydantic schemas
            type_field: Field name used for message type discrimination
        """
        self.yaml_path = Path(yaml_path)
        self.module_name = pydantic_module
        self.type_field = type_field
        self.report = AlignmentReport(
            spec_path=str(yaml_path),
            module_name=pydantic_module,
        )

        # Load YAML
        self._yaml_data: dict[str, Any] = {}
        self._pydantic_classes: dict[str, type[BaseModel]] = {}

    def _load_yaml(self) -> dict[str, Any]:
        """Load and cache the YAML spec."""
        if not self._yaml_data:
            if not self.yaml_path.exists():
                raise FileNotFoundError(f"YAML spec not found: {self.yaml_path}")
            with open(self.yaml_path) as f:
                self._yaml_data = yaml.safe_load(f) or {}
        return self._yaml_data

    def _load_pydantic_module(self) -> dict[str, type[BaseModel]]:
        """Load and cache Pydantic classes from the module."""
        if not self._pydantic_classes:
            import importlib

            try:
                module = importlib.import_module(self.module_name)
            except ImportError as e:
                raise ImportError(f"Failed to import {self.module_name}: {e}")

            # Find all BaseModel subclasses
            for name in dir(module):
                obj = getattr(module, name)
                if isinstance(obj, type) and issubclass(obj, BaseModel) and obj is not BaseModel:
                    self._pydantic_classes[name] = obj

        return self._pydantic_classes

    def _get_pydantic_type_values(self) -> dict[str, str]:
        """
        Get mapping of type field values to class names.

        Returns dict like {"chat.message": "ChatMessageInbound", ...}
        """
        classes = self._load_pydantic_module()
        type_map: dict[str, str] = {}

        for class_name, cls in classes.items():
            if self.type_field in cls.model_fields:
                field_info = cls.model_fields[self.type_field]
                default = field_info.default
                if isinstance(default, str):
                    type_map[default] = class_name

        return type_map

    def check_all(self) -> AlignmentReport:
        """Run all alignment checks."""
        self.report = AlignmentReport(
            spec_path=str(self.yaml_path),
            module_name=self.module_name,
        )

        self.check_message_types()
        self.check_channels()
        self.check_error_codes()

        return self.report

    def check_message_types(self) -> None:
        """Check that all YAML message types have Pydantic implementations."""
        yaml_data = self._load_yaml()
        pydantic_types = self._get_pydantic_type_values()

        yaml_message_types = set(yaml_data.get("message_types", {}).keys())
        pydantic_message_types = set(pydantic_types.keys())

        self.report.stats["yaml_message_types"] = len(yaml_message_types)
        self.report.stats["pydantic_message_types"] = len(pydantic_message_types)

        # Find missing (in YAML but not in Pydantic)
        missing = yaml_message_types - pydantic_message_types
        for msg_type in missing:
            self.report.add_issue(
                AlignmentIssue(
                    category="missing",
                    item_type="message_type",
                    item_name=msg_type,
                    message=f"YAML defines '{msg_type}' but no Pydantic schema has type='{msg_type}'",
                    spec_value=yaml_data["message_types"].get(msg_type),
                )
            )

        # Find orphans (in Pydantic but not in YAML) - excluding base types
        base_types = {"error", "ack"}  # These are OK to not be in YAML
        orphans = pydantic_message_types - yaml_message_types - base_types
        for msg_type in orphans:
            self.report.add_issue(
                AlignmentIssue(
                    category="orphan",
                    item_type="message_type",
                    item_name=msg_type,
                    message=f"Pydantic has type='{msg_type}' but YAML doesn't define it",
                    impl_value=pydantic_types.get(msg_type),
                )
            )

    def check_channels(self) -> None:
        """Check that all YAML channels are properly defined."""
        yaml_data = self._load_yaml()
        yaml_channels = yaml_data.get("channels", {})

        self.report.stats["yaml_channels"] = len(yaml_channels)

        for channel_name, channel_def in yaml_channels.items():
            # Check required channel fields
            required = ["name", "path"]
            for req in required:
                if req not in channel_def:
                    self.report.add_issue(
                        AlignmentIssue(
                            category="constraint_mismatch",
                            item_type="channel",
                            item_name=channel_name,
                            message=f"Channel missing required field '{req}'",
                        )
                    )

            # Check message_types structure
            msg_types = channel_def.get("message_types", {})
            if msg_types:
                if "inbound" not in msg_types and "outbound" not in msg_types:
                    self.report.add_issue(
                        AlignmentIssue(
                            category="constraint_mismatch",
                            item_type="channel",
                            item_name=channel_name,
                            message="Channel has message_types but missing inbound/outbound",
                        )
                    )

    def check_error_codes(self) -> None:
        """Check that error codes are consistent."""
        yaml_data = self._load_yaml()
        yaml_error_codes = set(yaml_data.get("error_codes", {}).keys())

        self.report.stats["yaml_error_codes"] = len(yaml_error_codes)

        # Check if Pydantic has WSErrorCode Literal that matches
        classes = self._load_pydantic_module()
        for name in ["WSErrorCode", "ErrorCode"]:
            if name in classes:
                # It's likely a Literal type alias, not a class
                pass

        # For now, just record the count
        # More sophisticated checking could compare Literal values


class WebSocketSpecChecker(SpecAlignmentChecker):
    """
    Specialized checker for WebSocket specifications.

    Usage:
        checker = WebSocketSpecChecker()  # Uses default paths
        report = checker.check_all()
    """

    def __init__(
        self,
        yaml_path: str | Path | None = None,
        pydantic_module: str = "lightwave.schema.pydantic.websockets",
    ):
        if yaml_path is None:
            # Default to the websockets.yaml from SST registry
            from lightwave.schema.core.paths import get_sst_path

            yaml_path = get_sst_path("websockets")

        super().__init__(
            yaml_path=yaml_path,
            pydantic_module=pydantic_module,
            type_field="type",
        )

    def check_channel_message_alignment(self) -> None:
        """
        Check that channel message_types reference valid message type definitions.
        """
        yaml_data = self._load_yaml()
        channels = yaml_data.get("channels", {})
        message_types = set(yaml_data.get("message_types", {}).keys())

        for channel_name, channel_def in channels.items():
            msg_types = channel_def.get("message_types", {})

            for direction in ["inbound", "outbound"]:
                for msg_type in msg_types.get(direction, []):
                    if msg_type not in message_types:
                        self.report.add_issue(
                            AlignmentIssue(
                                category="missing",
                                item_type="channel_message_ref",
                                item_name=f"{channel_name}.{direction}.{msg_type}",
                                message=f"Channel '{channel_name}' references undefined message type '{msg_type}'",
                            )
                        )

    def check_all(self) -> AlignmentReport:
        """Run all WebSocket-specific alignment checks."""
        super().check_all()
        self.check_channel_message_alignment()
        return self.report


# =============================================================================
# Pytest Integration
# =============================================================================


def assert_spec_aligned(
    yaml_path: str | Path,
    pydantic_module: str,
    **kwargs: Any,
) -> None:
    """
    Pytest assertion that spec and implementation are aligned.

    Usage:
        def test_websocket_spec_aligned():
            assert_spec_aligned(
                yaml_path="definitions/websockets.yaml",
                pydantic_module="lightwave.schema.pydantic.websockets",
            )
    """
    checker = SpecAlignmentChecker(yaml_path, pydantic_module, **kwargs)
    report = checker.check_all()
    assert report.is_aligned, report.summary()


def assert_websocket_spec_aligned() -> None:
    """
    Pytest assertion specifically for WebSocket spec alignment.

    Usage:
        def test_websocket_schemas():
            assert_websocket_spec_aligned()
    """
    checker = WebSocketSpecChecker()
    report = checker.check_all()
    assert report.is_aligned, report.summary()
