"""
PM Application Services — Resource, Cost, Risk & Supporting.

سرویس‌های لایه Application برای منبع، هزینه، ریسک و سایر موجودیت‌ها.
"""
import logging
from datetime import date, datetime
from typing import Optional, List
from uuid import UUID, uuid4
from decimal import Decimal

from apps.core.event_bus.events import event_bus

from ...domain.entities.resource import Resource, ResourceAssignment
from ...domain.entities.cost import Budget, CostEntry
from ...domain.entities.risk import Risk
from ...domain.entities.issue import Issue
from ...domain.entities.obs import OBSNode
from ...domain.entities.rbs import RBSNode
from ...domain.entities.quality import (
    QualityPlan, Inspection, NCR, QualityChecklist, QualityChecklistItem,
)
from ...domain.entities.project_document import ProjectDocument
from ...domain.entities.supporting import (
    Baseline, Calendar, Holiday, ChangeRequest,
    Timesheet, ProjectMember, Comment, ActivityLog,
)
from ...domain.events.pm_events import (
    ResourceAssigned, ResourceReleased, CostRecorded,
    RiskIdentified, RiskMitigated, BaselineCreated,
    ChangeRequestSubmitted, ChangeRequestApproved, ChangeRequestRejected,
    TimesheetSubmitted, TimesheetApproved,
    IssueCreated, IssueAssigned, IssueResolved, IssueClosed,
    OBSNodeCreated, OBSNodeUpdated, OBSNodeDeleted,
    RBSNodeCreated, RBSNodeUpdated, RBSNodeDeleted,
    InspectionCompleted, NCRCreated, NCRResolved,
)
from ...domain.exceptions.pm_exceptions import (
    ProjectNotFoundError, TaskNotFoundError,
    ResourceOverallocationError, BudgetExceededError,
    DuplicateCodeError,
)
from ...infrastructure.repositories import (
    DjangoResourceRepository,
    DjangoResourceAssignmentRepository,
    DjangoBudgetRepository,
    DjangoCostEntryRepository,
    DjangoRiskRepository,
    DjangoBaselineRepository,
    DjangoCalendarRepository,
    DjangoHolidayRepository,
    DjangoChangeRequestRepository,
    DjangoTimesheetRepository,
    DjangoProjectMemberRepository,
    DjangoCommentRepository,
    DjangoActivityLogRepository,
    DjangoProjectRepository,
    DjangoTaskRepository,
    DjangoIssueRepository,
    DjangoOBSNodeRepository,
    DjangoRBSNodeRepository,
    DjangoQualityPlanRepository,
    DjangoInspectionRepository,
    DjangoNCRRepository,
    DjangoQualityChecklistRepository,
    DjangoQualityChecklistItemRepository,
    DjangoProjectDocumentRepository,
)
from ..dtos.pm_dtos import (
    CreateResourceDTO, UpdateResourceDTO, ResourceResponseDTO,
    CreateAssignmentDTO, AssignmentResponseDTO,
    CreateBudgetDTO, BudgetResponseDTO,
    CreateCostEntryDTO, CostEntryResponseDTO,
    CreateRiskDTO, UpdateRiskDTO, RiskResponseDTO,
    CreateIssueDTO, UpdateIssueDTO, IssueResponseDTO,
    CreateOBSNodeDTO, UpdateOBSNodeDTO, OBSNodeResponseDTO,
    CreateRBSNodeDTO, UpdateRBSNodeDTO, RBSNodeResponseDTO,
    CreateQualityPlanDTO, UpdateQualityPlanDTO, QualityPlanResponseDTO,
    CreateInspectionDTO, UpdateInspectionDTO, InspectionResponseDTO,
    CreateNCRDTO, UpdateNCRDTO, NCRResponseDTO,
    CreateQualityChecklistDTO, UpdateQualityChecklistDTO, QualityChecklistResponseDTO,
    CreateQualityChecklistItemDTO, UpdateQualityChecklistItemDTO, QualityChecklistItemResponseDTO,
    ProjectDocumentCreateDTO, ProjectDocumentResponseDTO,
    CreateBaselineDTO, BaselineResponseDTO,
    CreateCalendarDTO, CalendarResponseDTO,
    CreateHolidayDTO, HolidayResponseDTO,
    CreateChangeRequestDTO, ChangeRequestResponseDTO,
    CreateTimesheetDTO, TimesheetResponseDTO,
    CreateMemberDTO, MemberResponseDTO,
    CreateCommentDTO, CommentResponseDTO,
    EVMReportDTO,
)

logger = logging.getLogger(__name__)


def _publish_event(event) -> None:
    """Publish a domain event via the platform event bus (fire-and-forget)."""
    try:
        event_bus.publish(event)
    except Exception as exc:
        logger.warning("event_publish_failed: %s — %s", type(event).__name__, exc)


