"""gRPC Server（AnalyticsService 12 RPC + HealthService）.

对齐 02-architecture-design.md §4.2：
  - AnalyticsService 12 RPC（含 1 个 server-streaming SubscribeMasteryUpdate）
  - HealthService（grpc.health.v1，供 K8s 探针）
  - 端口 50055
  - 所有 RPC 返回结构化 message（HTTP 层包装 ActionState 信封）

SubscribeMasteryUpdate（P5+）：
  - server-streaming RPC
  - 客户端订阅 student_id / class_id
  - 掌握度计算完成时推送 MasteryUpdateEvent
  - 基于 asyncio.Queue 实现事件分发

降级策略：
  - grpcio 未安装：gRPC server 不启动（仅 HTTP 服务）
  - 下游服务不可达：返回降级响应（degraded 字段标记）
"""

import asyncio
from typing import Any

import structlog

from . import analytics_service, warning_service
from .config import settings
from .shared.permissions import UserContext

logger = structlog.get_logger(__name__)

# SubscribeMasteryUpdate 订阅管理（P5+）
_subscribers: dict[str, asyncio.Queue] = {}
_subscribers_lock = asyncio.Lock()


async def _add_subscriber(key: str) -> asyncio.Queue:
    """添加订阅者."""
    async with _subscribers_lock:
        if key not in _subscribers:
            _subscribers[key] = asyncio.Queue(maxsize=100)
        return _subscribers[key]


async def _remove_subscriber(key: str) -> None:
    """移除订阅者."""
    async with _subscribers_lock:
        _subscribers.pop(key, None)


async def notify_mastery_update(
    student_id: str,
    knowledge_point_id: str,
    mastery_level: float,
    previous_level: float,
) -> None:
    """通知所有订阅者掌握度更新（由 mastery_service 调用）.

    订阅匹配规则：
      - 订阅 student_id 的客户端收到通知
      - 订阅 class_id 的客户端收到该班级所有学生的通知（需要 ExamCache 查 class_id）
    """
    import time
    import uuid

    event_id = str(uuid.uuid4())
    calculated_at = int(time.time())

    # 通知 student_id 订阅者
    queue = _subscribers.get(f"student:{student_id}")
    if queue is not None:
        try:
            queue.put_nowait(
                {
                    "event_id": event_id,
                    "student_id": student_id,
                    "knowledge_point_id": knowledge_point_id,
                    "mastery_level": mastery_level,
                    "previous_level": previous_level,
                    "calculated_at": calculated_at,
                }
            )
        except asyncio.QueueFull:
            logger.warning("mastery_update_queue_full_dropped", student_id=student_id)


# ===== AnalyticsService 实现 =====


