# -*- coding: utf-8 -*-
"""
助教服务 —— COACH-1 助教详情。

数据来源：
- ETL 直连（fdw_queries）：助教信息、绩效、TOP 客户、服务记录、历史月份
- 业务库（biz.*）：助教任务、备注

⚠️ DWD-DOC 强制规则：
- 规则 1: 金额使用 items_sum 口径（ledger_amount），禁止 consume_money
- 规则 2: 助教费用使用 assistant_pd_money + assistant_cx_money，禁止 service_fee
- DQ-6: 会员信息通过 member_id JOIN v_dim_member (scd2_is_current=1)
- DQ-7: 余额通过 member_id JOIN v_dim_member_card_account (scd2_is_current=1)
- 废单排除: is_delete = 0
"""

from __future__ import annotations

import datetime
import logging

from fastapi import HTTPException

from decimal import Decimal

from app.services import fdw_queries
from app.services.task_generator import compute_heart_icon
from app.services.task_manager import build_performance_summary
from app.trace.decorators import trace_service

logger = logging.getLogger(__name__)


# ── 颜色/样式映射 ──────────────────────────────────────────

LEVEL_COLOR_MAP = {
    "星级": "#FF6B6B",
    "高级": "#FFA726",
    "中级": "#42A5F5",
    "初级": "#66BB6A",
}

TASK_TYPE_MAP = {
    "follow_up_visit": {"label": "客户回访", "class": "callback"},
    "high_priority_recall": {"label": "高优先召回", "class": "high-priority"},
    "priority_recall": {"label": "优先召回", "class": "priority"},
    "relationship_building": {"label": "关系构建", "class": "relationship"},
}

# 头像渐变色池（循环使用）
AVATAR_GRADIENTS = [
    "from-blue-400 to-blue-600",
    "from-green-400 to-green-600",
    "from-purple-400 to-purple-600",
    "from-orange-400 to-orange-600",
    "from-pink-400 to-pink-600",
]

# CHANGE 2026-03-19 | feiqiu-data-rules 规则 6 修复 | 删除硬编码 DEFAULT_TIER_NODES，
# 档位节点改为从 cfg_performance_tier 配置表动态读取。
# 旧值 [0, 100, 130, 160, 190, 220] 与配置表实际值 [0, 120, 150, 180, 210] 不一致。
_FALLBACK_TIER_NODES: list[float] = [0, 120, 150, 180, 210]  # 仅在配置表查询失败时使用


def _get_biz_connection():
    """延迟导入业务库连接。"""
    from app.database import get_connection
    return get_connection()


def _get_initial(name: str) -> str:
    """取姓氏首字作为头像文字。"""
    return name[0] if name else "?"


def _get_avatar_gradient(index: int) -> str:
    """根据索引循环分配头像渐变色。"""
    return AVATAR_GRADIENTS[index % len(AVATAR_GRADIENTS)]


def _format_currency(amount: float) -> str:
    """格式化金额：¥6,950。"""
    if amount >= 10000:
        return f"¥{amount:,.0f}"
    return f"¥{amount:,.0f}"


# ── 6.1 核心函数 ──────────────────────────────────────────


@trace_service("获取助教 banner", "Get coach banner")
async def get_coach_banner(coach_id: int, site_id: int) -> dict:
    """
    助教 banner 轻量信息（仅 name / level / store_name）。

    用途：小程序需要展示助教 banner 但不需要详情页全套数据时
    （如 PERF-2 业绩明细页 banner）。比 get_coach_detail 快一个数量级
    （仅 2~3 条 SQL，跳过绩效/TOP/服务记录/任务/备注/历史月份）。
    """
    conn = _get_biz_connection()
    try:
        # 1. name + level（来自 v_dim_assistant + level_map）
        info = fdw_queries.get_assistant_info(conn, site_id, coach_id)
        if not info:
            raise HTTPException(status_code=404, detail="助教不存在")

        # 2. store_name（来自业务库 biz.sites）
        store_name = ""
        try:
            with conn.cursor() as cur:
                cur.execute(
                    "SELECT site_name FROM biz.sites WHERE site_id = %s",
                    (site_id,),
                )
                row = cur.fetchone()
                if row:
                    store_name = row[0] or ""
            conn.commit()
        except Exception:
            logger.warning("查询 store_name 失败，降级为空", exc_info=True)
            try:
                conn.rollback()
            except Exception:
                pass

        return {
            "id": coach_id,
            "name": info.get("name", ""),
            "level": info.get("level", ""),
            "store_name": store_name,
        }
    finally:
        conn.close()