class ResourceService:
    """سرویس مدیریت منابع."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._repo = DjangoResourceRepository(tenant_id)
        self._assign_repo = DjangoResourceAssignmentRepository(tenant_id)

    def create_resource(self, dto: CreateResourceDTO) -> ResourceResponseDTO:
        if dto.code:
            existing = self._repo.find_by_code(dto.code)
            if existing:
                raise DuplicateCodeError("resource", dto.code)

        resource = Resource(
            id=uuid4(),
            tenant_id=self._tenant_id,
            name=dto.name,
            code=dto.code,
            resource_type=dto.resource_type,
            employee_id=dto.employee_id,
            user_id=dto.user_id,
            company_id=dto.company_id,
            standard_rate=dto.standard_rate,
            overtime_rate=dto.overtime_rate,
            cost_per_use=dto.cost_per_use,
            currency_id=dto.currency_id,
            max_units=dto.max_units,
            calendar_id=dto.calendar_id,
            email=dto.email,
            phone=dto.phone,
            notes=dto.notes,
        )
        saved = self._repo.save(resource)
        return self._to_response(saved)

    def update_resource(self, dto: UpdateResourceDTO) -> ResourceResponseDTO:
        resource = self._repo.get_by_id(dto.id)
        if not resource:
            raise ProjectNotFoundError(dto.id)

        for attr in ["name", "code", "resource_type", "standard_rate",
                      "overtime_rate", "cost_per_use", "currency_id",
                      "max_units", "calendar_id", "email", "phone",
                      "is_active", "notes"]:
            val = getattr(dto, attr, None)
            if val is not None:
                setattr(resource, attr, val)

        saved = self._repo.save(resource)
        return self._to_response(saved)

    def get_resource(self, resource_id: UUID) -> ResourceResponseDTO:
        resource = self._repo.get_by_id(resource_id)
        if not resource:
            raise ProjectNotFoundError(resource_id)
        return self._to_response(resource)

    def list_resources(self, resource_type: Optional[str] = None) -> List[ResourceResponseDTO]:
        if resource_type:
            resources = self._repo.find_by_type(resource_type)
        else:
            resources = self._repo.find_active()
        return [self._to_response(r) for r in resources]

    def assign_resource(self, dto: CreateAssignmentDTO) -> AssignmentResponseDTO:
        assignment = ResourceAssignment(
            id=uuid4(),
            tenant_id=self._tenant_id,
            project_id=dto.project_id,
            task_id=dto.task_id,
            resource_id=dto.resource_id,
            units=dto.units,
            planned_hours=dto.planned_hours,
            planned_cost=dto.planned_cost,
            start_date=dto.start_date,
            end_date=dto.end_date,
            notes=dto.notes,
        )
        saved = self._assign_repo.save(assignment)

        _publish_event(ResourceAssigned(
            event_id=uuid4(), tenant_id=self._tenant_id,
            occurred_at=datetime.utcnow(),
            project_id=saved.project_id, task_id=saved.task_id,
            resource_id=saved.resource_id, units=saved.units,
        ))

        return self._assignment_to_response(saved)

    def list_assignments(self, project_id: Optional[UUID] = None,
                         task_id: Optional[UUID] = None,
                         resource_id: Optional[UUID] = None) -> List[AssignmentResponseDTO]:
        if task_id:
            items = self._assign_repo.find_by_task(task_id)
        elif resource_id:
            items = self._assign_repo.find_by_resource(resource_id)
        elif project_id:
            items = self._assign_repo.find_by_project(project_id)
        else:
            items = []
        return [self._assignment_to_response(a) for a in items]

    def remove_assignment(self, assignment_id: UUID) -> bool:
        assignment = self._assign_repo.get_by_id(assignment_id)
        result = self._assign_repo.delete(assignment_id)
        if result and assignment:
            _publish_event(ResourceReleased(
                event_id=uuid4(), tenant_id=self._tenant_id,
                occurred_at=datetime.utcnow(),
                project_id=assignment.project_id, task_id=assignment.task_id,
                resource_id=assignment.resource_id,
            ))
        return result

    def _to_response(self, r: Resource) -> ResourceResponseDTO:
        return ResourceResponseDTO(
            id=r.id, name=r.name, code=r.code, resource_type=r.resource_type,
            employee_id=r.employee_id, user_id=r.user_id, company_id=r.company_id,
            standard_rate=r.standard_rate, overtime_rate=r.overtime_rate,
            cost_per_use=r.cost_per_use, currency_id=r.currency_id,
            max_units=r.max_units, calendar_id=r.calendar_id,
            email=r.email, phone=r.phone, is_active=r.is_active,
            notes=r.notes, created_at=r.created_at, updated_at=r.updated_at,
        )

    def _assignment_to_response(self, a: ResourceAssignment) -> AssignmentResponseDTO:
        return AssignmentResponseDTO(
            id=a.id, project_id=a.project_id, task_id=a.task_id,
            resource_id=a.resource_id, units=a.units,
            planned_hours=a.planned_hours, actual_hours=a.actual_hours,
            planned_cost=a.planned_cost, actual_cost=a.actual_cost,
            start_date=a.start_date, end_date=a.end_date,
            is_active=a.is_active, notes=a.notes, created_at=a.created_at,
        )


class CostService:
    """سرویس مدیریت هزینه."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._budget_repo = DjangoBudgetRepository(tenant_id)
        self._cost_repo = DjangoCostEntryRepository(tenant_id)

    def create_budget(self, dto: CreateBudgetDTO) -> BudgetResponseDTO:
        budget = Budget(
            id=uuid4(), tenant_id=self._tenant_id,
            project_id=dto.project_id, task_id=dto.task_id,
            name=dto.name, description=dto.description,
            original_budget=dto.original_budget, revised_budget=dto.original_budget,
            currency_id=dto.currency_id,
            period_start=dto.period_start, period_end=dto.period_end,
            notes=dto.notes,
        )
        saved = self._budget_repo.save(budget)
        return self._budget_response(saved)

    def list_budgets(self, project_id: UUID) -> List[BudgetResponseDTO]:
        budgets = self._budget_repo.find_by_project(project_id)
        return [self._budget_response(b) for b in budgets]

    def get_budget_summary(self, project_id: UUID) -> dict:
        return self._budget_repo.get_project_budget_summary(project_id)

    def record_cost(self, dto: CreateCostEntryDTO) -> CostEntryResponseDTO:
        entry = CostEntry(
            id=uuid4(), tenant_id=self._tenant_id,
            project_id=dto.project_id, task_id=dto.task_id,
            resource_id=dto.resource_id, budget_id=dto.budget_id,
            description=dto.description, amount=dto.amount,
            cost_type=dto.cost_type, currency_id=dto.currency_id,
            entry_date=dto.entry_date or date.today(),
            reference_number=dto.reference_number, notes=dto.notes,
            created_by=dto.created_by,
        )
        saved = self._cost_repo.save(entry)

        _publish_event(CostRecorded(
            event_id=uuid4(), tenant_id=self._tenant_id,
            occurred_at=datetime.utcnow(),
            project_id=saved.project_id, task_id=saved.task_id,
            amount=saved.amount, cost_type=saved.cost_type,
        ))

        return self._cost_response(saved)

    def list_costs(self, project_id: UUID, task_id: Optional[UUID] = None) -> List[CostEntryResponseDTO]:
        if task_id:
            entries = self._cost_repo.find_by_task(task_id)
        else:
            entries = self._cost_repo.find_by_project(project_id)
        return [self._cost_response(e) for e in entries]

    def get_cost_breakdown(self, project_id: UUID) -> dict:
        return self._cost_repo.get_costs_by_type(project_id)

    def _budget_response(self, b: Budget) -> BudgetResponseDTO:
        variance = b.revised_budget - b.actual_cost if b.revised_budget else Decimal("0")
        return BudgetResponseDTO(
            id=b.id, project_id=b.project_id, task_id=b.task_id,
            name=b.name, description=b.description,
            original_budget=b.original_budget, revised_budget=b.revised_budget,
            committed_cost=b.committed_cost, actual_cost=b.actual_cost,
            currency_id=b.currency_id, period_start=b.period_start,
            period_end=b.period_end, is_active=b.is_active,
            notes=b.notes, created_at=b.created_at, variance=variance,
        )

    def _cost_response(self, e: CostEntry) -> CostEntryResponseDTO:
        return CostEntryResponseDTO(
            id=e.id, project_id=e.project_id, task_id=e.task_id,
            resource_id=e.resource_id, budget_id=e.budget_id,
            description=e.description, amount=e.amount,
            cost_type=e.cost_type, currency_id=e.currency_id,
            entry_date=e.entry_date, reference_number=e.reference_number,
            notes=e.notes, created_at=e.created_at, created_by=e.created_by,
        )


