"""美团联盟 CPS 开放接口客户端。

签名方式：类阿里云网关 S-Ca 头，但 stringToSign **只有 4 段**——
  METHOD\n + Content-MD5\n + Headers + Url
没有 Accept / Content-Type / Date 行（加了就签名失败）。
"""
from __future__ import annotations

import base64
import hashlib
import hmac
import json
import logging
import threading
import time
from typing import Any

import httpx

from app.core.config import settings

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


class MeituanCpsError(Exception):
    """美团 CPS 接口调用失败。"""


# ────────────────────── 共享 httpx.Client(连接池 + keep-alive) ──────────────────────
# 原实现每次 _call 都 `with httpx.Client(...)` 新建再关掉,等于每发一次请求就:
#   ① 重建一套 SSL 上下文(加载 certifi CA,实测几百 ms) ② 重做一次 TLS 握手 ③ 用完即弃连接。
# 首页 feed 一次翻页要向美团发好几次请求(外卖/到店两路,「距离最近」还要续页),这份固定开销被成倍放大,
# 是「滑到底部加载很慢」的一大块。改成进程内单例:握手一次、后续走 keep-alive 复用。
# httpx.Client 本身线程安全,可被 feed 的 ThreadPoolExecutor 两条抓取线程共用。
_client: httpx.Client | None = None
_client_lock = threading.Lock()


def get_client() -> httpx.Client:
    """取美团 CPS 共享 client(幂等,懒建)。lifespan 启动时预热,把建 SSL 的成本摊到启动。"""
    global _client
    if _client is None:
        with _client_lock:
            if _client is None:
                # 走 MT_CPS_PROXY(本机开发直连会 SSL EOF,必须走代理;线上留空=直连)。
                # trust_env=False:不读进程环境的 HTTP_PROXY,只认配置,避免被错误/失效代理误导。
                _client = httpx.Client(
                    proxy=settings.MT_CPS_PROXY or None,
                    trust_env=False,
                    timeout=settings.MT_CPS_TIMEOUT_SEC,
                    # 池子留足:feed 每个请求会起 2 条抓取线程,并发用户多时别在池上排队
                    # (排满会等到 MT_CPS_TIMEOUT_SEC 抛 PoolTimeout,表现成"又慢又降级")。
                    limits=httpx.Limits(max_keepalive_connections=16, max_connections=32),
                )
    return _client


def close_client() -> None:
    """lifespan 关停时调,优雅关连接池。"""
    global _client
    with _client_lock:
        if _client is not None:
            _client.close()
            _client = None


def _content_md5(body: bytes) -> str:
    return base64.b64encode(hashlib.md5(body).digest()).decode()


def _sign(secret: str, method: str, content_md5: str, path: str,
          signed_headers: dict[str, str]) -> str:
    block = "".join(
        f"{k}:{signed_headers[k]}\n"
        for k in sorted(signed_headers)
    )
    sts = f"{method}\n{content_md5}\n{block}{path}"
    return base64.b64encode(
        hmac.new(secret.encode(), sts.encode(), hashlib.sha256).digest()
    ).decode()


