"""
证件照输入合规预审：调用视觉模型 Qwen3-VL-235B-A22B-Instruct，判定图片是否适合做
「换底 + 轻量校正/裁剪」类预处理。本模块归属 sophnet-id-photo，不依赖其它 skill。
"""

import argparse
import base64
import json
import os
import re
import sys
import urllib.error
import urllib.request

import sophnet_tools

API_URL = "https://www.sophnet.com/api/open-apis/v1/chat/completions"
MODEL_ID_PHOTO_COMPLIANCE = "Qwen3-VL-235B-A22B-Instruct"

ID_PHOTO_STANDARD_BRIEF_CROPPED = (
    "当前画面为**算法裁切后的单人头肩区域**（默认仅保留一名主体），**勿再以合照或非单人**为由拒审。"
    "**主要拦截**："
    "（1）**口罩、墨镜**：遮挡口鼻的口罩，或遮挡眼部的墨镜（**透明光学眼镜可保留**）；"
    "（2）**人脸被遮挡**：头发、手、物体等遮挡面部或五官，影响辨认；"
    "（3）**双耳未在可见范围内露出**：双耳被头发、配饰或姿态完全挡住。"
    "其余宜通过：真人、彩色、非卡通/截图，脸部大致可辨。**不要**因背景杂物、衣着拒审。"
)

ID_PHOTO_STANDARD_BRIEF = (
    "预审**不限定场景**（杂乱背景可由后续换纯色底处理），**主要拦截**以下硬伤："
    "（1）**合照或非单人**：多人合影、或多名清晰人物并列导致无法认定唯一要处理的主体；"
    "（2）**口罩、墨镜**：遮挡口鼻的口罩，或遮挡眼部的墨镜（**透明光学眼镜可保留**）；"
    "（3）**人脸被遮挡**：头发、手、物体等遮挡面部或五官，影响辨认；"
    "（4）**双耳未在可见范围内露出**：双耳被头发、配饰或姿态完全挡住，不符合证件照常见露耳要求。"
    "其余情况宜判定为可通过：真人、彩色、非卡通/截图，脸部大致可辨。"
    "若人物在画面中偏小或远景/全身取景，只要上述硬伤不存在、双耳与人脸仍可据图判断，**仍宜通过**"
    "（后续会先做人脸检测与头肩裁切再审）。"
    "除所列硬伤外，**不要**因背景类型、生活/户外环境、衣着或杂物等拒审。"
)


def _is_likely_base64(raw: str) -> bool:
    text = raw.strip()
    if not text or any(ch.isspace() for ch in text):
        return False
    try:
        base64.b64decode(text, validate=True)
        return True
    except Exception:
        return False


def normalize_image_input(image_input: str, upload_timeout: int = 30) -> str:
    text = image_input.strip()
    if not text:
        raise ValueError("图片输入不能为空")

    if text.startswith(("http://", "https://")):
        return text

    if text.startswith("data:image/") and ";base64," in text:
        return text

    if os.path.exists(text):
        return sophnet_tools.upload_oss(text, upload_timeout)

    if _is_likely_base64(text):
        return text

    raise ValueError("无法识别图片输入：请传入 URL、base64、data URL 或本地文件路径")


def _post_chat_completions(payload: dict) -> dict:
    api_key = sophnet_tools.get_api_key()
    if not api_key:
        raise RuntimeError("未配置 API Key，无法进行证件照合规预审")
    body = json.dumps(payload).encode("utf-8")
    request = urllib.request.Request(
        url=API_URL,
        data=body,
        method="POST",
        headers={
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Authorization": f"Bearer {api_key}",
        },
    )
    try:
        with urllib.request.urlopen(request, timeout=180) as response:
            raw = response.read().decode("utf-8")
            return json.loads(raw)
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="ignore")
        raise RuntimeError(f"HTTPError: {exc.code} {exc.reason} | {detail}") from exc
    except urllib.error.URLError as exc:
        raise RuntimeError(f"URLError: {exc.reason}") from exc


def _extract_message_text(data: dict) -> str:
    try:
        return data["choices"][0]["message"]["content"]
    except (KeyError, IndexError, TypeError):
        return json.dumps(data, ensure_ascii=False, indent=2)


def _strip_json_fence(text: str) -> str:
    t = text.strip()
    m = re.match(r"^```(?:json)?\s*([\s\S]*?)\s*```\s*$", t, re.IGNORECASE)
    if m:
        return m.group(1).strip()
    return t