class AnalyticsServiceServicer:
    """AnalyticsService gRPC 实现（12 RPC）."""

    async def GetClassPerformance(self, request, context):
        """班级成绩分析."""
        user = _extract_user(context)
        result = await analytics_service.get_class_performance(
            user=user,
            class_id=request.class_id,
            subject_id=request.subject_id,
            start_date=request.start_date,
            end_date=request.end_date,
        )
        return _build_class_performance_response(result)

    async def GetStudentWeakness(self, request, context):
        """学生薄弱知识点."""

        user = _extract_user(context)
        result = await analytics_service.get_student_weakness(
            user=user,
            student_id=request.student_id,
            subject_id=request.subject_id,
        )
        return _build_student_weakness_response(result)

    async def GetLearningTrend(self, request, context):
        """学习趋势."""

        user = _extract_user(context)
        result = await analytics_service.get_learning_trend(
            user=user,
            student_id=request.student_id,
            subject_id=request.subject_id,
            start_date=request.start_date,
            end_date=request.end_date,
        )
        return _build_learning_trend_response(result)

    async def GetTeacherDashboard(self, request, context):
        """教师仪表盘."""

        user = _extract_user(context)
        result = await analytics_service.get_teacher_dashboard(
            user=user,
            class_id=request.class_id,
        )
        return _build_teacher_dashboard_response(result)

    async def GetStudentDashboard(self, request, context):
        """学生仪表盘."""

        user = _extract_user(context)
        result = await analytics_service.get_student_dashboard(
            user=user,
        )
        return _build_student_dashboard_response(result)

    async def GetParentDashboard(self, request, context):
        """家长仪表盘."""

        user = _extract_user(context)
        result = await analytics_service.get_parent_dashboard(
            user=user,
            child_id=request.student_id,
        )
        return _build_parent_dashboard_response(result)

    async def GetAdminDashboard(self, request, context):
        """管理员仪表盘."""

        user = _extract_user(context)
        result = await analytics_service.get_admin_dashboard(
            user=user,
            school_id=request.scope_id,
        )
        return _build_admin_dashboard_response(result)

    async def GetWarnings(self, request, context):
        """预警列表查询."""
        # 简化：按 class_id 查询（P6 改为多条件过滤）
        warnings = await warning_service.get_warnings(
            student_id="",  # gRPC 层按 class_id 查询，P6 扩展
            warning_type="",
        )
        return _build_warning_list_response(warnings)

    async def TriggerWarning(self, request, context):
        """手动触发预警."""
        result = await warning_service.trigger_warning_manual(
            target_id=request.target_id,
            warning_type=request.warning_type,
            threshold=0.0,
            current_value=0.0,
            severity=request.severity,
        )
        return _build_trigger_warning_response(result)

    async def GetMasteryDistribution(self, request, context):
        """班级掌握度分布."""

        user = _extract_user(context)
        result = await analytics_service.get_mastery_distribution(
            user=user,
            class_id=request.class_id,
            subject_id=request.subject_id,
            knowledge_point_id=request.knowledge_point_id,
        )
        return _build_mastery_distribution_response(result)

    async def GetStudentMastery(self, request, context):
        """学生知识点掌握度明细."""

        user = _extract_user(context)
        result = await analytics_service.get_student_mastery(
            user=user,
            student_id=request.student_id,
            subject_id=request.subject_id,
        )
        return _build_student_mastery_response(result)

    async def SubscribeMasteryUpdate(self, request, context):
        """订阅掌握度更新（server-streaming，P5+）."""
        # 确定订阅 key
        if request.student_id:
            sub_key = f"student:{request.student_id}"
        elif request.class_id:
            sub_key = f"class:{request.class_id}"
        else:
            sub_key = "global"

        queue = await _add_subscriber(sub_key)
        logger.info(
            "mastery_subscription_added",
            sub_key=sub_key,
            peer=context.peer() if hasattr(context, "peer") else "unknown",
        )

        try:
            while context.is_active():
                try:
                    event = await asyncio.wait_for(queue.get(), timeout=30.0)
                    yield _build_mastery_update_event(event)
                except TimeoutError:
                    # 心跳：30s 无事件时发一个空 keepalive（客户端应容忍）
                    continue
        except asyncio.CancelledError:
            logger.info("mastery_subscription_cancelled", sub_key=sub_key)
            raise
        finally:
            await _remove_subscriber(sub_key)
            logger.info("mastery_subscription_removed", sub_key=sub_key)


# ===== 辅助函数 =====


def _extract_user(context: Any) -> UserContext:
    """从 gRPC metadata 提取用户上下文."""
    user_id = ""
    roles: list[str] = []
    try:
        metadata = (
            dict(context.invocation_metadata()) if hasattr(context, "invocation_metadata") else {}
        )
        user_id = metadata.get("x-user-id", "")
        roles_str = metadata.get("x-user-roles", "")
        roles = [r.strip() for r in roles_str.split(",") if r.strip()] if roles_str else []
    except Exception:  # noqa: BLE001
        pass
    return UserContext(user_id=user_id, roles=roles)


def _build_class_performance_response(data: dict) -> Any:
    """构建 ClassPerformance proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    return analytics_pb2.ClassPerformance(
        class_id=data.get("classId", ""),
        average_score=data.get("averageScore", 0.0),
        pass_rate=data.get("passRate", 0.0),
        total_students=data.get("totalStudents", 0),
    )


def _build_student_weakness_response(data: dict) -> Any:
    """构建 StudentWeakness proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    weak_points = [
        analytics_pb2.WeakPoint(
            knowledge_point_id=wp.get("knowledgePointId", ""),
            title=wp.get("title", ""),
            mastery=wp.get("mastery", 0.0),
            error_count=wp.get("errorCount", 0),
        )
        for wp in data.get("weakPoints", [])
    ]
    return analytics_pb2.StudentWeakness(
        student_id=data.get("studentId", ""),
        weak_points=weak_points,
    )