@trace_service("获取助教详情", "Get coach detail")
async def get_coach_detail(coach_id: int, site_id: int) -> dict:
    """
    助教详情（COACH-1）。

    核心字段查询失败 → 500；扩展模块查询失败 → 空默认值（优雅降级）。
    """
    conn = _get_biz_connection()
    try:
        # ── 核心字段（失败直接抛 500）──
        assistant_info = fdw_queries.get_assistant_info(conn, site_id, coach_id)
        if not assistant_info:
            raise HTTPException(status_code=404, detail="助教不存在")

        # 业务时间锚：sandbox 模式下用 business_date，避免读到 sandbox_date 之后真实绩效
        from app.services.runtime_context import as_runtime_today_param
        now = as_runtime_today_param(site_id, conn=conn)

        # 门店名称（用于小程序 banner 展示，跟随被查看助教所在门店）
        # 必须在所有 fdw 查询前执行：后续任意 fdw 查询失败会污染事务
        # （psycopg2 的 InFailedSqlTransaction），导致此处 SELECT 拿不到结果。
        store_name = ""
        try:
            with conn.cursor() as cur:
                cur.execute(
                    "SELECT site_name FROM biz.sites WHERE site_id = %s",
                    (site_id,),
                )
                row = cur.fetchone()
                if row:
                    store_name = row[0] or ""
            conn.commit()
        except Exception:
            logger.warning("查询 store_name 失败，降级为空", exc_info=True)
            try:
                conn.rollback()
            except Exception:
                pass

        # 绩效数据：统一使用 build_performance_summary（与任务页同源，数据来自 monthly_summary 实时值）
        try:
            perf_summary = build_performance_summary(conn, site_id, coach_id)
        except Exception:
            logger.warning("build_performance_summary 失败，降级为空", exc_info=True)
            perf_summary = {}

        # customerBalance：该助教所有客户余额合计（绩效概览之外的扩展数据）
        customer_balance = 0.0
        try:
            top_custs = fdw_queries.get_coach_top_customers(conn, site_id, coach_id, limit=1000)
            member_ids = [c["member_id"] for c in top_custs if c.get("member_id")]
            if member_ids:
                balance_map = fdw_queries.get_member_balance(conn, site_id, member_ids)
                customer_balance = sum(float(v) for v in balance_map.values())
        except Exception:
            logger.warning("查询 customerBalance 失败，降级为 0", exc_info=True)

        # tasksCompleted + taskStats：当月已完成任务数，按类型分组
        # tasksCompleted + taskStats：当月已完成任务数（狭义：助教亲自完成，不含 resolved）
        tasks_completed = 0
        task_stats = {"callback": 0, "recall": 0}
        try:
            month_start = now.replace(day=1)
            with conn.cursor() as cur:
                cur.execute(
                    """
                    SELECT task_type, COUNT(*) AS cnt
                    FROM biz.coach_tasks
                    WHERE assistant_id = %s
                      AND status = 'completed'
                      AND updated_at >= %s
                    GROUP BY task_type
                    """,
                    (coach_id, month_start),
                )
                for row in cur.fetchall():
                    task_type, cnt = row[0], row[1]
                    tasks_completed += cnt
                    if task_type == "follow_up_visit":
                        task_stats["callback"] += cnt
                    elif task_type in ("high_priority_recall", "priority_recall"):
                        task_stats["recall"] += cnt
        except Exception:
            logger.warning("查询 tasksCompleted 失败，降级为 0", exc_info=True)

        # customerCount：从绩效概览获取，回退到独立查询
        customer_count = perf_summary.get("total_customers", 0)
        if not customer_count:
            try:
                cc_map = fdw_queries.get_monthly_customer_count(
                    conn, site_id, coach_id, [now.strftime("%Y-%m-01")]
                )
                customer_count = sum(cc_map.values())
            except Exception:
                logger.warning("查询 customerCount 失败，降级为 0", exc_info=True)

        # 构建 performance 字段：合并绩效概览 + 助教详情专属扩展字段
        performance = {
            **perf_summary,
            # 助教详情页专属字段（绩效概览中没有的）
            "customer_balance": customer_balance,
            "tasks_completed": tasks_completed,
            # 兼容旧字段名（前端渐进适配）
            "monthly_hours": perf_summary.get("total_hours", 0.0),
            "monthly_salary": perf_summary.get("total_income", 0.0),
        }

        # ── 扩展模块（独立 try/except 优雅降级）──

        # 收入明细
        try:
            income = _build_income(conn, site_id, coach_id, now)
        except Exception:
            logger.warning("构建 income 失败，降级为空", exc_info=True)
            income = {"this_month": [], "last_month": []}

        # TOP 客户
        try:
            top_customers = _build_top_customers(conn, site_id, coach_id)
        except Exception:
            logger.warning("构建 topCustomers 失败，降级为空列表", exc_info=True)
            top_customers = []

        # 近期服务记录
        try:
            service_records = _build_service_records(conn, site_id, coach_id)
        except Exception:
            logger.warning("构建 serviceRecords 失败，降级为空列表", exc_info=True)
            service_records = []

        # 任务分组
        try:
            task_groups = _build_task_groups(coach_id, site_id, conn)
        except Exception:
            logger.warning("构建 taskGroups 失败，降级为空", exc_info=True)
            task_groups = {"visible_tasks": [], "hidden_tasks": [], "abandoned_tasks": []}

        # 备注
        try:
            notes = _build_notes(coach_id, site_id, conn)
        except Exception:
            logger.warning("构建 notes 失败，降级为空列表", exc_info=True)
            notes = []

        # 历史月份
        try:
            history_months = _build_history_months(coach_id, site_id, conn)
        except Exception:
            logger.warning("构建 historyMonths 失败，降级为空列表", exc_info=True)
            history_months = []

        return {
            # 基础信息
            "id": coach_id,
            "name": assistant_info.get("name", ""),
            "avatar": assistant_info.get("avatar", ""),
            "level": perf_summary.get("current_tier_label", assistant_info.get("level", "")),
            "store_name": store_name,
            "skills": assistant_info.get("skills", []),
            "work_years": assistant_info.get("work_years", 0.0),
            "customer_count": customer_count,
            "hire_date": assistant_info.get("hire_date"),
            # 绩效（包含 tier_nodes、total_hours 等完整字段）
            "performance": performance,
            # 收入
            "income": income,
            # 档位（保留顶级字段兼容前端已有逻辑）
            "tier_nodes": perf_summary.get("tier_nodes", list(_FALLBACK_TIER_NODES)),
            # 当月任务完成统计（回访/召回分类）
            "task_stats": task_stats,
            # 任务分组
            "visible_tasks": task_groups["visible_tasks"],
            "hidden_tasks": task_groups["hidden_tasks"],
            "abandoned_tasks": task_groups["abandoned_tasks"],
            # TOP 客户
            "top_customers": top_customers,
            # 近期服务记录
            "service_records": service_records,
            # 历史月份
            "history_months": history_months,
            # 备注
            "notes": notes,
        }
    finally:
        conn.close()