class RiskService:
    """سرویس مدیریت ریسک."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._repo = DjangoRiskRepository(tenant_id)

    def create_risk(self, dto: CreateRiskDTO) -> RiskResponseDTO:
        risk = Risk(
            id=uuid4(), tenant_id=self._tenant_id,
            project_id=dto.project_id, code=dto.code,
            title=dto.title, description=dto.description,
            category=dto.category, probability=dto.probability,
            impact=dto.impact, response_strategy=dto.response_strategy,
            response_plan=dto.response_plan,
            mitigation_actions=dto.mitigation_actions,
            owner_id=dto.owner_id, cost_impact=dto.cost_impact,
            schedule_impact_days=dto.schedule_impact_days,
            affected_tasks=dto.affected_tasks,
            identified_date=dto.identified_date or date.today(),
            due_date=dto.due_date,
        )
        saved = self._repo.save(risk)

        _publish_event(RiskIdentified(
            event_id=uuid4(), tenant_id=self._tenant_id,
            occurred_at=datetime.utcnow(),
            project_id=saved.project_id, risk_id=saved.id,
            title=saved.title, probability=saved.probability,
            impact=saved.impact,
        ))

        return self._to_response(saved)

    def update_risk(self, dto: UpdateRiskDTO) -> RiskResponseDTO:
        risk = self._repo.get_by_id(dto.id)
        if not risk:
            raise ProjectNotFoundError(dto.id)

        for attr in ["title", "description", "category", "probability", "impact",
                      "status", "response_strategy", "response_plan",
                      "mitigation_actions", "owner_id", "cost_impact",
                      "schedule_impact_days", "due_date"]:
            val = getattr(dto, attr, None)
            if val is not None:
                setattr(risk, attr, val)

        saved = self._repo.save(risk)
        return self._to_response(saved)

    def list_risks(self, project_id: UUID, open_only: bool = False) -> List[RiskResponseDTO]:
        if open_only:
            risks = self._repo.find_open_risks(project_id)
        else:
            risks = self._repo.find_by_project(project_id)
        return [self._to_response(r) for r in risks]

    def get_risk_matrix(self, project_id: UUID) -> dict:
        """ماتریس ریسک."""
        risks = self._repo.find_open_risks(project_id)
        matrix = {}
        for r in risks:
            key = f"{r.probability}_{r.impact}"
            if key not in matrix:
                matrix[key] = []
            matrix[key].append({"id": str(r.id), "title": r.title})
        return matrix

    def _to_response(self, r: Risk) -> RiskResponseDTO:
        prob_weight = {"very_low": 1, "low": 2, "medium": 3, "high": 4, "very_high": 5}
        impact_weight = {"negligible": 1, "minor": 2, "moderate": 3, "major": 4, "critical": 5}
        score = prob_weight.get(r.probability, 3) * impact_weight.get(r.impact, 3)
        return RiskResponseDTO(
            id=r.id, project_id=r.project_id, code=r.code,
            title=r.title, description=r.description, category=r.category,
            probability=r.probability, impact=r.impact, status=r.status,
            response_strategy=r.response_strategy, response_plan=r.response_plan,
            mitigation_actions=r.mitigation_actions, owner_id=r.owner_id,
            cost_impact=r.cost_impact, schedule_impact_days=r.schedule_impact_days,
            affected_tasks=r.affected_tasks, identified_date=r.identified_date,
            due_date=r.due_date, closed_date=r.closed_date, notes=r.notes,
            risk_score=score, created_at=r.created_at, updated_at=r.updated_at,
        )


class BaselineService:
    """سرویس مدیریت خط پایه."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._repo = DjangoBaselineRepository(tenant_id)
        self._task_repo = DjangoTaskRepository(tenant_id)
        self._project_repo = DjangoProjectRepository(tenant_id)

    def create_baseline(self, dto: CreateBaselineDTO) -> BaselineResponseDTO:
        project = self._project_repo.get_by_id(dto.project_id)
        if not project:
            raise ProjectNotFoundError(dto.project_id)

        tasks = self._task_repo.find_by_project(dto.project_id)
        snapshot = {
            "tasks": [
                {
                    "id": str(t.id), "code": t.code, "title": t.title,
                    "planned_start": str(t.planned_start) if t.planned_start else None,
                    "planned_end": str(t.planned_end) if t.planned_end else None,
                    "duration": t.duration, "planned_cost": str(t.planned_cost),
                    "progress": t.progress,
                }
                for t in tasks
            ],
        }

        # غیرفعال‌کردن بیس‌لاین قبلی
        self._repo.deactivate_all(dto.project_id)

        baseline = Baseline(
            id=uuid4(), tenant_id=self._tenant_id,
            project_id=dto.project_id, name=dto.name,
            description=dto.description, baseline_type=dto.baseline_type,
            is_active=True, snapshot_date=date.today(),
            snapshot_data=snapshot, total_tasks=len(tasks),
            total_cost=sum(t.planned_cost for t in tasks),
            planned_start=project.planned_start,
            planned_end=project.planned_end,
            created_by=dto.created_by,
        )
        saved = self._repo.save(baseline)

        _publish_event(BaselineCreated(
            event_id=uuid4(), tenant_id=self._tenant_id,
            occurred_at=datetime.utcnow(),
            project_id=saved.project_id, baseline_id=saved.id,
            name=saved.name,
        ))

        return self._to_response(saved)

    def list_baselines(self, project_id: UUID) -> List[BaselineResponseDTO]:
        baselines = self._repo.find_by_project(project_id)
        return [self._to_response(b) for b in baselines]

    def get_active_baseline(self, project_id: UUID) -> Optional[BaselineResponseDTO]:
        bl = self._repo.find_active(project_id)
        return self._to_response(bl) if bl else None

    def _to_response(self, b: Baseline) -> BaselineResponseDTO:
        return BaselineResponseDTO(
            id=b.id, project_id=b.project_id, name=b.name,
            description=b.description, baseline_type=b.baseline_type,
            is_active=b.is_active, snapshot_date=b.snapshot_date,
            total_tasks=b.total_tasks, total_cost=b.total_cost,
            planned_start=b.planned_start, planned_end=b.planned_end,
            created_at=b.created_at, created_by=b.created_by,
        )


class CalendarService:
    """سرویس مدیریت تقویم."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._cal_repo = DjangoCalendarRepository(tenant_id)
        self._hol_repo = DjangoHolidayRepository(tenant_id)

    def create_calendar(self, dto: CreateCalendarDTO) -> CalendarResponseDTO:
        cal = Calendar(
            id=uuid4(), tenant_id=self._tenant_id,
            name=dto.name, description=dto.description,
            is_default=dto.is_default, work_start=dto.work_start,
            work_end=dto.work_end, hours_per_day=dto.hours_per_day,
            working_days=dto.working_days,
        )
        saved = self._cal_repo.save(cal)
        return self._cal_response(saved)

    def list_calendars(self) -> List[CalendarResponseDTO]:
        cals = self._cal_repo.find_all()
        return [self._cal_response(c) for c in cals]

    def add_holiday(self, dto: CreateHolidayDTO) -> HolidayResponseDTO:
        hol = Holiday(
            id=uuid4(), tenant_id=self._tenant_id,
            calendar_id=dto.calendar_id, name=dto.name,
            holiday_date=dto.holiday_date, is_recurring=dto.is_recurring,
            notes=dto.notes,
        )
        saved = self._hol_repo.save(hol)
        return self._hol_response(saved)

    def list_holidays(self, calendar_id: UUID) -> List[HolidayResponseDTO]:
        hols = self._hol_repo.find_by_calendar(calendar_id)
        return [self._hol_response(h) for h in hols]

    def _cal_response(self, c: Calendar) -> CalendarResponseDTO:
        return CalendarResponseDTO(
            id=c.id, name=c.name, description=c.description,
            is_default=c.is_default, work_start=c.work_start,
            work_end=c.work_end, hours_per_day=c.hours_per_day,
            working_days=c.working_days, created_at=c.created_at,
        )

    def _hol_response(self, h: Holiday) -> HolidayResponseDTO:
        return HolidayResponseDTO(
            id=h.id, calendar_id=h.calendar_id, name=h.name,
            holiday_date=h.holiday_date, is_recurring=h.is_recurring,
            notes=h.notes, created_at=h.created_at,
        )


class ChangeRequestService:
    """سرویس مدیریت درخواست تغییر."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._repo = DjangoChangeRequestRepository(tenant_id)

    def create_change_request(self, dto: CreateChangeRequestDTO) -> ChangeRequestResponseDTO:
        cr = ChangeRequest(
            id=uuid4(), tenant_id=self._tenant_id,
            project_id=dto.project_id, code=dto.code,
            title=dto.title, description=dto.description,
            justification=dto.justification, priority=dto.priority,
            scope_impact=dto.scope_impact, cost_impact=dto.cost_impact,
            schedule_impact_days=dto.schedule_impact_days,
            requester_id=dto.requester_id,
        )
        saved = self._repo.save(cr)
        return self._to_response(saved)

    def list_change_requests(self, project_id: UUID, status: Optional[str] = None) -> List[ChangeRequestResponseDTO]:
        if status:
            items = self._repo.find_by_status(project_id, status)
        else:
            items = self._repo.find_by_project(project_id)
        return [self._to_response(cr) for cr in items]

    def _to_response(self, cr: ChangeRequest) -> ChangeRequestResponseDTO:
        return ChangeRequestResponseDTO(
            id=cr.id, project_id=cr.project_id, code=cr.code,
            title=cr.title, description=cr.description,
            justification=cr.justification, status=cr.status,
            priority=cr.priority, scope_impact=cr.scope_impact,
            cost_impact=cr.cost_impact, schedule_impact_days=cr.schedule_impact_days,
            requester_id=cr.requester_id, reviewer_id=cr.reviewer_id,
            workflow_instance_id=cr.workflow_instance_id,
            submitted_date=cr.submitted_date, decision_date=cr.decision_date,
            implementation_date=cr.implementation_date,
            decision_notes=cr.decision_notes, created_at=cr.created_at,
        )


