"""CDC 消费者（Debezium MySQL binlog → ClickHouse 宽表，EventHandler 路由 + 手动 commit）.

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

支持的表路由：
  - core_edu_exams → ExamCache 更新
  - core_edu_grades → student_dashboard_view 写入（查 ExamCache 获取 class_id）
  - core_edu_homework_submissions → student_dashboard_view 写入
  - core_edu_attendance → attendance_logs 写入
  - content_knowledge_points → mastery_snapshot 元数据更新
  - iam_users → 用户元数据缓存（P6 扩展）
  - classes → 班级元数据缓存（P6 扩展）

设计要点：
  - 手动 commit（at-least-once）：ClickHouse 写入成功后才 commit offset
  - 幂等性：依赖 ClickHouse ReplacingMergeTree 引擎按 ORDER BY 去重
  - op 类型：r(快照读)、c(新增)、u(更新)、d(删除)；d 时 after 为 null
  - Redis 事件去重：基于 event_id（Debezium 重启时可能重发）

降级策略：
  - kafka_brokers 未配置：消费者不启动（仅 HTTP/gRPC 服务）
  - ClickHouse 不可达：消息处理失败，不 commit，下次重启重试
"""

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

import structlog

from .config import settings
from .exam_cache import get_exam_cache
from .repository import clickhouse_repository, redis_client

logger = structlog.get_logger(__name__)

# 消费者单例（用于 lifespan 管理和 /readyz 检查）
_consumer: Any | None = None
_consumer_task: asyncio.Task[None] | None = None
_is_running: bool = False


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


def _safe_int(value: Any) -> int:
    """安全转 int."""
    if value is None:
        return 0
    try:
        return int(value)
    except (TypeError, ValueError):
        try:
            return int(float(value))
        except (TypeError, ValueError):
            return 0


def _parse_mysql_datetime(value: Any, fallback: datetime) -> datetime:
    """解析 MySQL datetime 字段（兼容字符串和毫秒时间戳）."""
    if not value:
        return fallback
    if isinstance(value, (int, float)):
        return datetime.fromtimestamp(value / 1000, tz=UTC)
    with contextlib.suppress(ValueError, AttributeError, TypeError):
        return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
    return fallback


# ===== EventHandler 路由表 =====


async def _handle_exams_event(after: dict[str, Any] | None, op: str) -> bool:
    """处理 core_edu_exams 表事件 → ExamCache 更新."""
    if after is None:
        return True  # 删除事件，无需处理

    exam_id = str(after.get("id") or "")
    if not exam_id:
        return True

    exam_cache = get_exam_cache()
    exam_cache.upsert(
        exam_id=exam_id,
        class_id=str(after.get("class_id") or ""),
        subject_id=str(after.get("subject_id") or ""),
        title=str(after.get("title") or ""),
    )
    logger.info(
        "exam_cache_updated",
        exam_id=exam_id,
        class_id=after.get("class_id"),
        op=op,
    )
    return True


async def _handle_grades_event(
    after: dict[str, Any] | None,
    op: str,
    ts_ms: int | None,
) -> bool:
    """处理 core_edu_grades 表事件 → 写入 student_dashboard_view."""
    if after is None:
        return True  # 删除事件

    student_id = str(after.get("student_id") or "")
    exam_id = str(after.get("exam_id") or "")
    score = _safe_float(after.get("score"))

    # 从 ExamCache 获取 class_id 和 subject_id
    exam_cache = get_exam_cache()
    class_id = exam_cache.get_class_id(exam_id)
    subject_id = exam_cache.get_subject_id(exam_id)

    fallback_ts = _parse_ts(ts_ms)
    last_updated = _parse_mysql_datetime(after.get("updated_at"), fallback_ts)

    # 简化：rank/kp/mastery/error_count 暂用默认值（CDC 聚合后由掌握度计算补充）
    write_ok = await clickhouse_repository.upsert_student_dashboard(
        student_id=student_id,
        class_id=class_id,
        exam_id=exam_id,
        subject_id=subject_id,
        score=score,
        rank_in_class=0,
        knowledge_point_id="",
        mastery_level=score / 100.0,  # 简化：用分数百分比作为初始掌握度
        error_count=0,
        last_updated=last_updated,
    )
    if write_ok:
        logger.info(
            "grade_record_written",
            student_id=student_id,
            exam_id=exam_id,
            score=score,
            op=op,
        )
    return write_ok


