"""
Document Control Service — Business Logic Services.

This module contains the core business logic for:
- Document management (CRUD + checkout/checkin + publish + archive)
- Version management (create, revert, compare)
- Approval workflow (submit, approve, reject)
- Access control (grant, revoke, check)
- Audit logging
"""
import logging
from typing import Any, Optional
from uuid import UUID

from django.db import transaction
from django.db.models import Q
from django.utils import timezone

from .models import (
    Document,
    DocumentAccess,
    DocumentAccessType,
    DocumentApproval,
    DocumentAuditLog,
    DocumentCategory,
    DocumentVersion,
    AccessLevel,
    ApprovalStatus,
    AuditAction,
    DocumentStatus,
)

logger = logging.getLogger('apps')


# ──────────────────────────────────────────────────────────────
# Audit Helper
# ──────────────────────────────────────────────────────────────

def _log_audit(
    document: Document,
    action: str,
    user=None,
    details: dict | None = None,
    ip_address: str | None = None,
    user_agent: str = '',
):
    """Create an audit log entry for a document action."""
    try:
        DocumentAuditLog.objects.create(
            tenant=document.tenant,
            document=document,
            action=action,
            user=user,
            details=details or {},
            ip_address=ip_address,
            user_agent=user_agent,
        )
    except Exception as e:
        logger.error(f'Failed to create audit log: {e}')


# ──────────────────────────────────────────────────────────────
# Category Service
# ──────────────────────────────────────────────────────────────

class CategoryService:
    """Service for managing document categories."""

    @staticmethod
    def list_categories(tenant, parent_id: UUID | None = None, active_only: bool = True):
        """List categories, optionally filtered by parent."""
        qs = DocumentCategory.objects.filter(tenant=tenant)
        if active_only:
            qs = qs.filter(is_active=True)
        if parent_id is not None:
            qs = qs.filter(parent_id=parent_id)
        else:
            qs = qs.filter(parent__isnull=True)
        return qs.order_by('sort_order', 'name')

    @staticmethod
    def get_category(tenant, category_id: UUID) -> DocumentCategory:
        return DocumentCategory.objects.get(tenant=tenant, id=category_id)

    @staticmethod
    @transaction.atomic
    def create_category(
        tenant,
        name: str,
        code: str,
        parent_id: UUID | None = None,
        description: str = '',
        sort_order: int = 0,
    ) -> DocumentCategory:
        return DocumentCategory.objects.create(
            tenant=tenant,
            name=name,
            code=code,
            parent_id=parent_id,
            description=description,
            sort_order=sort_order,
        )

    @staticmethod
    @transaction.atomic
    def update_category(
        tenant,
        category_id: UUID,
        **kwargs,
    ) -> DocumentCategory:
        category = DocumentCategory.objects.get(tenant=tenant, id=category_id)
        for field, value in kwargs.items():
            if hasattr(category, field):
                setattr(category, field, value)
        category.save()
        return category

    @staticmethod
    @transaction.atomic
    def delete_category(tenant, category_id: UUID):
        """Soft-deactivate a category."""
        category = DocumentCategory.objects.get(tenant=tenant, id=category_id)
        category.is_active = False
        category.save(update_fields=['is_active'])
        return category

    @staticmethod
    def get_tree(tenant):
        """Return full category tree for the tenant."""
        return DocumentCategory.objects.filter(
            tenant=tenant, is_active=True,
        ).select_related('parent').order_by('sort_order', 'name')


# ──────────────────────────────────────────────────────────────
# Document Service
# ──────────────────────────────────────────────────────────────

