"""掌握度计算服务（加权滑动平均 + 遗忘曲线）.

算法对齐 02-architecture-design.md §9：
  - WEIGHTED_MOVING_AVG：w_i = 0.6^i，归一化（权重从最近到最远递减）
  - FORGETTING_CURVE：基于遗忘曲线的 max 叠加（P5+ 启用），
    half_life = 30 days，越久未练习掌握度越衰减.

输入：学生指定知识点的历史成绩序列（按时间倒序）
输出：mastery_level (0.0-1.0)
副作用：
  - 写 mastery_snapshot 表（upsert_mastery_snapshot）
  - 发布 mastery.updated 事件（Outbox 豁免）

降级策略：
  - ClickHouse 不可达：跳过计算（返回 None）
  - Kafka 不可达：计算继续，事件发布静默失败
"""

from datetime import UTC, datetime
from typing import Any

import structlog

from .config import settings
from .repository import clickhouse_repository, kafka_producer

logger = structlog.get_logger(__name__)


def _weighted_moving_avg(scores: list[dict[str, Any]]) -> float | None:
    """加权滑动平均算法.

    输入：按时间倒序的成绩列表（最近在前），元素含 score 字段
    输出：归一化的 mastery_level (0.0-1.0)，样本不足返回 None.

    权重：w_i = decay_base ^ i（i 从 0 开始，最近 attempt 权重最大）
    """
    if not scores:
        return None

    # 截取最近 N 次（window_size）
    window = scores[: settings.mastery_window_size]
    if len(window) < settings.mastery_min_samples:
        return None

    decay = settings.mastery_decay_base
    total_weight = 0.0
    weighted_sum = 0.0

    for i, record in enumerate(window):
        # 成绩归一化到 0.0-1.0（假设原始分 0-100）
        raw_score = record.get("score", 0.0)
        normalized = min(max(raw_score / 100.0, 0.0), 1.0)
        weight = decay**i
        weighted_sum += normalized * weight
        total_weight += weight

    if total_weight <= 0:
        return None

    mastery = weighted_sum / total_weight
    return round(max(0.0, min(1.0, mastery)), 4)


def _forgetting_curve_decay(
    mastery: float,
    last_attempt_at: datetime | None,
    now: datetime | None = None,
) -> float:
    """遗忘曲线衰减（P5+ 启用，对齐 02 §9）.

    距离上次练习越久，mastery 越衰减：
      decayed = mastery * exp(-ln(2) * days_since / half_life)

    half_life = 30 days（settings.mastery_forgetting_half_life_days）.
    """
    if last_attempt_at is None:
        return mastery

    now = now or datetime.now(UTC)
    if last_attempt_at.tzinfo is None:
        last_attempt_at = last_attempt_at.replace(tzinfo=UTC)

    days_since = (now - last_attempt_at).total_seconds() / 86400
    if days_since <= 0:
        return mastery

    half_life = settings.mastery_forgetting_half_life_days
    if half_life <= 0:
        return mastery

    import math

    decay_factor = math.exp(-math.log(2) * days_since / half_life)
    decayed = mastery * decay_factor
    return round(max(0.0, min(1.0, decayed)), 4)


def classify_mastery_label(level: float) -> str:
    """掌握度三档分类."""
    if level >= 0.8:
        return "mastered"
    if level >= 0.4:
        return "progressing"
    return "weak"


