"""外卖比价业务透传端点 + 后端 harvest 落库。

把客户端 POST /api/v1/intent/* 和 /api/v1/price/step 转发给 pricebot,同时**由 app-server
在透传里直接落库比价记录**(不再靠客户端 POST /compare/record):
  - 帧0(客户端首帧不带 trace_id)→ app-server 用 uuid 签发 trace_id、注入转发 body、回给
    客户端;并按 trace_id 建 running 行(harvest_running)。
  - 最终 done 帧 → 更新成 success/failed + 结果(harvest_done)。发奖不在这里:#113 已把
    邀请奖口径从「比价」移到「实际下单」(order.py)。
  - /trace/finalize(用户终止/Phase1 未识别,无 done)→ 更新成 cancelled/failed
    (harvest_abort,**不降级 success**)。
trace_url 从 pricebot 响应**顶层 trace_url** 取(pricebot 每帧都带,见其 goal_engine.process_step)。

鉴权:软鉴权(OptionalUser)——新客户端带 JWT → 绑 user_id;老客户端不带 → user_id 暂空,
由其后续 /compare/record 上报补(灰度期两条写路径按 trace_id reconcile,success 不被降级)。

真正的目标驱动比价逻辑在 pricebot-backend(GoalEngine,另一个 repo),本接口是透传壳 + 落库。
pricebot 协议文档: pricebot-backend/docs/main/02_api_protocol.md
"""
from __future__ import annotations

import json
import logging
import time
import uuid
from typing import Any

import httpx
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
from fastapi.concurrency import run_in_threadpool

from app.api.deps import DbSession, OptionalUser
from app.core.config import settings
from app.core.logging import trace_id_ctx
from app.core.pricebot_client import get_pricebot_client
from app.core.pricebot_router import pick_pricebot
from app.db.session import SessionLocal
from app.repositories import comparison as crud_compare
from app.repositories import risk as risk_repo
from app.services.comparison_llm_backfill import backfill_comparison_llm_cost

logger = logging.getLogger("shagua.compare")

router = APIRouter(prefix="/api/v1", tags=["compare"])


def _ensure_compare_allowed(user, db) -> None:
    if user and risk_repo.is_restricted(
        db,
        subject_type="user",
        subject_id=str(user.id),
        scope=risk_repo.SCOPE_ECONOMIC_ACCOUNT,
    ):
        raise HTTPException(status_code=403, detail="账号存在异常，该功能暂不可用")


# ============================================================
# harvest 落库(阻塞 SQLAlchemy → run_in_threadpool,独立 SessionLocal,不阻塞事件循环;
# 任何写库异常都吞掉、绝不连累比价透传返回 —— 同 coupon.py 现有 best-effort 写法)。
# ============================================================


def _harvest_running_blocking(
    trace_id: str, user_id: int | None, business_type: str,
    device_id: str | None, device_info: dict | None, trace_url: str | None,
) -> None:
    with SessionLocal() as db:
        rec = crud_compare.harvest_running(
            db, trace_id=trace_id, user_id=user_id, business_type=business_type,
            device_id=device_id, device_info=device_info, trace_url=trace_url,
        )
        if rec.user_id is not None:
            risk_repo.sync_compare_incident(
                db, user_id=rec.user_id, at=rec.created_at
            )
    logger.info(
        "harvest running row (user=%s)", user_id,
        extra={"phase": "harvest_running", "status": "running", "user_id": user_id},
    )


def _harvest_done_blocking(
    trace_id: str, user_id: int | None, done_params: dict, business_type: str,
    device_id: str | None, device_info: dict | None, trace_url: str | None,
) -> int:
    with SessionLocal() as db:
        rec, newly_success = crud_compare.harvest_done(
            db, trace_id=trace_id, user_id=user_id, done_params=done_params,
            business_type=business_type, device_id=device_id,
            device_info=device_info, trace_url=trace_url,
        )
        if rec.user_id is not None:
            risk_repo.sync_compare_incident(
                db, user_id=rec.user_id, at=rec.created_at
            )
        logger.info(
            "harvest done → %s saved=%s newly=%s", rec.status,
            rec.saved_amount_cents, newly_success,
            extra={"phase": "harvest_done", "status": rec.status,
                   "saved_cents": rec.saved_amount_cents,
                   "best_platform": rec.best_platform_id, "newly_success": newly_success,
                   "user_id": user_id},
        )
        # 不在此处发邀请奖:#113 已把发奖口径从「比价」移到「实际下单」(order.py),harvest
        # 只记录比价、不发奖。否则比价先于下单 + try_reward 幂等闸会让奖落在「比价」这步,
        # 架空 #113 的「下单才发奖」防刷意图(newly_success 仅留作日志观测)。
        return rec.id


