"""持久化 find_agent 的搜索轨迹、候选证据和分池结果。"""
from __future__ import annotations

import json
import logging
import uuid
from decimal import Decimal
from typing import Any

from supply_infra.video_discovery_gates import (
    build_rule_snapshot,
    normalize_duration_seconds,
    parse_datetime_value,
)
from supply_infra.services.video_discovery_service import (
    RunNotFoundError,
    format_db_error,
    get_video_discovery_service,
)

logger = logging.getLogger(__name__)

_RUN_STATUSES = {"running", "finished", "failed"}
_FINAL_DECISION_BUCKETS = {"primary", "rejected"}


def _json(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False, default=str)


def _input_error(message: str) -> str:
    return _json({"error": message, "input_error": True})


def _clean_text(value: Any, *, max_length: int | None = None) -> str | None:
    if value is None:
        return None
    text = str(value).strip()
    if not text:
        return None
    return text[:max_length] if max_length else text


def _nonnegative_int(value: Any) -> int | None:
    if value is None or value == "":
        return None
    try:
        return max(0, int(value))
    except (TypeError, ValueError):
        return None


def _optional_decimal(value: Any, places: int) -> Decimal | None:
    if value is None or value == "":
        return None
    try:
        number = Decimal(str(value))
    except (ArithmeticError, TypeError, ValueError):
        return None
    quantum = Decimal(1).scaleb(-places)
    return number.quantize(quantum)


def _optional_ratio_decimal(value: Any, places: int = 6) -> Decimal | None:
    number = _optional_decimal(value, places)
    if number is not None and not Decimal("0") <= number <= Decimal("1"):
        raise ValueError("比例字段必须在 0~1 范围内")
    return number


def _age_side(
    age_normalization: Any,
    side: str,
) -> dict[str, Any]:
    if not isinstance(age_normalization, dict):
        return {}
    value = age_normalization.get(side)
    return value if isinstance(value, dict) else {}


def _unwrap_age_normalization(value: Any) -> Any:
    """兼容模型把标准化结果包在额外 result 字段中的常见形态。"""
    if not isinstance(value, dict):
        return value
    if isinstance(value.get("content"), dict) or isinstance(value.get("account"), dict):
        return value
    for key in ("age_normalization_result", "normalization_result", "result"):
        nested = value.get(key)
        if isinstance(nested, dict):
            return nested
    return value


def _normalized_age_metric(side: dict[str, Any], field: str) -> Any:
    """画像未获取时保持 None，避免把标准化占位值 0 当作真实低占比。"""
    if not side:
        return None
    if side.get("has_age_portrait") is False or side.get("strength") == "missing":
        return None
    return side.get(field)


def create_video_discovery_run(
    demand_word: str,
    relevant_points: list[dict[str, Any]],
    seed_video_id: str | None = None,
    seed_video_title: str | None = None,
    demand_grade_id: int | None = None,
    intent_summary: str | None = None,
    run_id: str | None = None,
) -> str:
    """
    创建一次可追踪的视频发现运行。

    若用户消息已提供预创建 run_id，必须原样传入 run_id；工具会复用已有记录，
    不会重复创建。

    Args:
        demand_word: 用户给定需求词；它是输入语义，不强制作为实际搜索词。
        relevant_points: 参考视频中与需求相关的点位对象列表。
        seed_video_id: 兼容旧调用方的可选参考视频 id；调度调用不传。
        seed_video_title: 兼容旧调用方的可选参考视频标题；调度调用不传。
        demand_grade_id: 可选 demand_grade.id。
        intent_summary: Agent 对真正受欢迎内容的初步解释，可稍后更新。
        run_id: 系统预创建的运行 id；传入已存在记录时直接复用。

    Returns:
        JSON，包含后续存储工具必须使用的 run_id。
    """
    service = get_video_discovery_service()
    cleaned_run_id = _clean_text(run_id, max_length=64)
    if cleaned_run_id:
        try:
            existing = service.lookup_run(cleaned_run_id)
            if existing is not None:
                return _json(
                    {
                        "title": "视频发现运行已存在",
                        "run_id": cleaned_run_id,
                        "status": existing["status"],
                        "pre_created": True,
                        "output": f"run_id={cleaned_run_id}",
                    }
                )
        except Exception as exc:
            logger.error("create_video_discovery_run lookup failed: %s", exc, exc_info=True)
            return _json({"error": format_db_error(exc), "title": "查询视频发现运行失败"})

    demand = _clean_text(demand_word, max_length=256)
    if not demand:
        return _input_error("demand_word 不能为空")

    new_run_id = cleaned_run_id or uuid.uuid4().hex
    rule_snapshot = build_rule_snapshot()
    values = {
        "run_id": new_run_id,
        "demand_grade_id": demand_grade_id,
        "demand_word": demand,
        "seed_video_id": _clean_text(seed_video_id, max_length=64),
        "seed_video_title": _clean_text(seed_video_title, max_length=512),
        "relevant_points_json": _json(relevant_points or []),
        "intent_summary": _clean_text(intent_summary),
        "status": "running",
        "attempt_count": 1,
        "rule_version": rule_snapshot["rule_version"],
        "rule_config_json": _json(rule_snapshot),
    }
    try:
        created = service.create_run(values)
        return _json(
            {
                "title": "视频发现运行已创建",
                "run_id": created["run_id"],
                "status": created["status"],
                "output": f"run_id={created['run_id']}",
            }
        )
    except Exception as exc:
        logger.error("create_video_discovery_run failed: %s", exc, exc_info=True)
        return _json({"error": format_db_error(exc), "title": "创建视频发现运行失败"})


