"""
Schema Manager Service

This module provides manual schema evolution capabilities for MongoDB collections.
Users can add new fields to collections through the UI with explicit confirmation.

Key principles:
- Field addition is MANUAL ONLY (user-initiated)
- No automatic schema changes
- No field deletion or renaming
- Operations are idempotent (safe to re-run)
- Existing data is NEVER modified
- New fields default to null

Safety features:
- Check if field exists before adding
- Preview before applying
- Explicit user confirmation required
- updateMany with $exists check
"""

from typing import List, Dict, Any, Optional
from pymongo.collection import Collection


class SchemaManager:
    """
    Service for managing MongoDB collection schemas.
    
    Provides safe, manual schema evolution capabilities including
    adding new fields to all documents in a collection.
    """
    
    # Supported field types and their null values
    FIELD_TYPES = {
        'string': None,
        'number': None,
        'boolean': None,
        'date': None
    }
    
    def __init__(self, mongo_service):
        """
        Initialize schema manager with MongoDB connection.
        
        Args:
            mongo_service: MongoService instance
        """
        self.mongo = mongo_service
    
    def get_existing_fields(self, collection_name: str) -> List[str]:
        """
        Get list of all fields that exist in a collection.
        
        This scans all documents and returns the union of all field names.
        
        Args:
            collection_name: Name of the collection
            
        Returns:
            Sorted list of unique field names
        """
        collection = self.mongo.get_collection(collection_name)
        
        if collection is None:
            return []
        
        try:
            # Use aggregation to get all unique field names
            # This is more efficient than loading all documents
            pipeline = [
                {
                    '$project': {
                        'fields': {'$objectToArray': '$$ROOT'}
                    }
                },
                {
                    '$unwind': '$fields'
                },
                {
                    '$group': {
                        '_id': None,
                        'fieldNames': {'$addToSet': '$fields.k'}
                    }
                }
            ]
            
            result = list(collection.aggregate(pipeline))
            
            if result and 'fieldNames' in result[0]:
                fields = result[0]['fieldNames']
                # Sort with _id first
                fields = sorted(fields)
                if '_id' in fields:
                    fields.remove('_id')
                    fields.insert(0, '_id')
                return fields
            
            return []
        
        except Exception as e:
            print(f"Error getting fields for {collection_name}: {e}")
            return []
    
    def field_exists(self, collection_name: str, field_name: str) -> bool:
        """
        Check if a field exists in any document in the collection.
        
        Args:
            collection_name: Name of the collection
            field_name: Name of the field to check
            
        Returns:
            True if field exists in at least one document
        """
        collection = self.mongo.get_collection(collection_name)
        
        if collection is None:
            return False
        
        try:
            # Check if any document has this field
            count = collection.count_documents({field_name: {'$exists': True}})
            return count > 0
        
        except Exception:
            return False
    
    def preview_field_addition(
        self,
        collection_name: str,
        field_name: str,
        field_type: str
    ) -> Dict[str, Any]:
        """
        Preview what will happen when adding a field.
        
        Shows how many documents will be affected and validates the operation.
        
        Args:
            collection_name: Name of the collection
            field_name: Name of the field to add
            field_type: Type of the field (string, number, boolean, date)
            
        Returns:
            Dictionary with preview information
        """
        collection = self.mongo.get_collection(collection_name)
        
        if collection is None:
            return {
                'valid': False,
                'error': 'Collection not found or not connected to MongoDB'
            }
        
        # Validate field type
        if field_type not in self.FIELD_TYPES:
            return {
                'valid': False,
                'error': f'Invalid field type. Must be one of: {", ".join(self.FIELD_TYPES.keys())}'
            }
        
        # Validate field name
        if not field_name or not field_name.strip():
            return {
                'valid': False,
                'error': 'Field name cannot be empty'
            }
        
        # Check for invalid characters
        if '.' in field_name or '$' in field_name:
            return {
                'valid': False,
                'error': 'Field name cannot contain "." or "$" characters'
            }
        
        # Check if field already exists
        if self.field_exists(collection_name, field_name):
            return {
                'valid': False,
                'error': f'Field "{field_name}" already exists in this collection',
                'warning': True
            }
        
        # Count total documents
        total_docs = self.mongo.count_documents(collection_name)
        
        # Count documents that will be affected (don't have this field)
        docs_to_update = collection.count_documents({field_name: {'$exists': False}})
        
        return {
            'valid': True,
            'collection_name': collection_name,
            'field_name': field_name,
            'field_type': field_type,
            'default_value': self.FIELD_TYPES[field_type],
            'total_documents': total_docs,
            'documents_to_update': docs_to_update,
            'documents_already_have_field': total_docs - docs_to_update
        }
    
    def add_field_to_collection(
        self,
        collection_name: str,
        field_name: str,
        field_type: str
    ) -> Dict[str, Any]:
        """
        Add a new field to all documents in a collection.
        
        This operation:
        1. Checks if field already exists
        2. Adds field with null value to documents that don't have it
        3. Does NOT modify documents that already have the field
        4. Is idempotent (safe to run multiple times)
        
        SAFETY GUARANTEES:
        - Only adds field where it doesn't exist ($exists: false)
        - Never overwrites existing values
        - Uses $set with null value
        - Atomic operation per document
        
        Args:
            collection_name: Name of the collection
            field_name: Name of the field to add
            field_type: Type of the field (string, number, boolean, date)
            
        Returns:
            Dictionary with operation results
        """
        collection = self.mongo.get_collection(collection_name)
        
        if collection is None:
            return {
                'success': False,
                'error': 'Collection not found or not connected to MongoDB'
            }
        
        # Validate inputs using preview
        preview = self.preview_field_addition(collection_name, field_name, field_type)
        
        if not preview.get('valid'):
            return {
                'success': False,
                'error': preview.get('error', 'Invalid field addition')
            }
        
        try:
            # Get the default value for this field type
            default_value = self.FIELD_TYPES[field_type]
            
            # Add field to all documents that don't have it
            # SAFETY: Only updates documents where field doesn't exist
            # SAFETY: Uses $set to add field without modifying other fields
            result = collection.update_many(
                {field_name: {'$exists': False}},  # Only documents without this field
                {'$set': {field_name: default_value}}  # Set to null
            )
            
            return {
                'success': True,
                'collection_name': collection_name,
                'field_name': field_name,
                'field_type': field_type,
                'documents_matched': result.matched_count,
                'documents_modified': result.modified_count,
                'message': f'Successfully added field "{field_name}" to {result.modified_count} documents'
            }
        
        except Exception as e:
            return {
                'success': False,
                'error': f'Error adding field: {str(e)}'
            }
    
    def get_field_statistics(self, collection_name: str, field_name: str) -> Dict[str, Any]:
        """
        Get statistics about a field in a collection.
        
        Args:
            collection_name: Name of the collection
            field_name: Name of the field
            
        Returns:
            Dictionary with field statistics
        """
        collection = self.mongo.get_collection(collection_name)
        
        if collection is None:
            return {}
        
        try:
            total_docs = self.mongo.count_documents(collection_name)
            docs_with_field = collection.count_documents({field_name: {'$exists': True}})
            docs_with_null = collection.count_documents({field_name: None})
            docs_with_value = collection.count_documents({
                field_name: {'$exists': True, '$ne': None}
            })
            
            return {
                'total_documents': total_docs,
                'documents_with_field': docs_with_field,
                'documents_with_null': docs_with_null,
                'documents_with_value': docs_with_value,
                'coverage_percentage': round((docs_with_field / total_docs * 100), 2) if total_docs > 0 else 0
            }
        
        except Exception:
            return {}