class DocumentService:
    """
    Service for document CRUD + checkout/checkin + publish + archive.
    """

    @staticmethod
    def list_documents(
        tenant,
        search: str | None = None,
        status: str | None = None,
        category_id: UUID | None = None,
        owner_id: UUID | None = None,
        include_deleted: bool = False,
    ):
        """List documents with optional filters."""
        qs = Document.objects.filter(tenant=tenant)
        if not include_deleted:
            qs = qs.filter(is_deleted=False)
        if status:
            qs = qs.filter(status=status)
        if category_id:
            qs = qs.filter(category_id=category_id)
        if owner_id:
            qs = qs.filter(owner_id=owner_id)
        if search:
            qs = qs.filter(Q(title__icontains=search) | Q(code__icontains=search))
        return qs.select_related('category', 'owner').order_by('-created_at')

    @staticmethod
    def get_document(tenant, document_id: UUID) -> Document:
        return Document.objects.select_related(
            'category', 'owner', 'checked_out_by',
        ).get(tenant=tenant, id=document_id, is_deleted=False)

    @staticmethod
    @transaction.atomic
    def create_document(
        tenant,
        title: str,
        code: str,
        category_id: UUID | None = None,
        description: str = '',
        owner=None,
        metadata: dict | None = None,
        access_level: str = AccessLevel.VIEW,
        user=None,
        ip_address: str | None = None,
    ) -> Document:
        """Create a new document."""
        doc = Document.objects.create(
            tenant=tenant,
            title=title,
            code=code,
            category_id=category_id,
            description=description,
            owner=owner or user,
            status=DocumentStatus.DRAFT,
            metadata=metadata or {},
            access_level=access_level,
        )
        _log_audit(doc, AuditAction.CREATE, user=user, ip_address=ip_address)
        return doc

    @staticmethod
    @transaction.atomic
    def update_document(
        tenant,
        document_id: UUID,
        user=None,
        ip_address: str | None = None,
        **kwargs,
    ) -> Document:
        """Update document metadata (not file content — that's versioning)."""
        doc = Document.objects.get(tenant=tenant, id=document_id, is_deleted=False)
        changes = {}
        for field, value in kwargs.items():
            if hasattr(doc, field):
                old = getattr(doc, field)
                if old != value:
                    changes[field] = {'old': str(old), 'new': str(value)}
                    setattr(doc, field, value)
        doc.save()
        if changes:
            _log_audit(doc, AuditAction.UPDATE, user=user, details={'changes': changes}, ip_address=ip_address)
        return doc

    @staticmethod
    @transaction.atomic
    def delete_document(
        tenant,
        document_id: UUID,
        user=None,
        ip_address: str | None = None,
    ):
        """Soft-delete a document."""
        doc = Document.objects.get(tenant=tenant, id=document_id, is_deleted=False)
        doc.is_deleted = True
        doc.deleted_at = timezone.now()
        doc.save(update_fields=['is_deleted', 'deleted_at'])
        _log_audit(doc, AuditAction.DELETE, user=user, ip_address=ip_address)
        return doc

    @staticmethod
    @transaction.atomic
    def checkout(
        tenant,
        document_id: UUID,
        user=None,
        ip_address: str | None = None,
    ) -> Document:
        """Check out a document for exclusive editing."""
        doc = Document.objects.select_for_update().get(
            tenant=tenant, id=document_id, is_deleted=False,
        )
        if doc.is_checked_out:
            raise ValueError(f'سند قبلاً توسط {doc.checked_out_by} تحویل گرفته شده است.')
        doc.is_checked_out = True
        doc.checked_out_by = user
        doc.checked_out_at = timezone.now()
        doc.save(update_fields=['is_checked_out', 'checked_out_by', 'checked_out_at'])
        _log_audit(doc, AuditAction.CHECKOUT, user=user, ip_address=ip_address)
        return doc

    @staticmethod
    @transaction.atomic
    def checkin(
        tenant,
        document_id: UUID,
        user=None,
        ip_address: str | None = None,
    ) -> Document:
        """Check in a previously checked-out document."""
        doc = Document.objects.select_for_update().get(
            tenant=tenant, id=document_id, is_deleted=False,
        )
        if not doc.is_checked_out:
            raise ValueError('سند تحویل گرفته نشده است.')
        if doc.checked_out_by != user and user is not None:
            raise ValueError('فقط کاربری که سند را تحویل گرفته می‌تواند آن را برگرداند.')
        doc.is_checked_out = False
        doc.checked_out_by = None
        doc.checked_out_at = None
        doc.save(update_fields=['is_checked_out', 'checked_out_by', 'checked_out_at'])
        _log_audit(doc, AuditAction.CHECKIN, user=user, ip_address=ip_address)
        return doc

    @staticmethod
    @transaction.atomic
    def publish(
        tenant,
        document_id: UUID,
        user=None,
        ip_address: str | None = None,
    ) -> Document:
        """Publish (approve) a document — changes status to APPROVED."""
        doc = Document.objects.get(tenant=tenant, id=document_id, is_deleted=False)
        if doc.status not in (DocumentStatus.DRAFT, DocumentStatus.PENDING_REVIEW):
            raise ValueError(f'سند در وضعیت {doc.get_status_display()} قابل انتشار نیست.')
        doc.status = DocumentStatus.APPROVED
        doc.save(update_fields=['status'])
        _log_audit(doc, AuditAction.PUBLISH, user=user, ip_address=ip_address)
        return doc

    @staticmethod
    @transaction.atomic
    def archive(
        tenant,
        document_id: UUID,
        user=None,
        ip_address: str | None = None,
    ) -> Document:
        """Archive a document."""
        doc = Document.objects.get(tenant=tenant, id=document_id, is_deleted=False)
        doc.status = DocumentStatus.ARCHIVED
        doc.save(update_fields=['status'])
        _log_audit(doc, AuditAction.ARCHIVE, user=user, ip_address=ip_address)
        return doc