class TimesheetService:
    """سرویس مدیریت تایم‌شیت."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._repo = DjangoTimesheetRepository(tenant_id)

    def log_time(self, dto: CreateTimesheetDTO) -> TimesheetResponseDTO:
        ts = Timesheet(
            id=uuid4(), tenant_id=self._tenant_id,
            project_id=dto.project_id, task_id=dto.task_id,
            resource_id=dto.resource_id, user_id=dto.user_id,
            work_date=dto.work_date, regular_hours=dto.regular_hours,
            overtime_hours=dto.overtime_hours, description=dto.description,
            notes=dto.notes,
        )
        saved = self._repo.save(ts)
        return self._to_response(saved)

    def list_timesheets(self, project_id: UUID) -> List[TimesheetResponseDTO]:
        items = self._repo.find_by_project(project_id)
        return [self._to_response(t) for t in items]

    def get_total_hours(self, project_id: UUID, user_id: Optional[UUID] = None) -> dict:
        return self._repo.get_total_hours(project_id, user_id)

    def _to_response(self, t: Timesheet) -> TimesheetResponseDTO:
        return TimesheetResponseDTO(
            id=t.id, project_id=t.project_id, task_id=t.task_id,
            resource_id=t.resource_id, user_id=t.user_id,
            work_date=t.work_date, regular_hours=t.regular_hours,
            overtime_hours=t.overtime_hours,
            total_hours=t.regular_hours + t.overtime_hours,
            description=t.description, status=t.status,
            approved_by_id=t.approved_by_id, approved_date=t.approved_date,
            notes=t.notes, created_at=t.created_at,
        )


class MemberService:
    """سرویس مدیریت اعضای پروژه."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._repo = DjangoProjectMemberRepository(tenant_id)

    def add_member(self, dto: CreateMemberDTO) -> MemberResponseDTO:
        member = ProjectMember(
            id=uuid4(), tenant_id=self._tenant_id,
            project_id=dto.project_id, user_id=dto.user_id,
            role=dto.role, joined_date=dto.joined_date or date.today(),
            notes=dto.notes,
        )
        saved = self._repo.save(member)
        return self._to_response(saved)

    def list_members(self, project_id: UUID) -> List[MemberResponseDTO]:
        members = self._repo.find_by_project(project_id)
        return [self._to_response(m) for m in members]

    def remove_member(self, member_id: UUID) -> bool:
        return self._repo.delete(member_id)

    def _to_response(self, m: ProjectMember) -> MemberResponseDTO:
        return MemberResponseDTO(
            id=m.id, project_id=m.project_id, user_id=m.user_id,
            role=m.role, is_active=m.is_active,
            joined_date=m.joined_date, left_date=m.left_date,
            notes=m.notes, created_at=m.created_at,
        )


