from __future__ import annotations

from math import sqrt
from statistics import mean
from typing import Any


METRIC_CONFIG = {
    "soil_moisture": {
        "label": "رطوبت خاک",
        "unit": "%",
        "source": "history",
        "current_field": "soil_moisture",
    },
    "soil_temperature": {
        "label": "دمای خاک",
        "unit": "°C",
        "source": "history",
        "current_field": "soil_temperature",
    },
    "humidity": {
        "label": "رطوبت هوا",
        "unit": "%",
        "source": "forecast",
        "forecast_field": "humidity_mean",
    },
    "soil_ph": {
        "label": "pH خاک",
        "unit": "pH",
        "source": "history",
        "current_field": "soil_ph",
    },
    "electrical_conductivity": {
        "label": "هدایت الکتریکی",
        "unit": "dS/m",
        "source": "history",
        "current_field": "electrical_conductivity",
    },
}

METHOD_PRIORITY = {"IQR": 2, "Z_SCORE": 1}


def _percentile(sorted_values: list[float], percentile: float) -> float:
    if not sorted_values:
        return 0.0
    if len(sorted_values) == 1:
        return sorted_values[0]
    index = (len(sorted_values) - 1) * percentile
    lower = int(index)
    upper = min(lower + 1, len(sorted_values) - 1)
    fraction = index - lower
    return sorted_values[lower] + ((sorted_values[upper] - sorted_values[lower]) * fraction)


def _population_std(values: list[float]) -> float:
    if len(values) < 2:
        return 0.0
    center = mean(values)
    variance = sum((value - center) ** 2 for value in values) / len(values)
    return sqrt(variance)


def _severity_from_score(score: float) -> str:
    absolute = abs(score)
    if absolute >= 3.5:
        return "critical"
    if absolute >= 2.5:
        return "high"
    if absolute >= 1.5:
        return "medium"
    return "low"


def _history_series(history: list[Any], field_name: str) -> tuple[list[float], str | None, float | None]:
    values: list[float] = []
    latest_timestamp = None
    latest_value = None

    for item in history:
        value = getattr(item, field_name, None)
        if value is None:
            continue
        numeric = float(value)
        values.append(numeric)
        if latest_timestamp is None:
            recorded_at = getattr(item, "recorded_at", None)
            latest_timestamp = recorded_at.isoformat() if recorded_at is not None else None
            latest_value = numeric

    return list(reversed(values)), latest_timestamp, latest_value


def _forecast_series(forecasts: list[Any], field_name: str) -> tuple[list[float], str | None, float | None]:
    values: list[float] = []
    latest_timestamp = None
    latest_value = None

    for forecast in forecasts[:7]:
        value = getattr(forecast, field_name, None)
        if value is None:
            continue
        numeric = float(value)
        values.append(numeric)
        if latest_timestamp is None:
            forecast_date = getattr(forecast, "forecast_date", None)
            latest_timestamp = forecast_date.isoformat() if forecast_date is not None else None
            latest_value = numeric

    return values, latest_timestamp, latest_value


def _detect_with_z_score(values: list[float], observed_value: float) -> dict[str, Any] | None:
    if len(values) < 5:
        return None
    center = mean(values)
    std = _population_std(values)
    if std == 0:
        return None
    score = (observed_value - center) / std
    if abs(score) < 2.0:
        return None
    return {
        "anomaly_method": "Z_SCORE",
        "deviation_score": round(score, 3),
        "expected_range": [round(center - (2 * std), 2), round(center + (2 * std), 2)],
        "severity": _severity_from_score(score),
    }


def _detect_with_iqr(values: list[float], observed_value: float) -> dict[str, Any] | None:
    if len(values) < 5:
        return None
    sorted_values = sorted(values)
    q1 = _percentile(sorted_values, 0.25)
    q3 = _percentile(sorted_values, 0.75)
    iqr = q3 - q1
    if iqr == 0:
        return None
    lower = q1 - (1.5 * iqr)
    upper = q3 + (1.5 * iqr)
    if lower <= observed_value <= upper:
        return None

    if observed_value < lower:
        score = (observed_value - lower) / iqr
    else:
        score = (observed_value - upper) / iqr

    return {
        "anomaly_method": "IQR",
        "deviation_score": round(score, 3),
        "expected_range": [round(lower, 2), round(upper, 2)],
        "severity": _severity_from_score(score),
    }


def _select_detection_result(results: list[dict[str, Any]]) -> dict[str, Any] | None:
    if not results:
        return None
    return sorted(
        results,
        key=lambda item: (METHOD_PRIORITY[item["anomaly_method"]], abs(item["deviation_score"])),
        reverse=True,
    )[0]


