"""国际站后台询盘分配 API 封装。

客户授权场景使用的端点（v2.0 实测，2026-05-25）：
  POST https://message.alibaba.com/message/ajax/feedback/assign.htm
  Content-Type: application/x-www-form-urlencoded
  Body:
    inquiryIds = "13549021276,13705983028"   # 多个用逗号分隔
    memberSeq  = "200689478"                  # 目标业务员的 memberSeq
    ctoken     = "<ctoken>"              # CSRF token
  Headers:
    referer:      https://message.alibaba.com/message/default.htm
    x-csrf-token: <同 ctoken>
    cookie:       <完整登录 cookie>

子账号列表来源（前端 DOM 注入）:
  window.GDATA.user.newSubAccount  -> [{memberSeq, nick}, ...]
ctoken 来源:
  window.GDATA.system.csrfTokenVal

使用模式：
  装机时：browser 子代理跑一次 JS 注入，把 ctoken + sub_accounts 写入 config
  运行时：从 config 读 ctoken/cookie，直接 HTTP POST

CLI:
  python alibaba_assign.py --inquiry-ids 12345,67890 --member-seq 200689478 \
    --ctoken "<ctoken>" --cookie "<cookie_string>"
  python alibaba_assign.py --config assign_config.json --inquiry-ids 12345 --member-seq 200689478

config.json 结构（installer 装机时由 browser 子代理填充）:
  {
    "ctoken": "<ctoken>",
    "cookie": "<cookie_string>",
    "sub_accounts": [
      {"memberSeq": 200689478, "nick": "Ru by"},
      {"memberSeq": 272065862, "nick": "aaa aaaa"}
    ],
    "captured_at": "2026-05-25T...",
    "expires_hint_hours": 4
  }
"""

from __future__ import annotations

import argparse
import json
import sys
import time
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional


ASSIGN_URL = "https://message.alibaba.com/message/ajax/feedback/assign.htm"
DEFAULT_REFERER = "https://message.alibaba.com/message/default.htm"
DEFAULT_TIMEOUT = 15


class AssignError(Exception):
    pass


def assign_inquiry(
    inquiry_ids: List[str],
    member_seq: str,
    ctoken: str,
    cookie: str,
    *,
    referer: str = DEFAULT_REFERER,
    timeout: int = DEFAULT_TIMEOUT,
    user_agent: str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
                      "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
) -> Dict[str, Any]:
    """把一批询盘分配给指定业务员。

    返回:
      {
        "ok": bool,
        "http_status": int,
        "inquiry_ids": [...],
        "member_seq": "...",
        "response_body": "...",
        "error": "..."   # 仅失败时
      }

    判定成功的依据（授权测试）:
      HTTP 200 + 响应体为空对象 {} → 成功
      HTTP 200 + 响应体含 error 字段 → 失败（按字段判定）
      HTTP 4xx/5xx → 失败
    """
    if not inquiry_ids:
        return {"ok": False, "http_status": 0, "error": "empty inquiry_ids"}
    if not member_seq:
        return {"ok": False, "http_status": 0, "error": "empty member_seq"}
    if not ctoken:
        return {"ok": False, "http_status": 0, "error": "empty ctoken (token 已过期？重新装机)"}
    if not cookie:
        return {"ok": False, "http_status": 0, "error": "empty cookie (登录已失效？)"}

    ids_str = ",".join(str(x) for x in inquiry_ids)
    body = urllib.parse.urlencode({
        "inquiryIds": ids_str,
        "memberSeq": str(member_seq),
        "ctoken": ctoken,
    }).encode("utf-8")

    req = urllib.request.Request(ASSIGN_URL, data=body, method="POST")
    req.add_header("Content-Type", "application/x-www-form-urlencoded")
    req.add_header("Referer", referer)
    req.add_header("Origin", "https://message.alibaba.com")
    req.add_header("X-Csrf-Token", ctoken)
    req.add_header("Cookie", cookie)
    req.add_header("User-Agent", user_agent)
    req.add_header("Accept", "application/json, text/plain, */*")
    req.add_header("X-Requested-With", "XMLHttpRequest")

    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            status = resp.status
            body_text = resp.read().decode("utf-8", errors="replace")
    except urllib.error.HTTPError as e:
        return {
            "ok": False,
            "http_status": e.code,
            "inquiry_ids": inquiry_ids,
            "member_seq": str(member_seq),
            "response_body": (e.read().decode("utf-8", errors="replace") if hasattr(e, "read") else ""),
            "error": f"HTTP {e.code}",
        }
    except Exception as e:
        return {
            "ok": False,
            "http_status": 0,
            "inquiry_ids": inquiry_ids,
            "member_seq": str(member_seq),
            "error": f"{type(e).__name__}: {e}",
        }

    # 判定 ok：HTTP 200 + 响应体不含明显 error 字段
    ok = status == 200
    body_json = None
    body_stripped = body_text.strip()

    # 检测被登录拦截：响应是 HTML 页面而非 JSON
    body_lower = body_text.lower()  # 用原 body 不 strip，捕获前部 \r\n
    if ("<!doctype" in body_lower[:600] or "<html" in body_lower[:600]
            or "<head" in body_lower[:600]):
        return {
            "ok": False,
            "http_status": status,
            "inquiry_ids": inquiry_ids,
            "member_seq": str(member_seq),
            "response_body": body_text[:500],
            "error": "服务端返回 HTML（疑似登录已失效，cookie/ctoken 需重新抓取）",
        }

    try:
        body_json = json.loads(body_stripped) if body_stripped else {}
        # 已知失败信号
        if isinstance(body_json, dict):
            if body_json.get("error") or body_json.get("errorCode") or body_json.get("code") in ("FAIL", "ERROR"):
                ok = False
            # 已知成功信号
            if body_json.get("success") is True or body_json.get("code") in ("SUCCESS", 0, "0"):
                ok = True
    except json.JSONDecodeError:
        # 非 JSON 但 200 且非 HTML，仍视为成功（实测后台有时返回纯空体）
        body_json = None

    return {
        "ok": ok,
        "http_status": status,
        "inquiry_ids": inquiry_ids,
        "member_seq": str(member_seq),
        "response_body": body_text[:500],
        "response_json": body_json,
    }