async def calculate_mastery(
    student_id: str,
    knowledge_point_id: str,
    subject_id: str = "",
) -> dict[str, Any] | None:
    """计算学生指定知识点的掌握度.

    流程：
      1. 查询学生该知识点的历史成绩（按时间倒序）
      2. 加权滑动平均 → mastery_level
      3. 遗忘曲线衰减 → 最终 mastery_level
      4. 写 mastery_snapshot 表
      5. 发布 mastery.updated 事件（Outbox 豁免）

    返回：
      {
        "student_id": ...,
        "knowledge_point_id": ...,
        "subject_id": ...,
        "mastery_level": 0.0-1.0,
        "previous_level": 0.0-1.0 or None,
        "mastery_label": "mastered" / "progressing" / "weak",
        "degraded": bool,
      }
      ClickHouse 不可达时返回降级骨架（degraded=true）.
    """
    # 1. 查询历史成绩
    scores = await clickhouse_repository.query_student_scores_by_kp(
        student_id=student_id,
        knowledge_point_id=knowledge_point_id,
        limit=settings.mastery_window_size * 2,  # 多取一倍容错
    )

    if scores is None:
        # ClickHouse 不可达降级
        logger.warning(
            "mastery_calc_clickhouse_unavailable_degraded",
            student_id=student_id,
            knowledge_point_id=knowledge_point_id,
        )
        return {
            "student_id": student_id,
            "knowledge_point_id": knowledge_point_id,
            "subject_id": subject_id,
            "mastery_level": 0.0,
            "previous_level": None,
            "mastery_label": "weak",
            "degraded": True,
            "degraded_reason": "clickhouse_unavailable",
        }

    # 2. 加权滑动平均
    mastery = _weighted_moving_avg(scores)
    if mastery is None:
        # 样本不足，返回默认值
        return {
            "student_id": student_id,
            "knowledge_point_id": knowledge_point_id,
            "subject_id": subject_id,
            "mastery_level": 0.0,
            "previous_level": None,
            "mastery_label": "weak",
            "degraded": False,
            "degraded_reason": "insufficient_samples",
        }

    # 3. 遗忘曲线衰减（基于最近一次成绩的时间）
    last_attempt_at = None
    if scores:
        last_attempt_at = scores[0].get("timestamp")

    mastery_final = _forgetting_curve_decay(mastery, last_attempt_at)

    # 4. 查询上一次 mastery（用于事件对比）
    previous_snapshot = await clickhouse_repository.query_mastery_snapshot(
        student_id=student_id,
        subject_id=subject_id,
    )
    previous_level: float | None = None
    if previous_snapshot is not None:
        for kp in previous_snapshot.get("knowledgePoints", []):
            if kp.get("knowledge_point_id") == knowledge_point_id:
                previous_level = kp.get("mastery_level")
                break

    # 5. 写 mastery_snapshot 表
    calculated_at = datetime.now(UTC)
    write_ok = await clickhouse_repository.upsert_mastery_snapshot(
        student_id=student_id,
        knowledge_point_id=knowledge_point_id,
        subject_id=subject_id,
        mastery_level=mastery_final,
        calculated_at=calculated_at,
        calculation_method="weighted_moving_avg",
    )
    if not write_ok:
        logger.warning(
            "mastery_snapshot_write_failed_degraded",
            student_id=student_id,
            knowledge_point_id=knowledge_point_id,
        )

    # 6. 发布 mastery.updated 事件（Outbox 豁免）
    published = await kafka_producer.publish_mastery_updated(
        student_id=student_id,
        knowledge_point_id=knowledge_point_id,
        mastery_level=mastery_final,
        previous_level=previous_level if previous_level is not None else 0.0,
    )

    result = {
        "student_id": student_id,
        "knowledge_point_id": knowledge_point_id,
        "subject_id": subject_id,
        "mastery_level": mastery_final,
        "previous_level": previous_level,
        "mastery_label": classify_mastery_label(mastery_final),
        "degraded": not write_ok,
        "degraded_reason": "snapshot_write_failed" if not write_ok else "",
        "event_published": published,
    }
    logger.info(
        "mastery_calculated",
        student_id=student_id,
        knowledge_point_id=knowledge_point_id,
        mastery_level=mastery_final,
        previous_level=previous_level,
        label=result["mastery_label"],
        published=published,
    )
    return result


async def batch_calculate_mastery(
    student_id: str,
    knowledge_point_ids: list[str],
    subject_id: str = "",
) -> list[dict[str, Any]]:
    """批量计算学生多个知识点的掌握度."""
    results: list[dict[str, Any]] = []
    for kp_id in knowledge_point_ids:
        result = await calculate_mastery(
            student_id=student_id,
            knowledge_point_id=kp_id,
            subject_id=subject_id,
        )
        if result is not None:
            results.append(result)
    return results
