"""
SST Type Alignment Validator - Comprehensive validation for Single Source of Truth.

This module ensures the SST chain is trustworthy from YAML → Pydantic → Django.

Key validations:
1. Decision Tree Structure: All directories have __index.yaml
2. Field Options Alignment: All YAML values have Pydantic coverage
3. Cross-File References: All $ref and field_options references resolve
4. Type Safety: Pydantic models match YAML constraints
5. Django Integration: Helper for validating Django model → Pydantic alignment

Usage:
    from lightwave.schema.testing import SSTValidator

    # Full validation
    validator = SSTValidator()
    report = validator.validate_all()
    assert report.is_valid, report.format_errors()

    # Check specific aspects
    validator.validate_decision_tree()
    validator.validate_field_options_coverage()
    validator.validate_cross_references()

    # Django integration (in Django tests)
    from lightwave.schema.testing import validate_django_model_choices
    validate_django_model_choices(MyModel, "status", "page_statuses")
"""

from __future__ import annotations

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

import yaml
from pydantic import BaseModel

if TYPE_CHECKING:
    from django.db.models import Field as DjangoField
    from django.db.models import Model as DjangoModel

logger = logging.getLogger(__name__)


# =============================================================================
# Data Classes for Validation Results
# =============================================================================


@dataclass
class ValidationError:
    """A single validation error with context."""

    category: str  # "structure", "field_options", "cross_ref", "type_safety", "django"
    severity: str  # "error", "warning"
    path: str  # File or field path
    message: str
    expected: Any = None
    actual: Any = None

    def __str__(self) -> str:
        return f"[{self.severity.upper()}] {self.category}:{self.path} - {self.message}"


@dataclass
class ValidationReport:
    """Complete validation report for SST."""

    errors: list[ValidationError] = field(default_factory=list)
    warnings: list[ValidationError] = field(default_factory=list)
    stats: dict[str, int] = field(default_factory=dict)
    validated_files: list[str] = field(default_factory=list)

    @property
    def is_valid(self) -> bool:
        """True if no errors (warnings are OK)."""
        return len(self.errors) == 0

    @property
    def error_count(self) -> int:
        return len(self.errors)

    @property
    def warning_count(self) -> int:
        return len(self.warnings)

    def add_error(self, error: ValidationError) -> None:
        if error.severity == "warning":
            self.warnings.append(error)
        else:
            self.errors.append(error)

    def merge(self, other: ValidationReport) -> None:
        """Merge another report into this one."""
        self.errors.extend(other.errors)
        self.warnings.extend(other.warnings)
        self.validated_files.extend(other.validated_files)
        for key, value in other.stats.items():
            self.stats[key] = self.stats.get(key, 0) + value

    def format_errors(self) -> str:
        """Format all errors as a readable string."""
        lines = [
            "SST Validation Report",
            "=" * 50,
            f"Valid: {self.is_valid}",
            f"Errors: {self.error_count}",
            f"Warnings: {self.warning_count}",
            f"Files validated: {len(self.validated_files)}",
        ]

        if self.errors:
            lines.append("\nERRORS:")
            for err in self.errors:
                lines.append(f"  {err}")

        if self.warnings:
            lines.append("\nWARNINGS:")
            for warn in self.warnings:
                lines.append(f"  {warn}")

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

        return "\n".join(lines)


# =============================================================================
# Main SST Validator
# =============================================================================