def _build_learning_trend_response(data: dict) -> Any:
    """构建 LearningTrend proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    points = [
        analytics_pb2.TrendPoint(
            date=p.get("date", 0),
            score=p.get("score", 0.0),
        )
        for p in data.get("points", [])
    ]
    return analytics_pb2.LearningTrend(
        student_id=data.get("studentId", ""),
        points=points,
    )


def _build_teacher_dashboard_response(data: dict) -> Any:
    """构建 TeacherDashboard proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    classes = [
        analytics_pb2.ClassSummary(
            class_id=c.get("class_id", ""),
            class_name=c.get("class_name", ""),
            student_count=c.get("total_students", 0),
            average_score=c.get("average_score", 0.0),
        )
        for c in data.get("classes", [])
    ]
    return analytics_pb2.TeacherDashboard(
        user_id=data.get("userId", ""),
        total_classes=data.get("totalClasses", 0),
        total_students=data.get("totalStudents", 0),
        class_avg_score=data.get("classAvgScore", 0.0),
        pending_homework_count=data.get("pendingHomeworkCount", 0),
        classes=classes,
    )


def _build_student_dashboard_response(data: dict) -> Any:
    """构建 StudentDashboard proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    weak_points = [
        analytics_pb2.WeakPoint(
            knowledge_point_id=wp.get("knowledgePointId", ""),
            title=wp.get("title", ""),
            mastery=wp.get("mastery", 0.0),
            error_count=wp.get("errorCount", 0),
        )
        for wp in data.get("weakPoints", [])
    ]
    trends = [
        analytics_pb2.TrendPoint(date=p.get("date", 0), score=p.get("score", 0.0))
        for p in data.get("trend", [])
    ]
    return analytics_pb2.StudentDashboard(
        user_id=data.get("studentId", data.get("userId", "")),
        avg_score=data.get("averageScore", 0.0),
        class_rank=0,
        total_students=0,
        weak_points=weak_points,
        recent_trends=trends,
    )


def _build_parent_dashboard_response(data: dict) -> Any:
    """构建 ParentDashboard proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    weak_points = [
        analytics_pb2.WeakPoint(
            knowledge_point_id=wp.get("knowledgePointId", ""),
            title=wp.get("title", ""),
            mastery=wp.get("mastery", 0.0),
            error_count=wp.get("errorCount", 0),
        )
        for wp in data.get("weakPoints", [])
    ]
    return analytics_pb2.ParentDashboard(
        user_id=data.get("userId", ""),
        student_id=data.get("childId", ""),
        child_avg_score=data.get("averageScore", 0.0),
        child_class_rank=0,
        total_class_students=0,
        child_weak_points=weak_points,
    )


def _build_admin_dashboard_response(data: dict) -> Any:
    """构建 AdminDashboard proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    ai_usage_data = data.get("aiUsage", {})
    by_provider = [
        analytics_pb2.AIUsageByProvider(
            provider=p.get("provider", ""),
            request_count=p.get("requestCount", 0),
            total_tokens=p.get("totalTokens", 0),
            cost_cents=p.get("costCents", 0),
        )
        for p in ai_usage_data.get("byProvider", [])
    ]
    ai_usage = analytics_pb2.AIUsageSummary(
        total_requests=ai_usage_data.get("totalRequests", 0),
        total_tokens=ai_usage_data.get("totalTokens", 0),
        total_cost_cents=ai_usage_data.get("totalCostCents", 0),
        by_provider=by_provider,
    )
    return analytics_pb2.AdminDashboard(
        user_id=data.get("userId", ""),
        total_teachers=0,
        total_students=data.get("totalStudents", 0),
        total_classes=data.get("totalClasses", 0),
        school_avg_score=data.get("classAvgScore", 0.0),
        ai_usage=ai_usage,
    )


def _build_warning_list_response(warnings: list[dict]) -> Any:
    """构建 WarningList proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    warning_infos = [
        analytics_pb2.WarningInfo(
            warning_id=w.get("warning_id", ""),
            warning_type=w.get("warning_type", ""),
            target_id=w.get("target_id", ""),
            target_name=w.get("target_name", ""),
            threshold=w.get("threshold", 0.0),
            current_value=w.get("current_value", 0.0),
            severity=w.get("severity", "WARN"),
            occurred_at=int(w.get("occurred_at", 0)),
        )
        for w in warnings
    ]
    return analytics_pb2.WarningList(
        warnings=warning_infos,
        total=len(warning_infos),
    )


