"""
Strategy Application Service — KPI Service.
"""

from __future__ import annotations

import uuid
from typing import List, Optional
from uuid import UUID

from apps.core.event_bus.events import event_bus

from ...domain.entities.kpi_definition import KPIDefinition
from ...domain.entities.kpi_target import KPITarget
from ...domain.entities.kpi_actual import KPIActual
from ...domain.entities.kpi_assignment import KPIAssignment
from ...domain.events.strategy_events import (
    KPIDefinitionCreated, KPIDefinitionActivated, KPIDefinitionArchived,
    KPITargetSet, KPIActualRecorded, KPIAssigned,
)
from ...domain.services.kpi_cascade_engine import KPICascadeEngine, TrendPoint
from ...infrastructure.repositories import (
    DjangoKPIDefinitionRepository,
    DjangoKPITargetRepository,
    DjangoKPIActualRepository,
    DjangoKPIAssignmentRepository,
)
from ..dtos.strategy_dtos import (
    KPIDefinitionCreateDTO, KPIDefinitionUpdateDTO, KPIDefinitionResponseDTO,
    KPITargetCreateDTO, KPITargetUpdateDTO, KPITargetResponseDTO,
    KPIActualCreateDTO, KPIActualUpdateDTO, KPIActualResponseDTO,
    KPIAssignmentCreateDTO, KPIAssignmentUpdateDTO, KPIAssignmentResponseDTO,
    KPIScoreDTO, KPITrendDTO,
)