class CommentService:
    """سرویس مدیریت نظرات."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._repo = DjangoCommentRepository(tenant_id)

    def add_comment(self, dto: CreateCommentDTO) -> CommentResponseDTO:
        comment = Comment(
            id=uuid4(), tenant_id=self._tenant_id,
            project_id=dto.project_id, task_id=dto.task_id,
            user_id=dto.user_id, content=dto.content,
            parent_id=dto.parent_id, mentions=dto.mentions,
        )
        saved = self._repo.save(comment)
        return self._to_response(saved)

    def list_comments(self, project_id: UUID, task_id: Optional[UUID] = None) -> List[CommentResponseDTO]:
        comments = self._repo.find_root_comments(project_id, task_id)
        return [self._to_response(c) for c in comments]

    def _to_response(self, c: Comment) -> CommentResponseDTO:
        return CommentResponseDTO(
            id=c.id, project_id=c.project_id, task_id=c.task_id,
            user_id=c.user_id, content=c.content,
            parent_id=c.parent_id, mentions=c.mentions,
            is_edited=c.is_edited, is_deleted=c.is_deleted,
            created_at=c.created_at, updated_at=c.updated_at,
        )


class IssueService:
    """سرویس مدیریت مسائل (Issue Log)."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._repo = DjangoIssueRepository(tenant_id)

    def create_issue(self, dto: CreateIssueDTO) -> IssueResponseDTO:
        issue = Issue(
            id=uuid4(), tenant_id=self._tenant_id,
            project_id=dto.project_id, code=dto.code,
            title=dto.title, description=dto.description,
            category=dto.category, severity=dto.severity,
            priority=dto.priority, reporter_id=dto.reporter_id,
            assignee_id=dto.assignee_id,
            affected_tasks=dto.affected_tasks,
            related_risk_id=dto.related_risk_id,
            impact_description=dto.impact_description,
            cost_impact=dto.cost_impact,
            schedule_impact_days=dto.schedule_impact_days,
            identified_date=dto.identified_date or date.today(),
            due_date=dto.due_date,
        )
        saved = self._repo.save(issue)

        _publish_event(IssueCreated(
            event_id=uuid4(), tenant_id=self._tenant_id,
            occurred_at=datetime.utcnow(),
            project_id=saved.project_id, issue_id=saved.id,
            title=saved.title, severity=saved.severity,
            priority=saved.priority,
        ))

        return self._to_response(saved)

    def update_issue(self, dto: UpdateIssueDTO) -> IssueResponseDTO:
        issue = self._repo.get_by_id(dto.id)
        if not issue:
            raise ProjectNotFoundError(dto.id)

        for attr in ["title", "description", "category", "severity",
                      "priority", "assignee_id", "affected_tasks",
                      "related_risk_id", "impact_description",
                      "cost_impact", "schedule_impact_days", "due_date", "notes"]:
            val = getattr(dto, attr, None)
            if val is not None:
                setattr(issue, attr, val)

        saved = self._repo.save(issue)
        return self._to_response(saved)

    def assign_issue(self, issue_id: UUID, assignee_id: UUID) -> IssueResponseDTO:
        issue = self._repo.get_by_id(issue_id)
        if not issue:
            raise ProjectNotFoundError(issue_id)

        issue.assign(assignee_id)
        saved = self._repo.save(issue)

        _publish_event(IssueAssigned(
            event_id=uuid4(), tenant_id=self._tenant_id,
            occurred_at=datetime.utcnow(),
            project_id=saved.project_id, issue_id=saved.id,
            assignee_id=assignee_id,
        ))

        return self._to_response(saved)

    def resolve_issue(self, issue_id: UUID, resolution: str) -> IssueResponseDTO:
        issue = self._repo.get_by_id(issue_id)
        if not issue:
            raise ProjectNotFoundError(issue_id)

        issue.resolve(resolution)
        saved = self._repo.save(issue)

        _publish_event(IssueResolved(
            event_id=uuid4(), tenant_id=self._tenant_id,
            occurred_at=datetime.utcnow(),
            project_id=saved.project_id, issue_id=saved.id,
            resolution=resolution,
        ))

        return self._to_response(saved)

    def close_issue(self, issue_id: UUID) -> IssueResponseDTO:
        issue = self._repo.get_by_id(issue_id)
        if not issue:
            raise ProjectNotFoundError(issue_id)

        issue.close()
        saved = self._repo.save(issue)

        _publish_event(IssueClosed(
            event_id=uuid4(), tenant_id=self._tenant_id,
            occurred_at=datetime.utcnow(),
            project_id=saved.project_id, issue_id=saved.id,
        ))

        return self._to_response(saved)

    def reopen_issue(self, issue_id: UUID) -> IssueResponseDTO:
        issue = self._repo.get_by_id(issue_id)
        if not issue:
            raise ProjectNotFoundError(issue_id)

        issue.reopen()
        saved = self._repo.save(issue)
        return self._to_response(saved)

    def list_issues(
        self, project_id: UUID, open_only: bool = False,
    ) -> List[IssueResponseDTO]:
        if open_only:
            issues = self._repo.find_open_issues(project_id)
        else:
            issues = self._repo.find_by_project(project_id)
        return [self._to_response(i) for i in issues]

    def get_issue(self, issue_id: UUID) -> Optional[IssueResponseDTO]:
        issue = self._repo.get_by_id(issue_id)
        if not issue:
            return None
        return self._to_response(issue)

    def _to_response(self, i: Issue) -> IssueResponseDTO:
        return IssueResponseDTO(
            id=i.id, project_id=i.project_id, code=i.code,
            title=i.title, description=i.description,
            category=i.category, severity=i.severity,
            priority=i.priority, status=i.status,
            reporter_id=i.reporter_id, assignee_id=i.assignee_id,
            affected_tasks=i.affected_tasks,
            related_risk_id=i.related_risk_id,
            resolution=i.resolution,
            impact_description=i.impact_description,
            cost_impact=i.cost_impact,
            schedule_impact_days=i.schedule_impact_days,
            identified_date=i.identified_date,
            due_date=i.due_date, resolved_date=i.resolved_date,
            closed_date=i.closed_date, notes=i.notes,
            is_overdue=i.is_overdue, is_critical=i.is_critical,
            created_at=i.created_at, updated_at=i.updated_at,
        )


# ═══════════════════════════════════════════════════
# OBS Service
# ═══════════════════════════════════════════════════

class OBSService:
    """سرویس ساختار شکست سازمانی (OBS)."""

    def __init__(self, tenant_id: UUID):
        self.tenant_id = tenant_id
        self._repo = DjangoOBSNodeRepository(tenant_id)

    def create_node(self, data: CreateOBSNodeDTO) -> OBSNodeResponseDTO:
        """ایجاد گره جدید OBS."""
        # محاسبه سطح بر اساس والد
        level = 0
        if data.parent_id:
            parent = self._repo.get_by_id(data.parent_id)
            if parent:
                level = parent.level + 1

        node = OBSNode(
            id=uuid4(),
            tenant_id=self.tenant_id,
            project_id=data.project_id,
            parent_id=data.parent_id,
            code=data.code,
            name=data.name,
            description=data.description,
            node_type=data.node_type,
            level=level,
            manager_id=data.manager_id,
            org_unit_id=data.org_unit_id,
            responsibilities=data.responsibilities,
            sort_order=data.sort_order,
            metadata=data.metadata or {},
        )

        saved = self._repo.save(node)

        _publish_event(OBSNodeCreated(
            tenant_id=self.tenant_id,
            project_id=saved.project_id,
            node_id=saved.id,
            code=saved.code,
            name=saved.name,
            node_type=saved.node_type,
        ))

        return self._to_response(saved)

    def update_node(self, data: UpdateOBSNodeDTO) -> Optional[OBSNodeResponseDTO]:
        """به‌روزرسانی گره OBS."""
        node = self._repo.get_by_id(data.id)
        if not node:
            return None

        if data.name is not None:
            node.name = data.name
        if data.description is not None:
            node.description = data.description
        if data.node_type is not None:
            node.node_type = data.node_type
        if data.parent_id is not None:
            new_level = 0
            if data.parent_id:
                parent = self._repo.get_by_id(data.parent_id)
                if parent:
                    new_level = parent.level + 1
            node.move_to_parent(data.parent_id, new_level)
        if data.level is not None and data.parent_id is None:
            node.level = data.level
        if data.manager_id is not None:
            node.set_manager(data.manager_id)
        if data.org_unit_id is not None:
            node.org_unit_id = data.org_unit_id
        if data.responsibilities is not None:
            node.responsibilities = data.responsibilities
        if data.is_active is not None:
            if data.is_active:
                node.activate()
            else:
                node.deactivate()
        if data.sort_order is not None:
            node.sort_order = data.sort_order
        if data.metadata is not None:
            node.metadata = data.metadata

        saved = self._repo.save(node)

        _publish_event(OBSNodeUpdated(
            tenant_id=self.tenant_id,
            project_id=saved.project_id,
            node_id=saved.id,
            name=saved.name,
        ))

        return self._to_response(saved)

    def delete_node(self, node_id: UUID) -> bool:
        """حذف گره OBS."""
        node = self._repo.get_by_id(node_id)
        if not node:
            return False

        result = self._repo.delete(node_id)

        if result:
            _publish_event(OBSNodeDeleted(
                tenant_id=self.tenant_id,
                project_id=node.project_id,
                node_id=node.id,
                code=node.code,
            ))

        return result

    def get_node(self, node_id: UUID) -> Optional[OBSNodeResponseDTO]:
        """دریافت گره."""
        node = self._repo.get_by_id(node_id)
        if not node:
            return None
        return self._to_response(node)

    def list_nodes(self, project_id: UUID) -> List[OBSNodeResponseDTO]:
        """لیست همه گره‌های پروژه."""
        nodes = self._repo.find_by_project(project_id)
        return [self._to_response(n) for n in nodes]

    def get_tree(self, project_id: UUID) -> List[OBSNodeResponseDTO]:
        """دریافت گره‌های ریشه (برای ساخت درخت در فرانت)."""
        roots = self._repo.find_root_nodes(project_id)
        return [self._to_response(n) for n in roots]

    def get_children(self, parent_id: UUID) -> List[OBSNodeResponseDTO]:
        """دریافت فرزندان یک گره."""
        children = self._repo.find_children(parent_id)
        return [self._to_response(n) for n in children]

    def _to_response(self, n: OBSNode) -> OBSNodeResponseDTO:
        return OBSNodeResponseDTO(
            id=n.id, project_id=n.project_id,
            parent_id=n.parent_id, code=n.code,
            name=n.name, description=n.description,
            node_type=n.node_type, level=n.level,
            manager_id=n.manager_id, org_unit_id=n.org_unit_id,
            responsibilities=n.responsibilities,
            is_active=n.is_active, sort_order=n.sort_order,
            created_at=n.created_at, updated_at=n.updated_at,
        )


