import os
import sys
import uuid
import time
import shutil
import threading
import random
from datetime import datetime, timedelta
import numpy as np
import cv2
import insightface
from insightface.app import FaceAnalysis
from PIL import Image, ImageDraw, ImageFont
from flask import Flask, render_template, request, send_file, jsonify
from concurrent.futures import ThreadPoolExecutor

# MRZ 模块（ICAO Doc 9303 TD3）
sys.path.insert(0, "/passport")
from mrz import check_digit, build_line1, build_line2

BASE = "/passport"
FONT_DIR = "/usr/share/fonts/truetype/dejavu"

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024
app.config["UPLOAD_FOLDER"] = os.path.join(BASE, "web", "uploads")
app.config["RESULT_FOLDER"] = os.path.join(BASE, "web", "results")
app.config["PAIRED_PIN"] = os.path.join(BASE, "pin")
app.config["PAIRED_POUT"] = os.path.join(BASE, "pout")
app.config["TEMPLATES"] = os.path.join(BASE, "templates")

os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)
os.makedirs(app.config["RESULT_FOLDER"], exist_ok=True)
os.makedirs(app.config["PAIRED_PIN"], exist_ok=True)
os.makedirs(app.config["PAIRED_POUT"], exist_ok=True)
os.makedirs(app.config["TEMPLATES"], exist_ok=True)

PROVIDERS = ["CPUExecutionProvider"]
ROOT = BASE

print("[Init] Loading FaceAnalysis (buffalo_l)...", flush=True)
face_app = FaceAnalysis(name="buffalo_l", root=ROOT, providers=PROVIDERS)
face_app.prepare(ctx_id=-1, det_size=(640, 640))

print("[Init] Loading inswapper_128...", flush=True)
swapper = insightface.model_zoo.get_model(
    os.path.join(BASE, "models", "inswapper_128.onnx"), providers=PROVIDERS
)

TEMPLATE_GIRL = os.path.join(BASE, "templates", "mm_girl.jpg")
TEMPLATE_BOY  = os.path.join(BASE, "templates", "mm_boy.jpg")

def get_template(sex=""):
    """根据性别返回模板路径，男性优先 mm_boy.jpg，否则回退 mm_girl.jpg"""
    if sex == "M" and os.path.exists(TEMPLATE_BOY):
        return TEMPLATE_BOY
    return TEMPLATE_GIRL

executor = ThreadPoolExecutor(max_workers=2)
tasks = {}
tasks_lock = threading.Lock()

print("[Init] Ready!", flush=True)


# ===================== MRZ 计算 (delegates to /passport/mrz.py) =====================

def gen_passport_no():
    return "MK" + "".join(str(random.randint(0, 9)) for _ in range(6))

def paired_name():
    """生成配对文件名，同名不同夹：input/xxx.jpg ↔ output/xxx.png"""
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    uid = uuid.uuid4().hex[:6]
    return f"{ts}_{uid}"

def gen_random_date_range(start_year=1980, end_year=2005):
    y = random.randint(start_year, end_year)
    m = random.randint(1, 12)
    d = random.randint(1, 28)
    return datetime(y, m, d)

# 缅甸风格姓名库（性别匹配）
MALE_SURNAMES = ["AUNG", "KYAW", "ZAW", "HTUN", "WIN", "MYINT", "SOE", "NAING", "KO", "MIN"]
FEMALE_SURNAMES = ["EI", "SU", "KHIN", "MAY", "NU", "THAN", "THIDA", "NILAR", "MOE", "YIN"]
MALE_GIVEN = ["KYAW", "ZAW", "AUNG", "MIN", "HTET", "NAING", "THIHA", "LIN", "PHYO", "HEIN"]
FEMALE_GIVEN = ["SU", "MYAT", "THIN", "ZAR", "THU", "YADANAR", "HTAY", "KHINE", "WAI", "EI"]
BIRTH_PLACES = ["YANGON", "MANDALAY", "TAUNGGYI", "BAGO", "PATHEIN", "SITTWE", "MYITKYINA", "MAWLAMYINE", "MONYWA", "MEIKTILA"]