def check_id_photo_compliance(image_input: str, *, image_scope: str = "full") -> dict:
    """
    返回 dict：compliant, summary, violations, standard_brief, confidence, model；含 raw 供调试。
    image_scope: \"full\" 整张原图预审；\"cropped\" 裁切后头肩区预审（规范与 system 提示更短，不再强调合照）。
    """
    image_url = normalize_image_input(image_input)
    is_cropped = image_scope == "cropped"
    standard = ID_PHOTO_STANDARD_BRIEF_CROPPED if is_cropped else ID_PHOTO_STANDARD_BRIEF

    if is_cropped:
        system = (
            "你是证件照预处理预审助手。图为**已裁切的单人头肩候选区**，勿编造细节。"
            "**主要拦截**：佩戴口罩、佩戴墨镜、人脸被遮挡、双耳未露出；**不要**以合照、场景、背景为由拒审。"
            "violations 措辞优先：「请勿佩戴口罩」「请勿佩戴墨镜」「人脸被遮挡」「双耳未露出」。"
            "口罩与墨镜须分两条 violation。无硬伤时 compliant=true、violations=[]。"
            "输出且仅输出 JSON：compliant（布尔）、summary（一至两句中文）、violations（字符串数组）、"
            "confidence（high/medium/low）。"
        )
    else:
        system = (
            "你是证件照预处理预审助手。根据用户给出的「规范要点」判断 compliant，不编造图中没有的细节。"
            "**主要拦截**：合照或无法认定单人主体、佩戴口罩、佩戴墨镜、人脸被遮挡、双耳未露出。"
            "除此之外宜判定通过；**不要**针对具体场景（如咖啡馆、街道、室内陈设等）或手持物品类别做特化拒审。"
            "violations 为若干条**简短中文**，命中硬伤时**逐条列出**；"
            "措辞优先且尽量分列：「疑似合照，无法认定单一人物主体」「请勿佩戴口罩」「请勿佩戴墨镜」「人脸被遮挡」「双耳未露出」。"
            "若同时存在口罩与墨镜，须分成两条 violation，勿合并为一条。"
            "无硬伤时 compliant=true 且 violations=[]。"
            "输出且仅输出一个 JSON，不要 Markdown："
            '键 compliant（布尔）、summary（一至两句中文）、violations（字符串数组）、'
            "confidence（high/medium/low）。"
        )
    user = (
        "【规范要点】\n"
        + standard
        + "\n\n请审视所附图片，只输出 JSON。"
    )
    payload = {
        "messages": [
            {"role": "system", "content": system},
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": user},
                    {"type": "image_url", "image_url": {"url": image_url}},
                ],
            },
        ],
        "model": MODEL_ID_PHOTO_COMPLIANCE,
        "stream": False,
    }
    data = _post_chat_completions(payload)
    raw = _extract_message_text(data)
    try:
        obj = json.loads(_strip_json_fence(raw))
    except json.JSONDecodeError as e:
        raise RuntimeError(f"合规判定返回非 JSON：{raw[:500]}...") from e

    compliant = bool(obj.get("compliant"))
    violations = obj.get("violations") or []
    if not isinstance(violations, list):
        violations = [str(violations)]
    summary = str(obj.get("summary") or ("符合规范" if compliant else "不符合规范"))
    return {
        "compliant": compliant,
        "summary": summary,
        "violations": [str(v) for v in violations],
        "standard_brief": standard,
        "confidence": str(obj.get("confidence") or "unknown"),
        "model": MODEL_ID_PHOTO_COMPLIANCE,
        "raw": raw,
    }


def build_parser():
    p = argparse.ArgumentParser(description="证件照输入合规预审（视觉模型，本 skill 自包含）")
    p.add_argument(
        "--scope",
        choices=("full", "cropped"),
        default="full",
        help="full=整张原图「规范要点」；cropped=头肩裁切后精简要点（不再强调合照）",
    )
    p.add_argument(
        "image",
        help="图片：http(s) URL、data URL、base64 或本地路径（本地会先上传）",
    )
    return p


def main():
    args = build_parser().parse_args()
    try:
        report = check_id_photo_compliance(args.image, image_scope=args.scope)
    except Exception as e:
        print(str(e), file=sys.stderr)
        sys.exit(1)
    out = {k: v for k, v in report.items() if k != "raw"}
    print(json.dumps(out, ensure_ascii=False))
    sys.exit(0 if report["compliant"] else 2)


if __name__ == "__main__":
    main()
