"""Phase 6.3 collaboration application services."""
from __future__ import annotations

from datetime import datetime, timezone
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import InMemoryEventPublisher, get_event_publisher
from app.events.types import CrmEventType
from app.models.collaboration import (
    ActivityParticipant,
    ActivityReminder,
    Bookmark,
    CallLog,
    Comment,
    CommentVersion,
    CrmNotificationPreference,
    EmailMessage,
    EmailThread,
    Favorite,
    Meeting,
    MeetingAttendee,
    Mention,
    SalesCollaborationAudit,
    SalesTask,
    SalesTeamMember,
    SalesTimelineEvent,
    TaskChecklistItem,
    TeamAssignment,
    TeamRole,
    TimelineEventLink,
)
from app.models.foundation import SalesActivity
from app.models.types import (
    ActivityStatus,
    CollaborationEntityType,
    EmailDeliveryStatus,
    TimelineEventType,
)
from app.repositories.collaboration import (
    ActivityParticipantRepository,
    ActivityReminderRepository,
    BookmarkRepository,
    CallLogRepository,
    CommentRepository,
    CommentVersionRepository,
    CrmNotificationPreferenceRepository,
    EmailMessageRepository,
    EmailThreadRepository,
    FavoriteRepository,
    MeetingAttendeeRepository,
    MeetingRepository,
    MentionRepository,
    SalesCollaborationAuditRepository,
    SalesTaskRepository,
    SalesTeamMemberRepository,
    SalesTimelineRepository,
    TaskChecklistRepository,
    TeamAssignmentRepository,
    TeamRoleRepository,
    TimelineEventLinkRepository,
)
from app.repositories.foundation import SalesActivityRepository
from app.schemas.collaboration import (
    BookmarkCreate,
    CallLogCreate,
    ChecklistItemCreate,
    CommentCreate,
    CommentUpdate,
    EmailMessageCreate,
    EmailThreadCreate,
    FavoriteCreate,
    MeetingAttendeeCreate,
    MeetingCreate,
    MeetingFinishRequest,
    NotificationPreferenceUpsert,
    ParticipantCreate,
    ReminderCreate,
    SalesActivityCreate,
    SalesActivityUpdate,
    TaskCreate,
    TeamAssignmentCreate,
    TeamMemberCreate,
    TeamRoleCreate,
)
from app.validators import ensure_activity_completable, validate_non_empty
from app.validators.collaboration import (
    ensure_call_has_opportunity_link,
    ensure_meeting_window,
    ensure_timeline_immutable,
    validate_bookmark_entity,
    validate_comment_body,
    validate_duration_seconds,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


def _apply_update(entity, data: dict) -> None:
    for key, value in data.items():
        setattr(entity, key, value)


class CollaborationAuditService:
    def __init__(self, session: AsyncSession) -> None:
        self.repo = SalesCollaborationAuditRepository(session)

    async def record(
        self,
        *,
        tenant_id: UUID,
        entity_type: str,
        entity_id: UUID,
        action: str,
        actor_user_id: str | None = None,
        changes: dict | None = None,
        message: str | None = None,
    ) -> SalesCollaborationAudit:
        entry = SalesCollaborationAudit(
            tenant_id=tenant_id,
            entity_type=entity_type,
            entity_id=entity_id,
            action=action,
            actor_user_id=actor_user_id,
            changes=changes,
            message=message,
        )
        await self.repo.add(entry)
        return entry


class TimelineService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = SalesTimelineRepository(session)
        self.links = TimelineEventLinkRepository(session)
        self.publisher = publisher or get_event_publisher()

    async def append(
        self,
        *,
        tenant_id: UUID,
        event_type: TimelineEventType,
        entity_type: CollaborationEntityType,
        entity_id: UUID,
        title: str,
        actor_user_id: str | None = None,
        related_opportunity_id: UUID | None = None,
        related_lead_id: UUID | None = None,
        related_contact_id: UUID | None = None,
        related_organization_id: UUID | None = None,
        payload: dict | None = None,
        commit: bool = False,
    ) -> SalesTimelineEvent:
        now = datetime.now(timezone.utc)
        event = SalesTimelineEvent(
            tenant_id=tenant_id,
            event_type=event_type,
            entity_type=entity_type,
            entity_id=entity_id,
            related_opportunity_id=related_opportunity_id,
            related_lead_id=related_lead_id,
            related_contact_id=related_contact_id,
            related_organization_id=related_organization_id,
            actor_user_id=actor_user_id,
            title=title,
            payload=payload,
            occurred_at=now,
            is_immutable=True,
        )
        await self.repo.add(event)
        await self.links.add(
            TimelineEventLink(
                tenant_id=tenant_id,
                timeline_id=event.id,
                link_entity_type=entity_type,
                link_entity_id=entity_id,
            )
        )
        if commit:
            await self.session.commit()
            await self.session.refresh(event)
        self.publisher.publish(
            event_type=CrmEventType.TIMELINE_UPDATED,
            aggregate_type="sales_timeline",
            aggregate_id=event.id,
            tenant_id=tenant_id,
            payload={"event_type": event.event_type.value, "title": event.title},
        )
        return event

    async def list(
        self, tenant_id: UUID, *, offset: int, limit: int
    ):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def list_for_opportunity(self, tenant_id: UUID, opportunity_id: UUID):
        return await self.repo.list_for_opportunity(tenant_id, opportunity_id)

    async def reject_update(self, tenant_id: UUID, event_id: UUID) -> None:
        event = await self.repo.get(tenant_id, event_id)
        if event is None:
            raise NotFoundError("رویداد تایم‌لاین یافت نشد", error_code="timeline_not_found")
        ensure_timeline_immutable(event)


class SalesActivityService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = SalesActivityRepository(session)
        self.participants = ActivityParticipantRepository(session)
        self.reminders = ActivityReminderRepository(session)
        self.timeline = TimelineService(session, publisher)
        self.audit = CollaborationAuditService(session)
        self.publisher = publisher or get_event_publisher()

    async def create(
        self, tenant_id: UUID, body: SalesActivityCreate
    ) -> SalesActivity:
        subject = validate_non_empty(body.subject, field="subject")
        entity = SalesActivity(
            tenant_id=tenant_id,
            subject=subject,
            activity_type=body.activity_type,
            status=body.status,
            priority=body.priority,
            related_type=body.related_type,
            related_id=body.related_id,
            lead_id=body.lead_id,
            contact_id=body.contact_id,
            organization_id=body.organization_id,
            opportunity_id=body.opportunity_id,
            due_at=body.due_at,
            owner_user_id=body.owner_user_id,
            description=body.description,
            custom_type_label=body.custom_type_label,
        )
        await self.repo.add(entity)
        await self.timeline.append(
            tenant_id=tenant_id,
            event_type=TimelineEventType.ACTIVITY_CREATED,
            entity_type=CollaborationEntityType.ACTIVITY,
            entity_id=entity.id,
            title=f"Activity: {entity.subject}",
            related_opportunity_id=entity.opportunity_id,
            related_lead_id=entity.lead_id,
            related_contact_id=entity.contact_id,
            related_organization_id=entity.organization_id,
            payload={"activity_type": entity.activity_type.value},
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="activity",
            entity_id=entity.id,
            action="create",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.ACTIVITY_CREATED,
            aggregate_type="sales_activity",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"activity_type": entity.activity_type.value},
        )
        return entity

    async def update(
        self, tenant_id: UUID, activity_id: UUID, body: SalesActivityUpdate
    ) -> SalesActivity:
        entity = await self.repo.get(tenant_id, activity_id)
        if entity is None:
            raise NotFoundError("فعالیت یافت نشد", error_code="activity_not_found")
        data = body.model_dump(exclude_unset=True)
        if "subject" in data:
            data["subject"] = validate_non_empty(data["subject"], field="subject")
        _apply_update(entity, data)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def complete(self, tenant_id: UUID, activity_id: UUID) -> SalesActivity:
        entity = await self.repo.get(tenant_id, activity_id)
        if entity is None:
            raise NotFoundError("فعالیت یافت نشد", error_code="activity_not_found")
        ensure_activity_completable(entity.status)
        entity.status = ActivityStatus.COMPLETED
        entity.completed_at = datetime.now(timezone.utc)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="activity",
            entity_id=entity.id,
            action="complete",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.ACTIVITY_COMPLETED,
            aggregate_type="sales_activity",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"activity_type": entity.activity_type.value},
        )
        return entity

    async def get(self, tenant_id: UUID, activity_id: UUID) -> SalesActivity:
        entity = await self.repo.get(tenant_id, activity_id)
        if entity is None:
            raise NotFoundError("فعالیت یافت نشد", error_code="activity_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def add_participant(
        self, tenant_id: UUID, body: ParticipantCreate
    ) -> ActivityParticipant:
        await self.get(tenant_id, body.activity_id)
        entity = ActivityParticipant(
            tenant_id=tenant_id,
            activity_id=body.activity_id,
            user_id=body.user_id,
            contact_id=body.contact_id,
            role_label=body.role_label,
            is_required=body.is_required,
        )
        await self.participants.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def add_reminder(
        self, tenant_id: UUID, body: ReminderCreate
    ) -> ActivityReminder:
        await self.get(tenant_id, body.activity_id)
        entity = ActivityReminder(
            tenant_id=tenant_id,
            activity_id=body.activity_id,
            remind_at=body.remind_at,
            channel=body.channel,
        )
        await self.reminders.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class TaskService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = SalesTaskRepository(session)
        self.checklists = TaskChecklistRepository(session)
        self.timeline = TimelineService(session, publisher)
        self.audit = CollaborationAuditService(session)
        self.publisher = publisher or get_event_publisher()

    async def create(self, tenant_id: UUID, body: TaskCreate) -> SalesTask:
        entity = SalesTask(
            tenant_id=tenant_id,
            title=validate_non_empty(body.title, field="title"),
            description=body.description,
            owner_user_id=body.owner_user_id,
            assignee_user_id=body.assignee_user_id,
            priority=body.priority,
            due_at=body.due_at,
            recurrence=body.recurrence,
            opportunity_id=body.opportunity_id,
            lead_id=body.lead_id,
            contact_id=body.contact_id,
            organization_id=body.organization_id,
            activity_id=body.activity_id,
            status="open",
            completion_percent=0,
        )
        await self.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def add_checklist_item(
        self, tenant_id: UUID, body: ChecklistItemCreate
    ) -> TaskChecklistItem:
        task = await self.repo.get(tenant_id, body.task_id)
        if task is None:
            raise NotFoundError("وظیفه یافت نشد", error_code="task_not_found")
        item = TaskChecklistItem(
            tenant_id=tenant_id,
            task_id=body.task_id,
            title=validate_non_empty(body.title, field="title"),
            position=body.position,
        )
        await self.checklists.add(item)
        await self.session.commit()
        await self.session.refresh(item)
        return item

    async def complete_checklist_item(
        self, tenant_id: UUID, item_id: UUID
    ) -> TaskChecklistItem:
        item = await self.checklists.get(tenant_id, item_id)
        if item is None:
            raise NotFoundError("آیتم چک‌لیست یافت نشد", error_code="checklist_not_found")
        item.is_completed = True
        item.completed_at = datetime.now(timezone.utc)
        items = await self.checklists.list_by_task(tenant_id, item.task_id)
        done = sum(1 for i in items if i.is_completed or i.id == item.id)
        total = max(len(items), 1)
        task = await self.repo.get(tenant_id, item.task_id)
        if task is not None:
            task.completion_percent = int(round(100 * done / total))
        await self.session.commit()
        await self.session.refresh(item)
        return item

    async def complete(self, tenant_id: UUID, task_id: UUID) -> SalesTask:
        entity = await self.repo.get(tenant_id, task_id)
        if entity is None:
            raise NotFoundError("وظیفه یافت نشد", error_code="task_not_found")
        if entity.status == "completed":
            raise AppError(
                "وظیفه قبلاً تکمیل شده",
                status_code=409,
                error_code="task_already_completed",
            )
        entity.status = "completed"
        entity.completed_at = datetime.now(timezone.utc)
        entity.completion_percent = 100
        await self.timeline.append(
            tenant_id=tenant_id,
            event_type=TimelineEventType.TASK_COMPLETED,
            entity_type=CollaborationEntityType.TASK,
            entity_id=entity.id,
            title=f"Task completed: {entity.title}",
            related_opportunity_id=entity.opportunity_id,
            related_lead_id=entity.lead_id,
            related_contact_id=entity.contact_id,
            related_organization_id=entity.organization_id,
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="task",
            entity_id=entity.id,
            action="complete",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.TASK_COMPLETED,
            aggregate_type="task",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"title": entity.title},
        )
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class MeetingService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = MeetingRepository(session)
        self.attendees = MeetingAttendeeRepository(session)
        self.timeline = TimelineService(session, publisher)
        self.publisher = publisher or get_event_publisher()

    async def create(self, tenant_id: UUID, body: MeetingCreate) -> Meeting:
        ensure_meeting_window(body.start_at, body.end_at)
        entity = Meeting(
            tenant_id=tenant_id,
            subject=validate_non_empty(body.subject, field="subject"),
            start_at=body.start_at,
            end_at=body.end_at,
            location=body.location,
            online_meeting_url=body.online_meeting_url,
            agenda=body.agenda,
            notes=body.notes,
            owner_user_id=body.owner_user_id,
            opportunity_id=body.opportunity_id,
            lead_id=body.lead_id,
            contact_id=body.contact_id,
            organization_id=body.organization_id,
            activity_id=body.activity_id,
        )
        await self.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def add_attendee(
        self, tenant_id: UUID, body: MeetingAttendeeCreate
    ) -> MeetingAttendee:
        meeting = await self.repo.get(tenant_id, body.meeting_id)
        if meeting is None:
            raise NotFoundError("جلسه یافت نشد", error_code="meeting_not_found")
        entity = MeetingAttendee(
            tenant_id=tenant_id,
            meeting_id=body.meeting_id,
            user_id=body.user_id,
            contact_id=body.contact_id,
            email=body.email,
            display_name=body.display_name,
        )
        await self.attendees.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def finish(
        self,
        tenant_id: UUID,
        meeting_id: UUID,
        body: MeetingFinishRequest | None = None,
    ) -> Meeting:
        entity = await self.repo.get(tenant_id, meeting_id)
        if entity is None:
            raise NotFoundError("جلسه یافت نشد", error_code="meeting_not_found")
        entity.is_finished = True
        entity.finished_at = datetime.now(timezone.utc)
        if body:
            if body.outcome is not None:
                entity.outcome = body.outcome
            if body.notes is not None:
                entity.notes = body.notes
        await self.timeline.append(
            tenant_id=tenant_id,
            event_type=TimelineEventType.MEETING_HELD,
            entity_type=CollaborationEntityType.MEETING,
            entity_id=entity.id,
            title=f"Meeting finished: {entity.subject}",
            related_opportunity_id=entity.opportunity_id,
            related_lead_id=entity.lead_id,
            related_contact_id=entity.contact_id,
            related_organization_id=entity.organization_id,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.MEETING_FINISHED,
            aggregate_type="meeting",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"subject": entity.subject},
        )
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class CallService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = CallLogRepository(session)
        self.timeline = TimelineService(session, publisher)
        self.publisher = publisher or get_event_publisher()

    async def log(self, tenant_id: UUID, body: CallLogCreate) -> CallLog:
        ensure_call_has_opportunity_link(body.opportunity_id)
        called_at = body.called_at or datetime.now(timezone.utc)
        entity = CallLog(
            tenant_id=tenant_id,
            activity_id=body.activity_id,
            direction=body.direction,
            subject=body.subject,
            duration_seconds=validate_duration_seconds(body.duration_seconds),
            outcome_id=body.outcome_id,
            outcome_label=body.outcome_label,
            recording_ref=body.recording_ref,
            notes=body.notes,
            owner_user_id=body.owner_user_id,
            opportunity_id=body.opportunity_id,
            contact_id=body.contact_id,
            lead_id=body.lead_id,
            organization_id=body.organization_id,
            called_at=called_at,
        )
        await self.repo.add(entity)
        await self.timeline.append(
            tenant_id=tenant_id,
            event_type=TimelineEventType.CALL_LOGGED,
            entity_type=CollaborationEntityType.CALL,
            entity_id=entity.id,
            title=f"Call logged ({entity.direction.value})",
            related_opportunity_id=entity.opportunity_id,
            related_lead_id=entity.lead_id,
            related_contact_id=entity.contact_id,
            related_organization_id=entity.organization_id,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.CALL_LOGGED,
            aggregate_type="call_log",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "direction": entity.direction.value,
                "opportunity_id": str(entity.opportunity_id),
            },
        )
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class EmailMetadataService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.threads = EmailThreadRepository(session)
        self.messages = EmailMessageRepository(session)
        self.timeline = TimelineService(session, publisher)

    async def create_thread(
        self, tenant_id: UUID, body: EmailThreadCreate
    ) -> EmailThread:
        entity = EmailThread(
            tenant_id=tenant_id,
            subject=validate_non_empty(body.subject, field="subject"),
            opportunity_id=body.opportunity_id,
            contact_id=body.contact_id,
            organization_id=body.organization_id,
            lead_id=body.lead_id,
        )
        await self.threads.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def add_message(
        self, tenant_id: UUID, body: EmailMessageCreate
    ) -> EmailMessage:
        thread = await self.threads.get(tenant_id, body.thread_id)
        if thread is None:
            raise NotFoundError("رشته ایمیل یافت نشد", error_code="email_thread_not_found")
        now = datetime.now(timezone.utc)
        entity = EmailMessage(
            tenant_id=tenant_id,
            thread_id=body.thread_id,
            subject=validate_non_empty(body.subject, field="subject"),
            status=body.status,
            opened=body.opened or body.status == EmailDeliveryStatus.OPENED,
            clicked=body.clicked or body.status == EmailDeliveryStatus.CLICKED,
            opportunity_id=body.opportunity_id or thread.opportunity_id,
            notification_ref=body.notification_ref,
            sent_at=now if body.status != EmailDeliveryStatus.DRAFT else None,
        )
        await self.messages.add(entity)
        thread.last_message_at = now
        if entity.status in {
            EmailDeliveryStatus.SENT,
            EmailDeliveryStatus.OPENED,
            EmailDeliveryStatus.CLICKED,
        }:
            await self.timeline.append(
                tenant_id=tenant_id,
                event_type=TimelineEventType.EMAIL_SENT,
                entity_type=CollaborationEntityType.EMAIL,
                entity_id=entity.id,
                title=f"Email: {entity.subject}",
                related_opportunity_id=entity.opportunity_id,
                related_lead_id=thread.lead_id,
                related_contact_id=thread.contact_id,
                related_organization_id=thread.organization_id,
            )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class CommentService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = CommentRepository(session)
        self.versions = CommentVersionRepository(session)
        self.mentions = MentionRepository(session)
        self.timeline = TimelineService(session, publisher)
        self.publisher = publisher or get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: CommentCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> Comment:
        actor_id = actor.user_id if actor else None
        body_text = validate_comment_body(body.body)
        entity = Comment(
            tenant_id=tenant_id,
            entity_type=body.entity_type,
            entity_id=body.entity_id,
            parent_comment_id=body.parent_comment_id,
            body=body_text,
            body_format=body.body_format,
            version=1,
            is_pinned=body.is_pinned,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.versions.add(
            CommentVersion(
                tenant_id=tenant_id,
                comment_id=entity.id,
                version=1,
                body=body_text,
                edited_by=actor_id,
            )
        )
        for user_id in body.mention_user_ids:
            mention = Mention(
                tenant_id=tenant_id,
                comment_id=entity.id,
                entity_type=body.entity_type,
                entity_id=body.entity_id,
                mentioned_user_id=user_id,
                mentioned_by=actor_id,
                is_read=False,
                notification_event_emitted=True,
            )
            await self.mentions.add(mention)
            self.publisher.publish(
                event_type=CrmEventType.MENTION_CREATED,
                aggregate_type="mention",
                aggregate_id=mention.id,
                tenant_id=tenant_id,
                payload={"mentioned_user_id": user_id},
            )
            await self.timeline.append(
                tenant_id=tenant_id,
                event_type=TimelineEventType.MENTION_ADDED,
                entity_type=body.entity_type,
                entity_id=body.entity_id,
                title=f"Mention @{user_id}",
                actor_user_id=actor_id,
                payload={"mentioned_user_id": user_id},
            )
        await self.timeline.append(
            tenant_id=tenant_id,
            event_type=TimelineEventType.COMMENT_ADDED,
            entity_type=body.entity_type,
            entity_id=body.entity_id,
            title="Comment added",
            actor_user_id=actor_id,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.COMMENT_CREATED,
            aggregate_type="comment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"entity_type": entity.entity_type.value},
        )
        return entity

    async def update(
        self,
        tenant_id: UUID,
        comment_id: UUID,
        body: CommentUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> Comment:
        entity = await self.repo.get(tenant_id, comment_id)
        if entity is None:
            raise NotFoundError("نظر یافت نشد", error_code="comment_not_found")
        actor_id = actor.user_id if actor else None
        data = body.model_dump(exclude_unset=True)
        if "body" in data and data["body"] is not None:
            data["body"] = validate_comment_body(data["body"])
            entity.version += 1
            await self.versions.add(
                CommentVersion(
                    tenant_id=tenant_id,
                    comment_id=entity.id,
                    version=entity.version,
                    body=data["body"],
                    edited_by=actor_id,
                )
            )
        _apply_update(entity, data)
        entity.updated_by = actor_id
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_for_entity(
        self,
        tenant_id: UUID,
        entity_type: CollaborationEntityType,
        entity_id: UUID,
    ):
        return await self.repo.list_for_entity(tenant_id, entity_type, entity_id)


class MentionService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = MentionRepository(session)

    async def list_unread(self, tenant_id: UUID, user_id: str):
        return await self.repo.list_unread_for_user(tenant_id, user_id)

    async def mark_read(self, tenant_id: UUID, mention_id: UUID) -> Mention:
        entity = await self.repo.get(tenant_id, mention_id)
        if entity is None:
            raise NotFoundError("منشن یافت نشد", error_code="mention_not_found")
        entity.is_read = True
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class BookmarkService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.bookmarks = BookmarkRepository(session)
        self.favorites = FavoriteRepository(session)

    async def add_bookmark(
        self, tenant_id: UUID, user_id: str, body: BookmarkCreate
    ) -> Bookmark:
        validate_bookmark_entity(body.entity_type.value)
        existing = await self.bookmarks.get_user_bookmark(
            tenant_id, user_id, body.entity_type, body.entity_id
        )
        if existing is not None:
            return existing
        entity = Bookmark(
            tenant_id=tenant_id,
            user_id=user_id,
            entity_type=body.entity_type,
            entity_id=body.entity_id,
            label=body.label,
        )
        await self.bookmarks.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def add_favorite(
        self, tenant_id: UUID, user_id: str, body: FavoriteCreate
    ) -> Favorite:
        entity = Favorite(
            tenant_id=tenant_id,
            user_id=user_id,
            entity_type=body.entity_type,
            entity_id=body.entity_id,
        )
        await self.favorites.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_bookmarks(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.bookmarks.list_by_tenant(
            tenant_id, offset=offset, limit=limit
        )


class SalesTeamService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.roles = TeamRoleRepository(session)
        self.members = SalesTeamMemberRepository(session)
        self.assignments = TeamAssignmentRepository(session)
        self.prefs = CrmNotificationPreferenceRepository(session)

    async def create_role(self, tenant_id: UUID, body: TeamRoleCreate) -> TeamRole:
        entity = TeamRole(
            tenant_id=tenant_id,
            code=validate_non_empty(body.code, field="code"),
            name=validate_non_empty(body.name, field="name"),
            role_kind=body.role_kind,
            is_active=body.is_active,
        )
        await self.roles.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def add_member(
        self, tenant_id: UUID, body: TeamMemberCreate
    ) -> SalesTeamMember:
        entity = SalesTeamMember(
            tenant_id=tenant_id,
            user_id=validate_non_empty(body.user_id, field="user_id"),
            display_name=body.display_name,
            team_key=validate_non_empty(body.team_key, field="team_key"),
            role_id=body.role_id,
            role_code=body.role_code,
            is_active=body.is_active,
        )
        await self.members.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def assign(
        self,
        tenant_id: UUID,
        body: TeamAssignmentCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> TeamAssignment:
        entity = TeamAssignment(
            tenant_id=tenant_id,
            team_key=body.team_key,
            member_id=body.member_id,
            entity_type=body.entity_type,
            entity_id=body.entity_id,
            assigned_by=actor.user_id if actor else None,
        )
        await self.assignments.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def upsert_preferences(
        self, tenant_id: UUID, user_id: str, body: NotificationPreferenceUpsert
    ) -> CrmNotificationPreference:
        existing = await self.prefs.get_for_user(tenant_id, user_id)
        if existing is None:
            existing = CrmNotificationPreference(
                tenant_id=tenant_id,
                user_id=user_id,
                mentions_enabled=body.mentions_enabled,
                task_reminders_enabled=body.task_reminders_enabled,
                meeting_reminders_enabled=body.meeting_reminders_enabled,
                timeline_digest_enabled=body.timeline_digest_enabled,
            )
            await self.prefs.add(existing)
        else:
            existing.mentions_enabled = body.mentions_enabled
            existing.task_reminders_enabled = body.task_reminders_enabled
            existing.meeting_reminders_enabled = body.meeting_reminders_enabled
            existing.timeline_digest_enabled = body.timeline_digest_enabled
        await self.session.commit()
        await self.session.refresh(existing)
        return existing

    async def list_members(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.members.list_by_tenant(tenant_id, offset=offset, limit=limit)
