"""
年龄相貌模拟用人脸裁剪：Sophnet detect_and_embed，多张达标人脸时取检测框面积最大者，
按目标输出尺寸的宽高比裁切头肩区域。逻辑与一键证件照的裁切链路一致。
"""

from __future__ import annotations

import base64
import os
import re
import sys
import tempfile
from typing import Any

import requests
from PIL import Image

SOPH_BASE_URL = os.environ.get("SOPH_BASE_URL", "https://www.sophnet.com").rstrip("/")
FACE_API_URL = f"{SOPH_BASE_URL}/api/open-apis/projects/detect_and_embed"


def parse_output_size(size_str: str) -> tuple[int, int]:
    m = re.match(r"^(\d+)\s*\*\s*(\d+)$", (size_str or "").strip())
    if not m:
        raise ValueError(f"invalid size: {size_str!r}")
    return int(m.group(1)), int(m.group(2))


def _decode_data_uri(data_uri: str) -> bytes:
    if not data_uri.startswith("data:") or ";base64," not in data_uri:
        raise ValueError("not a data URI")
    _meta, b64 = data_uri.split(";base64,", 1)
    return base64.b64decode(b64)


def materialize_image(ref: str, api_key: str, download_timeout: int = 120) -> tuple[str, list[str]]:
    raw = ref.lstrip("@").strip()
    cleanup: list[str] = []

    if raw.startswith(("http://", "https://")):
        headers = {"Authorization": f"Bearer {api_key}"}
        resp = requests.get(raw, headers=headers, timeout=download_timeout, stream=True)
        resp.raise_for_status()
        ext = os.path.splitext(raw.split("?")[0])[-1].lower() or ".jpg"
        if ext not in (".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"):
            ext = ".jpg"
        fd, tmp = tempfile.mkstemp(suffix=ext, prefix="age_appr_src_")
        os.close(fd)
        with open(tmp, "wb") as f:
            for chunk in resp.iter_content(8192):
                if chunk:
                    f.write(chunk)
        cleanup.append(tmp)
        return tmp, cleanup

    if raw.startswith("data:image/"):
        blob = _decode_data_uri(raw)
        fd, tmp = tempfile.mkstemp(suffix=".jpg", prefix="age_appr_src_")
        os.close(fd)
        with open(tmp, "wb") as f:
            f.write(blob)
        cleanup.append(tmp)
        return tmp, cleanup

    if os.path.isfile(raw):
        return os.path.realpath(raw), cleanup

    raise FileNotFoundError(raw)


def detect_faces(image_path: str, api_key: str, timeout: int = 30) -> dict[str, Any]:
    if not os.path.isfile(image_path):
        raise FileNotFoundError(image_path)

    ext = os.path.splitext(image_path)[1].lower()
    mime_types = {
        ".jpg": "image/jpeg",
        ".jpeg": "image/jpeg",
        ".png": "image/png",
        ".webp": "image/webp",
        ".bmp": "image/bmp",
        ".gif": "image/gif",
    }
    mime_type = mime_types.get(ext, "image/jpeg")

    with open(image_path, "rb") as f:
        files = {"file": (os.path.basename(image_path), f, mime_type)}
        headers = {"Authorization": f"Bearer {api_key}"}
        resp = requests.post(FACE_API_URL, files=files, headers=headers, timeout=timeout)

    if resp.status_code != 200:
        raise RuntimeError(f"detect_and_embed HTTP {resp.status_code}: {resp.text[:300]}")

    body = resp.json()
    return body.get("result") or {}


def get_largest_face(faces: list[dict[str, Any]]) -> dict[str, Any] | None:
    """取检测框面积 (x2-x1)*(y2-y1) 最大的人脸。"""
    if not faces:
        return None
    best: dict[str, Any] | None = None
    best_area = 0.0
    for face in faces:
        box = face.get("box") or []
        if len(box) < 4:
            continue
        x1, y1, x2, y2 = (float(box[0]), float(box[1]), float(box[2]), float(box[3]))
        area = max(0.0, x2 - x1) * max(0.0, y2 - y1)
        if area > best_area:
            best_area = area
            best = face
    return best


def compute_portrait_crop(
    img_w: int,
    img_h: int,
    x1: float,
    y1: float,
    x2: float,
    y2: float,
    out_w: int,
    out_h: int,
) -> tuple[int, int, int, int] | None:
    """按目标宽高比头肩裁切，得到 (left, top, right, bottom)。"""
    x1 = max(0.0, min(float(img_w), float(x1)))
    y1 = max(0.0, min(float(img_h), float(y1)))
    x2 = max(0.0, min(float(img_w), float(x2)))
    y2 = max(0.0, min(float(img_h), float(y2)))
    if x2 <= x1 or y2 <= y1:
        return None

    fh = y2 - y1
    cx = (x1 + x2) * 0.5
    aspect = out_w / float(out_h)

    margin_top = 0.58 * fh
    margin_bot = 1.08 * fh
    top_ref = y1 - margin_top
    bot_ref = y2 + margin_bot
    ref_h = bot_ref - top_ref
    ref_h = max(ref_h, fh * 2.15)
    ref_h = min(float(img_h), ref_h)

    crop_h = ref_h
    crop_w = crop_h * aspect
    if crop_w > img_w:
        crop_w = float(img_w)
        crop_h = crop_w / aspect
    if crop_h > img_h:
        crop_h = float(img_h)
        crop_w = crop_h * aspect

    left = cx - crop_w * 0.5
    left = max(0.0, min(left, float(img_w) - crop_w))

    top = y1 - 0.30 * crop_h
    top = max(0.0, min(top, float(img_h) - crop_h))

    crop_w_i = int(round(crop_w))
    crop_h_i = int(round(crop_h))
    left_i = int(round(left))
    top_i = int(round(top))
    right_i = left_i + crop_w_i
    bottom_i = top_i + crop_h_i

    if right_i > img_w:
        right_i = img_w
        left_i = max(0, right_i - crop_w_i)
    if bottom_i > img_h:
        bottom_i = img_h
        top_i = max(0, bottom_i - crop_h_i)

    if right_i - left_i < 32 or bottom_i - top_i < 32:
        return None
    return left_i, top_i, right_i, bottom_i