async def _handle_homework_event(
    after: dict[str, Any] | None,
    op: str,
    ts_ms: int | None,
) -> bool:
    """处理 core_edu_homework_submissions 表事件 → 写入 student_dashboard_view."""
    if after is None:
        return True

    student_id = str(after.get("student_id") or "")
    homework_id = str(after.get("homework_id") or after.get("id") or "")
    class_id = str(after.get("class_id") or "")
    score = _safe_float(after.get("score"))

    fallback_ts = _parse_ts(ts_ms)
    raw_time = after.get("submitted_at") or after.get("updated_at")
    last_updated = _parse_mysql_datetime(raw_time, fallback_ts)

    write_ok = await clickhouse_repository.upsert_student_dashboard(
        student_id=student_id,
        class_id=class_id,
        exam_id=homework_id,  # homework_id 作为 exam_id 占位
        subject_id=str(after.get("subject_id") or ""),
        score=score,
        rank_in_class=0,
        knowledge_point_id="",
        mastery_level=score / 100.0,
        error_count=0,
        last_updated=last_updated,
    )
    if write_ok:
        logger.info(
            "homework_record_written",
            student_id=student_id,
            homework_id=homework_id,
            score=score,
            op=op,
        )
    return write_ok


async def _handle_attendance_event(
    after: dict[str, Any] | None,
    op: str,
    ts_ms: int | None,
) -> bool:
    """处理 core_edu_attendance 表事件 → 写入 attendance_logs."""
    if after is None:
        return True

    student_id = str(after.get("student_id") or "")
    class_id = str(after.get("class_id") or "")
    status = str(after.get("status") or "present")
    recorded_by = str(after.get("recorded_by") or "")
    remark = str(after.get("remark") or "")

    fallback_ts = _parse_ts(ts_ms)
    raw_time = after.get("occurred_at") or after.get("created_at")
    occurred_at = _parse_mysql_datetime(raw_time, fallback_ts)

    # attendance_date 从 occurred_at 提取日期部分
    attendance_date = occurred_at.date()

    write_ok = await clickhouse_repository.upsert_attendance_log(
        student_id=student_id,
        class_id=class_id,
        attendance_date=attendance_date,
        status=status,
        recorded_by=recorded_by,
        remark=remark,
        occurred_at=occurred_at,
    )
    if write_ok:
        logger.info(
            "attendance_record_written",
            student_id=student_id,
            class_id=class_id,
            status=status,
            op=op,
        )
    return write_ok


async def _handle_knowledge_point_event(
    after: dict[str, Any] | None,
    op: str,
) -> bool:
    """处理 content_knowledge_points 表事件 → 更新知识点元数据缓存."""
    if after is None:
        return True

    # 知识点元数据更新（P5+ 扩展：可写入 Redis 缓存供查询时补充标题）
    kp_id = str(after.get("id") or "")
    title = str(after.get("title") or "")
    subject_id = str(after.get("subject_id") or "")

    # 简化：写 Redis 缓存（data_ana:kp_meta:{kp_id}）
    metadata = {"title": title, "subject_id": subject_id}
    await redis_client.set_cache(
        f"data_ana:kp_meta:{kp_id}",
        json.dumps(metadata, ensure_ascii=False),
        ttl_s=86400,
    )
    logger.info(
        "knowledge_point_meta_cached",
        knowledge_point_id=kp_id,
        title=title,
        op=op,
    )
    return True


