"""
Analytics Service — Data Aggregation Services.

SnapshotEngine, RollupEngine, TrendCalculator.
"""

import logging
import time
from datetime import date, timedelta
from decimal import Decimal
from typing import Dict, List, Optional, Any
from uuid import UUID

from django.db.models import Avg, Count, Max, Min, Sum, Q
from django.utils import timezone

from .models import (
    AggregationDefinition, AggregationSnapshot,
    KPIRollup, TrendDirection,
)

logger = logging.getLogger(__name__)


class SnapshotEngine:
    """موتور اسنپ‌شات — استخراج دوره‌ای داده از ماژول‌ها."""

    AGGREGATION_FUNCS = {
        "count": Count,
        "sum": Sum,
        "average": Avg,
        "min": Min,
        "max": Max,
    }

    def take_snapshot(
        self,
        definition: AggregationDefinition,
        snapshot_date: Optional[date] = None,
    ) -> AggregationSnapshot:
        """اجرای تجمیع و ذخیره اسنپ‌شات."""
        if snapshot_date is None:
            snapshot_date = timezone.now().date()

        start_time = time.monotonic()

        try:
            data, record_count = self._execute_aggregation(definition)
        except Exception as e:
            logger.error(
                "Snapshot failed for definition=%s: %s",
                definition.id, str(e),
            )
            data = {"error": str(e)}
            record_count = 0

        execution_time_ms = int((time.monotonic() - start_time) * 1000)

        snapshot = AggregationSnapshot.objects.create(
            tenant=definition.tenant,
            definition=definition,
            snapshot_date=snapshot_date,
            data=data,
            record_count=record_count,
            execution_time_ms=execution_time_ms,
        )

        logger.info(
            "Snapshot taken: definition=%s, date=%s, records=%d, time=%dms",
            definition.name, snapshot_date, record_count, execution_time_ms,
        )
        return snapshot

    def take_all_active_snapshots(self, tenant_id: UUID) -> List[AggregationSnapshot]:
        """اسنپ‌شات از تمام تعریف‌های فعال."""
        definitions = AggregationDefinition.objects.filter(
            tenant_id=tenant_id, is_active=True,
        )
        snapshots = []
        for defn in definitions:
            try:
                snap = self.take_snapshot(defn)
                snapshots.append(snap)
            except Exception as e:
                logger.error("Snapshot failed for %s: %s", defn.name, e)
        return snapshots

    def _execute_aggregation(
        self, definition: AggregationDefinition
    ) -> tuple:
        """اجرای واقعی تجمیع — بر اساس ماژول منبع."""
        # Dynamic model resolution based on source_module + source_entity
        model_class = self._resolve_model(
            definition.source_module, definition.source_entity,
        )
        if model_class is None:
            return {"error": f"Model not found: {definition.source_module}.{definition.source_entity}"}, 0

        qs = model_class.objects.filter(tenant=definition.tenant)

        # Apply filter criteria
        if definition.filter_criteria:
            qs = qs.filter(**definition.filter_criteria)

        record_count = qs.count()

        # Apply aggregation
        agg_type = definition.aggregation_type
        agg_field = definition.aggregation_field

        if agg_type == "count":
            result = {"value": record_count}
        elif agg_type in self.AGGREGATION_FUNCS and agg_field:
            func = self.AGGREGATION_FUNCS[agg_type]
            agg_result = qs.aggregate(result=func(agg_field))
            result = {"value": float(agg_result["result"] or 0)}
        else:
            result = {"value": record_count}

        # Apply group_by
        if definition.group_by:
            group_data = list(
                qs.values(*definition.group_by)
                .annotate(count=Count("id"))
                .order_by(*definition.group_by)
            )
            result["groups"] = group_data

        return result, record_count

    @staticmethod
    def _resolve_model(module_name: str, entity_name: str):
        """Dynamic model resolution based on module + entity."""
        from django.apps import apps
        # Try common patterns
        try:
            return apps.get_model(module_name, entity_name)
        except LookupError:
            pass
        # Try with 'Model' suffix
        try:
            return apps.get_model(module_name, f"{entity_name}Model")
        except LookupError:
            pass
        logger.warning("Could not resolve model: %s.%s", module_name, entity_name)
        return None