# ── 6.2 收入明细 + 档位 ──────────────────────────────────


def _build_income(
    conn, site_id: int, coach_id: int, now: datetime.date
) -> dict:
    """
    构建 income 模块。

    thisMonth/lastMonth 各含 4 项：
    - 基础课时费（base_income / assistant_pd_money）
    - 激励课时费（bonus_income / assistant_cx_money）
    - 充值提成
    - 酒水提成

    ⚠️ DWD-DOC 规则 2: 使用 assistant_pd_money + assistant_cx_money 拆分。
    """
    # 当月
    salary_this = fdw_queries.get_salary_calc(
        conn, site_id, coach_id, now.year, now.month
    ) or {}

    # 上月
    if now.month == 1:
        last_year, last_month = now.year - 1, 12
    else:
        last_year, last_month = now.year, now.month - 1

    salary_last = fdw_queries.get_salary_calc(
        conn, site_id, coach_id, last_year, last_month
    ) or {}

    def _make_items(salary: dict) -> list[dict]:
        return [
            {
                "label": "基础课时费",
                "amount": f"¥{salary.get('assistant_pd_money_total', 0.0):,.0f}",
                "color": "primary",
            },
            {
                "label": "激励课时费",
                "amount": f"¥{salary.get('assistant_cx_money_total', 0.0):,.0f}",
                "color": "success",
            },
            {
                "label": "充值提成",
                "amount": f"¥{salary.get('bonus_money', 0.0):,.0f}",
                "color": "warning",
            },
            {
                "label": "酒水提成",
                "amount": f"¥{salary.get('room_income', 0.0):,.0f}",
                "color": "purple",
            },
        ]

    return {
        "this_month": _make_items(salary_this),
        "last_month": _make_items(salary_last),
    }


