"""
批量保存需求分级结果到 demand_grade 表。
"""
from __future__ import annotations

import json
import logging
from decimal import Decimal
from typing import Any, Optional

from agents.demand_grade_agent.tools.demand_priority import build_demand_priority_index
from agents.demand_grade_agent.tools.shared import (
    VALID_GRADES,
    collect_strategies,
    dump_int_list,
    merge_video_ids,
    normalize_biz_dt,
)
from supply_agent.tools import tool
from supply_infra.db.repositories.demand_grade_category_rel_repo import (
    DemandGradeCategoryRelRepository,
)
from supply_infra.db.repositories.demand_grade_repo import DemandGradeRepository
from supply_infra.db.repositories.multi_demand_pool_di_repo import MultiDemandPoolDiRepository
from supply_infra.db.session import get_session

logger = logging.getLogger(__name__)
_MAX_ERROR_DETAILS = 10


def _format_errors(errors: list[str]) -> str:
    if not errors:
        return ""
    if len(errors) <= _MAX_ERROR_DETAILS:
        return "；".join(errors)
    hidden = len(errors) - _MAX_ERROR_DETAILS
    return "；".join(errors[:_MAX_ERROR_DETAILS]) + f"；...另有 {hidden} 条类似错误"


def _coerce_items(raw: Any) -> tuple[list[Any], str | None]:
    """将 Agent 传入的 items 规范为列表，兼容误传 JSON 字符串。"""
    if raw is None:
        return [], "items 不能为空"
    if isinstance(raw, str):
        text = raw.strip()
        if not text:
            return [], "items 不能为空"
        try:
            raw = json.loads(text)
        except json.JSONDecodeError:
            return [], "items 必须是对象数组，不能把未解析的 JSON 字符串直接传入"
    if isinstance(raw, dict):
        return [raw], None
    if not isinstance(raw, list):
        return [], f"items 必须是数组，当前类型: {type(raw).__name__}"
    return raw, None


def _optional_decimal(value: Any, field: str, idx: int, errors: list[str]) -> Decimal | None:
    if value is None or value == "":
        return None
    try:
        return Decimal(str(value))
    except Exception:
        errors.append(f"第 {idx} 项 {field} 无效: {value!r}")
        return None


def _optional_int_list(value: Any, field: str, idx: int, errors: list[str]) -> list[int]:
    if value is None:
        return []
    if not isinstance(value, list):
        errors.append(f"第 {idx} 项 {field} 必须是数组: {value!r}")
        return []
    out: list[int] = []
    for v in value:
        try:
            out.append(int(v))
        except (TypeError, ValueError):
            errors.append(f"第 {idx} 项 {field} 含无效元素: {v!r}")
            return []
    return out