# ═══════════════════════════════════════════════════
# RBS Service
# ═══════════════════════════════════════════════════

class RBSService:
    """سرویس ساختار شکست منابع (RBS)."""

    def __init__(self, tenant_id: UUID):
        self.tenant_id = tenant_id
        self._repo = DjangoRBSNodeRepository(tenant_id)

    def create_node(self, data: CreateRBSNodeDTO) -> RBSNodeResponseDTO:
        """ایجاد گره جدید RBS."""
        level = 0
        if data.parent_id:
            parent = self._repo.get_by_id(data.parent_id)
            if parent:
                level = parent.level + 1

        node = RBSNode(
            id=uuid4(),
            tenant_id=self.tenant_id,
            project_id=data.project_id,
            parent_id=data.parent_id,
            code=data.code,
            name=data.name,
            description=data.description,
            node_type=data.node_type,
            level=level,
            resource_type=data.resource_type,
            unit=data.unit,
            default_rate=data.default_rate,
            sort_order=data.sort_order,
            metadata=data.metadata or {},
        )

        saved = self._repo.save(node)

        _publish_event(RBSNodeCreated(
            tenant_id=self.tenant_id,
            project_id=saved.project_id,
            node_id=saved.id,
            code=saved.code,
            name=saved.name,
            node_type=saved.node_type,
        ))

        return self._to_response(saved)

    def update_node(self, data: UpdateRBSNodeDTO) -> Optional[RBSNodeResponseDTO]:
        """به‌روزرسانی گره RBS."""
        node = self._repo.get_by_id(data.id)
        if not node:
            return None

        if data.name is not None:
            node.name = data.name
        if data.description is not None:
            node.description = data.description
        if data.node_type is not None:
            node.node_type = data.node_type
        if data.parent_id is not None:
            new_level = 0
            if data.parent_id:
                parent = self._repo.get_by_id(data.parent_id)
                if parent:
                    new_level = parent.level + 1
            node.move_to_parent(data.parent_id, new_level)
        if data.level is not None and data.parent_id is None:
            node.level = data.level
        if data.resource_type is not None:
            node.resource_type = data.resource_type
        if data.unit is not None:
            node.unit = data.unit
        if data.default_rate is not None:
            node.default_rate = data.default_rate
        if data.is_active is not None:
            if data.is_active:
                node.activate()
            else:
                node.deactivate()
        if data.sort_order is not None:
            node.sort_order = data.sort_order
        if data.metadata is not None:
            node.metadata = data.metadata

        saved = self._repo.save(node)

        _publish_event(RBSNodeUpdated(
            tenant_id=self.tenant_id,
            project_id=saved.project_id,
            node_id=saved.id,
            name=saved.name,
        ))

        return self._to_response(saved)

    def delete_node(self, node_id: UUID) -> bool:
        """حذف گره RBS."""
        node = self._repo.get_by_id(node_id)
        if not node:
            return False

        result = self._repo.delete(node_id)

        if result:
            _publish_event(RBSNodeDeleted(
                tenant_id=self.tenant_id,
                project_id=node.project_id,
                node_id=node.id,
                code=node.code,
            ))

        return result

    def get_node(self, node_id: UUID) -> Optional[RBSNodeResponseDTO]:
        """دریافت گره."""
        node = self._repo.get_by_id(node_id)
        if not node:
            return None
        return self._to_response(node)

    def list_nodes(self, project_id: UUID) -> List[RBSNodeResponseDTO]:
        """لیست همه گره‌های منابع پروژه."""
        nodes = self._repo.find_by_project(project_id)
        return [self._to_response(n) for n in nodes]

    def get_tree(self, project_id: UUID) -> List[RBSNodeResponseDTO]:
        """دریافت گره‌های ریشه (برای ساخت درخت)."""
        roots = self._repo.find_root_nodes(project_id)
        return [self._to_response(n) for n in roots]

    def get_children(self, parent_id: UUID) -> List[RBSNodeResponseDTO]:
        """دریافت فرزندان یک گره."""
        children = self._repo.find_children(parent_id)
        return [self._to_response(n) for n in children]

    def _to_response(self, n: RBSNode) -> RBSNodeResponseDTO:
        return RBSNodeResponseDTO(
            id=n.id, project_id=n.project_id,
            parent_id=n.parent_id, code=n.code,
            name=n.name, description=n.description,
            node_type=n.node_type, level=n.level,
            resource_type=n.resource_type, unit=n.unit,
            default_rate=n.default_rate,
            is_active=n.is_active, sort_order=n.sort_order,
            created_at=n.created_at, updated_at=n.updated_at,
        )


# ═══════════════════════════════════════════════════
# Quality Plan Service
# ═══════════════════════════════════════════════════

class QualityPlanService:
    """سرویس طرح کیفیت."""

    def __init__(self, tenant_id: UUID):
        self.tenant_id = tenant_id
        self._repo = DjangoQualityPlanRepository(tenant_id)

    def create(self, dto: CreateQualityPlanDTO) -> QualityPlanResponseDTO:
        entity = QualityPlan(
            id=uuid4(), tenant_id=self.tenant_id,
            project_id=dto.project_id, name=dto.name,
            description=dto.description, status=dto.status,
            criteria=dto.criteria, standards=dto.standards,
            objectives=dto.objectives, scope=dto.scope,
            quality_manager_id=dto.quality_manager_id,
            effective_date=dto.effective_date,
            review_date=dto.review_date,
            version=dto.version, notes=dto.notes,
            metadata=dto.metadata or {},
        )
        saved = self._repo.save(entity)
        return self._to_response(saved)

    def update(self, dto: UpdateQualityPlanDTO) -> Optional[QualityPlanResponseDTO]:
        entity = self._repo.get_by_id(dto.id)
        if not entity:
            return None
        for field in ('name', 'description', 'status', 'criteria', 'standards',
                      'objectives', 'scope', 'quality_manager_id',
                      'effective_date', 'review_date', 'version', 'notes', 'metadata'):
            val = getattr(dto, field, None)
            if val is not None:
                setattr(entity, field, val)
        saved = self._repo.save(entity)
        return self._to_response(saved)

    def delete(self, plan_id: UUID) -> bool:
        return self._repo.delete(plan_id)

    def get(self, plan_id: UUID) -> Optional[QualityPlanResponseDTO]:
        entity = self._repo.get_by_id(plan_id)
        return self._to_response(entity) if entity else None

    def list_by_project(self, project_id: UUID) -> List[QualityPlanResponseDTO]:
        return [self._to_response(e) for e in self._repo.find_by_project(project_id)]

    def _to_response(self, e: QualityPlan) -> QualityPlanResponseDTO:
        return QualityPlanResponseDTO(
            id=e.id, project_id=e.project_id,
            name=e.name, description=e.description,
            status=e.status, criteria=e.criteria,
            standards=e.standards, objectives=e.objectives,
            scope=e.scope, quality_manager_id=e.quality_manager_id,
            effective_date=e.effective_date, review_date=e.review_date,
            version=e.version, notes=e.notes,
            created_at=e.created_at, updated_at=e.updated_at,
        )