# ──────────────────────────────────────────────────────────────
# Version Service
# ──────────────────────────────────────────────────────────────

class VersionService:
    """
    Service for document version management.
    """

    @staticmethod
    def list_versions(tenant, document_id: UUID):
        """List all versions of a document."""
        return DocumentVersion.objects.filter(
            tenant=tenant, document_id=document_id,
        ).select_related('created_by').order_by('-version_number')

    @staticmethod
    def get_version(tenant, version_id: UUID) -> DocumentVersion:
        return DocumentVersion.objects.select_related(
            'document', 'created_by',
        ).get(tenant=tenant, id=version_id)

    @staticmethod
    @transaction.atomic
    def create_version(
        tenant,
        document_id: UUID,
        file_url: str,
        file_name: str,
        file_size: int = 0,
        mime_type: str = '',
        change_description: str = '',
        created_by=None,
        ip_address: str | None = None,
    ) -> DocumentVersion:
        """Create a new version for a document."""
        doc = Document.objects.select_for_update().get(
            tenant=tenant, id=document_id, is_deleted=False,
        )
        # Unmark previous current version
        DocumentVersion.objects.filter(
            document=doc, is_current=True,
        ).update(is_current=False)

        new_version_number = doc.current_version + 1
        version = DocumentVersion.objects.create(
            tenant=tenant,
            document=doc,
            version_number=new_version_number,
            file_url=file_url,
            file_name=file_name,
            file_size=file_size,
            mime_type=mime_type,
            change_description=change_description,
            created_by=created_by,
            is_current=True,
        )
        doc.current_version = new_version_number
        doc.save(update_fields=['current_version'])

        _log_audit(
            doc, AuditAction.VERSION_CREATE,
            user=created_by,
            details={'version_number': new_version_number, 'file_name': file_name},
            ip_address=ip_address,
        )
        return version

    @staticmethod
    @transaction.atomic
    def revert_version(
        tenant,
        document_id: UUID,
        target_version_number: int,
        user=None,
        ip_address: str | None = None,
    ) -> DocumentVersion:
        """Revert a document to a previous version by creating a new version from it."""
        doc = Document.objects.select_for_update().get(
            tenant=tenant, id=document_id, is_deleted=False,
        )
        target = DocumentVersion.objects.get(
            document=doc, version_number=target_version_number,
        )

        # Create a new version that copies the target
        DocumentVersion.objects.filter(
            document=doc, is_current=True,
        ).update(is_current=False)

        new_version_number = doc.current_version + 1
        version = DocumentVersion.objects.create(
            tenant=tenant,
            document=doc,
            version_number=new_version_number,
            file_url=target.file_url,
            file_name=target.file_name,
            file_size=target.file_size,
            mime_type=target.mime_type,
            change_description=f'بازگشت به نسخه {target_version_number}',
            created_by=user,
            is_current=True,
        )
        doc.current_version = new_version_number
        doc.save(update_fields=['current_version'])

        _log_audit(
            doc, AuditAction.VERSION_CREATE,
            user=user,
            details={
                'reverted_from': target_version_number,
                'new_version': new_version_number,
            },
            ip_address=ip_address,
        )
        return version

    @staticmethod
    def compare_versions(
        tenant,
        document_id: UUID,
        version_a: int,
        version_b: int,
    ) -> dict:
        """Compare two versions of a document (metadata comparison)."""
        va = DocumentVersion.objects.get(
            tenant=tenant,
            document_id=document_id,
            version_number=version_a,
        )
        vb = DocumentVersion.objects.get(
            tenant=tenant,
            document_id=document_id,
            version_number=version_b,
        )
        return {
            'version_a': {
                'version_number': va.version_number,
                'file_name': va.file_name,
                'file_size': va.file_size,
                'mime_type': va.mime_type,
                'change_description': va.change_description,
                'created_at': va.created_at.isoformat() if va.created_at else None,
            },
            'version_b': {
                'version_number': vb.version_number,
                'file_name': vb.file_name,
                'file_size': vb.file_size,
                'mime_type': vb.mime_type,
                'change_description': vb.change_description,
                'created_at': vb.created_at.isoformat() if vb.created_at else None,
            },
        }