def _harvest_abort_blocking(
    trace_id: str, status_hint: str, reason: str | None, trace_url: str | None,
) -> int | None:
    with SessionLocal() as db:
        rec = crud_compare.harvest_abort(
            db, trace_id=trace_id, status=status_hint, reason=reason, trace_url=trace_url,
        )
    logger.info(
        "harvest abort → %s", (rec.status if rec else "no-row"),
        extra={"phase": "harvest_abort",
               "status": (rec.status if rec else None), "reason": reason},
    )
    return rec.id if rec is not None else None


async def _forward(
    request: Request, upstream_path: str, user, *, harvest_first_frame: bool = True,
) -> tuple[dict[str, Any], str, dict[str, Any]]:
    """透传给 pricebot 的 {upstream_path},并 mint trace_id + 首帧建 running 行。
    返回 (resp_json, trace_id, meta)。

    - 客户端首帧不带 trace_id → app-server 用 uuid 签发、写进转发 body、回填进响应顶层
      `trace_id`(客户端据此拿到后续帧都带上)。带了(老客户端 / 后续帧)→ 原样用、走原始 bytes 快路。
    - 仅**本次 mint(=首帧)**建 running 行(idempotent);后续帧不再写库。
    """
    raw = await request.body()
    try:
        meta = json.loads(raw)
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"invalid json body: {e}") from e
    if not isinstance(meta, dict):
        meta = {}

    trace_id = meta.get("trace_id")
    minted = False
    if not trace_id:
        trace_id = str(uuid.uuid4())
        meta["trace_id"] = trace_id
        raw = json.dumps(meta).encode()  # 仅首帧重新序列化(注入 trace_id);后续帧走原始 bytes
        minted = True

    # 请求级 trace_id 贯穿:此后本请求(含 run_in_threadpool 里的 harvest)每行日志自动带 trace_id
    trace_id_ctx.set(trace_id)

    base = pick_pricebot(trace_id)
    url = f"{base.rstrip('/')}{upstream_path}"
    timeout = settings.PRICEBOT_COMPARE_TIMEOUT_SEC
    step = meta.get("step")

    logger.info(
        "→ pricebot %s step=%s%s", upstream_path, step, " [mint]" if minted else "",
        extra={"phase": "forward", "endpoint": upstream_path, "step": step,
               "device_id": meta.get("device_id"), "minted": minted,
               "query": meta.get("query"), "user_id": (user.id if user else None)},
    )

    t0 = time.monotonic()
    try:
        client = get_pricebot_client()
        resp = await client.post(
            url, content=raw, headers={"Content-Type": "application/json"}, timeout=timeout
        )
    except httpx.RequestError as e:
        logger.error(
            "← pricebot %s 不可达: %s", upstream_path, e,
            extra={"phase": "resp", "endpoint": upstream_path, "step": step,
                   "error": "upstream_unreachable"},
        )
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail=f"pricebot upstream unreachable: {e}",
        ) from e

    if resp.status_code >= 500:
        logger.error(
            "← pricebot %s 5xx=%d", upstream_path, resp.status_code,
            extra={"phase": "resp", "endpoint": upstream_path, "step": step,
                   "http_status": resp.status_code, "error": "upstream_5xx"},
        )
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail=f"pricebot upstream returned {resp.status_code}",
        )

    resp_json = resp.json()
    cost_ms = int((time.monotonic() - t0) * 1000)
    if not isinstance(resp_json, dict):
        return {}, trace_id, meta
    # 回传 app-server 签发的 trace_id(新客户端首帧据此拿到,后续帧都带上它)
    resp_json.setdefault("trace_id", trace_id)
    _action = resp_json.get("action") or {}
    logger.info(
        "← pricebot %s cmd=%s cont=%s %dms", upstream_path,
        _action.get("command"), resp_json.get("continue"), cost_ms,
        extra={"phase": "resp", "endpoint": upstream_path, "step": step,
               "command": _action.get("command"), "continue": resp_json.get("continue"),
               "cost_ms": cost_ms},
    )

    # 首帧建 running 行(仅本次 mint;老客户端自带 trace_id → 不 mint → 由 done / 其 POST 建行)
    if minted and harvest_first_frame:
        try:
            await run_in_threadpool(
                _harvest_running_blocking, trace_id,
                (user.id if user else None), "food",
                meta.get("device_id"), meta.get("device_info"),
                resp_json.get("trace_url"),
            )
        except Exception as e:  # noqa: BLE001
            logger.warning("harvest_running failed trace=%s: %s", trace_id, e)

    return resp_json, trace_id, meta


