import uuid
from datetime import datetime
from mongoengine import DynamicDocument, DateTimeField, BooleanField, ListField, StringField, DynamicField

class Base(DynamicDocument):
    """
    Base class for MongoDB models with common fields and methods.
    
    This base class provides common fields and methods for MongoDB models, including fields for tags, soft deletion, and timestamps. It also supports additional custom fields through the use of a DynamicField.

    Attributes:
        public_id (str): A unique identifier for the document. It's automatically generated using UUID4.
        tags (list of str): A list of tags associated with the document.
        deleted (bool): A flag indicating whether the document has been 'soft deleted.'
        created_at (datetime): Date and time of creation.
        updated_at (datetime): Date and time of the last update.
        deleted_at (datetime): Date and time of deletion, if applicable. 

    Indexes:
        - public_id: A unique index on the public_id field, facilitating fast lookups by the unique identifier.
        - created_at: An index for sorting documents by creation date.

    Methods:
        - save(): Custom save method to update 'updated_at' field before saving.
        - archive(): Soft delete the document by setting the 'deleted' flag and timestamp.
        - restore(): Restore a soft-deleted document.
        - add_tag(tag): Add a tag to the document if it's not already present.
        - remove_tag(tag): Remove a tag from the document if it exists.
        - to_dict(): Convert the document to a dictionary with formatted datetime fields.
        - _format_datetime(dt): Helper method to format a datetime object to ISO 8601 string.
        - get_paginated(page, per_page): Get a paginated list of documents.

    Example:
        To create a new document with tags and save it:
        >>> new_doc = Base(tags=['tag1', 'tag2'])
        >>> new_doc.save()

    Example:
        To retrieve a paginated list of documents:
        >>> page = 1
        >>> per_page = 10
        >>> result = Base.get_paginated(page, per_page)
        >>> total = result['total']
        >>> items = result['items']

    Example:
        To add a custom field to a document:
        >>> document.extra_fields['custom_data'] = 'Some additional data'
        >>> document.save()
    """
     
    public_id  = StringField(default=str(uuid.uuid4()), unique=True, help_text="A unique identifier for the document.")
    tags       = ListField(StringField(max_length=150), description='Tags', help_text="List of tags associated with the document.")
    deleted    = BooleanField(default=False, help_text="A flag indicating whether the document has been 'soft deleted.'")

    created_at = DateTimeField(default=datetime.utcnow, readonly=True, help_text='Date and time of creation')
    updated_at = DateTimeField(default=None, readonly=True, help_text='Date and time of the last update')
    deleted_at = DateTimeField(default=None, help_text='Date and time of deletion, if applicable')
 
    meta = {
        'allow_inheritance': True, 
        'abstract': True,
        'indexes': ['public_id'],
        'ordering': ['-created_at'],
    }

    def save(self, *args, **kwargs):
        """
        Custom save method to update 'updated_at' field before saving.
        """
        self.updated_at = datetime.utcnow()
        super(Base, self).save(*args, **kwargs)

    def archive(self):
        """
        Soft delete the document by setting the 'deleted' flag and timestamp.
        """
        self.deleted = True
        self.deleted_at = datetime.utcnow()
        self.save()

    def restore(self):
        """
        Restore a soft-deleted document.
        """
        self.deleted = False
        self.deleted_at = None
        self.save()

    def add_tag(self, tag):
        """
        Add a tag to the document if it's not already present.
        """
        if tag not in self.tags:
            self.tags.append(tag)
            self.save()

    def remove_tag(self, tag):
        """
        Remove a tag from the document if it exists.
        """
        if tag in self.tags:
            self.tags.remove(tag)
            self.save()

    def to_dict(self):
        """
        Convert the document to a dictionary.
        """
        document_dict = self.to_mongo().to_dict()
        document_dict['id'] = str(document_dict.pop('_id'))
        # Convert datetime objects to ISO 8601 strings
        document_dict['created_at'] = self._format_datetime(document_dict.get('created_at'))
        document_dict['updated_at'] = self._format_datetime(document_dict.get('updated_at'))
        document_dict['deleted_at'] = self._format_datetime(document_dict.get('deleted_at'))
        # Remove any '_cls' field
        document_dict.pop('_cls', None)
        return document_dict
    
    def _format_datetime(self, dt):
        """
        Helper method to format a datetime object to ISO 8601 string.
        """
        if dt and isinstance(dt, datetime):
            return dt.isoformat()
        return None
    
    @classmethod
    def get_paginated(cls, page, per_page):
        """
        Get a paginated list of documents.
        """
        page = max(1, page)
        per_page = min(max(1, per_page), 100)  # Limit per_page to a reasonable value
        total = cls.objects(deleted=False).count()
        documents = cls.objects(deleted=False).skip((page - 1) * per_page).limit(per_page)
        return {
            'total': total,
            'page': page,
            'per_page': per_page,
            'items': [doc.to_dict() for doc in documents]
        }