# ──────────────────────────────────────────────────────────────
# Approval Service
# ──────────────────────────────────────────────────────────────

class ApprovalService:
    """
    Service for document approval workflow.
    """

    @staticmethod
    def list_approvals(tenant, document_id: UUID | None = None, status: str | None = None):
        """List approval records with optional filters."""
        qs = DocumentApproval.objects.filter(tenant=tenant)
        if document_id:
            qs = qs.filter(document_id=document_id)
        if status:
            qs = qs.filter(status=status)
        return qs.select_related('document', 'version', 'approver').order_by('-created_at')

    @staticmethod
    def get_pending_approvals(tenant, approver=None):
        """Get pending approval requests, optionally for a specific approver."""
        qs = DocumentApproval.objects.filter(
            tenant=tenant, status=ApprovalStatus.PENDING,
        )
        if approver:
            qs = qs.filter(approver=approver)
        return qs.select_related('document', 'version').order_by('-created_at')

    @staticmethod
    @transaction.atomic
    def submit_for_approval(
        tenant,
        document_id: UUID,
        approver_id: UUID,
        version_id: UUID | None = None,
        user=None,
        ip_address: str | None = None,
    ) -> DocumentApproval:
        """Submit a document (specific version) for approval."""
        doc = Document.objects.get(tenant=tenant, id=document_id, is_deleted=False)

        # Use current version if not specified
        if version_id:
            version = DocumentVersion.objects.get(tenant=tenant, id=version_id, document=doc)
        else:
            version = DocumentVersion.objects.get(
                document=doc, is_current=True,
            )

        doc.status = DocumentStatus.PENDING_REVIEW
        doc.save(update_fields=['status'])

        approval = DocumentApproval.objects.create(
            tenant=tenant,
            document=doc,
            version=version,
            approver_id=approver_id,
            status=ApprovalStatus.PENDING,
        )
        _log_audit(
            doc, AuditAction.UPDATE,
            user=user,
            details={'action': 'submit_for_approval', 'approver_id': str(approver_id)},
            ip_address=ip_address,
        )
        return approval

    @staticmethod
    @transaction.atomic
    def approve(
        tenant,
        approval_id: UUID,
        comments: str = '',
        user=None,
        ip_address: str | None = None,
    ) -> DocumentApproval:
        """Approve a document."""
        approval = DocumentApproval.objects.select_related('document').get(
            tenant=tenant, id=approval_id,
        )
        if approval.status != ApprovalStatus.PENDING:
            raise ValueError('این درخواست تأیید قبلاً پردازش شده است.')
        approval.status = ApprovalStatus.APPROVED
        approval.comments = comments
        approval.approved_at = timezone.now()
        approval.save(update_fields=['status', 'comments', 'approved_at'])

        # Update document status
        doc = approval.document
        doc.status = DocumentStatus.APPROVED
        doc.save(update_fields=['status'])

        _log_audit(doc, AuditAction.APPROVE, user=user, details={'approval_id': str(approval_id)}, ip_address=ip_address)
        return approval

    @staticmethod
    @transaction.atomic
    def reject(
        tenant,
        approval_id: UUID,
        comments: str = '',
        user=None,
        ip_address: str | None = None,
    ) -> DocumentApproval:
        """Reject a document."""
        approval = DocumentApproval.objects.select_related('document').get(
            tenant=tenant, id=approval_id,
        )
        if approval.status != ApprovalStatus.PENDING:
            raise ValueError('این درخواست تأیید قبلاً پردازش شده است.')
        approval.status = ApprovalStatus.REJECTED
        approval.comments = comments
        approval.approved_at = timezone.now()
        approval.save(update_fields=['status', 'comments', 'approved_at'])

        # Update document status
        doc = approval.document
        doc.status = DocumentStatus.REJECTED
        doc.save(update_fields=['status'])

        _log_audit(doc, AuditAction.REJECT, user=user, details={'approval_id': str(approval_id)}, ip_address=ip_address)
        return approval