def prepare_age_portrait_crop(
    image_ref: str,
    api_key: str,
    size_str: str,
    *,
    skip_face_crop: bool = False,
    strict_face_gate: bool = False,
    det_thr: float = 0.5,
    detect_timeout: int = 90,
    download_timeout: int = 120,
) -> dict[str, Any]:
    """
    返回:
      edit_ref — 送入 image-edit 的引用
      cleanup — 结束后应 unlink 的临时文件
      face_crop — applied | skipped_disabled | skipped_no_face | failed_* | rejected_no_face
      error — strict 且无脸时为 no_face
      face_detected_count — applied 时为达标人脸数
    """
    ref = image_ref.lstrip("@")
    if skip_face_crop:
        return {"edit_ref": ref, "cleanup": [], "face_crop": "skipped_disabled"}

    try:
        tw, th = parse_output_size(size_str)
    except ValueError as e:
        print(f"Warning: {e}，将不进行人脸裁切。", file=sys.stderr)
        return {"edit_ref": ref, "cleanup": [], "face_crop": "failed_bad_size"}

    try:
        local_path, material_cleanup = materialize_image(ref, api_key, download_timeout=download_timeout)
    except Exception as e:
        print(f"Warning: 无法落地输入以便人脸检测与裁切，改用原始输入: {e}", file=sys.stderr)
        return {"edit_ref": ref, "cleanup": [], "face_crop": "failed_materialize"}

    try:
        api_result = detect_faces(local_path, api_key, timeout=detect_timeout)
    except Exception as e:
        print(f"Warning: 人脸检测接口失败，改用未裁切落地图: {e}", file=sys.stderr)
        if material_cleanup:
            return {"edit_ref": local_path, "cleanup": list(material_cleanup), "face_crop": "failed_detect"}
        return {"edit_ref": local_path, "cleanup": [], "face_crop": "failed_detect"}

    faces = api_result.get("output") or []
    faces = [f for f in faces if float(f.get("det_score", 0) or 0) >= det_thr]

    def _cleanup_material_only():
        for p in material_cleanup:
            try:
                if p and os.path.isfile(p):
                    os.unlink(p)
            except OSError:
                pass

    if strict_face_gate and len(faces) == 0:
        _cleanup_material_only()
        return {
            "error": "no_face",
            "edit_ref": ref,
            "cleanup": [],
            "face_crop": "rejected_no_face",
            "face_count": 0,
        }

    if len(faces) > 1:
        print(
            f"Notice: 检出 {len(faces)} 张达标人脸，按检测框面积最大者裁切头肩。",
            file=sys.stderr,
        )

    face = get_largest_face(faces)

    try:
        pil_image = Image.open(local_path).convert("RGB")
    except OSError:
        pil_image = None
    if pil_image is None:
        print("Warning: 无法读取落地图用于裁切，改用原始输入引用", file=sys.stderr)
        for p in material_cleanup:
            try:
                if os.path.isfile(p):
                    os.unlink(p)
            except OSError:
                pass
        return {"edit_ref": ref, "cleanup": [], "face_crop": "failed_read"}

    iw, ih = pil_image.size

    if not face:
        print("Warning: 未检出足够置信度的人脸，整图送图改图", file=sys.stderr)
        if material_cleanup:
            return {"edit_ref": local_path, "cleanup": list(material_cleanup), "face_crop": "skipped_no_face"}
        return {"edit_ref": local_path, "cleanup": [], "face_crop": "skipped_no_face"}

    box = face.get("box") or []
    if len(box) < 4:
        if material_cleanup:
            return {"edit_ref": local_path, "cleanup": list(material_cleanup), "face_crop": "skipped_no_face"}
        return {"edit_ref": local_path, "cleanup": [], "face_crop": "skipped_no_face"}

    rect = compute_portrait_crop(iw, ih, float(box[0]), float(box[1]), float(box[2]), float(box[3]), tw, th)
    if not rect:
        print("Warning: 无法计算裁切框，使用整图送图改图", file=sys.stderr)
        if material_cleanup:
            return {"edit_ref": local_path, "cleanup": list(material_cleanup), "face_crop": "failed_crop_math"}
        return {"edit_ref": local_path, "cleanup": [], "face_crop": "failed_crop_math"}

    l, t, r, b = rect
    cropped_pil = pil_image.crop((l, t, r, b))
    fd, out_path = tempfile.mkstemp(suffix=".jpg", prefix="age_appr_crop_")
    os.close(fd)
    ok = False
    try:
        cropped_pil.save(out_path, "JPEG", quality=95)
        ok = True
    except OSError:
        ok = False
    if not ok:
        print("Warning: 裁切图写入失败，使用未裁切落地图", file=sys.stderr)
        try:
            os.unlink(out_path)
        except OSError:
            pass
        if material_cleanup:
            return {"edit_ref": local_path, "cleanup": list(material_cleanup), "face_crop": "failed_write"}
        return {"edit_ref": local_path, "cleanup": [], "face_crop": "failed_write"}

    cleanup_all = list(material_cleanup) + [out_path]
    return {
        "edit_ref": out_path,
        "cleanup": cleanup_all,
        "face_crop": "applied",
        "face_detected_count": len(faces),
    }
