"""
UI Alignment Checker.

Compares TypeScript interfaces from lightwave-ui with Pydantic schemas
from lightwave-core to detect drift and ensure the 1:1 mirror is maintained.
"""

from __future__ import annotations

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

from lightwave.schema.ui_alignment.pydantic_discovery import (
    PydanticSchema,
    PydanticSchemaDiscovery,
)
from lightwave.schema.ui_alignment.ts_parser import (
    TSInterface,
    TypeScriptInterfaceExtractor,
)

# Type mapping from TypeScript to Python
TS_TO_PYTHON_TYPES: dict[str, set[str]] = {
    # Primitives
    "string": {"str"},
    "number": {"int", "float"},
    "boolean": {"bool"},
    "null": {"None"},
    "undefined": {"None"},
    "any": {"Any", "dict", "dict[str, Any]"},
    "unknown": {"Any"},
    "void": {"None"},
    "never": {"None"},
    # Arrays
    "string[]": {"list[str]"},
    "number[]": {"list[int]", "list[float]"},
    "boolean[]": {"list[bool]"},
    "Array<string>": {"list[str]"},
    "Array<number>": {"list[int]", "list[float]"},
    # Objects
    "object": {"dict", "dict[str, Any]"},
    "Record<string, any>": {"dict", "dict[str, Any]"},
    "Record<string, string>": {"dict[str, str]"},
    # React-specific
    "ReactNode": {"str", "dict", "Any"},
    "React.ReactNode": {"str", "dict", "Any"},
    "JSX.Element": {"str", "dict"},
}


@dataclass
class AlignmentIssue:
    """A single alignment issue between TypeScript and Pydantic."""

    category: Literal[
        "missing_pydantic",  # TS interface exists, no Pydantic schema
        "orphan_pydantic",  # Pydantic schema exists, no TS interface
        "property_missing",  # Property in TS, not in Pydantic
        "property_extra",  # Property in Pydantic, not in TS
        "type_mismatch",  # Different types
        "alias_mismatch",  # Wrong serialization alias
        "optionality_mismatch",  # Different optional status
    ]
    interface_name: str
    property_name: str | None = None
    message: str = ""
    ts_value: str | None = None
    pydantic_value: str | None = None
    severity: Literal["error", "warning"] = "error"

    def __repr__(self) -> str:
        icon = "[E]" if self.severity == "error" else "[W]"
        prop = f".{self.property_name}" if self.property_name else ""
        return f"{icon} {self.interface_name}{prop}: {self.message}"


@dataclass
class UIAlignmentReport:
    """Report of UI schema alignment check."""

    ts_interfaces_count: int = 0
    pydantic_schemas_count: int = 0
    matched_count: int = 0
    issues: list[AlignmentIssue] = field(default_factory=list)

    @property
    def is_aligned(self) -> bool:
        """Check if there are no errors (warnings are acceptable)."""
        return self.error_count == 0

    @property
    def error_count(self) -> int:
        """Count of error-severity issues."""
        return len([i for i in self.issues if i.severity == "error"])

    @property
    def warning_count(self) -> int:
        """Count of warning-severity issues."""
        return len([i for i in self.issues if i.severity == "warning"])

    def summary(self) -> str:
        """Generate human-readable summary report."""
        lines = [
            "=" * 60,
            "UI Schema Alignment Report",
            "=" * 60,
            f"TypeScript Interfaces: {self.ts_interfaces_count}",
            f"Pydantic Schemas: {self.pydantic_schemas_count}",
            f"Matched: {self.matched_count}",
            f"Aligned: {'YES' if self.is_aligned else 'NO'}",
            f"Errors: {self.error_count}",
            f"Warnings: {self.warning_count}",
        ]

        if self.issues:
            lines.append("")
            lines.append("Issues:")

            # Group by category
            by_category: dict[str, list[AlignmentIssue]] = {}
            for issue in self.issues:
                by_category.setdefault(issue.category, []).append(issue)

            for category, cat_issues in sorted(by_category.items()):
                lines.append(f"\n  {category.upper()} ({len(cat_issues)}):")
                for issue in cat_issues[:10]:  # Limit per category
                    lines.append(f"    {issue!r}")
                if len(cat_issues) > 10:
                    lines.append(f"    ... and {len(cat_issues) - 10} more")

        return "\n".join(lines)

    def to_dict(self) -> dict:
        """Convert report to dictionary for JSON serialization."""
        return {
            "aligned": self.is_aligned,
            "ts_interfaces_count": self.ts_interfaces_count,
            "pydantic_schemas_count": self.pydantic_schemas_count,
            "matched_count": self.matched_count,
            "error_count": self.error_count,
            "warning_count": self.warning_count,
            "issues": [
                {
                    "category": i.category,
                    "interface": i.interface_name,
                    "property": i.property_name,
                    "message": i.message,
                    "severity": i.severity,
                    "ts_value": i.ts_value,
                    "pydantic_value": i.pydantic_value,
                }
                for i in self.issues
            ],
        }