def _build_trigger_warning_response(data: dict) -> Any:
    """构建 TriggerWarningResponse proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    return analytics_pb2.TriggerWarningResponse(
        warning_id=data.get("warning_id", ""),
        triggered=data.get("triggered", False),
    )


def _build_mastery_distribution_response(data: dict) -> Any:
    """构建 MasteryDistribution proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    return analytics_pb2.MasteryDistribution(
        class_id=data.get("classId", ""),
        mastered_count=data.get("masteredCount", 0),
        progressing_count=data.get("progressingCount", 0),
        weak_count=data.get("weakCount", 0),
        total_students=data.get("totalStudents", 0),
    )


def _build_student_mastery_response(data: dict) -> Any:
    """构建 StudentMastery proto 响应."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    knowledge_points = [
        analytics_pb2.KnowledgePointMastery(
            knowledge_point_id=kp.get("knowledge_point_id", ""),
            title=kp.get("title", ""),
            subject_id=kp.get("subject_id", ""),
            mastery_level=kp.get("mastery_level", 0.0),
            mastery_label=kp.get("mastery_label", "weak"),
            calculated_at=kp.get("calculated_at", 0),
        )
        for kp in data.get("knowledgePoints", [])
    ]
    return analytics_pb2.StudentMastery(
        student_id=data.get("studentId", ""),
        knowledge_points=knowledge_points,
        overall_mastery=data.get("overallMastery", 0.0),
    )


def _build_mastery_update_event(event: dict) -> Any:
    """构建 MasteryUpdateEvent proto 响应（streaming）."""
    from generated_proto import analytics_pb2  # type: ignore[import-not-found]

    return analytics_pb2.MasteryUpdateEvent(
        event_id=event.get("event_id", ""),
        student_id=event.get("student_id", ""),
        knowledge_point_id=event.get("knowledge_point_id", ""),
        mastery_level=event.get("mastery_level", 0.0),
        previous_level=event.get("previous_level", 0.0),
        calculated_at=event.get("calculated_at", 0),
    )


# ===== gRPC Server 管理 =====

_server: Any | None = None


async def start_grpc_server() -> Any | None:
    """启动 gRPC server :50055（lifespan 调用）.

    降级策略：
      - grpcio 未安装：返回 None，仅 HTTP 服务
      - 启动失败：仅记录错误，不阻塞 FastAPI 主流程
    """
    global _server

    try:
        import grpc  # type: ignore[import-not-found]
        from generated_proto import analytics_pb2_grpc  # type: ignore[import-not-found]
    except ImportError as exc:
        logger.warning("grpc_dependencies_not_installed_degraded", error=str(exc))
        return None

    _server = grpc.aio.server()

    # 注册 AnalyticsService
    servicer = AnalyticsServiceServicer()
    analytics_pb2_grpc.add_AnalyticsServiceServicer_to_server(servicer, _server)

    # 注册 HealthService
    try:
        from grpc_health.v1 import (  # type: ignore[import-not-found]
            health,
            health_pb2,
            health_pb2_grpc,
        )

        health_servicer = health.aio.HealthServicer()
        await health_servicer.set("analytics", health_pb2.HealthCheckResponse.SERVING)
        await health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)  # overall
        health_pb2_grpc.add_HealthServicer_to_server(health_servicer, _server)
    except ImportError:
        logger.warning("grpc_health_not_installed_skip_health_service")

    # 绑定端口
    bind_address = f"[::]:{settings.grpc_port}"
    _server.add_insecure_port(bind_address)

    try:
        await _server.start()
        logger.info(
            "grpc_server_started",
            port=settings.grpc_port,
            rpc_count=12,
        )
        return _server
    except Exception as exc:  # noqa: BLE001
        logger.error("grpc_server_start_failed", error=str(exc), port=settings.grpc_port)
        _server = None
        return None


async def stop_grpc_server() -> None:
    """停止 gRPC server（lifespan 退出时调用）."""
    global _server
    if _server is not None:
        try:
            await _server.stop(grace=5)
            logger.info("grpc_server_stopped")
        except Exception as exc:  # noqa: BLE001
            logger.warning("grpc_server_stop_failed", error=str(exc))
        finally:
            _server = None


def is_running() -> bool:
    """gRPC server 是否运行中."""
    return _server is not None