def random_passport_data():
    """返回一份随机护照数据，男女名字匹配；男性年龄固定 26 岁"""
    sex = random.choice(["M", "F"])
    if sex == "M":
        surname = random.choice(MALE_SURNAMES)
        given = random.choice(MALE_GIVEN)
    else:
        surname = random.choice(FEMALE_SURNAMES)
        given = random.choice(FEMALE_GIVEN)

    # 男性固定 26 岁 (2026-26=2000)，女性维持随机 1980-2005
    if sex == "M":
        dob = gen_random_date_range(2000, 2000)
    else:
        dob = gen_random_date_range(1980, 2005)
    issue = gen_random_date_range(2020, 2025)
    expiry = issue + timedelta(days=5*365)

    MONTHS = ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"]
    def fmt_d(dt):
        return f"{dt.day:02d} {MONTHS[dt.month-1]} {dt.year}"

    return {
        "passport_no": gen_passport_no(),
        "sex": sex,
        "surname": surname,
        "given_name": given,
        "dob": fmt_d(dob),
        "birth_place": random.choice(BIRTH_PLACES),
        "issue_date": fmt_d(issue),
        "expiry_date": fmt_d(expiry),
    }

def build_mrz(data):
    """
    data keys: type, country_code, passport_no, surname, given_name,
               nationality, dob, sex, expiry_date, issue_date (unused in MRZ)
    Returns (line1, line2)
    — delegates to /passport/mrz.py (ICAO Doc 9303 TD3)
    """
    # --- extract & normalise ---
    surname = (data.get("surname", "") or "").upper().replace(" ", "<")
    given = (data.get("given_name", "") or "").upper().replace(" ", "<")
    country = (data.get("country_code", "") or "MMR").upper()[:3]
    nat = (data.get("country_code", "") or "MMR").upper()[:3]
    gender = (data.get("sex", "") or "F").upper()[:1]
    if gender not in ("M", "F"):
        gender = "F"
    dtype = (data.get("type", "") or "PV").upper()

    # clean passport number (remove < and spaces, keep raw data)
    pas = (data.get("passport_no", "") or "").upper().replace(" ", "").replace("<", "")
    if not pas:
        pas = gen_passport_no()

    # --- parse dates → yymmdd ---
    def _parse_dob(s):
        fmts = ["%d %b %Y", "%d %B %Y", "%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y"]
        for f in fmts:
            try:
                return datetime.strptime(s, f)
            except:
                pass
        return None

    dob_raw = data.get("dob", "") or "06 FEB 2000"
    exp_raw = data.get("expiry_date", "") or "23 NOV 2030"
    dob_dt = _parse_dob(dob_raw) or datetime(2000, 2, 6)
    exp_dt = _parse_dob(exp_raw) or datetime(2030, 11, 23)
    dob_str = dob_dt.strftime("%y%m%d")
    exp_str = exp_dt.strftime("%y%m%d")

    # --- delegate to mrz module ---
    line1 = build_line1(surname, given, dtype, country)
    # trim passport to 8 data chars (mrz.py adds the '<' separator + check digit)
    line2 = build_line2(pas[:8], dob_str, exp_str, gender, nat)

    return line1, line2


# ===================== 护照图片生成 =====================

