"""
Schema Validator.

Validates events against their registered schemas and checks
backward compatibility between versions.
"""
import logging
from typing import Any, Dict, List, Optional, Tuple

from .models import EventSchemaRegistry
from .envelope import EventEnvelope

logger = logging.getLogger(__name__)


def _validate_json_schema(data: Dict, schema: Dict) -> List[str]:
    """
    Validate data against a JSON Schema.
    
    Uses jsonschema library if available, falls back to basic validation.
    """
    try:
        import jsonschema
        validator = jsonschema.Draft7Validator(schema)
        errors = list(validator.iter_errors(data))
        return [f"{e.json_path}: {e.message}" for e in errors]
    except ImportError:
        # Fallback: basic validation without jsonschema library
        errors = []
        required = schema.get('required', [])
        properties = schema.get('properties', {})

        for field_name in required:
            if field_name not in data:
                errors.append(f"$.{field_name}: Required field missing")

        for field_name, field_value in data.items():
            if field_name in properties:
                field_schema = properties[field_name]
                expected_type = field_schema.get('type')
                if expected_type:
                    type_map = {
                        'string': str,
                        'integer': int,
                        'number': (int, float),
                        'boolean': bool,
                        'array': list,
                        'object': dict,
                    }
                    expected = type_map.get(expected_type)
                    if expected and field_value is not None and not isinstance(field_value, expected):
                        errors.append(
                            f"$.{field_name}: Expected {expected_type}, "
                            f"got {type(field_value).__name__}"
                        )

        return errors