def _build_tier_nodes(conn: Any, site_id: int) -> list[float]:
    """
    从 cfg_performance_tier 配置表构建 tierNodes 档位节点数组。

    ⚠️ feiqiu-data-rules 规则 6: 绩效档位必须从配置表读取，禁止硬编码。
    """
    # CHANGE 2026-03-19 | feiqiu-data-rules 规则 6 修复 | 从配置表动态读取档位节点
    try:
        tiers = fdw_queries.get_performance_tiers(conn, site_id)
        if tiers:
            return [t["min_hours"] for t in tiers]
    except Exception:
        logger.warning("查询 cfg_performance_tier 失败，使用 fallback", exc_info=True)
    return list(_FALLBACK_TIER_NODES)


# ── 6.3 TOP 客户 + 近期服务记录 ──────────────────────────


def _build_top_customers(
    conn, site_id: int, coach_id: int
) -> list[dict]:
    """
    构建 topCustomers 模块（最多 20 条）。

    ⚠️ DQ-6: 客户姓名通过 v_dim_member 获取。
    ⚠️ DQ-7: 余额通过 v_dim_member_card_account 获取。
    ⚠️ DWD-DOC 规则 1: consume 使用 ledger_amount（items_sum 口径）。

    heartEmoji 四级映射（P6 AC3，rs_display 0-10 刻度）：
    - score > 8.5 → "💖"
    - score > 7   → "🧡"
    - score > 5   → "💛"
    - score ≤ 5   → "💙"
    """
    raw = fdw_queries.get_coach_top_customers(conn, site_id, coach_id, limit=20)
    if not raw:
        return []

    # 获取关系指数（用于 heartEmoji）
    # 批量获取所有客户的关系指数
    member_ids = [c["member_id"] for c in raw if c.get("member_id")]
    relation_map: dict[int, float] = {}
    for mid in member_ids:
        try:
            rels = fdw_queries.get_relation_index(conn, site_id, mid)
            for r in rels:
                if r.get("assistant_id") == coach_id:
                    relation_map[mid] = r.get("relation_index", 0.0)
                    break
        except Exception:
            pass

    result = []
    for i, cust in enumerate(raw):
        mid = cust.get("member_id")
        # 散客（member_id ≤ 0）展示"散客待转换会员"，头像首字统一为"?"
        is_scattered = not mid or mid <= 0
        if is_scattered:
            name = "散客待转换会员"
        else:
            name = cust.get("customer_name", "")
        score = relation_map.get(mid, 0.0)

        # 四级 heart icon 映射（P6 AC3，rs_display 0-10 刻度）
        heart_emoji = compute_heart_icon(Decimal(str(score)))
        if score > 8.5:
            score_color = "#FF6B6B"
        elif score > 7:
            score_color = "#FF8C00"
        elif score > 5:
            score_color = "#FFA726"
        else:
            score_color = "#5B9BD5"

        balance = cust.get("customer_balance", 0.0)
        consume = cust.get("total_consume", 0.0)

        # CHANGE 2026-03-29 | coach-detail-500 修复 | relation_score → score，对齐 TopCustomer.score Schema
        result.append({
            "id": mid or 0,
            "name": name,
            "initial": "?" if is_scattered else _get_initial(name),
            "is_scattered": is_scattered,
            "avatar_gradient": _get_avatar_gradient(i),
            "heart_emoji": heart_emoji,
            "score": f"{score:.2f}",
            "score_color": score_color,
            "service_count": cust.get("service_count", 0),
            "balance": float(balance) if balance else 0.0,
            "consume": float(consume) if consume else 0.0,
        })

    return result