class SSTValidator:
    """
    Comprehensive SST validation for the definitions directory.

    Validates:
    1. Decision tree structure (directories have proper __index.yaml)
    2. Field options coverage (all values accessible via FieldOptions)
    3. Cross-file references (all $ref paths resolve)
    4. YAML file validity (all files parse correctly)

    Usage:
        validator = SSTValidator()
        report = validator.validate_all()
        if not report.is_valid:
            print(report.format_errors())
    """

    # Directories that MUST have __index.yaml for decision tree structure
    INDEXED_DIRECTORIES = [
        "config/field_options",
        "governance/rbac",
        "governance/security",
        "governance/legal",
        "workflows",
        "workflows/agentic",
        "workflows/collaboration",
        "workflows/journeys",
        "workflows/epics",
        "workflows/infrastructure",
        "workflows/compliance",
    ]

    def __init__(self, definitions_path: Path | None = None) -> None:
        """
        Initialize the validator.

        Args:
            definitions_path: Path to definitions directory. Defaults to
                              lightwave/schema/definitions.
        """
        if definitions_path is None:
            from lightwave.schema.core.paths import DEFINITIONS_DIR

            definitions_path = DEFINITIONS_DIR
        self.definitions_path = definitions_path
        self.report = ValidationReport()

    def validate_all(self) -> ValidationReport:
        """Run all SST validations."""
        self.report = ValidationReport()

        self.validate_yaml_files()
        self.validate_decision_tree()
        self.validate_field_options_coverage()
        self.validate_cross_references()

        return self.report

    def validate_yaml_files(self) -> None:
        """Validate all YAML files parse correctly."""
        yaml_files = list(self.definitions_path.rglob("*.yaml"))
        self.report.stats["yaml_files"] = len(yaml_files)

        for yaml_file in yaml_files:
            rel_path = yaml_file.relative_to(self.definitions_path)
            try:
                with open(yaml_file) as f:
                    yaml.safe_load(f)
                self.report.validated_files.append(str(rel_path))
            except yaml.YAMLError as e:
                self.report.add_error(
                    ValidationError(
                        category="structure",
                        severity="error",
                        path=str(rel_path),
                        message=f"YAML parse error: {e}",
                    )
                )

    def validate_decision_tree(self) -> None:
        """
        Validate decision tree structure.

        Every directory in INDEXED_DIRECTORIES must have an __index.yaml.
        """
        for dir_path in self.INDEXED_DIRECTORIES:
            full_path = self.definitions_path / dir_path
            if not full_path.exists():
                self.report.add_error(
                    ValidationError(
                        category="structure",
                        severity="warning",
                        path=dir_path,
                        message="Directory in decision tree doesn't exist",
                    )
                )
                continue

            index_file = full_path / "__index.yaml"
            if not index_file.exists():
                self.report.add_error(
                    ValidationError(
                        category="structure",
                        severity="error",
                        path=dir_path,
                        message="Missing __index.yaml - required for decision tree",
                    )
                )
            else:
                # Validate index file has required fields
                try:
                    with open(index_file) as f:
                        data = yaml.safe_load(f) or {}

                    if "_meta" not in data:
                        self.report.add_error(
                            ValidationError(
                                category="structure",
                                severity="warning",
                                path=f"{dir_path}/__index.yaml",
                                message="Missing _meta section in index file",
                            )
                        )
                except yaml.YAMLError as e:
                    self.report.add_error(
                        ValidationError(
                            category="structure",
                            severity="error",
                            path=f"{dir_path}/__index.yaml",
                            message=f"Invalid YAML: {e}",
                        )
                    )

    def validate_field_options_coverage(self) -> None:
        """
        Validate FieldOptions can load and covers all split files.

        Ensures the merge strategy works correctly.
        """
        from lightwave.schema.pydantic.core.validators.field_options import FieldOptions

        try:
            # Clear cache to force fresh load
            FieldOptions.clear_cache()

            # Load all field options
            all_options = FieldOptions.load_all()
            self.report.stats["field_options_count"] = len(all_options)

            # Get validation report
            options_report = FieldOptions.get_validation_report()
            self.report.stats["field_options_mode"] = options_report.load_mode
            self.report.stats["field_options_files"] = len(options_report.source_files)

            # Check for validation errors in the report
            for error in options_report.validation_errors:
                self.report.add_error(
                    ValidationError(
                        category="field_options",
                        severity="error",
                        path="field_options",
                        message=error,
                    )
                )

            # Validate each field has proper structure
            for field_name, definition in all_options.items():
                if not isinstance(definition, dict):
                    self.report.add_error(
                        ValidationError(
                            category="field_options",
                            severity="error",
                            path=f"field_options.{field_name}",
                            message=f"Invalid definition type: {type(definition).__name__}",
                            expected="dict",
                            actual=type(definition).__name__,
                        )
                    )
                    continue

                # Check required keys
                if "options" not in definition:
                    self.report.add_error(
                        ValidationError(
                            category="field_options",
                            severity="error",
                            path=f"field_options.{field_name}",
                            message="Missing 'options' key",
                        )
                    )
                elif not isinstance(definition["options"], list):
                    self.report.add_error(
                        ValidationError(
                            category="field_options",
                            severity="error",
                            path=f"field_options.{field_name}",
                            message="'options' must be a list",
                            expected="list",
                            actual=type(definition["options"]).__name__,
                        )
                    )
                else:
                    # Validate each option has value and label
                    for i, opt in enumerate(definition["options"]):
                        if not isinstance(opt, dict):
                            self.report.add_error(
                                ValidationError(
                                    category="field_options",
                                    severity="error",
                                    path=f"field_options.{field_name}.options[{i}]",
                                    message="Option must be a dict",
                                )
                            )
                        elif "value" not in opt:
                            self.report.add_error(
                                ValidationError(
                                    category="field_options",
                                    severity="error",
                                    path=f"field_options.{field_name}.options[{i}]",
                                    message="Option missing 'value' key",
                                )
                            )

        except FileNotFoundError as e:
            self.report.add_error(
                ValidationError(
                    category="field_options",
                    severity="error",
                    path="field_options",
                    message=f"Could not load field options: {e}",
                )
            )
        except Exception as e:
            self.report.add_error(
                ValidationError(
                    category="field_options",
                    severity="error",
                    path="field_options",
                    message=f"Unexpected error loading field options: {e}",
                )
            )

    def validate_cross_references(self) -> None:
        """
        Validate that cross-file references resolve.

        Checks:
        - $ref references to other files
        - field_options.X references
        - workflow imports
        """
        yaml_files = list(self.definitions_path.rglob("*.yaml"))
        ref_count = 0
        broken_refs = 0

        for yaml_file in yaml_files:
            rel_path = yaml_file.relative_to(self.definitions_path)
            try:
                with open(yaml_file) as f:
                    data = yaml.safe_load(f) or {}

                # Find all $ref values
                refs = self._find_refs(data)
                ref_count += len(refs)

                for ref in refs:
                    if not self._resolve_ref(yaml_file.parent, ref):
                        broken_refs += 1
                        self.report.add_error(
                            ValidationError(
                                category="cross_ref",
                                severity="error",
                                path=str(rel_path),
                                message=f"Unresolved reference: {ref}",
                            )
                        )

            except yaml.YAMLError:
                pass  # Already reported in validate_yaml_files

        self.report.stats["cross_references"] = ref_count
        self.report.stats["broken_references"] = broken_refs

    def _find_refs(self, data: Any, prefix: str = "") -> list[str]:
        """Recursively find all $ref values in data."""
        refs: list[str] = []

        if isinstance(data, dict):
            for key, value in data.items():
                if key == "$ref" and isinstance(value, str):
                    refs.append(value)
                elif key == "ref" and isinstance(value, str):
                    refs.append(value)
                else:
                    refs.extend(self._find_refs(value, f"{prefix}.{key}"))
        elif isinstance(data, list):
            for i, item in enumerate(data):
                refs.extend(self._find_refs(item, f"{prefix}[{i}]"))

        return refs

    def _resolve_ref(self, base_path: Path, ref: str) -> bool:
        """Check if a reference can be resolved."""
        # Skip field_options references - validated separately
        if ref.startswith("field_options."):
            from lightwave.schema.pydantic.core.validators.field_options import FieldOptions

            field_name = ref.replace("field_options.", "")
            try:
                FieldOptions.get_field_definition(field_name)
                return True
            except KeyError:
                return False

        # Handle file references
        if ref.startswith("./") or ref.startswith("../"):
            ref_path = (base_path / ref).resolve()
            if not ref_path.suffix:
                ref_path = ref_path.with_suffix(".yaml")
            return ref_path.exists()

        # Handle absolute paths within definitions
        if ref.startswith("/"):
            ref_path = self.definitions_path / ref.lstrip("/")
            if not ref_path.suffix:
                ref_path = ref_path.with_suffix(".yaml")
            return ref_path.exists()

        # For other refs, assume they're valid (e.g., schema references)
        return True