def _build_contextual_interpretation(anomalies: list[dict[str, Any]], ai_bundle: dict | None = None) -> dict[str, Any]:
    ai_bundle = ai_bundle or {}
    ai_payload = ai_bundle.get("anomalyDetectionCard", {}) if isinstance(ai_bundle, dict) else {}
    if isinstance(ai_payload, dict) and all(ai_payload.get(key) for key in ("explanation", "likely_cause", "recommended_action")):
        return {
            "explanation": ai_payload["explanation"],
            "likely_cause": ai_payload["likely_cause"],
            "recommended_action": ai_payload["recommended_action"],
        }

    metric_types = {item["metric_type"] for item in anomalies}
    if {"soil_temperature", "soil_moisture"} <= metric_types:
        return {
            "explanation": "هم‌زمانی ناهنجاری دمای خاک و رطوبت خاک نشان می‌دهد تنش ترکیبی در ناحیه ریشه در حال شکل‌گیری است.",
            "likely_cause": "احتمالاً الگوی آبیاری، موج گرما یا افت ناگهانی ظرفیت نگهداشت رطوبت خاک عامل اصلی است.",
            "recommended_action": "زمان‌بندی آبیاری و وضعیت زهکشی/تبخیر بررسی و قرائت‌های سنسور در ۲۴ ساعت آینده دوباره پایش شود.",
        }
    if "electrical_conductivity" in metric_types and "soil_moisture" in metric_types:
        return {
            "explanation": "هم‌زمانی ناهنجاری EC و رطوبت می‌تواند نشان‌دهنده فشار شوری یا تجمع نمک در بستر باشد.",
            "likely_cause": "کیفیت آب آبیاری، کوددهی اخیر یا کاهش شست‌وشوی خاک می‌تواند عامل این الگو باشد.",
            "recommended_action": "EC آب و برنامه کوددهی بازبینی و در صورت نیاز شست‌وشوی کنترل‌شده خاک بررسی شود.",
        }
    if anomalies:
        top = anomalies[0]
        return {
            "explanation": f"در شاخص {top['label']} یک ناهنجاری آماری با روش {top['anomaly_method']} شناسایی شده است.",
            "likely_cause": "این رخداد می‌تواند ناشی از تغییر ناگهانی شرایط محیطی، خطای فرایندی یا نیاز به کالیبراسیون سنسور باشد.",
            "recommended_action": "روند همان شاخص و داده‌های پیرامونی بازبینی و در صورت تداوم، اقدام اصلاحی مزرعه اجرا شود.",
        }
    return {
        "explanation": "ناهنجاری آماری معناداری در داده‌های اخیر شناسایی نشد.",
        "likely_cause": "داده‌های فعلی با الگوی تاریخی سازگار هستند.",
        "recommended_action": "پایش عادی ادامه یابد.",
    }


def build_anomaly_detection_card(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
    context = context or {}
    sensor = context.get("sensor")
    history = context.get("history", [])
    forecasts = context.get("forecasts", [])
    if sensor is None:
        return {"anomalies": [], "interpretation": None}

    anomalies: list[dict[str, Any]] = []

    for metric_type, config in METRIC_CONFIG.items():
        if config["source"] == "history":
            values, timestamp, observed_value = _history_series(history, config["current_field"])
            current_value = getattr(sensor, config["current_field"], None)
            if current_value is not None:
                observed_value = float(current_value)
                timestamp = getattr(sensor, "updated_at", None)
                timestamp = timestamp.isoformat() if timestamp is not None else timestamp
        else:
            values, timestamp, observed_value = _forecast_series(forecasts, config["forecast_field"])

        if observed_value is None or len(values) < 5:
            continue

        detection = _select_detection_result(
            [
                result
                for result in (
                    _detect_with_z_score(values, observed_value),
                    _detect_with_iqr(values, observed_value),
                )
                if result is not None
            ]
        )
        if detection is None:
            continue

        anomalies.append(
            {
                "metric_type": metric_type,
                "label": config["label"],
                "timestamp": timestamp,
                "observed_value": round(observed_value, 2),
                "expected_range": detection["expected_range"],
                "deviation_score": detection["deviation_score"],
                "anomaly_method": detection["anomaly_method"],
                "severity": detection["severity"],
                "unit": config["unit"],
            }
        )

    anomalies.sort(key=lambda item: abs(item["deviation_score"]), reverse=True)
    interpretation = _build_contextual_interpretation(anomalies, ai_bundle=ai_bundle)

    return {
        "anomalies": anomalies,
        "interpretation": interpretation,
    }