def _normalize_items(
    items: list[dict[str, Any]],
    *,
    default_biz_dt: str | None,
) -> tuple[list[dict[str, Any]], list[list[int]], list[list[int]], list[str]]:
    """校验并规范化待落库行，返回 (rows, related_pool_id_lists, category_id_lists, errors)。"""
    rows: list[dict[str, Any]] = []
    related_pool_id_lists: list[list[int]] = []
    category_id_lists: list[list[int]] = []
    errors: list[str] = []
    seen_keys: set[tuple[str, str]] = set()

    for idx, item in enumerate(items):
        if not isinstance(item, dict):
            errors.append(f"第 {idx} 项不是对象")
            continue

        demand_name = str(item.get("demand_name") or "").strip()
        if not demand_name:
            errors.append(f"第 {idx} 项缺少 demand_name")
            continue

        grade = str(item.get("grade") or "").strip().upper()
        if grade not in VALID_GRADES:
            errors.append(f"第 {idx} 项 grade 无效（只能是 {'/'.join(VALID_GRADES)}）: {item.get('grade')!r}")
            continue

        reason = str(item.get("reason") or "").strip()
        if not reason:
            errors.append(f"第 {idx} 项缺少 reason")
            continue

        item_biz_dt, err = normalize_biz_dt(item.get("biz_dt"))
        if err:
            errors.append(f"第 {idx} 项 {err}")
            continue
        resolved_biz_dt = item_biz_dt or default_biz_dt
        if not resolved_biz_dt:
            errors.append(f"第 {idx} 项缺少 biz_dt（且未传入默认 biz_dt）")
            continue

        dedupe_key = (resolved_biz_dt, demand_name)
        if dedupe_key in seen_keys:
            errors.append(f"第 {idx} 项在本次请求中重复: biz_dt={resolved_biz_dt}, demand_name={demand_name}")
            continue

        related_pool_ids = _optional_int_list(item.get("related_pool_ids"), "related_pool_ids", idx, errors)
        if not related_pool_ids and item.get("pool_id") is not None:
            try:
                related_pool_ids = [int(item["pool_id"])]
            except (TypeError, ValueError):
                errors.append(f"第 {idx} 项 pool_id 无效: {item.get('pool_id')!r}")
                continue
        if not related_pool_ids:
            errors.append(
                f"第 {idx} 项缺少 related_pool_ids（必填，需先用 search_related_pool_demands 找到对应的 "
                f"multi_demand_pool_di.id）"
            )
            continue

        seen_keys.add(dedupe_key)

        prior_raw = item.get("prior_total_score")
        if prior_raw is None or prior_raw == "" or prior_raw == "—":
            prior_total_score = None
        else:
            parsed_prior = _optional_decimal(prior_raw, "prior_total_score", idx, errors)
            prior_total_score = None if parsed_prior is None or parsed_prior == 0 else parsed_prior
        posterior_rov_avg = _optional_decimal(item.get("posterior_rov_avg"), "posterior_rov_avg", idx, errors)

        posterior_rov_count = 0
        if item.get("posterior_rov_count") is not None:
            try:
                posterior_rov_count = int(item.get("posterior_rov_count"))
            except (TypeError, ValueError):
                errors.append(f"第 {idx} 项 posterior_rov_count 无效: {item.get('posterior_rov_count')!r}")
                continue

        category_ids = _optional_int_list(item.get("category_ids"), "category_ids", idx, errors)

        has_posterior = 1 if posterior_rov_count > 0 else 0

        rows.append(
            {
                "biz_dt": resolved_biz_dt,
                "demand_name": demand_name,
                "category_ids": dump_int_list(category_ids),
                "grade": grade,
                # 保存阶段会基于当日全量需求池确定性重算，禁止由模型自由填写。
                "score": None,
                "prior_total_score": prior_total_score,
                "posterior_rov_avg": posterior_rov_avg,
                "posterior_rov_count": posterior_rov_count,
                "has_posterior": has_posterior,
                "related_pool_ids": dump_int_list(related_pool_ids),
                "reason": reason,
            }
        )
        related_pool_id_lists.append(related_pool_ids)
        category_id_lists.append(category_ids)

    return rows, related_pool_id_lists, category_id_lists, errors


