"""Metadata validators for ensuring RichChoice metadata meets FSM requirements."""

from dataclasses import dataclass, field
from typing import Any

from apps.core.state_machine.builder import StateTransitionBuilder


@dataclass
class ValidationError:
    """A validation error with details."""

    code: str
    message: str
    state: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)

    def __str__(self):
        """String representation of the error."""
        if self.state:
            return f"[{self.code}] {self.state}: {self.message}"
        return f"[{self.code}] {self.message}"


@dataclass
class ValidationWarning:
    """A validation warning with details."""

    code: str
    message: str
    state: str | None = None

    def __str__(self):
        """String representation of the warning."""
        if self.state:
            return f"[{self.code}] {self.state}: {self.message}"
        return f"[{self.code}] {self.message}"


@dataclass
class ValidationResult:
    """Result of metadata validation."""

    is_valid: bool
    errors: list[ValidationError] = field(default_factory=list)
    warnings: list[ValidationWarning] = field(default_factory=list)

    def add_error(self, code: str, message: str, state: str | None = None):
        """Add a validation error."""
        self.errors.append(ValidationError(code, message, state))
        self.is_valid = False

    def add_warning(self, code: str, message: str, state: str | None = None):
        """Add a validation warning."""
        self.warnings.append(ValidationWarning(code, message, state))


