"""CDC 消费者（Debezium MySQL binlog → ClickHouse 宽表）.

链路：
  MySQL binlog → Debezium Connect → Kafka topic
    edu-cdc.next_edu_cloud.<table>
  → 本消费者 → 解析 Debezium 事件 → 写入 ClickHouse student_dashboard_view

设计要点：
- 监听多张表，按 source.table 路由
- 内存缓存 exam_id → (class_id, subject_id) 映射（来自 core_edu_exams 快照+流）
- 监听 core_edu_grades 时用缓存扩展为宽表记录写入 ClickHouse
- 幂等性：依赖 ClickHouse ReplacingMergeTree 引擎按 ORDER BY 去重
  （schema 需用 ReplacingMergeTree(last_updated)，当前为简化版 MergeTree）
- op 类型：r(快照读)、c(新增)、u(更新)、d(删除)；d 时 after 为 null
"""

import asyncio
import contextlib
import json
from datetime import UTC, datetime
from typing import Any

import structlog

from .clickhouse_client import upsert_student_dashboard
from .config import settings

logger = structlog.get_logger(__name__)


def _parse_ts(ts_ms: int | None) -> datetime:
    """Debezium ts_ms（毫秒）→ datetime."""
    if ts_ms is None:
        return datetime.now(UTC)
    return datetime.fromtimestamp(ts_ms / 1000, tz=UTC)


def _safe_float(value: Any) -> float:
    """安全转 float（Debezium 数值字段可能是字符串）."""
    if value is None:
        return 0.0
    try:
        return float(value)
    except (TypeError, ValueError):
        return 0.0


class ExamCache:
    """内存缓存 exam_id → (class_id, subject_id).

    从 core_edu_exams 表的 CDC 事件构建。subject_id 在 exams 表中暂无字段，
    这里占位为空字符串，后续扩展 schema 时再补充。
    """

    def __init__(self) -> None:
        self._data: dict[str, tuple[str, str]] = {}

    def upsert(self, exam_id: str, class_id: str, subject_id: str = "") -> None:
        if exam_id:
            self._data[exam_id] = (class_id, subject_id)

    def get(self, exam_id: str) -> tuple[str, str] | None:
        return self._data.get(exam_id) if exam_id else None


# 全局缓存（进程级单例）
_exam_cache = ExamCache()


async def _handle_exams_event(after: dict[str, Any] | None) -> None:
    """处理 core_edu_exams 表事件."""
    if after is None:
        return
    exam_id = after.get("id")
    class_id = after.get("class_id", "")
    if exam_id:
        _exam_cache.upsert(exam_id, class_id)
        logger.info("exam_cache_updated", exam_id=exam_id, class_id=class_id)


async def _handle_grades_event(
    after: dict[str, Any] | None,
    op: str,
    ts_ms: int | None,
) -> None:
    """处理 core_edu_grades 表事件 → 写入 ClickHouse 宽表.

    - op=r/c/u：after 为新数据，写入宽表
    - op=d：after 为 null，暂不处理（宽表保留历史记录）
    """
    if after is None:
        return

    student_id = after.get("student_id", "")
    exam_id = after.get("exam_id", "")
    score = _safe_float(after.get("score"))

    # 从缓存拿 class_id
    class_id = ""
    if exam_id:
        cached = _exam_cache.get(exam_id)
        if cached:
            class_id = cached[0]

    last_updated = _parse_ts(ts_ms)
    if after.get("updated_at"):
        # 优先用 MySQL 的 updated_at 字段
        with contextlib.suppress(ValueError, AttributeError):
            last_updated = datetime.fromisoformat(after["updated_at"].replace("Z", "+00:00"))

    # 简化：rank/kp/mastery/error_count 暂用默认值
    # 真实场景应通过其他 CDC 事件或聚合计算得到
    await upsert_student_dashboard(
        student_id=student_id,
        class_id=class_id,
        exam_id=exam_id,
        subject_id="",  # 占位
        score=score,
        rank_in_class=0,
        knowledge_point_id="",  # 占位
        mastery_level=score / 100.0,  # 简化：用分数百分比作为掌握度
        error_count=0,
        last_updated=last_updated,
    )


async def _process_message(topic: str, value: bytes | str) -> None:
    """处理单条 Kafka 消息.

    Debezium 事件格式（简化后，schemas.enable=false）：
    {
      "before": {...} | null,
      "after": {...} | null,
      "source": {"table": "...", "db": "...", ...},
      "op": "r|c|u|d",
      "ts_ms": 1783572350928
    }
    """
    try:
        value_str = value.decode("utf-8") if isinstance(value, bytes) else value
        event = json.loads(value_str)
    except (json.JSONDecodeError, UnicodeDecodeError) as exc:
        logger.warning("cdc_message_decode_failed", error=str(exc), topic=topic)
        return

    source = event.get("source") or {}
    table = source.get("table", "")
    op = event.get("op", "")
    ts_ms = event.get("ts_ms")
    after = event.get("after")

    logger.info(
        "cdc_event_received",
        topic=topic,
        table=table,
        op=op,
        ts_ms=ts_ms,
    )

    if table == "core_edu_exams":
        await _handle_exams_event(after)
    elif table == "core_edu_grades":
        await _handle_grades_event(after, op, ts_ms)
    else:
        # 其他表暂不处理，仅记录
        logger.debug("cdc_event_skipped", table=table)


async def run_consumer() -> None:
    """CDC 消费者主循环（lifespan 启动）.

    - kafka_brokers 未配置：直接返回，不启动消费者（降级模式）
    - 启动失败：仅记录错误，不阻塞 FastAPI 主流程
    """
    if not settings.kafka_brokers:
        logger.info("cdc_consumer_disabled_no_kafka_brokers")
        return

    try:
        from aiokafka import AIOKafkaConsumer
    except ImportError:
        logger.warning("cdc_consumer_aiokafka_not_installed")
        return

    topics = [t.strip() for t in settings.kafka_cdc_topics.split(",") if t.strip()]
    if not topics:
        logger.warning("cdc_consumer_no_topics_configured")
        return

    brokers = [b.strip() for b in settings.kafka_brokers.split(",") if b.strip()]

    consumer = AIOKafkaConsumer(
        *topics,
        bootstrap_servers=brokers,
        group_id=settings.kafka_group_id,
        auto_offset_reset=settings.kafka_auto_offset_reset,
        enable_auto_commit=True,
        value_deserializer=lambda v: v,  # 保留原始 bytes，由 _process_message 解码
    )

    try:
        await consumer.start()
        logger.info(
            "cdc_consumer_started",
            brokers=brokers,
            topics=topics,
            group_id=settings.kafka_group_id,
        )
    except Exception as exc:  # noqa: BLE001
        logger.error("cdc_consumer_start_failed", error=str(exc))
        return

    try:
        async for msg in consumer:
            try:
                await _process_message(msg.topic, msg.value)
            except Exception as exc:  # noqa: BLE001
                logger.error(
                    "cdc_message_process_failed",
                    error=str(exc),
                    topic=msg.topic,
                    partition=msg.partition,
                    offset=msg.offset,
                )
    except asyncio.CancelledError:
        logger.info("cdc_consumer_cancelled")
        raise
    finally:
        try:
            await consumer.stop()
            logger.info("cdc_consumer_stopped")
        except Exception as exc:  # noqa: BLE001
            logger.warning("cdc_consumer_stop_failed", error=str(exc))