def assign_batch(
    assignments: List[Dict[str, Any]],
    config: Dict[str, Any],
    *,
    sleep_ms_between: int = 300,
) -> Dict[str, Any]:
    """批量分配。assignments = [{inquiry_ids:[...], member_seq:"..."}].

    sleep_ms_between：每两次调用间睡眠（防风控）。
    """
    ctoken = config.get("ctoken", "")
    cookie = config.get("cookie", "")
    results = []
    ok_count = 0
    for item in assignments:
        r = assign_inquiry(
            inquiry_ids=item.get("inquiry_ids") or [],
            member_seq=item.get("member_seq", ""),
            ctoken=ctoken,
            cookie=cookie,
        )
        results.append(r)
        if r.get("ok"):
            ok_count += 1
        if sleep_ms_between > 0:
            time.sleep(sleep_ms_between / 1000)

    return {
        "total": len(assignments),
        "ok": ok_count,
        "failed": len(assignments) - ok_count,
        "config_captured_at": config.get("captured_at"),
        "details": results,
    }


def lookup_sub_account(config: Dict[str, Any], key: str) -> Optional[Dict[str, Any]]:
    """从 config.sub_accounts 里按 nick 模糊匹配或按 memberSeq 精确匹配。"""
    subs = config.get("sub_accounts") or []
    if not subs:
        return None
    # 精确 memberSeq
    for s in subs:
        if str(s.get("memberSeq")) == str(key):
            return s
    # 模糊 nick（去空格大小写不敏感）
    needle = key.replace(" ", "").lower()
    for s in subs:
        nick = (s.get("nick") or "").replace(" ", "").lower()
        if needle and needle in nick:
            return s
    return None


def main():
    ap = argparse.ArgumentParser(description="国际站后台询盘分配 API 封装")
    ap.add_argument("--inquiry-ids", required=True, help="询盘 ID，多个用逗号分隔")
    ap.add_argument("--member-seq", help="目标业务员 memberSeq；或用 --nick 模糊匹配")
    ap.add_argument("--nick", help="按业务员昵称查找（需 --config 提供 sub_accounts）")
    ap.add_argument("--config", help="assign_config.json 路径（装机时由 browser 生成）")
    ap.add_argument("--ctoken", help="（不带 config 时必填）CSRF token")
    ap.add_argument("--cookie", help="（不带 config 时必填）完整 cookie 字符串")
    ap.add_argument("--list-subs", action="store_true", help="只打印业务员列表然后退出")
    args = ap.parse_args()

    config = {}
    if args.config:
        config = json.loads(Path(args.config).read_text(encoding="utf-8"))

    if args.list_subs:
        print(json.dumps(config.get("sub_accounts") or [], ensure_ascii=False, indent=2))
        return

    ctoken = args.ctoken or config.get("ctoken", "")
    cookie = args.cookie or config.get("cookie", "")

    # 解析 member_seq
    member_seq = args.member_seq
    if not member_seq and args.nick:
        sub = lookup_sub_account(config, args.nick)
        if not sub:
            print(json.dumps({"ok": False, "error": f"nick '{args.nick}' not found in sub_accounts"},
                             ensure_ascii=False))
            sys.exit(1)
        member_seq = str(sub["memberSeq"])

    if not member_seq:
        print(json.dumps({"ok": False, "error": "must provide --member-seq or --nick"},
                         ensure_ascii=False))
        sys.exit(1)

    ids = [x.strip() for x in args.inquiry_ids.split(",") if x.strip()]
    result = assign_inquiry(ids, member_seq, ctoken, cookie)
    print(json.dumps(result, ensure_ascii=False, indent=2))
    sys.exit(0 if result.get("ok") else 1)


if __name__ == "__main__":
    main()