def _build_service_records(
    conn, site_id: int, coach_id: int
) -> list[dict]:
    """
    构建 serviceRecords 模块。

    ⚠️ DWD-DOC 规则 1: income 使用 ledger_amount。
    ⚠️ 废单排除: is_delete = 0。
    """
    raw = fdw_queries.get_coach_service_records(
        conn, site_id, coach_id, limit=20, offset=0
    )
    if not raw:
        return []

    result = []
    for i, rec in enumerate(raw):
        # 散客（member_id ≤ 0）展示"散客待转换会员"，头像首字统一为"?"
        mid = rec.get("member_id")
        is_scattered = not mid or mid <= 0
        if is_scattered:
            name = "散客待转换会员"
        else:
            name = rec.get("customer_name", "")
        course_type = rec.get("course_type", "")

        # type_class 映射
        if "激励" in course_type or "超休" in course_type:
            type_class = "tag-bonus"
        else:
            type_class = "tag-base"

        create_time = rec.get("create_time")
        date_str = create_time.strftime("%Y-%m-%d %H:%M") if create_time else ""

        hours = rec.get("service_hours", 0.0)
        income = rec.get("income", 0.0)

        result.append({
            "customer_id": rec.get("member_id"),
            "customer_name": name,
            "initial": "?" if is_scattered else _get_initial(name),
            "is_scattered": is_scattered,
            "avatar_gradient": _get_avatar_gradient(i),
            "type": course_type or "课程",
            "type_class": type_class,
            "table": rec.get("table_name") or "",
            "duration": f"{hours:.1f}h",
            "income": float(income),
            "date": date_str,
            "perf_hours": None,
        })

    return result


# ── 6.4 任务分组 + 备注 ──────────────────────────────────