class MetadataValidator:
    """Validator for RichChoice metadata in state machine context."""

    def __init__(self, choice_group: str, domain: str = "core"):
        """
        Initialize validator.

        Args:
            choice_group: Choice group name
            domain: Domain namespace
        """
        self.choice_group = choice_group
        self.domain = domain
        self.builder = StateTransitionBuilder(choice_group, domain)

    def validate_choice_group(self) -> ValidationResult:
        """
        Validate entire choice group.

        Returns:
            ValidationResult with all errors and warnings
        """
        result = ValidationResult(is_valid=True)

        # Run all validation checks
        result.errors.extend(self.validate_transitions())
        result.errors.extend(self.validate_terminal_states())
        result.errors.extend(self.validate_permission_consistency())
        result.warnings.extend(self.validate_no_cycles())  # Cycles are warnings, not errors
        result.errors.extend(self.validate_reachability())

        # Set validity based on errors
        result.is_valid = len(result.errors) == 0

        return result

    def validate_transitions(self) -> list[ValidationError]:
        """
        Check all can_transition_to references exist.

        Returns:
            List of validation errors
        """
        from django.core.exceptions import ImproperlyConfigured

        errors = []
        all_states = set(self.builder.get_all_states())

        for state in all_states:
            # Check if can_transition_to is explicitly defined
            metadata = self.builder.get_choice_metadata(state)
            if "can_transition_to" not in metadata:
                errors.append(
                    ValidationError(
                        code="MISSING_CAN_TRANSITION_TO",
                        message=(
                            "State metadata must explicitly define " "'can_transition_to' (use [] for terminal states)"
                        ),
                        state=state,
                    )
                )
                continue

            # Validate transition targets exist, catching configuration errors
            try:
                transitions = self.builder.extract_valid_transitions(state)
            except ImproperlyConfigured as e:
                # Convert ImproperlyConfigured to ValidationError
                errors.append(
                    ValidationError(
                        code="INVALID_TRANSITION_TARGET",
                        message=str(e),
                        state=state,
                    )
                )
                continue

            # Double-check each target exists
            for target in transitions:
                if target not in all_states:
                    errors.append(
                        ValidationError(
                            code="INVALID_TRANSITION_TARGET",
                            message=(f"Transition target '{target}' does not exist"),
                            state=state,
                        )
                    )

        return errors

    def validate_terminal_states(self) -> list[ValidationError]:
        """
        Ensure terminal states have no outgoing transitions.

        Returns:
            List of validation errors
        """
        errors = []
        all_states = self.builder.get_all_states()

        for state in all_states:
            if self.builder.is_terminal_state(state):
                transitions = self.builder.extract_valid_transitions(state)
                if transitions:
                    errors.append(
                        ValidationError(
                            code="TERMINAL_STATE_HAS_TRANSITIONS",
                            message=(
                                f"Terminal state has {len(transitions)} "
                                f"outgoing transitions: {', '.join(transitions)}"
                            ),
                            state=state,
                        )
                    )

        return errors

    def validate_permission_consistency(self) -> list[ValidationError]:
        """
        Check permission requirements are consistent.

        Returns:
            List of validation errors
        """
        errors = []
        all_states = self.builder.get_all_states()

        for state in all_states:
            perms = self.builder.extract_permission_requirements(state)

            # Check for contradictory permissions
            if perms.get("requires_admin_approval") and not perms.get("requires_moderator"):
                errors.append(
                    ValidationError(
                        code="PERMISSION_INCONSISTENCY",
                        message=("State requires admin approval but not moderator " "(admin should imply moderator)"),
                        state=state,
                    )
                )

        return errors

    def validate_no_cycles(self) -> list[ValidationWarning]:
        """
        Detect state cycles (excluding self-loops).
        
        Note: Cycles are allowed in many FSMs (e.g., status transitions that allow
        reopening or revival). This method returns warnings, not errors, since
        cycles are often intentional in operational status FSMs.

        Returns:
            List of validation warnings
        """
        warnings = []
        graph = self.builder.build_transition_graph()

        # Detect cycles using DFS
        visited: set[str] = set()
        rec_stack: set[str] = set()

        def has_cycle(node: str, path: list[str]) -> list[str] | None:
            visited.add(node)
            rec_stack.add(node)
            path.append(node)

            for neighbor in graph.get(node, []):
                if neighbor not in visited:
                    cycle = has_cycle(neighbor, path.copy())
                    if cycle:
                        return cycle
                elif neighbor in rec_stack:
                    # Found a cycle
                    cycle_start = path.index(neighbor)
                    return path[cycle_start:] + [neighbor]

            rec_stack.remove(node)
            return None

        for state in graph:
            if state not in visited:
                cycle = has_cycle(state, [])
                if cycle:
                    warnings.append(
                        ValidationWarning(
                            code="STATE_CYCLE_EXISTS",
                            message=(f"Cycle exists (may be intentional): {' -> '.join(cycle)}"),
                            state=cycle[0],
                        )
                    )
                    break  # Report first cycle only

        return warnings

    def validate_reachability(self) -> list[ValidationError]:
        """
        Ensure all states are reachable from initial states.

        Returns:
            List of validation errors
        """
        errors = []
        graph = self.builder.build_transition_graph()
        all_states = set(self.builder.get_all_states())

        # Find states with no incoming transitions (potential initial states)
        incoming: dict[str, list[str]] = {state: [] for state in all_states}
        for source, targets in graph.items():
            for target in targets:
                incoming[target].append(source)

        initial_states = [state for state in all_states if not incoming[state]]

        if not initial_states:
            errors.append(
                ValidationError(
                    code="NO_INITIAL_STATE",
                    message="No initial state found (no state without incoming)",
                )
            )
            return errors

        # BFS from initial states to find reachable states
        reachable: set[str] = set(initial_states)
        queue = list(initial_states)

        while queue:
            current = queue.pop(0)
            for target in graph.get(current, []):
                if target not in reachable:
                    reachable.add(target)
                    queue.append(target)

        # Check for unreachable states
        unreachable = all_states - reachable
        for state in unreachable:
            # Terminal states might be unreachable if they're end states
            if not self.builder.is_terminal_state(state):
                errors.append(
                    ValidationError(
                        code="UNREACHABLE_STATE",
                        message="State is not reachable from initial states",
                        state=state,
                    )
                )

        return errors

    def generate_validation_report(self) -> str:
        """
        Create human-readable validation report.

        Returns:
            Formatted validation report
        """
        result = self.validate_choice_group()

        lines = []
        lines.append(f"Validation Report for {self.domain}.{self.choice_group}")
        lines.append("=" * 60)
        lines.append(f"Status: {'VALID' if result.is_valid else 'INVALID'}")
        lines.append(f"Errors: {len(result.errors)}")
        lines.append(f"Warnings: {len(result.warnings)}")
        lines.append("")

        if result.errors:
            lines.append("ERRORS:")
            lines.append("-" * 60)
            for error in result.errors:
                lines.append(f"  {error}")
            lines.append("")

        if result.warnings:
            lines.append("WARNINGS:")
            lines.append("-" * 60)
            for warning in result.warnings:
                lines.append(f"  {warning}")
            lines.append("")

        return "\n".join(lines)


def validate_on_registration(choice_group: str, domain: str = "core") -> bool:
    """
    Validate choice group when registering.

    Args:
        choice_group: Choice group name
        domain: Domain namespace

    Returns:
        True if validation passes

    Raises:
        ValueError: If validation fails
    """
    validator = MetadataValidator(choice_group, domain)
    result = validator.validate_choice_group()

    if not result.is_valid:
        error_messages = [str(e) for e in result.errors]
        raise ValueError(f"Validation failed for {domain}.{choice_group}:\n" + "\n".join(error_messages))

    return True


__all__ = [
    "MetadataValidator",
    "ValidationResult",
    "ValidationError",
    "ValidationWarning",
    "validate_on_registration",
]