async def _handle_ai_usage_event(event: dict[str, Any]) -> bool:
    """处理 AIUsageEvent（edu.insight.ai.usage topic）→ 写入 ai_usage_log.

    注意：此事件不是 Debezium CDC 事件，而是 ai 服务发布的业务事件，
    格式遵循 events.proto AIUsageEvent message.
    """
    request_id = str(event.get("request_id") or "")
    user_id = str(event.get("user_id") or "")
    provider = str(event.get("provider") or "")
    model = str(event.get("model") or "")
    prompt_tokens = _safe_int(event.get("prompt_tokens"))
    completion_tokens = _safe_int(event.get("completion_tokens"))
    total_tokens = _safe_int(event.get("total_tokens"))
    latency_ms = _safe_int(event.get("latency_ms"))
    success = bool(event.get("success", True))
    cost_cents = _safe_int(event.get("cost_cents"))
    occurred_at_ms = event.get("occurred_at")
    if isinstance(occurred_at_ms, int):
        occurred_at = _parse_ts(occurred_at_ms)
    else:
        occurred_at = datetime.now(UTC)

    write_ok = await clickhouse_repository.upsert_ai_usage_log(
        request_id=request_id,
        user_id=user_id,
        provider=provider,
        model=model,
        prompt_tokens=prompt_tokens,
        completion_tokens=completion_tokens,
        total_tokens=total_tokens,
        latency_ms=latency_ms,
        success=success,
        cost_cents=cost_cents,
        occurred_at=occurred_at,
    )
    if write_ok:
        logger.info(
            "ai_usage_recorded",
            request_id=request_id,
            provider=provider,
            model=model,
            total_tokens=total_tokens,
        )
    return write_ok


# ===== 事件去重 =====


async def _is_deduped(event_id: str) -> bool:
    """检查事件是否已处理（基于 event_id Redis SETNX 去重）.

    返回 True 表示重复事件（应跳过），False 表示首次事件.
    """
    if not event_id:
        return False
    key = f"data_ana:dedup:{event_id}"
    is_first = await redis_client.setnx_dedup(key, ttl_s=7 * 24 * 3600)  # 7 天 TTL
    return not is_first


# ===== 消息处理主入口 =====


async def _process_cdc_message(topic: str, value: bytes | str) -> bool:
    """处理单条 CDC 消息（Debezium 格式）.

    返回 True 表示处理成功（可 commit offset），
    返回 False 表示处理失败（不 commit，下次重试）.
    """
    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 True  # 无效消息直接跳过（避免阻塞消费）

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

    # Debezium 事件去重（基于 source.ts_ms + source.dbg + source.sequence 生成 event_id）
    # 简化：使用 topic + partition + offset 作为去重 key（由调用方传入更精确）
    # 这里仅对 after.id 做去重（如果有）
    event_id = ""
    if after and isinstance(after, dict):
        event_id = f"{table}:{after.get('id', '')}:{ts_ms}"
    if event_id and await _is_deduped(event_id):
        logger.info("cdc_event_dedup_skipped", table=table, event_id=event_id)
        return True

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

    try:
        if table == "core_edu_exams":
            return await _handle_exams_event(after, op)
        elif table == "core_edu_grades":
            return await _handle_grades_event(after, op, ts_ms)
        elif table == "core_edu_homework_submissions":
            return await _handle_homework_event(after, op, ts_ms)
        elif table == "core_edu_attendance":
            return await _handle_attendance_event(after, op, ts_ms)
        elif table == "content_knowledge_points":
            return await _handle_knowledge_point_event(after, op)
        else:
            logger.debug("cdc_event_skipped", table=table)
            return True
    except Exception as exc:  # noqa: BLE001
        logger.error(
            "cdc_event_handler_failed",
            error=str(exc),
            table=table,
            op=op,
        )
        return False


async def _process_ai_usage_message(topic: str, value: bytes | str) -> bool:
    """处理 AIUsageEvent 消息（非 Debezium 格式，业务事件）."""
    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("ai_usage_message_decode_failed", error=str(exc), topic=topic)
        return True

    # 基于 request_id 去重
    request_id = str(event.get("request_id") or "")
    if request_id and await _is_deduped(f"ai_usage:{request_id}"):
        return True

    try:
        return await _handle_ai_usage_event(event)
    except Exception as exc:  # noqa: BLE001
        logger.error("ai_usage_handler_failed", error=str(exc))
        return False