class UIAlignmentChecker:
    """
    Check alignment between TypeScript interfaces and Pydantic schemas.

    Validates that the 1:1 mirror between lightwave-ui TypeScript and
    lightwave-core Pydantic schemas is maintained.

    Usage:
        checker = UIAlignmentChecker()
        report = checker.check_all()

        if not report.is_aligned:
            print(report.summary())
            raise SystemExit(1)

    Checks performed:
        - Missing Pydantic schemas (TS exists, no Pydantic) - ERROR
        - Orphan Pydantic schemas (Pydantic exists, no TS) - WARNING
        - Missing properties (in TS, not in Pydantic) - ERROR
        - Extra properties (in Pydantic, not in TS) - WARNING
        - Type mismatches - ERROR
        - Serialization alias mismatches - ERROR
        - Optionality mismatches - WARNING
    """

    def __init__(
        self,
        ts_src_path: Path | None = None,
        pydantic_module: str = "lightwave.schema.pydantic.ui",
    ):
        """
        Initialize checker.

        Args:
            ts_src_path: Path to lightwave-ui/src directory.
                        If None, auto-detected.
            pydantic_module: Module path for Pydantic UI schemas.
        """
        self.ts_extractor = TypeScriptInterfaceExtractor(ts_src_path)
        self.pydantic_discovery = PydanticSchemaDiscovery(pydantic_module)
        self.report = UIAlignmentReport()

    def check_all(self) -> UIAlignmentReport:
        """
        Run all alignment checks.

        Returns:
            UIAlignmentReport with all discovered issues
        """
        self.report = UIAlignmentReport()

        # Extract TypeScript interfaces
        ts_interfaces = self.ts_extractor.extract_all()
        self.report.ts_interfaces_count = len(ts_interfaces)

        # Discover Pydantic schemas
        pydantic_schemas = self.pydantic_discovery.discover_all()
        self.report.pydantic_schemas_count = len(pydantic_schemas)

        # Track matched schemas
        matched = set()

        # Check for missing Pydantic schemas
        self._check_missing_pydantic(ts_interfaces, pydantic_schemas, matched)

        # Check for orphan Pydantic schemas
        self._check_orphan_pydantic(ts_interfaces, pydantic_schemas)

        # Check property alignment for matching schemas
        self._check_property_alignment(ts_interfaces, pydantic_schemas, matched)

        self.report.matched_count = len(matched)

        return self.report

    def _check_missing_pydantic(
        self,
        ts_interfaces: dict[str, TSInterface],
        pydantic_schemas: dict[str, PydanticSchema],
        matched: set[str],
    ) -> None:
        """Find TypeScript interfaces without Pydantic schemas."""
        for ts_name, ts_interface in ts_interfaces.items():
            # Only check Props interfaces
            if not ts_name.endswith("Props"):
                continue

            # Skip internal/private interfaces
            if ts_name.startswith("_"):
                continue

            # Check if Pydantic equivalent exists
            if ts_name in pydantic_schemas:
                matched.add(ts_name)
            else:
                self.report.issues.append(
                    AlignmentIssue(
                        category="missing_pydantic",
                        interface_name=ts_name,
                        message="No Pydantic schema for TypeScript interface",
                        ts_value=ts_interface.source_file,
                        severity="error",
                    )
                )

    def _check_orphan_pydantic(
        self,
        ts_interfaces: dict[str, TSInterface],
        pydantic_schemas: dict[str, PydanticSchema],
    ) -> None:
        """Find Pydantic schemas without TypeScript interfaces."""
        for py_name, py_schema in pydantic_schemas.items():
            if py_name not in ts_interfaces:
                self.report.issues.append(
                    AlignmentIssue(
                        category="orphan_pydantic",
                        interface_name=py_name,
                        message="No TypeScript interface for Pydantic schema",
                        pydantic_value=py_schema.source_file,
                        severity="warning",  # May be intentional
                    )
                )

    def _check_property_alignment(
        self,
        ts_interfaces: dict[str, TSInterface],
        pydantic_schemas: dict[str, PydanticSchema],
        matched: set[str],
    ) -> None:
        """Check property alignment for matching schemas."""
        for ts_name in matched:
            ts_interface = ts_interfaces[ts_name]
            py_schema = pydantic_schemas[ts_name]

            # Build property map using serialization aliases
            py_props_by_alias: dict[str, tuple[str, any]] = {}
            for py_name, py_prop in py_schema.properties.items():
                # Use serialization alias as the key for matching
                alias = py_prop.camel_case_name
                py_props_by_alias[alias] = (py_name, py_prop)

            # Check TypeScript properties exist in Pydantic
            for ts_prop_name, ts_prop in ts_interface.properties.items():
                if ts_prop_name not in py_props_by_alias:
                    self.report.issues.append(
                        AlignmentIssue(
                            category="property_missing",
                            interface_name=ts_name,
                            property_name=ts_prop_name,
                            message="TypeScript property not found in Pydantic",
                            ts_value=ts_prop.ts_type,
                            severity="error",
                        )
                    )
                    continue

                py_name, py_prop = py_props_by_alias[ts_prop_name]

                # Check type compatibility
                if not self._types_compatible(ts_prop.ts_type, py_prop.python_type):
                    self.report.issues.append(
                        AlignmentIssue(
                            category="type_mismatch",
                            interface_name=ts_name,
                            property_name=ts_prop_name,
                            message=f"Type mismatch: TS '{ts_prop.ts_type}' vs Pydantic '{py_prop.python_type}'",
                            ts_value=ts_prop.ts_type,
                            pydantic_value=py_prop.python_type,
                            severity="error",
                        )
                    )

                # Check optionality (warning only - may differ intentionally)
                if ts_prop.optional != py_prop.optional:
                    self.report.issues.append(
                        AlignmentIssue(
                            category="optionality_mismatch",
                            interface_name=ts_name,
                            property_name=ts_prop_name,
                            message=f"Optionality mismatch: TS {'optional' if ts_prop.optional else 'required'} vs Pydantic {'optional' if py_prop.optional else 'required'}",
                            ts_value=str(ts_prop.optional),
                            pydantic_value=str(py_prop.optional),
                            severity="warning",
                        )
                    )

            # Check for extra Pydantic properties
            ts_prop_names = set(ts_interface.properties.keys())
            for alias, (py_name, py_prop) in py_props_by_alias.items():
                if alias not in ts_prop_names:
                    self.report.issues.append(
                        AlignmentIssue(
                            category="property_extra",
                            interface_name=ts_name,
                            property_name=py_name,
                            message=f"Pydantic property '{py_name}' (alias: {alias}) not in TypeScript",
                            pydantic_value=py_prop.python_type,
                            severity="warning",
                        )
                    )

    def _types_compatible(self, ts_type: str, py_type: str) -> bool:
        """
        Check if TypeScript and Python types are compatible.

        Handles common mappings between TypeScript and Python types.
        Optionality (| None) is handled separately - types are compatible
        if base types match, regardless of optionality.
        """
        # Normalize types for comparison
        ts_normalized = ts_type.strip().replace(" ", "")
        py_normalized = py_type.strip().replace(" ", "")

        # Strip optionality for comparison (handled separately)
        py_base = py_normalized.replace("|None", "").strip()
        ts_base = ts_normalized.replace("|undefined", "").replace("|null", "").strip()

        # Check direct mapping (using base types without optionality)
        if ts_base in TS_TO_PYTHON_TYPES:
            if any(py_base.startswith(eq) for eq in TS_TO_PYTHON_TYPES[ts_base]):
                return True

        # Check for Props types (same name = compatible, ignoring optionality)
        if ts_base.endswith("Props") and py_base.endswith("Props"):
            return ts_base == py_base

        # Check for optional types (T | undefined in TS, T | None in Python)
        if "|" in ts_normalized:
            ts_parts = {p.strip() for p in ts_normalized.split("|")}
            ts_parts.discard("undefined")
            ts_parts.discard("null")

            if "|" in py_normalized:
                py_parts = {p.strip() for p in py_normalized.split("|")}
                py_parts.discard("None")

                # Compare base types
                if len(ts_parts) == 1 and len(py_parts) == 1:
                    return self._types_compatible(ts_parts.pop(), py_parts.pop())

        # Check for array types (using base types)
        array_match = self._check_array_compatible(ts_base, py_base)
        if array_match is not None:
            return array_match

        # Check for string literals (same as string)
        if ts_base.startswith('"') or ts_base.startswith("'"):
            return py_base.startswith("str") or py_base.startswith("Literal")

        # Fallback: exact match after normalization (using base types)
        return ts_base.lower() == py_base.lower()

    def _check_array_compatible(self, ts_type: str, py_type: str) -> bool | None:
        """Check array type compatibility. Returns None if not array types."""
        # TS: Type[] or Array<Type>
        # Python: list[Type]

        ts_inner = None
        if ts_type.endswith("[]"):
            ts_inner = ts_type[:-2]
        elif ts_type.startswith("Array<") and ts_type.endswith(">"):
            ts_inner = ts_type[6:-1]

        py_inner = None
        if py_type.startswith("list[") and py_type.endswith("]"):
            py_inner = py_type[5:-1]

        if ts_inner is not None and py_inner is not None:
            return self._types_compatible(ts_inner, py_inner)

        if ts_inner is not None or py_inner is not None:
            # One is array, other is not
            return False

        return None  # Neither is array


# Convenience function for quick checks
def check_ui_alignment(
    ts_src_path: Path | None = None,
    strict: bool = False,
) -> UIAlignmentReport:
    """
    Quick check of UI schema alignment.

    Args:
        ts_src_path: Path to lightwave-ui/src (auto-detected if None)
        strict: If True, warnings are treated as errors

    Returns:
        UIAlignmentReport

    Raises:
        SystemExit: If not aligned (or warnings in strict mode)
    """
    checker = UIAlignmentChecker(ts_src_path=ts_src_path)
    report = checker.check_all()

    if not report.is_aligned:
        print(report.summary())
        raise SystemExit(1)

    if strict and report.warning_count > 0:
        print(report.summary())
        raise SystemExit(1)

    return report


if __name__ == "__main__":
    # Run check when executed directly
    check_ui_alignment()