def _normalize_candidate_update(item: dict[str, Any]) -> dict[str, Any]:
    try:
        candidate_id = int(item.get("candidate_id"))
    except (TypeError, ValueError):
        raise ValueError("candidate_id 必须是整数") from None
    if candidate_id <= 0:
        raise ValueError("candidate_id 必须大于 0")

    content_age = item.get("content_age_evidence")
    account_age = item.get("account_age_evidence")
    age_normalization = _unwrap_age_normalization(item.get("age_normalization"))
    content_normalization = _age_side(age_normalization, "content")
    account_normalization = _age_side(age_normalization, "account")
    decision_bucket = (
        _clean_text(item.get("decision_bucket"), max_length=24)
        or ""
    )
    if decision_bucket not in _FINAL_DECISION_BUCKETS:
        raise ValueError(
            "decision_bucket 必须是 primary 或 rejected"
        )

    publish_at = parse_datetime_value(item.get("publish_at"))
    duration = normalize_duration_seconds(
        item.get("duration_seconds")
        if item.get("duration_seconds") not in (None, "")
        else item.get("video_duration")
    )
    content_ratio = item.get("content_50_plus_ratio")
    if content_ratio is None:
        content_ratio = _normalized_age_metric(content_normalization, "older_ratio")
    content_tgi = item.get("content_50_plus_tgi")
    if content_tgi is None:
        content_tgi = _normalized_age_metric(content_normalization, "older_tgi")
    account_ratio = item.get("account_50_plus_ratio")
    if account_ratio is None:
        account_ratio = _normalized_age_metric(account_normalization, "older_ratio")
    account_tgi = item.get("account_50_plus_tgi")
    if account_tgi is None:
        account_tgi = _normalized_age_metric(account_normalization, "older_tgi")
    temporal_evidence = item.get("temporal_evidence")
    mapping = {
        "candidate_id": candidate_id,
        "title": _clean_text(item.get("title"), max_length=512),
        "content_link": _clean_text(item.get("content_link"), max_length=1024),
        "author_name": _clean_text(item.get("author_name"), max_length=256),
        "author_sec_uid": _clean_text(item.get("author_sec_uid"), max_length=256),
        "tags_json": item.get("tags") if "tags" in item else None,
        "publish_at": (
            publish_at.replace(tzinfo=None) if publish_at is not None else None
        ),
        "duration_seconds": duration,
        "play_count": _nonnegative_int(item.get("play_count")),
        "like_count": _nonnegative_int(item.get("like_count")),
        "comment_count": _nonnegative_int(item.get("comment_count")),
        "collect_count": _nonnegative_int(item.get("collect_count")),
        "share_count": _nonnegative_int(item.get("share_count")),
        "content_age_evidence_json": (
            _json(content_age) if content_age is not None else None
        ),
        "account_age_evidence_json": (
            _json(account_age) if account_age is not None else None
        ),
        "age_normalization_json": (
            _json(age_normalization) if age_normalization is not None else None
        ),
        "content_50_plus_ratio": _optional_ratio_decimal(content_ratio),
        "content_50_plus_tgi": _optional_decimal(content_tgi, 4),
        "account_50_plus_ratio": _optional_ratio_decimal(account_ratio),
        "account_50_plus_tgi": _optional_decimal(account_tgi, 4),
        "temporal_type": _clean_text(item.get("temporal_type"), max_length=24),
        "temporal_status": _clean_text(item.get("temporal_status"), max_length=16),
        "temporal_evidence_json": (
            _json(temporal_evidence) if temporal_evidence is not None else None
        ),
        "reject_reason_code": _clean_text(
            item.get("reject_reason_code"),
            max_length=64,
        ),
        "relevance_score": _optional_decimal(item.get("relevance_score"), 6),
        "elder_score": _optional_decimal(item.get("elder_score"), 6),
        "share_score": _optional_decimal(item.get("share_score"), 6),
        "value_score": _optional_decimal(item.get("value_score"), 2),
        "decision_reason": _clean_text(item.get("decision_reason")),
        "decision_bucket": decision_bucket,
    }
    return mapping