@router.post("/intent/recognize", summary="外卖比价 Phase 1 意图识别 (透传 + 建 running 行)")
async def intent_recognize(
    request: Request, user: OptionalUser, db: DbSession
) -> dict[str, Any]:
    _ensure_compare_allowed(user, db)
    resp, _, _ = await _forward(request, "/api/intent/recognize", user)
    return resp


@router.post("/intent/step", summary="外卖比价 Phase 1 多帧意图识别 (透传, 仅淘宝源)")
async def intent_step(request: Request, user: OptionalUser, db: DbSession) -> dict[str, Any]:
    _ensure_compare_allowed(user, db)
    resp, _, _ = await _forward(request, "/api/intent/step", user)
    return resp


@router.post("/intent/precoupon/step", summary="外卖比价 Phase 0 识别前先用券 (透传, 仅美团源)")
async def intent_precoupon_step(
    request: Request, user: OptionalUser, db: DbSession
) -> dict[str, Any]:
    _ensure_compare_allowed(user, db)
    resp, _, _ = await _forward(request, "/api/intent/precoupon/step", user)
    return resp


@router.post("/price/step", summary="外卖比价 Phase 2 步进 (透传 + done 落库)")
async def price_step(
    request: Request,
    background_tasks: BackgroundTasks,
    user: OptionalUser,
    db: DbSession,
) -> dict[str, Any]:
    _ensure_compare_allowed(user, db)
    resp, trace_id, meta = await _forward(request, "/api/price/step", user)
    # 最终 done 帧(command=done 且 continue=false)→ harvest 更新成终态。
    # (单平台中途 done 被 pricebot 改写成 wait+continue=true,不会命中这里,同 coupon 语义。)
    action = resp.get("action") or {}
    if action.get("command") == "done" and not resp.get("continue", True):
        done_params = action.get("params") or {}
        try:
            record_id = await run_in_threadpool(
                _harvest_done_blocking, trace_id, (user.id if user else None),
                done_params, "food",
                meta.get("device_id"), meta.get("device_info"),
                resp.get("trace_url") or done_params.get("trace_url"),
            )
            background_tasks.add_task(
                backfill_comparison_llm_cost, record_id, trace_id
            )
        except Exception as e:  # noqa: BLE001
            logger.warning("harvest_done failed trace=%s: %s", trace_id, e)
    return resp


@router.post("/trace/epilogue", summary="比价结果页尾声帧 (透传到 pricebot, 用户视角截图入 trace)")
async def trace_epilogue(
    request: Request, user: OptionalUser, db: DbSession
) -> dict[str, Any]:
    _ensure_compare_allowed(user, db)
    # App 收到 done、渲染完结果页后,把自己页面的截图(base64)传给 pricebot 存进 trace
    # 目录并触发重传 —— trace 里补上"用户实际看到的汇总页"(步骤帧只有目标 App 画面)。
    # body ~几百 KB(截图 base64), 纯透传壳(harvest_first_frame=False:不建行/不落库,
    # 该 trace 的记录已由 done/finalize 落终态)。#112 原走 _passthrough,合并到 harvest
    # 分支后统一走 _forward(_passthrough 已并入它)。
    resp, _, _ = await _forward(
        request, "/api/trace/epilogue", user, harvest_first_frame=False,
    )
    return resp


@router.post("/trace/finalize", summary="比价 trace 收尾上云 (透传 + 夭折落库)")
async def trace_finalize(
    request: Request,
    background_tasks: BackgroundTasks,
    user: OptionalUser,
    db: DbSession,
) -> dict[str, Any]:
    _ensure_compare_allowed(user, db)
    # 用户终止 / Phase1 未识别没到 done 帧: pricebot 打包半截上云返回 {trace_url};
    # app-server 顺手把该 trace 的 running 行更新成夭折终态(**不降级 success**)。
    # 客户端 finalize body 带 status(cancelled/failed)+ reason;老客户端只带 trace_id →
    # 默认 cancelled,其后续 /compare/record 上报再补精确态。
    resp, trace_id, meta = await _forward(
        request, "/api/trace/finalize", user, harvest_first_frame=False,
    )
    try:
        record_id = await run_in_threadpool(
            _harvest_abort_blocking, trace_id,
            (meta.get("status") or "cancelled"),
            (meta.get("reason") or meta.get("information")),
            (resp.get("trace_url") if isinstance(resp, dict) else None),
        )
        if record_id is not None:
            background_tasks.add_task(
                backfill_comparison_llm_cost, record_id, trace_id
            )
    except Exception as e:  # noqa: BLE001
        logger.warning("harvest_abort failed trace=%s: %s", trace_id, e)
    return resp