def _call(path: str, body_obj: dict[str, Any]) -> dict[str, Any]:
    if not settings.MT_CPS_APP_KEY or not settings.MT_CPS_APP_SECRET:
        raise MeituanCpsError("MT_CPS_APP_KEY / MT_CPS_APP_SECRET not configured")

    body = json.dumps(body_obj, ensure_ascii=False).encode("utf-8")
    content_md5 = _content_md5(body)
    ts = str(int(time.time() * 1000))

    signed_headers = {"S-Ca-App": settings.MT_CPS_APP_KEY, "S-Ca-Timestamp": ts}
    signature = _sign(settings.MT_CPS_APP_SECRET, "POST", content_md5, path, signed_headers)

    headers = {
        "Content-Type": "application/json",
        "Content-MD5": content_md5,
        "S-Ca-App": settings.MT_CPS_APP_KEY,
        "S-Ca-Timestamp": ts,
        "S-Ca-Signature-Headers": "S-Ca-Timestamp,S-Ca-App",
        "S-Ca-Signature": signature,
    }

    url = f"{settings.MT_CPS_HOST}{path}"
    try:
        resp = get_client().post(url, content=body, headers=headers)
    except httpx.HTTPError as e:
        logger.exception("[MT] http error calling %s", url)
        raise MeituanCpsError(f"meituan http error: {e}") from e

    if resp.status_code != 200:
        logger.error("[MT] HTTP %s body=%s", resp.status_code, resp.text[:500])
        raise MeituanCpsError(f"meituan http {resp.status_code}")

    data = resp.json()
    if data.get("code") != 0:
        logger.error("[MT] api error code=%s message=%s", data.get("code"), data.get("message"))
        raise MeituanCpsError(f"code={data.get('code')} {data.get('message')}")

    return data


# ────────────────────── 业务方法 ──────────────────────

def query_coupon(
    *,
    longitude: float,
    latitude: float,
    platform: int = 1,
    biz_line: int | None = None,
    list_topic_id: int | None = 3,
    search_text: str | None = None,
    search_id: str | None = None,
    sort_field: int | None = None,
    page_no: int = 1,
    page_size: int = 20,
) -> dict[str, Any]:
    body: dict[str, Any] = {
        "platform": platform,
        "longitude": int(longitude * 1_000_000),
        "latitude": int(latitude * 1_000_000),
        "pageNo": page_no,
        "pageSize": page_size,
    }
    if biz_line is not None:
        body["bizLine"] = biz_line

    if search_text:
        body["searchText"] = search_text
        if sort_field is None:
            sort_field = 6
    elif list_topic_id is not None:
        body["listTopiId"] = list_topic_id

    if sort_field is not None:
        body["sortField"] = sort_field
    if search_id:
        body["searchId"] = search_id

    return _call("/cps_open/common/api/v1/query_coupon", body)


def get_referral_link(
    *,
    product_view_sign: str | None = None,
    act_id: str | None = None,
    platform: int = 1,
    biz_line: int | None = None,
    sid: str | None = None,
    link_type_list: list[int] | None = None,
) -> dict[str, Any]:
    """换推广链接。actId(活动物料)与 productViewSign(商品券)二选一。
    用 actId 入参时,出参 data 为 null,链接全在 referralLinkMap(实测)。
    """
    if not act_id and not product_view_sign:
        raise MeituanCpsError("act_id 或 product_view_sign 必填其一")
    body: dict[str, Any] = {}
    if act_id:
        body["actId"] = act_id
    else:
        body["productViewSign"] = product_view_sign
    if platform == 2:
        body["platform"] = 2
        if biz_line is not None:
            body["bizLine"] = biz_line

    body["sid"] = sid or settings.MT_CPS_DEFAULT_SID
    body["linkTypeList"] = link_type_list or [1, 3]

    return _call("/cps_open/common/api/v1/get_referral_link", body)


def query_order(
    *,
    sid: str | None = None,
    start_time: int,
    end_time: int,
    query_time_type: int = 1,
    page: int = 1,
    limit: int = 100,
) -> dict[str, Any]:
    """按时间窗(+可选 sid)拉 CPS 订单做对账。实测要点:
    - 分页是 page / limit(不是 pageNo/pageSize);startTime/endTime 是 10 位秒级时间戳
    - query_time_type: 1 按支付时间 2 按更新时间
    - 响应 data.dataList[],每条含 orderId / sid / payPrice(元) / profit(元) /
      commissionRate / status(2付款 3完成 4取消 5风控 6结算) / payTime(秒) / actId 等
    """
    body: dict[str, Any] = {
        "queryTimeType": query_time_type,
        "startTime": start_time,
        "endTime": end_time,
        "page": page,
        "limit": limit,
    }
    if sid:
        body["sid"] = sid
    return _call("/cps_open/common/api/v1/query_order", body)