def generate_passport_image(data, source_photo_path, output_path, base_image=None):
    """用 data 字典 + 源照片生成护照图片。
    如果传入 base_image，则在它之上叠加文字字段（模板换脸后使用）；
    否则从头生成空白护照。"""
    W, H = 900, 630

    if base_image and os.path.exists(base_image):
        # ── 在模板基础上叠加文字，等比缩放不变形 ──
        base = Image.open(base_image).convert("RGB")
        bw, bh = base.size
        # 等比缩放到刚好能放入画布
        scale = min(W / bw, H / bh)
        nw, nh = int(bw * scale), int(bh * scale)
        base = base.resize((nw, nh), Image.LANCZOS)
        # 居中放置，四周用护照底色填充
        img = Image.new("RGB", (W, H), "#EBE5D9")
        ox, oy = (W - nw) // 2, (H - nh) // 2
        img.paste(base, (ox, oy))
    else:
        img = Image.new("RGB", (W, H), "#EBE5D9")
    draw = ImageDraw.Draw(img)

    def _font(name, size):
        try:
            return ImageFont.truetype(os.path.join(FONT_DIR, name), size)
        except:
            return ImageFont.load_default()

    title_f = _font("DejaVuSans-Bold.ttf", 24)
    pass_f  = _font("DejaVuSans-Bold.ttf", 17)
    label_f = _font("DejaVuSans.ttf", 11)
    val_f   = _font("DejaVuSans-Bold.ttf", 13)
    mrz_f   = _font("DejaVuSansMono.ttf", 19)
    tiny_f  = _font("DejaVuSans.ttf", 8)

    # ── 底纹 ──
    for i in range(0, H, 18):
        draw.line([0, i, W, i], fill="#f0ece0", width=1)

    # ── 边框 ──
    draw.rectangle([4, 4, W-5, H-5], outline="#6B5B3D", width=4)
    draw.rectangle([10, 10, W-11, H-11], outline="#8B7B5D", width=1)

    # ── 国徽 ──
    ex, ey = 35, 30
    ew, eh = 80, 100
    draw.ellipse([ex, ey, ex+ew, ey+eh], outline="#6B5B3D", width=1)
    draw.text((ex+40, ey+28), "★", fill="#C4A35A", font=_font("DejaVuSans.ttf", 42), anchor="mm")
    draw.text((ex+40, ey+60), "MYANMAR", fill="#6B5B3D", font=tiny_f, anchor="mt")

    # ── 标题 ──
    title_y = 30
    draw.text((W//2, title_y), "REPUBLIC OF THE UNION OF MYANMAR", fill="#1a1a1a", font=title_f, anchor="mt")
    title_y += 36
    draw.rectangle([W//2-120, title_y-8, W//2+120, title_y+6], outline="#C4A35A", width=1)
    draw.text((W//2, title_y), "P A S S P O R T", fill="#6B0000", font=pass_f, anchor="mt")
    title_y += 24
    draw.rectangle([W//2-70, title_y-6, W//2+70, title_y+4], outline="#C4A35A", width=1)

    # ── 护照号 & 类型 ──
    pas = data.get("passport_no", "") or "MK565477"
    dtype = (data.get("type", "") or "PV").upper()
    ccode = (data.get("country_code", "") or "MMR").upper()
    draw.text((W-30, 25), f"Passport No  {pas}", fill="#1a1a1a", font=label_f, anchor="rt")
    draw.text((W-30, 45), f"Type  {dtype}    Code  {ccode}", fill="#555", font=tiny_f, anchor="rt")

    # ── 照片 ──
    px, py = 280, 118
    pw, ph = 165, 200
    draw.rectangle([px-3, py-3, px+pw+3, py+ph+3], outline="#8B7B5D", width=1)
    draw.rectangle([px, py, px+pw, py+ph], outline="#aaa", width=1)

    # 如果提供了源照片，裁剪并粘贴到照片区
    # 如果 face_swapped=True，说明照片已经是换脸后的结果，直接使用
    if source_photo_path and os.path.exists(source_photo_path):
        face_img = Image.open(source_photo_path).convert("RGB")
        fw, fh = face_img.size
        target_ratio = pw / ph
        cur_ratio = fw / fh
        if cur_ratio > target_ratio:
            nfw = int(fh * target_ratio)
            face_img = face_img.crop(((fw-nfw)//2, 0, (fw+nfw)//2, fh))
        else:
            nfh = int(fw / target_ratio)
            face_img = face_img.crop((0, (fh-nfh)//2, fw, (fh+nfh)//2))
        face_img = face_img.resize((pw, ph), Image.LANCZOS)
        img.paste(face_img, (px, py))
    else:
        draw.text((px+pw//2, py+ph//2), "PHOTO" + chr(10) + "35x45", fill="#aaa", font=label_f, anchor="mm")

    # ── 数据字段 ──
    surname = data.get("surname", "") or "EI AM"
    given_name = data.get("given_name", "") or ""
    nationality = data.get("nationality", "") or "MYANMAR"
    dob = data.get("dob", "") or "06 FEB 2000"
    sex = data.get("sex", "") or "F"
    issue = data.get("issue_date", "") or "24 NOV 2025"
    expiry = data.get("expiry_date", "") or "23 NOV 2030"
    birthplace = data.get("birth_place", "") or "MATMAN"
    authority = data.get("authority", "") or "MOHA, KYAINGTONG"

    fields = [
        ("Surname / Nom",          surname,           "Given Name / Prenoms", given_name or "—"),
        ("Nationality",            nationality,        "",                      ""),
        ("Date of birth",          dob,                "Sex / Sexe",            sex),
        ("Place of birth",         birthplace,         "",                      ""),
        ("Date of issue",          issue,              "Date of expiry",        expiry),
        ("Authority / Autorite",   authority,          "",                      ""),
    ]

    row_y = 360
    gap = 28
    c_l1, c_v1 = 35, 170
    c_l2, c_v2 = 480, 610

    for l1, v1, l2, v2 in fields:
        draw.text((c_l1, row_y), l1, fill="#666", font=label_f)
        draw.text((c_v1, row_y), v1, fill="#111", font=val_f)
        if l2:
            draw.text((c_l2, row_y), l2, fill="#666", font=label_f)
            draw.text((c_v2, row_y), v2, fill="#111", font=val_f)
        row_y += gap

    # ── 签名 ──
    sig_y = row_y + 5
    draw.text((c_l1, sig_y), "Holder's signature / Signature du titulaire", fill="#666", font=label_f)
    draw.line([c_v1, sig_y+28, c_v1+200, sig_y+28], fill="#aaa", width=1)

    # ── MRZ ──
    line1, line2 = build_mrz(data)
    sep_y = sig_y + 60
    draw.line([15, sep_y, W-15, sep_y], fill="#8B7B5D", width=2)
    mrz_y = sep_y + 14
    draw.rectangle([20, mrz_y-10, W-20, mrz_y+52], fill="#FCFAF5", outline="#c8b898", width=1)
    draw.text((20, mrz_y-6), "P<", fill="#111", font=mrz_f)
    draw.text((W//2, mrz_y), line1, fill="#111", font=mrz_f, anchor="mt")
    draw.text((W//2, mrz_y+24), line2, fill="#111", font=mrz_f, anchor="mt")

    # ── 水印 ──
    wm_f = _font("DejaVuSans-Bold.ttf", 26)
    for i in range(3):
        draw.text((W//2+i*120-120, H//2), "MYANMAR", fill="#e8e2d0", font=wm_f, anchor="mm")

    img.save(output_path, dpi=(300, 300))
    return output_path


def _extract_passport_photo(swapped_image_path, output_photo_path):
    """从换脸后的图像中提取护照照片（检测人脸 → 裁剪头像+上半身）"""
    img = cv2.imread(swapped_image_path)
    if img is None:
        # 回退：直接复制原图
        shutil.copy2(swapped_image_path, output_photo_path)
        return
    faces = face_app.get(img)
    if not faces:
        shutil.copy2(swapped_image_path, output_photo_path)
        return
    # 取检测分数最高的人脸
    face = max(faces, key=lambda f: f.det_score)
    bbox = face.bbox.astype(int)
    x1, y1, x2, y2 = bbox
    fw, fh = x2 - x1, y2 - y1
    cx, cy = (x1 + x2) // 2, (y1 + y2) // 2
    # 向上多留空间（头顶），向下多留（肩膀），左右扩展
    crop_w = int(fw * 2.2)
    crop_h = int(fh * 3.5)
    nx1 = max(0, cx - crop_w // 2)
    ny1 = max(0, cy - int(fh * 1.4))  # 上方留 1.4 倍脸高
    nx2 = min(img.shape[1], nx1 + crop_w)
    ny2 = min(img.shape[0], ny1 + crop_h)
    crop = img[ny1:ny2, nx1:nx2]
    cv2.imwrite(output_photo_path, crop)


def make_id_photo(source_path, output_path, bg_color=(255, 255, 255),
                  out_w=413, out_h=531):
    """将任意大头照制作为护照标准 35x45mm 证件照 (413x531 @300dpi)。
    规则：白底纯色填充；脸居中上 1/3；肩部可见；正脸对齐。"""
    img = cv2.imread(source_path)
    if img is None:
        return "无法读取源图片"
    faces = face_app.get(img)
    if not faces:
        return "源图片未检测到人脸"
    face = max(faces, key=lambda f: f.det_score)
    x1, y1, x2, y2 = face.bbox.astype(int)
    fw, fh = x2 - x1, y2 - y1
    cx, cy = (x1 + x2) // 2, (y1 + y2) // 2

    # 标准比例：脸高占整图 0.5（顶部留 0.2 头顶，脸部 0.5，下方 0.3 肩膀）
    scale = (out_h * 0.5) / max(fh, 1)
    new_fw, new_fh = int(fw * scale), int(fh * scale)
    # 脸部中心 y：放在 out_h * 0.45 处（上部 45% 位置，符合护照照）
    target_cy = int(out_h * 0.45)
    target_cx = out_w // 2

    # 仿射变换矩阵：把源图脸区域 warp 到目标位置
    src_pts = np.float32([
        [cx - fw / 2, y1 - fh * 0.3],   # 左上：头顶上方
        [cx + fw / 2, y1 - fh * 0.3],   # 右上
        [cx - fw / 2, y2 + fh * 1.0],   # 左下：肩膀下方
    ])
    dst_pts = np.float32([
        [target_cx - new_fw / 2, target_cy - new_fh * 0.5 - int(new_fh * 0.15)],
        [target_cx + new_fw / 2, target_cy - new_fh * 0.5 - int(new_fh * 0.15)],
        [target_cx - new_fw / 2, target_cy - new_fh * 0.5 + int(new_fh * 1.5)],
    ])
    M = cv2.getAffineTransform(src_pts, dst_pts)
    warped = cv2.warpAffine(img, M, (out_w, out_h),
                            borderMode=cv2.BORDER_CONSTANT,
                            borderValue=bg_color)
    cv2.imwrite(output_path, warped)
    return ""


# ===================== 换脸核心 =====================

def swap_faces(source_path, target_path, output_path):
    img_src = cv2.imread(source_path)
    img_dst = cv2.imread(target_path)
    if img_src is None:
        return "无法读取源图片"
    if img_dst is None:
        return "无法读取目标图片"
    faces_src = face_app.get(img_src)
    faces_dst = face_app.get(img_dst)
    if len(faces_src) == 0:
        return "源图片未检测到人脸"
    if len(faces_dst) == 0:
        return "目标图片未检测到人脸"
    src_face = max(faces_src, key=lambda f: f.det_score)
    result = img_dst.copy()
    for face in faces_dst:
        result = swapper.get(result, face, src_face, paste_back=True)
    cv2.imwrite(output_path, result)
    return ""


# ===================== 路由 =====================

@app.route("/")
def index():
    return render_template("index.html")


@app.route("/random_passport_data")
def random_data():
    """返回随机护照数据（男女名字匹配）"""
    return jsonify({"ok": True, **random_passport_data()})


@app.route("/compute_mrz", methods=["POST"])
def compute_mrz():
    """前端实时计算 MRZ 预览"""
    data = request.get_json(force=True) or {}
    line1, line2 = build_mrz(data)
    return jsonify({"ok": True, "line1": line1, "line2": line2})


@app.route("/generate_passport", methods=["POST"])
def gen_passport():
    """生成护照 + 换脸"""
    if "source" not in request.files:
        return jsonify({"ok": False, "error": "请上传大头照"}), 400
    sf = request.files["source"]
    if sf.filename == "":
        return jsonify({"ok": False, "error": "请选择源图片"}), 400

    # 读取表单数据
    data = {}
    for key in ["type", "country_code", "passport_no", "surname", "given_name",
                "nationality", "dob", "sex", "issue_date", "expiry_date",
                "birth_place", "authority"]:
        data[key] = (request.form.get(key, "") or "").strip()

    # 固定项
    if not data["type"]: data["type"] = "PV"
    if not data["country_code"]: data["country_code"] = "MMR"
    if not data["nationality"]: data["nationality"] = "MYANMAR"
    if not data["authority"]: data["authority"] = "MOHA, KYAINGTONG"

    # 随机填充：未填项用 random_passport_data 生成，保证男女名字匹配
    rnd = random_passport_data()
    if not data["passport_no"]: data["passport_no"] = rnd["passport_no"]
    if not data["sex"]:          data["sex"] = rnd["sex"]
    # DOB: 男性固定 26 岁 (2000)，女性随机 1980-2005
    if not data["dob"]:
        if data["sex"] == "M":
            dob_dt = gen_random_date_range(2000, 2000)
            MONTHS_ABBR = ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"]
            data["dob"] = f"{dob_dt.day:02d} {MONTHS_ABBR[dob_dt.month-1]} {dob_dt.year}"
        else:
            data["dob"] = rnd["dob"]
    if not data["birth_place"]:  data["birth_place"] = rnd["birth_place"]
    if not data["issue_date"]:   data["issue_date"] = rnd["issue_date"]
    if not data["expiry_date"]:  data["expiry_date"] = rnd["expiry_date"]

    # 名字：如果填了性别用匹配名字，否则用随机性别匹配
    if not data["surname"] or not data["given_name"]:
        sex_for_names = data["sex"] if data["sex"] in ("M", "F") else rnd["sex"]
        if sex_for_names == "M":
            if not data["surname"]:   data["surname"] = random.choice(MALE_SURNAMES)
            if not data["given_name"]: data["given_name"] = random.choice(MALE_GIVEN)
        else:
            if not data["surname"]:   data["surname"] = random.choice(FEMALE_SURNAMES)
            if not data["given_name"]: data["given_name"] = random.choice(FEMALE_GIVEN)

    # 保存源图（临时 + 永久配对）
    src_ext = os.path.splitext(sf.filename)[1] or ".jpg"
    src_name = f"src_{uuid.uuid4().hex}{src_ext}"
    src_path = os.path.join(app.config["UPLOAD_FOLDER"], src_name)
    sf.save(src_path)

    # 配对命名：同名不同夹
    pair_name = paired_name()
    paired_src_path = os.path.join(app.config["PAIRED_PIN"], f"{pair_name}{src_ext}")
    shutil.copy2(src_path, paired_src_path)
    print(f"[Paired] 源照已存档 → {paired_src_path}", flush=True)

    # 选择模板：男→mm_boy.jpg，女→mm_girl.jpg
    # 支持显式指定模板：use_template=boy|girl 覆盖性别自动选择
    use_template = (request.form.get("use_template", "") or "").strip().lower()
    if use_template == "boy":
        base_tpl = TEMPLATE_BOY
    elif use_template == "girl":
        base_tpl = TEMPLATE_GIRL
    else:
        base_tpl = get_template(data["sex"])

    # ── 新管线：先把源照制成 413x531 标准证件照，再用证件照去换双脸模板 ──
    # Step 1: 把上传的大头照制成护照标准 35x45mm 白底证件照
    id_name = f"id_{uuid.uuid4().hex}.jpg"
    id_path = os.path.join(app.config["UPLOAD_FOLDER"], id_name)
    err = make_id_photo(src_path, id_path)
    if err:
        return jsonify({"ok": False, "error": f"制证失败: {err}"}), 500

    # Step 2: 用标准证件照替换模板双头像（全分辨率，不变形）
    swapped_name = f"swapped_{uuid.uuid4().hex}.png"
    swapped_path = os.path.join(app.config["UPLOAD_FOLDER"], swapped_name)
    err = swap_faces(id_path, base_tpl, swapped_path)
    if err:
        return jsonify({"ok": False, "error": f"换脸失败: {err}"}), 500

    # Step 3: 标准证件照本身（已含背景）就是护照照片
    photo_path = id_path

    # Step 3: 生成护照（使用提取的换脸照片）
    out_name = f"passport_{uuid.uuid4().hex}.png"
    out_path = os.path.join(app.config["RESULT_FOLDER"], out_name)
    task_id = out_name

    with tasks_lock:
        tasks[task_id] = {"status": "processing", "mode": "passport"}

    def do_gen():
        print(f"[Passport] {task_id} started", flush=True)
        # 以换脸后的双人模板为底图，在上面叠加护照文字字段
        generate_passport_image(data, photo_path, out_path, base_image=swapped_path)
        result_url = f"/result/{out_name}"
        with tasks_lock:
            # 永久配对存档
            paired_out = os.path.join(app.config["PAIRED_POUT"], f"{pair_name}.png")
            shutil.copy2(out_path, paired_out)
            print(f"[Paired] 护照已存档 → {paired_out}", flush=True)
            tasks[task_id] = {
                "status": "done",
                "result_url": result_url,
                "mode": "passport",
                "paired_name": pair_name,
            }
        print(f"[Passport] {task_id} done", flush=True)
        # 清理临时文件（id_path 保留：作为下载的证件照）
        for p in [src_path, swapped_path]:
            try:
                os.remove(p)
            except OSError:
                pass

    executor.submit(do_gen)
    return jsonify({"ok": True, "task_id": task_id})


@app.route("/make_id_photo", methods=["POST"])
def make_id_photo_api():
    """单步：上传大头照 → 制作 413x531 护照标准证件照 → 返回预览"""
    if "source" not in request.files:
        return jsonify({"ok": False, "error": "请上传大头照"}), 400
    sf = request.files["source"]
    if sf.filename == "":
        return jsonify({"ok": False, "error": "请选择源图片"}), 400
    bg = (request.form.get("bg", "white") or "white").lower()
    bg_map = {"white": (255, 255, 255), "blue": (214, 230, 242),
              "red": (220, 60, 60), "gray": (240, 240, 240)}
    bg_color = bg_map.get(bg, (255, 255, 255))

    src_ext = os.path.splitext(sf.filename)[1] or ".jpg"
    src_name = f"src_{uuid.uuid4().hex}{src_ext}"
    src_path = os.path.join(app.config["UPLOAD_FOLDER"], src_name)
    sf.save(src_path)

    id_name = f"id_{uuid.uuid4().hex}.jpg"
    id_path = os.path.join(app.config["UPLOAD_FOLDER"], id_name)
    err = make_id_photo(src_path, id_path, bg_color=bg_color)
    if err:
        return jsonify({"ok": False, "error": err}), 500
    try:
        os.remove(src_path)
    except OSError:
        pass
    return jsonify({"ok": True, "result_url": f"/result/{id_name}",
                    "filename": id_name})


@app.route("/swap", methods=["POST"])
def swap():
    if "source" not in request.files:
        return jsonify({"ok": False, "error": "请上传大头照"}), 400
    source_file = request.files["source"]
    if source_file.filename == "":
        return jsonify({"ok": False, "error": "请选择源图片"}), 400

    target_file = request.files.get("target")

    src_ext = os.path.splitext(source_file.filename)[1] or ".jpg"
    src_name = f"src_{uuid.uuid4().hex}{src_ext}"
    src_path = os.path.join(app.config["UPLOAD_FOLDER"], src_name)
    source_file.save(src_path)

    if target_file and target_file.filename != "":
        tgt_ext = os.path.splitext(target_file.filename)[1] or ".jpg"
        tgt_name = f"tgt_{uuid.uuid4().hex}{tgt_ext}"
        tgt_path = os.path.join(app.config["UPLOAD_FOLDER"], tgt_name)
        target_file.save(tgt_path)
        is_default = False
    else:
        sex = (request.form.get("sex", "") or "").strip().upper()
        tgt_path = get_template(sex)
        is_default = True

    out_name = f"result_{uuid.uuid4().hex}.png"
    out_path = os.path.join(app.config["RESULT_FOLDER"], out_name)
    task_id = out_name

    with tasks_lock:
        tasks[task_id] = {"status": "processing", "is_default_target": is_default, "mode": "swap"}

    def do_swap():
        print(f"[Swap] {task_id} started", flush=True)
        error = swap_faces(src_path, tgt_path, out_path)
        result_url = f"/result/{out_name}"
        with tasks_lock:
            if error:
                tasks[task_id] = {"status": "error", "error": error}
            else:
                tasks[task_id] = {
                    "status": "done", "result_url": result_url,
                    "is_default_target": is_default, "mode": "swap",
                }
        print(f"[Swap] {task_id} {tasks[task_id]['status']}", flush=True)
        try:
            os.remove(src_path)
        except OSError:
            pass
        if target_file and target_file.filename != "":
            try:
                os.remove(tgt_path)
            except OSError:
                pass

    executor.submit(do_swap)
    return jsonify({"ok": True, "task_id": task_id})


@app.route("/status/<task_id>")
def task_status(task_id):
    with tasks_lock:
        task = tasks.get(task_id)
    if task is None:
        return jsonify({"ok": False, "error": "任务不存在"}), 404
    return jsonify(task)


@app.route("/result/<filename>")
def download_result(filename):
    for folder in (app.config["RESULT_FOLDER"], app.config["UPLOAD_FOLDER"]):
        path = os.path.join(folder, filename)
        if os.path.exists(path):
            mime = "image/jpeg" if filename.lower().endswith((".jpg", ".jpeg")) else "image/png"
            return send_file(path, mimetype=mime)
    return "文件不存在", 404


def cleanup_old_files(folder, max_age_seconds=3600):
    now = time.time()
    for fname in os.listdir(folder):
        fpath = os.path.join(folder, fname)
        if os.path.isfile(fpath) and (now - os.path.getmtime(fpath) > max_age_seconds):
            try:
                os.remove(fpath)
            except OSError:
                pass


@app.route("/cleanup", methods=["POST"])
def cleanup():
    cleanup_old_files(app.config["UPLOAD_FOLDER"], 1800)
    cleanup_old_files(app.config["RESULT_FOLDER"], 1800)
    with tasks_lock:
        tasks.clear()
    return jsonify({"ok": True})


if __name__ == "__main__":
    cleanup_old_files(app.config["UPLOAD_FOLDER"], 3600)
    cleanup_old_files(app.config["RESULT_FOLDER"], 3600)
    app.run(host="0.0.0.0", port=7860, debug=False, threaded=True)