# =============================================================================
# Django Integration Validators
# =============================================================================


class DjangoModelValidator:
    """
    Validate Django models derive from SST correctly.

    Usage:
        validator = DjangoModelValidator()
        report = validator.validate_model(
            model=MyModel,
            field_mappings={
                "status": "page_statuses",
                "type": "page_types",
            }
        )
    """

    def __init__(self) -> None:
        self.report = ValidationReport()

    def validate_choices(
        self,
        model_class: type[DjangoModel],
        field_name: str,
        yaml_field: str,
    ) -> ValidationReport:
        """
        Validate a Django model field choices match YAML field options.

        Args:
            model_class: Django model class
            field_name: Name of the field on the model
            yaml_field: Name in field_options.yaml

        Returns:
            ValidationReport with any mismatches
        """
        from lightwave.schema.pydantic.core.validators.field_options import FieldOptions

        self.report = ValidationReport()
        model_name = model_class.__name__

        # Get Django field choices
        try:
            django_field: DjangoField[Any, Any] = model_class._meta.get_field(field_name)
            if hasattr(django_field, "choices") and django_field.choices:
                django_choices = {c[0] for c in django_field.choices}
            else:
                self.report.add_error(
                    ValidationError(
                        category="django",
                        severity="error",
                        path=f"{model_name}.{field_name}",
                        message="Field has no choices defined",
                    )
                )
                return self.report
        except Exception as e:
            self.report.add_error(
                ValidationError(
                    category="django",
                    severity="error",
                    path=f"{model_name}.{field_name}",
                    message=f"Could not get field: {e}",
                )
            )
            return self.report

        # Get YAML field options
        try:
            yaml_values = set(FieldOptions.get_values(yaml_field))
        except KeyError:
            self.report.add_error(
                ValidationError(
                    category="django",
                    severity="error",
                    path=f"field_options.{yaml_field}",
                    message="Field not found in field_options.yaml",
                )
            )
            return self.report

        # Compare
        missing_in_django = yaml_values - django_choices
        extra_in_django = django_choices - yaml_values

        if missing_in_django:
            self.report.add_error(
                ValidationError(
                    category="django",
                    severity="error",
                    path=f"{model_name}.{field_name}",
                    message=f"Missing choices from YAML: {missing_in_django}",
                    expected=yaml_values,
                    actual=django_choices,
                )
            )

        if extra_in_django:
            self.report.add_error(
                ValidationError(
                    category="django",
                    severity="error",
                    path=f"{model_name}.{field_name}",
                    message=f"Extra choices not in YAML: {extra_in_django}",
                    expected=yaml_values,
                    actual=django_choices,
                )
            )

        return self.report