class SchemaValidator:
    """
    Validates events against their registered schemas.
    
    Features:
    - Event payload validation against JSON Schema
    - Backward compatibility checking between versions
    - Migration suggestions between versions
    
    Example:
        validator = SchemaValidator()
        
        # Validate an event
        is_valid, errors = validator.validate_event(event_envelope)
        
        # Check compatibility
        is_compatible, issues = validator.check_compatibility(
            'plm.part.created', '1.0.0', '2.0.0'
        )
    """

    @staticmethod
    def validate_event(
        event: EventEnvelope,
        schema_version: Optional[str] = None,
    ) -> Tuple[bool, List[str]]:
        """
        Validate an event against its registered schema.
        
        Args:
            event: EventEnvelope to validate
            schema_version: Optional specific version; defaults to event's schema_version
            
        Returns:
            Tuple of (is_valid, list_of_errors)
        """
        version = schema_version or event.schema_version
        errors = []

        # Basic envelope validation
        if not event.event_type:
            errors.append("event_type is required")
        if not event.event_id:
            errors.append("event_id is required")
        if not event.timestamp:
            errors.append("timestamp is required")

        if errors:
            return False, errors

        # Look up schema
        try:
            schema_entry = EventSchemaRegistry.objects.get(
                event_type=event.event_type,
                version=version,
            )
        except EventSchemaRegistry.DoesNotExist:
            # If no schema registered, assume valid (graceful degradation)
            logger.warning(
                f"No schema registered for {event.event_type} v{version}, "
                f"skipping payload validation"
            )
            return True, []

        # Warn if deprecated
        if schema_entry.is_deprecated:
            logger.warning(
                f"Event {event.event_type} v{version} is deprecated. "
                f"Consider migrating to a newer version."
            )

        # Validate payload against JSON Schema
        schema_errors = _validate_json_schema(event.payload, schema_entry.schema)
        if schema_errors:
            return False, schema_errors

        return True, []

    @staticmethod
    def validate_payload(
        event_type: str,
        version: str,
        payload: Dict[str, Any],
    ) -> Tuple[bool, List[str]]:
        """
        Validate a payload directly against a registered schema.
        
        Args:
            event_type: Event type
            version: Schema version
            payload: Payload data to validate
            
        Returns:
            Tuple of (is_valid, list_of_errors)
        """
        try:
            schema_entry = EventSchemaRegistry.objects.get(
                event_type=event_type,
                version=version,
            )
        except EventSchemaRegistry.DoesNotExist:
            return True, []

        errors = _validate_json_schema(payload, schema_entry.schema)
        return len(errors) == 0, errors

    @staticmethod
    def check_compatibility(
        event_type: str,
        old_version: str,
        new_version: str,
    ) -> Tuple[bool, List[str]]:
        """
        Check backward compatibility between two schema versions.
        
        A new version is backward compatible if:
        - No required fields are removed
        - No field types are changed incompatibly
        - New required fields have defaults or are optional
        
        Args:
            event_type: Event type
            old_version: Previous schema version
            new_version: New schema version
            
        Returns:
            Tuple of (is_compatible, list_of_issues)
        """
        try:
            old_schema_entry = EventSchemaRegistry.objects.get(
                event_type=event_type,
                version=old_version,
            )
            new_schema_entry = EventSchemaRegistry.objects.get(
                event_type=event_type,
                version=new_version,
            )
        except EventSchemaRegistry.DoesNotExist as e:
            return False, [f"Schema not found: {e}"]

        old_schema = old_schema_entry.schema
        new_schema = new_schema_entry.schema

        issues = []

        old_props = old_schema.get('properties', {})
        new_props = new_schema.get('properties', {})
        old_required = set(old_schema.get('required', []))
        new_required = set(new_schema.get('required', []))

        # Check for removed properties that were required
        for field_name in old_required:
            if field_name not in new_props:
                issues.append(
                    f"BREAKING: Required field '{field_name}' was removed"
                )

        # Check for type changes
        for field_name in old_props:
            if field_name in new_props:
                old_type = old_props[field_name].get('type')
                new_type = new_props[field_name].get('type')
                if old_type and new_type and old_type != new_type:
                    issues.append(
                        f"BREAKING: Field '{field_name}' type changed "
                        f"from '{old_type}' to '{new_type}'"
                    )

        # Check for new required fields (potentially breaking)
        new_required_fields = new_required - old_required
        for field_name in new_required_fields:
            if field_name not in old_props:
                # New field that is required → breaking for consumers
                new_field_schema = new_props.get(field_name, {})
                if 'default' not in new_field_schema:
                    issues.append(
                        f"WARNING: New required field '{field_name}' "
                        f"without default — may break existing consumers"
                    )

        is_compatible = not any(
            issue.startswith('BREAKING') for issue in issues
        )
        return is_compatible, issues

    @staticmethod
    def suggest_migration(
        event_type: str,
        from_version: str,
        to_version: str,
    ) -> Dict[str, Any]:
        """
        Suggest migration steps between two versions.
        
        Returns:
            Dictionary with migration information
        """
        try:
            from_entry = EventSchemaRegistry.objects.get(
                event_type=event_type,
                version=from_version,
            )
            to_entry = EventSchemaRegistry.objects.get(
                event_type=event_type,
                version=to_version,
            )
        except EventSchemaRegistry.DoesNotExist as e:
            return {'error': f"Schema not found: {e}"}

        from_props = from_entry.schema.get('properties', {})
        to_props = to_entry.schema.get('properties', {})
        from_required = set(from_entry.schema.get('required', []))
        to_required = set(to_entry.schema.get('required', []))

        added_fields = set(to_props.keys()) - set(from_props.keys())
        removed_fields = set(from_props.keys()) - set(to_props.keys())
        common_fields = set(from_props.keys()) & set(to_props.keys())

        type_changes = {}
        for field_name in common_fields:
            old_type = from_props[field_name].get('type')
            new_type = to_props[field_name].get('type')
            if old_type != new_type:
                type_changes[field_name] = {
                    'from': old_type,
                    'to': new_type,
                }

        return {
            'event_type': event_type,
            'from_version': from_version,
            'to_version': to_version,
            'added_fields': list(added_fields),
            'removed_fields': list(removed_fields),
            'type_changes': type_changes,
            'new_required_fields': list(to_required - from_required),
            'removed_required_fields': list(from_required - to_required),
            'migration_notes': to_entry.migration_notes or '',
            'migration_script': to_entry.migration_script or '',
            'is_breaking': bool(removed_fields or type_changes),
        }

    @staticmethod
    def migrate_payload(
        event_type: str,
        payload: Dict[str, Any],
        from_version: str,
        to_version: str,
    ) -> Tuple[Dict[str, Any], List[str]]:
        """
        Attempt to migrate a payload from one version to another.
        
        For minor changes (added optional fields), auto-migration is possible.
        For breaking changes, the migration_script from the schema is used.
        
        Args:
            event_type: Event type
            payload: Current payload data
            from_version: Current version
            to_version: Target version
            
        Returns:
            Tuple of (migrated_payload, warnings)
        """
        warnings = []
        migrated = dict(payload)

        try:
            to_entry = EventSchemaRegistry.objects.get(
                event_type=event_type,
                version=to_version,
            )
        except EventSchemaRegistry.DoesNotExist:
            return migrated, [f"Target schema {event_type} v{to_version} not found"]

        to_props = to_entry.schema.get('properties', {})
        to_required = set(to_entry.schema.get('required', []))

        # Try migration script first
        if to_entry.migration_script:
            try:
                local_vars = {'payload': dict(migrated)}
                exec(to_entry.migration_script, {}, local_vars)  # noqa: S102
                if 'result' in local_vars:
                    return local_vars['result'], warnings
            except Exception as e:
                warnings.append(f"Migration script failed: {e}")

        # Auto-migration: add missing fields with defaults
        for field_name in to_required:
            if field_name not in migrated:
                field_schema = to_props.get(field_name, {})
                if 'default' in field_schema:
                    migrated[field_name] = field_schema['default']
                    warnings.append(
                        f"Added field '{field_name}' with default value"
                    )
                else:
                    warnings.append(
                        f"Required field '{field_name}' missing — "
                        f"manual migration needed"
                    )

        return migrated, warnings