@tool
def batch_save_demand_grades(items: list[dict[str, Any]], biz_dt: Optional[str] = None) -> str:
    """
    批量保存需求分级结果到 demand_grade 表（按 biz_dt+demand_name upsert，可重复调用覆盖修正）。

    video_list（关联视频列表）与 strategies（来源策略列表）会自动从 related_pool_ids 对应的
    multi_demand_pool_di 原始行推导写入，无需手工传入。category_ids 除了写入展示快照字段，
    也会同步写入 demand_grade_category_rel 映射表，供前端按分类高效查询。

    Args:
        items: 待保存列表，每项字段：
            - demand_name (必填): 需求名称
            - grade (必填): S/A/B/C/D 之一
            - reason (必填): 判断依据，需引用具体的先验/后验数值
            - related_pool_ids (必填): 该需求对应的 multi_demand_pool_di.id 列表；若输入里给了
              pool_id，也可直接写 "pool_id": 123 代替 related_pool_ids
            - score: 无需传入；保存时按当日全量需求池自动计算需求自身来源归一分（0-100），
              即各 strategy 内独立排名归一化后，对该需求已有来源取均值
            - category_ids (可选): 归属的树节点 id 列表，会写入 demand_grade_category_rel 映射表
            - prior_total_score (可选): 落库时的先验 total_score 快照
            - posterior_rov_avg / posterior_rov_count (可选): 落库时的后验 rov_diff 快照（real_rov_7d_avg）；
              count>0 时自动标记为「有后验数据」
            - biz_dt (可选): 覆盖本项使用的业务日，不传则用调用时的 biz_dt 参数
        biz_dt: 本次调用的默认业务日期 YYYYMMDD；items 内每项也可单独指定 biz_dt 覆盖。

    Returns:
        保存结果摘要，包含成功条数与校验失败说明。
    """
    if not items:
        return "items 不能为空"

    default_biz_dt, err = normalize_biz_dt(biz_dt)
    if err:
        return err

    coerced_items, coerce_err = _coerce_items(items)
    if coerce_err:
        return coerce_err

    rows, related_pool_id_lists, category_id_lists, errors = _normalize_items(
        coerced_items, default_biz_dt=default_biz_dt
    )
    if not rows:
        detail = _format_errors(errors) if errors else "无有效数据"
        return f"没有可保存的数据: {detail}"

    try:
        with get_session() as session:
            pool_repo = MultiDemandPoolDiRepository(session)
            all_pool_ids = sorted({pid for ids in related_pool_id_lists for pid in ids})
            pool_rows = pool_repo.get_by_ids(all_pool_ids) if all_pool_ids else []
            pool_by_id = {int(r.id): r for r in pool_rows}
            priority_by_biz_dt = {
                resolved_dt: build_demand_priority_index(pool_repo.list_by_biz_dt(resolved_dt))
                for resolved_dt in sorted({row["biz_dt"] for row in rows})
            }

            final_rows: list[dict[str, Any]] = []
            saved_indices: list[int] = []
            for i, (row, pool_ids) in enumerate(zip(rows, related_pool_id_lists)):
                matched = [pool_by_id[pid] for pid in pool_ids if pid in pool_by_id]
                missing = [pid for pid in pool_ids if pid not in pool_by_id]
                if not matched:
                    errors.append(
                        f"demand_name={row['demand_name']!r} 的 related_pool_ids={pool_ids} "
                        f"均未在 multi_demand_pool_di 中找到，跳过该项"
                    )
                    continue
                if missing:
                    errors.append(
                        f"demand_name={row['demand_name']!r} 的 related_pool_ids 中 {missing} 未找到，已忽略"
                    )
                priority = priority_by_biz_dt[row["biz_dt"]].get(row["demand_name"])
                source_rank_score = priority.get("source_rank_score") if priority else None
                row["score"] = (
                    Decimal(str(source_rank_score)) if source_rank_score is not None else None
                )
                row["video_list"] = merge_video_ids(matched)
                row["strategies"] = collect_strategies(matched)
                final_rows.append(row)
                saved_indices.append(i)

            if not final_rows:
                detail = _format_errors(errors) if errors else "无有效数据"
                return f"没有可保存的数据: {detail}"

            grade_repo = DemandGradeRepository(session)
            affected = grade_repo.bulk_upsert(final_rows)

            names_by_biz_dt: dict[str, list[str]] = {}
            for i in saved_indices:
                names_by_biz_dt.setdefault(rows[i]["biz_dt"], []).append(rows[i]["demand_name"])

            id_by_biz_dt_name: dict[tuple[str, str], int] = {}
            for bd, names in names_by_biz_dt.items():
                for name, demand_grade_id in grade_repo.get_ids_by_names(bd, names).items():
                    id_by_biz_dt_name[(bd, name)] = demand_grade_id

            rel_repo = DemandGradeCategoryRelRepository(session)
            for i in saved_indices:
                key = (rows[i]["biz_dt"], rows[i]["demand_name"])
                demand_grade_id = id_by_biz_dt_name.get(key)
                if demand_grade_id is not None:
                    rel_repo.replace_for_demand_grade(demand_grade_id, category_id_lists[i])

        parts = [f"提交 {len(rows)} 条，成功写入/更新 {affected} 条（{len(final_rows)} 条通过校验）"]
        if errors:
            parts.append(f"校验失败/警告 {len(errors)} 条: {_format_errors(errors)}")

        message = "。".join(parts)
        logger.info("batch_save_demand_grades completed: %s", message)
        return message

    except Exception as e:
        logger.error("batch_save_demand_grades failed: %s", e, exc_info=True)
        return f"批量保存需求分级失败: {e}"