def batch_update_video_discovery_candidates(
    run_id: str,
    items: list[dict[str, Any]],
) -> str:
    """
    严格按 candidate_id 批量更新候选详情、证据、评分和最终分池。

    只更新 video_discovery_candidate；不会新增候选、修改搜索记录或修改运行状态。

    Args:
        run_id: 发现运行 id。
        items: 候选数组。每项必须包含搜索工具返回的 candidate_id，并直接提供
            decision_bucket；可更新标题、链接、作者、发布时间、时长、互动量、标签、
            双侧年龄证据、画像标准化结果、时间判断、R/E/S/V 和最终分池理由。
            primary 会在数据库事务内强制校验本次运行的 P0 规则快照。

    Returns:
        JSON，包含 updated_count 和按 candidate_id 更新后的候选记录。
    """
    run_text = _clean_text(run_id, max_length=64)
    if not run_text:
        return _input_error("run_id 不能为空")

    rows: list[dict[str, Any]] = []
    errors: list[str] = []
    for index, item in enumerate(items or []):
        if not isinstance(item, dict):
            errors.append(f"[{index}] 不是对象")
            continue
        try:
            rows.append(_normalize_candidate_update(dict(item)))
        except ValueError as exc:
            errors.append(f"[{index}] {exc}")
    if errors:
        return _json(
            {
                "error": "候选更新参数不合法",
                "input_error": True,
                "errors": errors,
            }
        )
    if not rows:
        return _input_error("items 不能为空")

    try:
        updated = get_video_discovery_service().update_candidates(
            run_text,
            rows,
        )
        reclassified = list(updated.get("reclassified") or [])
        candidate_summaries = []
        for candidate in updated["candidates"]:
            gate_results = candidate.get("gate_results") or {}
            candidate_summaries.append(
                {
                    "candidate_id": candidate.get("candidate_id"),
                    "aweme_id": candidate.get("aweme_id"),
                    "decision_bucket": candidate.get("decision_bucket"),
                    "gate_status": candidate.get("gate_status"),
                    "failed_reason_codes": gate_results.get("failed_reason_codes") or [],
                    "pending_reason_codes": gate_results.get("pending_reason_codes") or [],
                    "evidence_version": candidate.get("evidence_version"),
                    "gate_evidence_version": candidate.get("gate_evidence_version"),
                    "latest_gate_evaluation_id": candidate.get(
                        "latest_gate_evaluation_id"
                    ),
                }
            )
        payload = {
            "title": "候选已更新",
            "run_id": run_text,
            "updated_count": updated["updated_count"],
            "reclassified_count": int(
                updated.get("reclassified_count", len(reclassified))
            ),
            "reclassified": reclassified,
            "candidates": candidate_summaries,
            "output": (
                f"更新 {updated['updated_count']} 条候选；"
                f"门禁调整 {int(updated.get('reclassified_count', len(reclassified)))} 条"
            ),
        }
        return _json(payload)
    except RunNotFoundError as exc:
        return _input_error(str(exc))
    except ValueError as exc:
        return _input_error(str(exc))
    except Exception as exc:
        logger.error(
            "batch_update_video_discovery_candidates failed: %s",
            exc,
            exc_info=True,
        )
        return _json({"error": format_db_error(exc), "title": "更新候选失败"})


def update_video_discovery_run_status(
    run_id: str,
    status: str,
    intent_summary: str | None = None,
    stop_reason: str | None = None,
) -> str:
    """单独更新 video_discovery_run 的状态、意图摘要和停止原因。"""
    run_text = _clean_text(run_id, max_length=64)
    if not run_text:
        return _input_error("run_id 不能为空")
    if status not in _RUN_STATUSES:
        return _input_error(f"status 必须是: {sorted(_RUN_STATUSES)}")
    try:
        run = get_video_discovery_service().update_run_status(
            run_text,
            status=status,
            intent_summary=_clean_text(intent_summary),
            stop_reason=_clean_text(stop_reason),
        )
        return _json(
            {
                "title": "视频发现运行状态已更新",
                "run": run,
                "output": (
                    f"run_id={run_text}，status={run['status']}，"
                    f"primary_count={run['primary_count']}"
                ),
            }
        )
    except RunNotFoundError as exc:
        return _input_error(str(exc))
    except Exception as exc:
        logger.error(
            "update_video_discovery_run_status failed: %s",
            exc,
            exc_info=True,
        )
        return _json({"error": format_db_error(exc), "title": "更新运行状态失败"})


def query_video_discovery_state(
    run_id: str,
    include_rejected: bool = True,
    limit: int = 100,
) -> str:
    """
    查询一次运行已经保存的搜索轨迹、主推荐与淘汰候选。

    用于长搜索过程恢复状态、检查是否真的翻页和扩词，也用于最终自动保留判断。
    """
    run_text = _clean_text(run_id, max_length=64)
    if not run_text:
        return _json({"error": "run_id 不能为空"})
    try:
        state = get_video_discovery_service().get_full_state(
            run_text,
            include_rejected=include_rejected,
            limit=limit,
        )
        run = state["run"]
        payload = {
            "title": f"视频发现状态: {run_text}",
            "run": run,
            "searches": state["searches"],
            "candidates": state["candidates"],
            "output": (
                f"搜索页 {run['search_count']}；主推荐 {run['primary_count']}"
            ),
        }
        return _json(payload)
    except RunNotFoundError:
        return _json({"error": f"run_id 不存在: {run_text}"})
    except Exception as exc:
        logger.error("query_video_discovery_state failed: %s", exc, exc_info=True)
        return _json({"error": format_db_error(exc), "title": "查询视频发现状态失败"})