# ──────────────────────────────────────────────────────────────
# Access Service
# ──────────────────────────────────────────────────────────────

class AccessService:
    """
    Service for document access control.
    """

    @staticmethod
    def list_access(tenant, document_id: UUID):
        """List all access rules for a document."""
        return DocumentAccess.objects.filter(
            tenant=tenant, document_id=document_id,
        ).select_related('user', 'granted_by').order_by('-created_at')

    @staticmethod
    @transaction.atomic
    def grant_access(
        tenant,
        document_id: UUID,
        permission_level: str = AccessLevel.VIEW,
        user_id: UUID | None = None,
        role_id: UUID | None = None,
        granted_by=None,
        ip_address: str | None = None,
    ) -> DocumentAccess:
        """Grant access to a document for a user or role."""
        doc = Document.objects.get(tenant=tenant, id=document_id, is_deleted=False)

        if user_id:
            access_type = DocumentAccessType.USER
        elif role_id:
            access_type = DocumentAccessType.ROLE
        else:
            raise ValueError('باید حداقل یک کاربر یا نقش مشخص شود.')

        access = DocumentAccess.objects.create(
            tenant=tenant,
            document=doc,
            access_type=access_type,
            user_id=user_id,
            role_id=role_id,
            permission_level=permission_level,
            granted_by=granted_by,
        )
        _log_audit(
            doc, AuditAction.ACCESS_GRANT,
            user=granted_by,
            details={
                'access_type': access_type,
                'user_id': str(user_id) if user_id else None,
                'role_id': str(role_id) if role_id else None,
                'level': permission_level,
            },
            ip_address=ip_address,
        )
        return access

    @staticmethod
    @transaction.atomic
    def revoke_access(
        tenant,
        access_id: UUID,
        user=None,
        ip_address: str | None = None,
    ):
        """Revoke a specific access rule."""
        access = DocumentAccess.objects.select_related('document').get(
            tenant=tenant, id=access_id,
        )
        doc = access.document
        _log_audit(
            doc, AuditAction.ACCESS_REVOKE,
            user=user,
            details={
                'access_type': access.access_type,
                'user_id': str(access.user_id) if access.user_id else None,
                'role_id': str(access.role_id) if access.role_id else None,
                'level': access.permission_level,
            },
            ip_address=ip_address,
        )
        access.delete()

    @staticmethod
    def check_access(
        tenant,
        document_id: UUID,
        user=None,
        required_level: str = AccessLevel.VIEW,
    ) -> bool:
        """Check if a user has the required access level on a document."""
        doc = Document.objects.get(tenant=tenant, id=document_id, is_deleted=False)

        # Owner always has full access
        if doc.owner == user:
            return True

        # Check explicit user access
        level_hierarchy = [AccessLevel.VIEW, AccessLevel.EDIT, AccessLevel.APPROVE, AccessLevel.ADMIN]
        required_idx = level_hierarchy.index(required_level)

        user_access = DocumentAccess.objects.filter(
            document=doc,
            access_type=DocumentAccessType.USER,
            user=user,
        ).first()

        if user_access:
            user_idx = level_hierarchy.index(user_access.permission_level)
            if user_idx >= required_idx:
                return True

        return False