# =============================================================================
# Pydantic Model Introspection
# =============================================================================


class PydanticSchemaValidator:
    """
    Validate Pydantic models derive from SST correctly.

    Usage:
        validator = PydanticSchemaValidator()
        report = validator.validate_literal_field(
            model=MySchema,
            field_name="status",
            yaml_field="page_statuses",
        )
    """

    def __init__(self) -> None:
        self.report = ValidationReport()

    def validate_literal_field(
        self,
        model_class: type[BaseModel],
        field_name: str,
        yaml_field: str,
    ) -> ValidationReport:
        """
        Validate a Pydantic Literal field matches YAML field options.

        Args:
            model_class: Pydantic model class
            field_name: Name of the field
            yaml_field: Name in field_options.yaml

        Returns:
            ValidationReport with any mismatches
        """
        from typing import get_args, get_origin

        from lightwave.schema.pydantic.core.validators.field_options import FieldOptions

        self.report = ValidationReport()
        model_name = model_class.__name__

        # Get Pydantic field
        if field_name not in model_class.model_fields:
            self.report.add_error(
                ValidationError(
                    category="type_safety",
                    severity="error",
                    path=f"{model_name}.{field_name}",
                    message="Field not found in Pydantic model",
                )
            )
            return self.report

        field_info = model_class.model_fields[field_name]
        annotation = field_info.annotation

        # Extract Literal values
        origin = get_origin(annotation)
        if origin is None:
            # Not a generic type, might be a simple type
            self.report.add_error(
                ValidationError(
                    category="type_safety",
                    severity="warning",
                    path=f"{model_name}.{field_name}",
                    message=f"Field is not a Literal type: {annotation}",
                )
            )
            return self.report

        # For Literal types
        try:
            args = get_args(annotation)
            if not args:
                self.report.add_error(
                    ValidationError(
                        category="type_safety",
                        severity="warning",
                        path=f"{model_name}.{field_name}",
                        message="Could not extract Literal values",
                    )
                )
                return self.report

            pydantic_values = set(args)
        except Exception as e:
            self.report.add_error(
                ValidationError(
                    category="type_safety",
                    severity="error",
                    path=f"{model_name}.{field_name}",
                    message=f"Error extracting Literal values: {e}",
                )
            )
            return self.report

        # Get YAML values
        try:
            yaml_values = set(FieldOptions.get_values(yaml_field))
        except KeyError:
            self.report.add_error(
                ValidationError(
                    category="type_safety",
                    severity="error",
                    path=f"field_options.{yaml_field}",
                    message="Field not found in field_options.yaml",
                )
            )
            return self.report

        # Compare
        missing = yaml_values - pydantic_values
        extra = pydantic_values - yaml_values

        if missing:
            self.report.add_error(
                ValidationError(
                    category="type_safety",
                    severity="error",
                    path=f"{model_name}.{field_name}",
                    message=f"Missing Literal values from YAML: {missing}",
                    expected=yaml_values,
                    actual=pydantic_values,
                )
            )

        if extra:
            self.report.add_error(
                ValidationError(
                    category="type_safety",
                    severity="error",
                    path=f"{model_name}.{field_name}",
                    message=f"Extra Literal values not in YAML: {extra}",
                    expected=yaml_values,
                    actual=pydantic_values,
                )
            )

        return self.report