# ═══════════════════════════════════════════════════
# Inspection Service
# ═══════════════════════════════════════════════════

class InspectionService:
    """سرویس بازرسی."""

    def __init__(self, tenant_id: UUID):
        self.tenant_id = tenant_id
        self._repo = DjangoInspectionRepository(tenant_id)

    def create(self, dto: CreateInspectionDTO) -> InspectionResponseDTO:
        entity = Inspection(
            id=uuid4(), tenant_id=self.tenant_id,
            project_id=dto.project_id, code=dto.code,
            title=dto.title, description=dto.description,
            inspection_type=dto.inspection_type, status=dto.status,
            result=dto.result, quality_plan_id=dto.quality_plan_id,
            task_id=dto.task_id, inspector_id=dto.inspector_id,
            findings=dto.findings, recommendations=dto.recommendations,
            planned_date=dto.planned_date, actual_date=dto.actual_date,
            notes=dto.notes, metadata=dto.metadata or {},
        )
        saved = self._repo.save(entity)
        return self._to_response(saved)

    def update(self, dto: UpdateInspectionDTO) -> Optional[InspectionResponseDTO]:
        entity = self._repo.get_by_id(dto.id)
        if not entity:
            return None
        for field in ('title', 'description', 'inspection_type', 'status', 'result',
                      'quality_plan_id', 'task_id', 'inspector_id',
                      'findings', 'recommendations', 'planned_date', 'actual_date',
                      'notes', 'metadata'):
            val = getattr(dto, field, None)
            if val is not None:
                setattr(entity, field, val)
        saved = self._repo.save(entity)

        if saved.status == 'completed':
            _publish_event(InspectionCompleted(
                tenant_id=self.tenant_id,
                project_id=saved.project_id,
                inspection_id=saved.id,
                code=saved.code,
                result=saved.result,
            ))

        return self._to_response(saved)

    def delete(self, inspection_id: UUID) -> bool:
        return self._repo.delete(inspection_id)

    def get(self, inspection_id: UUID) -> Optional[InspectionResponseDTO]:
        entity = self._repo.get_by_id(inspection_id)
        return self._to_response(entity) if entity else None

    def list_by_project(self, project_id: UUID) -> List[InspectionResponseDTO]:
        return [self._to_response(e) for e in self._repo.find_by_project(project_id)]

    def _to_response(self, e: Inspection) -> InspectionResponseDTO:
        return InspectionResponseDTO(
            id=e.id, project_id=e.project_id,
            code=e.code, title=e.title,
            description=e.description,
            inspection_type=e.inspection_type,
            status=e.status, result=e.result,
            quality_plan_id=e.quality_plan_id,
            task_id=e.task_id, inspector_id=e.inspector_id,
            findings=e.findings, recommendations=e.recommendations,
            planned_date=e.planned_date, actual_date=e.actual_date,
            notes=e.notes,
            created_at=e.created_at, updated_at=e.updated_at,
        )


# ═══════════════════════════════════════════════════
# NCR Service
# ═══════════════════════════════════════════════════

class NCRService:
    """سرویس عدم انطباق (NCR)."""

    def __init__(self, tenant_id: UUID):
        self.tenant_id = tenant_id
        self._repo = DjangoNCRRepository(tenant_id)

    def create(self, dto: CreateNCRDTO) -> NCRResponseDTO:
        entity = NCR(
            id=uuid4(), tenant_id=self.tenant_id,
            project_id=dto.project_id, code=dto.code,
            title=dto.title, description=dto.description,
            severity=dto.severity, status=dto.status,
            inspection_id=dto.inspection_id, task_id=dto.task_id,
            reported_by_id=dto.reported_by_id,
            assigned_to_id=dto.assigned_to_id,
            root_cause=dto.root_cause,
            corrective_action=dto.corrective_action,
            preventive_action=dto.preventive_action,
            cost_impact=dto.cost_impact,
            schedule_impact_days=dto.schedule_impact_days,
            identified_date=dto.identified_date,
            due_date=dto.due_date,
            notes=dto.notes, metadata=dto.metadata or {},
        )
        saved = self._repo.save(entity)

        _publish_event(NCRCreated(
            tenant_id=self.tenant_id,
            project_id=saved.project_id,
            ncr_id=saved.id,
            code=saved.code,
            severity=saved.severity,
        ))

        return self._to_response(saved)

    def update(self, dto: UpdateNCRDTO) -> Optional[NCRResponseDTO]:
        entity = self._repo.get_by_id(dto.id)
        if not entity:
            return None
        for field in ('title', 'description', 'severity', 'status',
                      'inspection_id', 'task_id', 'assigned_to_id',
                      'root_cause', 'corrective_action', 'preventive_action',
                      'cost_impact', 'schedule_impact_days', 'due_date',
                      'resolved_date', 'closed_date', 'notes', 'metadata'):
            val = getattr(dto, field, None)
            if val is not None:
                setattr(entity, field, val)
        saved = self._repo.save(entity)

        if saved.status == 'resolved':
            _publish_event(NCRResolved(
                tenant_id=self.tenant_id,
                project_id=saved.project_id,
                ncr_id=saved.id,
                code=saved.code,
            ))

        return self._to_response(saved)

    def delete(self, ncr_id: UUID) -> bool:
        return self._repo.delete(ncr_id)

    def get(self, ncr_id: UUID) -> Optional[NCRResponseDTO]:
        entity = self._repo.get_by_id(ncr_id)
        return self._to_response(entity) if entity else None

    def list_by_project(self, project_id: UUID) -> List[NCRResponseDTO]:
        return [self._to_response(e) for e in self._repo.find_by_project(project_id)]

    def list_open_by_project(self, project_id: UUID) -> List[NCRResponseDTO]:
        return [self._to_response(e) for e in self._repo.find_open_by_project(project_id)]

    def _to_response(self, e: NCR) -> NCRResponseDTO:
        return NCRResponseDTO(
            id=e.id, project_id=e.project_id,
            code=e.code, title=e.title,
            description=e.description,
            severity=e.severity, status=e.status,
            inspection_id=e.inspection_id, task_id=e.task_id,
            reported_by_id=e.reported_by_id,
            assigned_to_id=e.assigned_to_id,
            root_cause=e.root_cause,
            corrective_action=e.corrective_action,
            preventive_action=e.preventive_action,
            cost_impact=e.cost_impact,
            schedule_impact_days=e.schedule_impact_days,
            identified_date=e.identified_date,
            due_date=e.due_date,
            resolved_date=e.resolved_date,
            closed_date=e.closed_date,
            notes=e.notes,
            created_at=e.created_at, updated_at=e.updated_at,
        )