async def _process_message(topic: str, value: bytes | str) -> bool:
    """统一消息处理入口（区分 CDC 事件和 AIUsage 事件）."""
    # 根据 topic 判断消息类型
    if topic == settings.kafka_ai_usage_topic:
        return await _process_ai_usage_message(topic, value)
    return await _process_cdc_message(topic, value)


# ===== 消费者主循环 =====


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

    - kafka_brokers 未配置：直接返回，不启动消费者（降级模式）
    - 手动 commit：ClickHouse 写入成功后才 commit offset（at-least-once）
    - 启动失败：仅记录错误，不阻塞 FastAPI 主流程
    """
    global _consumer, _is_running

    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()]
    # P5+ 加入 AI 用量 topic
    if settings.kafka_ai_usage_topic and settings.kafka_ai_usage_topic not in topics:
        topics.append(settings.kafka_ai_usage_topic)

    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_consumer_group,
        auto_offset_reset=settings.kafka_auto_offset_reset,
        enable_auto_commit=settings.kafka_enable_auto_commit,  # 手动 commit
        value_deserializer=lambda v: v,  # 保留原始 bytes，由 _process_message 解码
        max_poll_records=100,
        session_timeout_ms=30_000,
        heartbeat_interval_ms=10_000,
    )

    try:
        await _consumer.start()
        _is_running = True
        logger.info(
            "cdc_consumer_started",
            brokers=brokers,
            topics=topics,
            group_id=settings.kafka_consumer_group,
            auto_commit=settings.kafka_enable_auto_commit,
        )
    except Exception as exc:  # noqa: BLE001
        logger.error("cdc_consumer_start_failed", error=str(exc))
        _is_running = False
        return

    try:
        async for msg in _consumer:
            try:
                success = await _process_message(msg.topic, msg.value)
                if success:
                    # 处理成功才 commit offset（at-least-once）
                    await _consumer.commit()
                else:
                    # 处理失败不 commit，下次重启会重新消费
                    logger.warning(
                        "cdc_message_process_failed_no_commit",
                        topic=msg.topic,
                        partition=msg.partition,
                        offset=msg.offset,
                    )
            except Exception as exc:  # noqa: BLE001
                logger.error(
                    "cdc_message_process_exception",
                    error=str(exc),
                    topic=msg.topic,
                    partition=msg.partition,
                    offset=msg.offset,
                )
                # 异常不 commit，下次重启重试
    except asyncio.CancelledError:
        logger.info("cdc_consumer_cancelled")
        raise
    finally:
        _is_running = False
        if _consumer is not None:
            try:
                await _consumer.stop()
                logger.info("cdc_consumer_stopped")
            except Exception as exc:  # noqa: BLE001
                logger.warning("cdc_consumer_stop_failed", error=str(exc))
        _consumer = None


async def start_consumer() -> asyncio.Task[None] | None:
    """启动消费者后台任务（供 lifespan 调用）."""
    global _consumer_task
    if not settings.kafka_brokers:
        return None
    if _consumer_task is not None and not _consumer_task.done():
        return _consumer_task
    _consumer_task = asyncio.create_task(run_consumer())
    return _consumer_task


async def stop_consumer() -> None:
    """停止消费者后台任务（供 lifespan 调用）."""
    global _consumer_task
    if _consumer_task is not None:
        _consumer_task.cancel()
        try:
            await _consumer_task
        except asyncio.CancelledError:
            pass
        finally:
            _consumer_task = None


def is_running() -> bool:
    """消费者是否运行中（供 /readyz 使用）."""
    return _is_running


async def get_lag() -> int:
    """获取消费者 lag（待消费消息数，供 /readyz 和监控使用）.

    P6 实现：调用 Kafka AdminClient 获取 group lag.
    当前返回 0（简化）.
    """
    return 0