class RollupEngine:
    """موتور تجمیع KPI — از سطح فردی تا سطح سازمان."""

    def calculate_rollup(
        self,
        tenant_id: UUID,
        kpi_code: str,
        period: str,
        level: str,
        entity_id: Optional[UUID] = None,
        value: Optional[Decimal] = None,
        target_value: Optional[Decimal] = None,
    ) -> KPIRollup:
        """محاسبه و ذخیره تجمیع KPI."""
        rollup, created = KPIRollup.objects.update_or_create(
            tenant_id=tenant_id,
            kpi_code=kpi_code,
            level=level,
            entity_id=entity_id,
            period=period,
            defaults={
                "value": value or Decimal("0"),
                "target_value": target_value,
            },
        )

        # Calculate trend
        if not created and rollup.previous_value is not None:
            rollup.trend = TrendCalculator.calculate_trend_direction(
                rollup.previous_value, rollup.value,
            )
            if rollup.previous_value != 0:
                rollup.change_percentage = (
                    (rollup.value - rollup.previous_value)
                    / rollup.previous_value * 100
                )
            rollup.save()

        return rollup

    def rollup_to_parent(
        self,
        tenant_id: UUID,
        kpi_code: str,
        period: str,
        child_level: str,
        parent_level: str,
        parent_entity_id: Optional[UUID] = None,
    ) -> Optional[KPIRollup]:
        """تجمیع از سطح فرزند به سطح والد (میانگین)."""
        children = KPIRollup.objects.filter(
            tenant_id=tenant_id,
            kpi_code=kpi_code,
            period=period,
            level=child_level,
        )
        if not children.exists():
            return None

        agg = children.aggregate(
            avg_value=Avg("value"),
            total=Count("id"),
        )

        return self.calculate_rollup(
            tenant_id=tenant_id,
            kpi_code=kpi_code,
            period=period,
            level=parent_level,
            entity_id=parent_entity_id,
            value=Decimal(str(agg["avg_value"] or 0)),
        )


class TrendCalculator:
    """محاسبه‌گر روند — slope، moving average، پیش‌بینی."""

    @staticmethod
    def calculate_trend_direction(
        old_value: Decimal, new_value: Decimal, threshold: Decimal = Decimal("0.01"),
    ) -> str:
        """محاسبه جهت روند."""
        if old_value == 0:
            if new_value > 0:
                return TrendDirection.UP
            elif new_value < 0:
                return TrendDirection.DOWN
            return TrendDirection.FLAT

        change_ratio = abs((new_value - old_value) / old_value)
        if change_ratio < threshold:
            return TrendDirection.FLAT
        elif new_value > old_value:
            return TrendDirection.UP
        else:
            return TrendDirection.DOWN

    @staticmethod
    def moving_average(values: List[Decimal], window: int = 3) -> List[Optional[Decimal]]:
        """محاسبه میانگین متحرک."""
        if len(values) < window:
            return [None] * len(values)

        result = [None] * (window - 1)
        for i in range(window - 1, len(values)):
            avg = sum(values[i - window + 1: i + 1]) / window
            result.append(avg)
        return result

    @staticmethod
    def linear_slope(values: List[Decimal]) -> Optional[Decimal]:
        """محاسبه شیب خطی (رگرسیون ساده)."""
        n = len(values)
        if n < 2:
            return None

        x_values = list(range(n))
        x_mean = sum(x_values) / n
        y_mean = sum(values) / n

        numerator = sum(
            (x - x_mean) * (y - y_mean)
            for x, y in zip(x_values, values)
        )
        denominator = sum((x - x_mean) ** 2 for x in x_values)

        if denominator == 0:
            return Decimal("0")

        return Decimal(str(numerator / denominator))

    @staticmethod
    def simple_forecast(values: List[Decimal], periods_ahead: int = 1) -> Optional[Decimal]:
        """پیش‌بینی ساده بر اساس شیب خطی."""
        slope = TrendCalculator.linear_slope(values)
        if slope is None:
            return None
        last_value = values[-1]
        return last_value + slope * periods_ahead