# =============================================================================
# Pytest Helpers
# =============================================================================


def validate_sst() -> ValidationReport:
    """
    Run full SST validation and return report.

    Usage in pytest:
        def test_sst_valid():
            report = validate_sst()
            assert report.is_valid, report.format_errors()
    """
    validator = SSTValidator()
    return validator.validate_all()


def validate_django_model_choices(
    model_class: type[DjangoModel],
    field_name: str,
    yaml_field: str,
) -> None:
    """
    Assert Django model choices match YAML field options.

    Usage:
        def test_page_status_choices():
            validate_django_model_choices(Page, "status", "page_statuses")
    """
    validator = DjangoModelValidator()
    report = validator.validate_choices(model_class, field_name, yaml_field)
    assert report.is_valid, report.format_errors()


def validate_pydantic_literal(
    model_class: type[BaseModel],
    field_name: str,
    yaml_field: str,
) -> None:
    """
    Assert Pydantic Literal field matches YAML field options.

    Usage:
        def test_status_literal():
            validate_pydantic_literal(PageSchema, "status", "page_statuses")
    """
    validator = PydanticSchemaValidator()
    report = validator.validate_literal_field(model_class, field_name, yaml_field)
    assert report.is_valid, report.format_errors()


# =============================================================================
# CLI Entry Point
# =============================================================================


def main() -> int:
    """Run SST validation from command line."""

    validator = SSTValidator()
    report = validator.validate_all()

    print(report.format_errors())

    return 0 if report.is_valid else 1


if __name__ == "__main__":
    import sys

    sys.exit(main())