# ═══════════════════════════════════════════════════
# Quality Checklist Service
# ═══════════════════════════════════════════════════

class QualityChecklistService:
    """سرویس چک‌لیست کیفیت."""

    def __init__(self, tenant_id: UUID):
        self.tenant_id = tenant_id
        self._repo = DjangoQualityChecklistRepository(tenant_id)
        self._item_repo = DjangoQualityChecklistItemRepository(tenant_id)

    def create(self, dto: CreateQualityChecklistDTO) -> QualityChecklistResponseDTO:
        entity = QualityChecklist(
            id=uuid4(), tenant_id=self.tenant_id,
            project_id=dto.project_id, name=dto.name,
            description=dto.description,
            quality_plan_id=dto.quality_plan_id,
            task_id=dto.task_id,
            notes=dto.notes, metadata=dto.metadata or {},
        )
        saved = self._repo.save(entity)
        return self._to_response(saved)

    def update(self, dto: UpdateQualityChecklistDTO) -> Optional[QualityChecklistResponseDTO]:
        entity = self._repo.get_by_id(dto.id)
        if not entity:
            return None
        for field in ('name', 'description', 'quality_plan_id', 'task_id',
                      'is_completed', 'completed_by_id', 'notes', 'metadata'):
            val = getattr(dto, field, None)
            if val is not None:
                setattr(entity, field, val)
        saved = self._repo.save(entity)
        return self._to_response(saved)

    def delete(self, checklist_id: UUID) -> bool:
        return self._repo.delete(checklist_id)

    def get(self, checklist_id: UUID) -> Optional[QualityChecklistResponseDTO]:
        entity = self._repo.get_by_id(checklist_id)
        return self._to_response(entity) if entity else None

    def list_by_project(self, project_id: UUID) -> List[QualityChecklistResponseDTO]:
        return [self._to_response(e) for e in self._repo.find_by_project(project_id)]

    # --- Checklist Item methods ---

    def create_item(self, dto: CreateQualityChecklistItemDTO) -> QualityChecklistItemResponseDTO:
        entity = QualityChecklistItem(
            id=uuid4(), tenant_id=self.tenant_id,
            checklist_id=dto.checklist_id, title=dto.title,
            description=dto.description, sort_order=dto.sort_order,
        )
        saved = self._item_repo.save(entity)
        return self._item_to_response(saved)

    def update_item(self, dto: UpdateQualityChecklistItemDTO) -> Optional[QualityChecklistItemResponseDTO]:
        entity = self._item_repo.get_by_id(dto.id)
        if not entity:
            return None
        for field in ('title', 'description', 'sort_order', 'is_checked',
                      'checked_by_id', 'result', 'remarks'):
            val = getattr(dto, field, None)
            if val is not None:
                setattr(entity, field, val)
        saved = self._item_repo.save(entity)
        return self._item_to_response(saved)

    def delete_item(self, item_id: UUID) -> bool:
        return self._item_repo.delete(item_id)

    def list_items(self, checklist_id: UUID) -> List[QualityChecklistItemResponseDTO]:
        return [self._item_to_response(e) for e in self._item_repo.find_by_checklist(checklist_id)]

    def _to_response(self, e: QualityChecklist) -> QualityChecklistResponseDTO:
        return QualityChecklistResponseDTO(
            id=e.id, project_id=e.project_id,
            name=e.name, description=e.description,
            quality_plan_id=e.quality_plan_id,
            task_id=e.task_id,
            is_completed=e.is_completed,
            completed_date=e.completed_date,
            completed_by_id=e.completed_by_id,
            notes=e.notes,
            created_at=e.created_at, updated_at=e.updated_at,
        )

    def _item_to_response(self, e: QualityChecklistItem) -> QualityChecklistItemResponseDTO:
        return QualityChecklistItemResponseDTO(
            id=e.id, checklist_id=e.checklist_id,
            title=e.title, description=e.description,
            sort_order=e.sort_order,
            is_checked=e.is_checked,
            checked_by_id=e.checked_by_id,
            checked_date=e.checked_date,
            result=e.result, remarks=e.remarks,
            created_at=e.created_at, updated_at=e.updated_at,
        )


# ═══════════════════════════════════════════════════
# Project Document Service — ارتباط اسناد با پروژه
# ═══════════════════════════════════════════════════

class ProjectDocumentService:
    """سرویس مدیریت ارتباط اسناد با پروژه."""

    def __init__(self, tenant_id: UUID):
        self._tenant_id = tenant_id
        self._repo = DjangoProjectDocumentRepository(tenant_id)

    # ── Link ──
    def link_document(
        self, dto: ProjectDocumentCreateDTO, linked_by: UUID | None = None,
    ) -> ProjectDocumentResponseDTO:
        entity = ProjectDocument(
            id=uuid4(), tenant_id=self._tenant_id,
            project_id=dto.project_id,
            document_id=dto.document_id,
            entity_type=dto.entity_type,
            entity_id=dto.entity_id,
            title=dto.title,
            document_code=dto.document_code,
            linked_by=linked_by,
            notes=dto.notes,
        )
        saved = self._repo.save(entity)

        from ...domain.events.pm_events import DocumentLinked
        _publish_event(DocumentLinked(
            tenant_id=str(self._tenant_id),
            project_id=str(dto.project_id),
            document_id=str(dto.document_id),
            entity_type=dto.entity_type,
            entity_id=str(dto.entity_id) if dto.entity_id else None,
        ))
        return self._to_response(saved)

    # ── Unlink ──
    def unlink_document(self, project_document_id: UUID) -> bool:
        entity = self._repo.get_by_id(project_document_id)
        if not entity:
            return False
        result = self._repo.delete(project_document_id)
        if result:
            from ...domain.events.pm_events import DocumentUnlinked
            _publish_event(DocumentUnlinked(
                tenant_id=str(self._tenant_id),
                project_id=str(entity.project_id),
                document_id=str(entity.document_id),
                entity_type=entity.entity_type,
                entity_id=str(entity.entity_id) if entity.entity_id else None,
            ))
        return result

    # ── Query ──
    def list_by_project(self, project_id: UUID) -> List[ProjectDocumentResponseDTO]:
        entities = self._repo.find_by_project(project_id)
        return [self._to_response(e) for e in entities]

    def list_by_entity(self, entity_type: str, entity_id: UUID) -> List[ProjectDocumentResponseDTO]:
        entities = self._repo.find_by_entity(entity_type, entity_id)
        return [self._to_response(e) for e in entities]

    def get_by_id(self, project_document_id: UUID) -> Optional[ProjectDocumentResponseDTO]:
        entity = self._repo.get_by_id(project_document_id)
        return self._to_response(entity) if entity else None

    # ── Mapper ──
    def _to_response(self, e: ProjectDocument) -> ProjectDocumentResponseDTO:
        return ProjectDocumentResponseDTO(
            id=e.id, project_id=e.project_id,
            document_id=e.document_id,
            entity_type=e.entity_type,
            entity_id=e.entity_id,
            title=e.title,
            document_code=e.document_code,
            linked_by=e.linked_by,
            notes=e.notes,
            created_at=e.created_at,
            updated_at=e.updated_at,
        )