def _build_task_groups(
    coach_id: int, site_id: int, conn
) -> dict:
    """
    构建任务分组。

    1. 查询 biz.coach_tasks WHERE assistant_id=coach_id
    2. 按 status 分组：active→visibleTasks, inactive→hiddenTasks, abandoned→abandonedTasks
    3. visible/hidden：关联 biz.notes 获取备注列表
    4. abandoned：取 abandon_reason
    """
    with conn.cursor() as cur:
        cur.execute(
            """
            SELECT id, member_id, task_type, status, abandon_reason
            FROM biz.coach_tasks
            WHERE assistant_id = %s
              AND status IN ('active', 'inactive', 'abandoned')
            ORDER BY
              CASE task_type
                WHEN 'high_priority_recall' THEN 0
                WHEN 'priority_recall' THEN 1
                WHEN 'follow_up_visit' THEN 2
                WHEN 'relationship_building' THEN 3
                ELSE 4
              END,
              created_at DESC
            """,
            (coach_id,),
        )
        rows = cur.fetchall()

    if not rows:
        return {"visible_tasks": [], "hidden_tasks": [], "abandoned_tasks": []}

    # 收集客户 ID 批量查询姓名
    member_ids = list({r[1] for r in rows if r[1]})
    member_name_map: dict[int, str] = {}
    if member_ids:
        try:
            info_map = fdw_queries.get_member_info(conn, site_id, member_ids)
            for mid, info in info_map.items():
                member_name_map[mid] = info.get("nickname", "")
        except Exception:
            logger.warning("批量查询客户姓名失败", exc_info=True)

    # 收集任务 ID 批量查询备注
    task_ids = [r[0] for r in rows if r[3] in ("active", "inactive")]
    task_notes_map: dict[int, list[dict]] = {}
    if task_ids:
        try:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    SELECT task_id, is_pinned, content, created_at
                    FROM biz.notes
                    WHERE task_id = ANY(%s)
                    ORDER BY created_at DESC
                    """,
                    (task_ids,),
                )
                for nr in cur.fetchall():
                    tid = nr[0]
                    if tid not in task_notes_map:
                        task_notes_map[tid] = []
                    task_notes_map[tid].append({
                        "pinned": bool(nr[1]),
                        "text": nr[2] or "",
                        "date": nr[3].isoformat() if nr[3] else "",
                    })
        except Exception:
            logger.warning("批量查询任务备注失败", exc_info=True)

    visible_tasks = []
    hidden_tasks = []
    abandoned_tasks = []

    for row in rows:
        task_id, member_id, task_type, status, abandon_reason = row
        customer_name = member_name_map.get(member_id, "")
        task_meta = TASK_TYPE_MAP.get(task_type, {"label": task_type or "", "class": "tag-default"})

        if status == "abandoned":
            abandoned_tasks.append({
                "customer_name": customer_name,
                "reason": abandon_reason or "",
            })
        else:
            notes_list = task_notes_map.get(task_id, [])
            item = {
                "type_label": task_meta["label"],
                "type_class": task_meta["class"],
                "customer_name": customer_name,
                "customer_id": member_id,
                "note_count": len(notes_list),
                "pinned": any(n.get("pinned") for n in notes_list),
                "notes": notes_list if notes_list else None,
            }
            if status == "active":
                visible_tasks.append(item)
            else:
                hidden_tasks.append(item)

    return {
        "visible_tasks": visible_tasks,
        "hidden_tasks": hidden_tasks,
        "abandoned_tasks": abandoned_tasks,
    }


def _build_notes(coach_id: int, site_id: int, conn) -> list[dict]:
    """
    构建助教相关备注列表（最多 20 条）。

    查询 biz.notes 中与该助教任务关联的备注，按 created_at 倒序。
    ⚠️ DQ-6: 客户姓名通过 member_id JOIN v_dim_member。
    """
    with conn.cursor() as cur:
        cur.execute(
            """
            SELECT n.id, n.content, n.created_at, n.ai_score,
                   n.type AS tag_label,
                   ct.member_id
            FROM biz.notes n
            LEFT JOIN biz.coach_tasks ct ON n.task_id = ct.id
            WHERE ct.assistant_id = %s
            ORDER BY n.created_at DESC
            LIMIT 20
            """,
            (coach_id,),
        )
        rows = cur.fetchall()

    if not rows:
        return []

    # 批量获取客户姓名（DQ-6）
    member_ids = list({r[5] for r in rows if r[5]})
    member_name_map: dict[int, str] = {}
    if member_ids:
        try:
            info_map = fdw_queries.get_member_info(conn, site_id, member_ids)
            for mid, info in info_map.items():
                member_name_map[mid] = info.get("nickname", "")
        except Exception:
            logger.warning("查询备注客户姓名失败", exc_info=True)

    result = []
    for r in rows:
        # CHANGE 2026-03-29 | coach-detail-500 修复 | ai_score → score，对齐 CoachNoteItem.score Schema
        result.append({
            "id": r[0],
            "content": r[1] or "",
            "timestamp": r[2].isoformat() if r[2] else "",
            "score": r[3],
            "customer_name": member_name_map.get(r[5], ""),
            "tag_label": r[4] or "",
            "created_at": r[2].isoformat() if r[2] else "",
        })

    return result


# ── 6.5 历史月份统计（T2-6）──────────────────────────────


def _build_history_months(
    coach_id: int, site_id: int, conn
) -> list[dict]:
    """
    构建 historyMonths 模块。

    1. fdw_queries.get_salary_calc_multi_months() → 最近 6 个月工时/工资
    2. fdw_queries.get_monthly_customer_count() → 各月客户数
    3. biz.coach_tasks → 各月回访/召回完成数
    4. 本月 estimated=True，历史月份 estimated=False
    5. 格式化：customers→"22人"，hours→"87.5h"，salary→"¥6,950"
    """
    # 业务时间锚：sandbox 模式下用 business_date 计算最近 6 个月
    from app.services.runtime_context import as_runtime_today_param
    now = as_runtime_today_param(site_id, conn=conn)

    # 生成最近 6 个月的月份列表（含本月）
    months: list[str] = []
    for i in range(6):
        y = now.year
        m = now.month - i
        while m <= 0:
            m += 12
            y -= 1
        months.append(f"{y}-{m:02d}-01")

    # 批量查询绩效数据
    salary_map = fdw_queries.get_salary_calc_multi_months(
        conn, site_id, coach_id, months
    )

    # 批量查询各月客户数
    customer_count_map = fdw_queries.get_monthly_customer_count(
        conn, site_id, coach_id, months
    )

    # 查询各月回访/召回完成数
    callback_map: dict[str, int] = {}
    recall_map: dict[str, int] = {}
    try:
        six_months_ago = months[-1]  # 最早的月份
        with conn.cursor() as cur:
            cur.execute(
                """
                SELECT DATE_TRUNC('month', updated_at)::date AS month,
                       task_type,
                       COUNT(*) AS cnt
                FROM biz.coach_tasks
                WHERE assistant_id = %s
                  AND status = 'completed'
                  AND updated_at >= %s::date
                GROUP BY DATE_TRUNC('month', updated_at)::date, task_type
                """,
                (coach_id, six_months_ago),
            )
            for row in cur.fetchall():
                month_key = str(row[0])
                task_type = row[1]
                cnt = row[2] or 0
                if task_type == "follow_up_visit":
                    callback_map[month_key] = callback_map.get(month_key, 0) + cnt
                elif task_type in ("high_priority_recall", "priority_recall"):
                    recall_map[month_key] = recall_map.get(month_key, 0) + cnt
    except Exception:
        logger.warning("查询回访/召回完成数失败", exc_info=True)

    # 构建结果
    current_month_str = now.strftime("%Y-%m-01")
    result = []
    for i, month_str in enumerate(months):
        salary = salary_map.get(month_str, {})
        customers = customer_count_map.get(month_str, 0)
        hours = salary.get("effective_hours", 0.0)
        salary_amount = salary.get("gross_salary", 0.0)
        callback_done = callback_map.get(month_str, 0)
        recall_done = recall_map.get(month_str, 0)

        # 月份标签
        if i == 0:
            month_label = "本月"
        elif i == 1:
            month_label = "上月"
        else:
            # 提取月份数字
            m_num = int(month_str.split("-")[1])
            month_label = f"{m_num}月"

        result.append({
            "month": month_label,
            "estimated": month_str == current_month_str,
            "customers": customers,
            "hours": float(hours),
            "salary": float(salary_amount),
            "callback_done": callback_done,
            "recall_done": recall_done,
        })

    return result