class KPIService:
    """سرویس مدیریت شاخص‌های کلیدی عملکرد."""

    def __init__(self):
        self.kpi_repo = DjangoKPIDefinitionRepository()
        self.target_repo = DjangoKPITargetRepository()
        self.actual_repo = DjangoKPIActualRepository()
        self.assignment_repo = DjangoKPIAssignmentRepository()

    # ── KPI Definition CRUD ──────────────────────

    def create_kpi(self, dto: KPIDefinitionCreateDTO) -> KPIDefinitionResponseDTO:
        entity = KPIDefinition(
            id=uuid.uuid4(),
            tenant_id=dto.tenant_id,
            name=dto.name,
            code=dto.code,
            description=dto.description,
            category=dto.category,
            unit_of_measure=dto.unit_of_measure,
            direction=dto.direction,
            frequency=dto.frequency,
            data_source=dto.data_source,
            formula=dto.formula,
            threshold_red=dto.threshold_red,
            threshold_yellow=dto.threshold_yellow,
            threshold_green=dto.threshold_green,
            owner_id=dto.owner_id,
            responsible_role=dto.responsible_role,
            tags=dto.tags or [],
            metadata=dto.metadata or {},
        )
        saved = self.kpi_repo.save(entity)

        event_bus.publish(KPIDefinitionCreated(
            tenant_id=saved.tenant_id,
            kpi_id=saved.id,
            name=saved.name,
            code=saved.code,
        ))

        return self._kpi_to_response(saved)

    def get_kpi(self, kpi_id: UUID) -> Optional[KPIDefinitionResponseDTO]:
        entity = self.kpi_repo.find_by_id(kpi_id)
        return self._kpi_to_response(entity) if entity else None

    def list_kpis(self, tenant_id: UUID) -> List[KPIDefinitionResponseDTO]:
        entities = self.kpi_repo.find_by_tenant(tenant_id)
        return [self._kpi_to_response(e) for e in entities]

    def update_kpi(self, kpi_id: UUID, dto: KPIDefinitionUpdateDTO) -> Optional[KPIDefinitionResponseDTO]:
        entity = self.kpi_repo.find_by_id(kpi_id)
        if not entity:
            return None
        for field_name in [
            "name", "code", "description", "category", "unit_of_measure",
            "direction", "frequency", "data_source", "formula",
            "threshold_red", "threshold_yellow", "threshold_green",
            "owner_id", "responsible_role", "tags", "metadata",
        ]:
            value = getattr(dto, field_name, None)
            if value is not None:
                setattr(entity, field_name, value)
        saved = self.kpi_repo.save(entity)
        return self._kpi_to_response(saved)

    def delete_kpi(self, kpi_id: UUID) -> bool:
        return self.kpi_repo.delete(kpi_id)

    def activate_kpi(self, kpi_id: UUID) -> Optional[KPIDefinitionResponseDTO]:
        entity = self.kpi_repo.find_by_id(kpi_id)
        if not entity:
            return None
        entity.activate()
        saved = self.kpi_repo.save(entity)
        event_bus.publish(KPIDefinitionActivated(tenant_id=saved.tenant_id, kpi_id=saved.id))
        return self._kpi_to_response(saved)

    def archive_kpi(self, kpi_id: UUID) -> Optional[KPIDefinitionResponseDTO]:
        entity = self.kpi_repo.find_by_id(kpi_id)
        if not entity:
            return None
        entity.archive()
        saved = self.kpi_repo.save(entity)
        event_bus.publish(KPIDefinitionArchived(tenant_id=saved.tenant_id, kpi_id=saved.id))
        return self._kpi_to_response(saved)

    # ── KPI Target CRUD ──────────────────────────

    def create_target(self, dto: KPITargetCreateDTO) -> KPITargetResponseDTO:
        entity = KPITarget(
            id=uuid.uuid4(),
            tenant_id=dto.tenant_id,
            kpi_id=dto.kpi_id,
            period_start=dto.period_start,
            period_end=dto.period_end,
            target_value=dto.target_value,
            stretch_value=dto.stretch_value,
            notes=dto.notes,
            metadata=dto.metadata or {},
        )
        saved = self.target_repo.save(entity)
        event_bus.publish(KPITargetSet(
            tenant_id=saved.tenant_id,
            kpi_target_id=saved.id,
            kpi_id=saved.kpi_id,
            target_value=str(saved.target_value),
        ))
        return self._target_to_response(saved)

    def get_target(self, target_id: UUID) -> Optional[KPITargetResponseDTO]:
        entity = self.target_repo.find_by_id(target_id)
        return self._target_to_response(entity) if entity else None

    def list_targets_by_kpi(self, kpi_id: UUID) -> List[KPITargetResponseDTO]:
        entities = self.target_repo.find_by_kpi(kpi_id)
        return [self._target_to_response(e) for e in entities]

    def update_target(self, target_id: UUID, dto: KPITargetUpdateDTO) -> Optional[KPITargetResponseDTO]:
        entity = self.target_repo.find_by_id(target_id)
        if not entity:
            return None
        for f in ["target_value", "stretch_value", "period_start", "period_end", "notes", "metadata"]:
            value = getattr(dto, f, None)
            if value is not None:
                setattr(entity, f, value)
        saved = self.target_repo.save(entity)
        return self._target_to_response(saved)

    def delete_target(self, target_id: UUID) -> bool:
        return self.target_repo.delete(target_id)

    # ── KPI Actual CRUD ──────────────────────────

    def record_actual(self, dto: KPIActualCreateDTO) -> KPIActualResponseDTO:
        entity = KPIActual(
            id=uuid.uuid4(),
            tenant_id=dto.tenant_id,
            kpi_id=dto.kpi_id,
            period_start=dto.period_start,
            period_end=dto.period_end,
            actual_value=dto.actual_value,
            notes=dto.notes,
            source=dto.source,
            recorded_by=dto.recorded_by,
            metadata=dto.metadata or {},
        )
        saved = self.actual_repo.save(entity)
        event_bus.publish(KPIActualRecorded(
            tenant_id=saved.tenant_id,
            kpi_actual_id=saved.id,
            kpi_id=saved.kpi_id,
            actual_value=str(saved.actual_value),
        ))
        return self._actual_to_response(saved)

    def get_actual(self, actual_id: UUID) -> Optional[KPIActualResponseDTO]:
        entity = self.actual_repo.find_by_id(actual_id)
        return self._actual_to_response(entity) if entity else None

    def list_actuals_by_kpi(self, kpi_id: UUID) -> List[KPIActualResponseDTO]:
        entities = self.actual_repo.find_by_kpi(kpi_id)
        return [self._actual_to_response(e) for e in entities]

    def update_actual(self, actual_id: UUID, dto: KPIActualUpdateDTO) -> Optional[KPIActualResponseDTO]:
        entity = self.actual_repo.find_by_id(actual_id)
        if not entity:
            return None
        for f in ["actual_value", "period_start", "period_end", "notes", "source", "metadata"]:
            value = getattr(dto, f, None)
            if value is not None:
                setattr(entity, f, value)
        saved = self.actual_repo.save(entity)
        return self._actual_to_response(saved)

    def delete_actual(self, actual_id: UUID) -> bool:
        return self.actual_repo.delete(actual_id)

    # ── KPI Assignment CRUD ──────────────────────

    def assign_kpi(self, dto: KPIAssignmentCreateDTO) -> KPIAssignmentResponseDTO:
        entity = KPIAssignment(
            id=uuid.uuid4(),
            tenant_id=dto.tenant_id,
            kpi_id=dto.kpi_id,
            level=dto.level,
            entity_id=dto.entity_id,
            weight=dto.weight,
            notes=dto.notes,
            metadata=dto.metadata or {},
        )
        saved = self.assignment_repo.save(entity)
        event_bus.publish(KPIAssigned(
            tenant_id=saved.tenant_id,
            assignment_id=saved.id,
            kpi_id=saved.kpi_id,
            level=saved.level,
            entity_id=saved.entity_id,
        ))
        return self._assignment_to_response(saved)

    def get_assignment(self, assignment_id: UUID) -> Optional[KPIAssignmentResponseDTO]:
        entity = self.assignment_repo.find_by_id(assignment_id)
        return self._assignment_to_response(entity) if entity else None

    def list_assignments_by_kpi(self, kpi_id: UUID) -> List[KPIAssignmentResponseDTO]:
        entities = self.assignment_repo.find_by_kpi(kpi_id)
        return [self._assignment_to_response(e) for e in entities]

    def list_assignments_by_entity(self, level: str, entity_id: UUID) -> List[KPIAssignmentResponseDTO]:
        entities = self.assignment_repo.find_by_entity(level, entity_id)
        return [self._assignment_to_response(e) for e in entities]

    def update_assignment(self, assignment_id: UUID, dto: KPIAssignmentUpdateDTO) -> Optional[KPIAssignmentResponseDTO]:
        entity = self.assignment_repo.find_by_id(assignment_id)
        if not entity:
            return None
        for f in ["level", "entity_id", "weight", "notes", "metadata"]:
            value = getattr(dto, f, None)
            if value is not None:
                setattr(entity, f, value)
        saved = self.assignment_repo.save(entity)
        return self._assignment_to_response(saved)

    def deactivate_assignment(self, assignment_id: UUID) -> Optional[KPIAssignmentResponseDTO]:
        entity = self.assignment_repo.find_by_id(assignment_id)
        if not entity:
            return None
        entity.deactivate()
        saved = self.assignment_repo.save(entity)
        return self._assignment_to_response(saved)

    def delete_assignment(self, assignment_id: UUID) -> bool:
        return self.assignment_repo.delete(assignment_id)

    # ── KPI Score & Trend ────────────────────────

    def calculate_score(self, kpi_id: UUID, period_start=None, period_end=None) -> Optional[KPIScoreDTO]:
        """محاسبه امتیاز KPI برای یک دوره."""
        kpi = self.kpi_repo.find_by_id(kpi_id)
        if not kpi:
            return None

        # Get target and actual for the period
        targets = self.target_repo.find_by_kpi(kpi_id)
        actuals = self.actual_repo.find_by_kpi(kpi_id)

        if period_start and period_end:
            targets = [t for t in targets if t.period_start == period_start and t.period_end == period_end]
            actuals = [a for a in actuals if a.period_start == period_start and a.period_end == period_end]

        target_value = targets[0].target_value if targets else 0
        actual_value = sum(a.actual_value for a in actuals) if actuals else 0

        score = KPICascadeEngine.calculate_score(
            actual_value=actual_value,
            target_value=target_value,
            direction=kpi.direction,
            threshold_red=kpi.threshold_red,
            threshold_yellow=kpi.threshold_yellow,
            threshold_green=kpi.threshold_green,
        )

        return KPIScoreDTO(
            kpi_id=kpi_id,
            actual_value=score.actual_value,
            target_value=score.target_value,
            achievement_rate=score.achievement_rate,
            rag_status=score.rag_status,
        )

    def calculate_trend(self, kpi_id: UUID) -> Optional[KPITrendDTO]:
        """تحلیل روند KPI از مقادیر واقعی."""
        kpi = self.kpi_repo.find_by_id(kpi_id)
        if not kpi:
            return None

        actuals = self.actual_repo.find_by_kpi(kpi_id)
        if not actuals:
            return KPITrendDTO(kpi_id=kpi_id, slope=0.0, direction="stable")

        # Sort by period and create trend points
        sorted_actuals = sorted(actuals, key=lambda a: (a.period_start or a.created_at))
        points = [
            TrendPoint(
                period=str(a.period_start) if a.period_start else str(a.created_at),
                value=a.actual_value,
            )
            for a in sorted_actuals
        ]

        result = KPICascadeEngine.trend_analysis(points)

        return KPITrendDTO(
            kpi_id=kpi_id,
            slope=result.slope,
            direction=result.direction,
            forecast_next=result.forecast_next,
            points=[{"period": p.period, "value": p.value} for p in result.points],
        )

    def cascade_targets(self, kpi_id: UUID, child_weights: list[float]) -> list[float]:
        """انتشار هدف KPI به سطوح پایین‌تر."""
        targets = self.target_repo.find_by_kpi(kpi_id)
        if not targets:
            return [0.0] * len(child_weights)
        parent_target = targets[0].target_value
        return KPICascadeEngine.cascade_targets(parent_target, child_weights)

    # ── Private Mappers ──────────────────────────

    @staticmethod
    def _kpi_to_response(entity: KPIDefinition) -> KPIDefinitionResponseDTO:
        return KPIDefinitionResponseDTO(
            id=entity.id,
            tenant_id=entity.tenant_id,
            name=entity.name,
            code=entity.code,
            description=entity.description,
            category=entity.category,
            unit_of_measure=entity.unit_of_measure,
            direction=entity.direction,
            frequency=entity.frequency,
            data_source=entity.data_source,
            formula=entity.formula,
            threshold_red=entity.threshold_red,
            threshold_yellow=entity.threshold_yellow,
            threshold_green=entity.threshold_green,
            owner_id=entity.owner_id,
            responsible_role=entity.responsible_role,
            status=entity.status,
            tags=entity.tags,
            metadata=entity.metadata,
            created_at=entity.created_at,
            updated_at=entity.updated_at,
        )

    @staticmethod
    def _target_to_response(entity: KPITarget) -> KPITargetResponseDTO:
        return KPITargetResponseDTO(
            id=entity.id,
            tenant_id=entity.tenant_id,
            kpi_id=entity.kpi_id,
            period_start=entity.period_start,
            period_end=entity.period_end,
            target_value=entity.target_value,
            stretch_value=entity.stretch_value,
            notes=entity.notes,
            metadata=entity.metadata,
            created_at=entity.created_at,
            updated_at=entity.updated_at,
        )

    @staticmethod
    def _actual_to_response(entity: KPIActual) -> KPIActualResponseDTO:
        return KPIActualResponseDTO(
            id=entity.id,
            tenant_id=entity.tenant_id,
            kpi_id=entity.kpi_id,
            period_start=entity.period_start,
            period_end=entity.period_end,
            actual_value=entity.actual_value,
            notes=entity.notes,
            source=entity.source,
            recorded_by=entity.recorded_by,
            metadata=entity.metadata,
            created_at=entity.created_at,
            updated_at=entity.updated_at,
        )

    @staticmethod
    def _assignment_to_response(entity: KPIAssignment) -> KPIAssignmentResponseDTO:
        return KPIAssignmentResponseDTO(
            id=entity.id,
            tenant_id=entity.tenant_id,
            kpi_id=entity.kpi_id,
            level=entity.level,
            entity_id=entity.entity_id,
            weight=entity.weight,
            status=entity.status,
            notes=entity.notes,
            metadata=entity.metadata,
            created_at=entity.created_at,
            updated_at=entity.updated_at,
        )
