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

sys.path.insert(0, "/data/port")
sys.path.insert(0, "/data/port/scripts")
from mrz import build_line1, build_line2
from bisenet import change_background
import auto_text
import passport_m03

BASE = "/data/port"
FONT_DIR = "/usr/share/fonts/truetype/dejavu"

app = Flask(__name__)
app.config["TEMPLATES_AUTO_RELOAD"] = True
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")

for d in (app.config["UPLOAD_FOLDER"], app.config["RESULT_FOLDER"],
          app.config["PAIRED_PIN"], app.config["PAIRED_POUT"], app.config["TEMPLATES"]):
    os.makedirs(d, 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))

_face_det_lock = threading.Lock()

def _detect_faces(img):
    """人脸检测，带超大脸回退：默认 det_size 检不出时缩小尺度重试。
    swap 用到的小尺寸证件照若人脸过大(street 大脸)会检不出，需缩小 det_size。"""
    fs = face_app.get(img)
    if fs:
        return fs
    with _face_det_lock:
        saved = tuple(face_app.det_size)
        try:
            for det in ((512, 512), (448, 448), (320, 320)):
                face_app.prepare(ctx_id=-1, det_size=det)
                fs = face_app.get(img)
                if fs:
                    break
        finally:
            face_app.prepare(ctx_id=-1, det_size=saved)
    return fs

def detect_gender(img_bgr):
    """用 insightface genderage 检测性别，返回 'M'/'F'，不确定返回 None。
    仅用于上传后预选性别下拉框，不参与 MRZ 计算。"""
    try:
        fs = _detect_faces(img_bgr)
        if not fs:
            return None
        f = max(fs, key=lambda x: x.det_score)
        if f.det_score < 0.5:
            return None
        return "M" if f.gender == 1 else "F"
    except Exception as e:
        print(f"[Gender] detect failed: {e!r}", flush=True)
        return None


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, "模板", "m03.png")
TEMPLATE_TEXT  = os.path.join(BASE, "模板", "M001.jpg")
TEXT_LAYOUT    = os.path.join(BASE, "模板", "M001_layout.json")

# 缅甸姓名库
FEMALE_SURNAMES = ["EI", "SU", "KHIN", "MAY", "NU", "THAN", "THIDA", "NILAR", "MOE", "YIN", "HTWE", "WIN", "MYA"]
FEMALE_GIVEN = ["SU MYAT", "THIN", "ZAR", "THU", "YADANAR", "HTAY", "KHINE", "WAI", "EI", "MON", "THIRI", "AYE", "PHYU"]
MALE_SURNAMES = ["AUNG", "KYAW", "WIN", "MYINT", "ZAW", "HTET", "TUN", "PHYO", "MIN", "THEIN", "SAN", "KYI", "YE"]
MALE_GIVEN = ["AUNG", "KYAW", "WIN", "MIN", "HTET", "ZAW", "PHYO", "THEIN", "MYO", "TUN", "NAING", "YE", "HTUN"]
BIRTH_PLACES = ["YANGON", "MANDALAY", "TAUNGGYI", "BAGO", "PATHEIN", "SITTWE",
                "MYITKYINA", "MAWLAMYINE", "MONYWA", "MEIKTILA", "MATMAN", "KYAINGTONG"]
AUTHORITIES = ["MOHA, KYAINGTONG", "MOHA, YANGON", "MOHA, MANDALAY"]

MONTHS_ABBR = ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"]

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

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


# ===================== 工具 =====================

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

def paired_name():
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    uid = uuid.uuid4().hex[:6]
    return f"{ts}_{uid}"

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

def fmt_date(dt):
    return f"{dt.day:02d} {MONTHS_ABBR[dt.month-1]} {dt.year}"


# ===================== 随机数据 =====================

def random_passport_data(sex=None):
    sex = (sex or "F").upper()
    if sex not in ("M", "F"):
        sex = "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)
    dob = gen_random_date(1980, 2005)
    issue = gen_random_date(2020, 2025)
    expiry = issue + timedelta(days=5 * 365)
    return {
        "passport_no": gen_passport_no(),
        "sex": sex,
        "name": f"{surname} {given}",
        "country_code": "MMR",
        "dob": fmt_date(dob),
        "birth_place": random.choice(BIRTH_PLACES),
        "issue_date": fmt_date(issue),
        "expiry_date": fmt_date(expiry),
    }


# ===================== MRZ =====================

def build_mrz(data):
    # 缅甸姓名不分姓氏/名字，整段姓名视为一项(Surname)。
    # 支持 name(合并字段) 或旧格式 surname+given_name，一律不再按空格拆分。
    name = (data.get("name", "") or "").upper()
    if not name:
        s = (data.get("surname", "") or "").strip()
        g = (data.get("given_name", "") or "").strip()
        name = " ".join(x for x in (s, g) if x).upper()
    surname = name
    given = ""
    country = (data.get("country_code", "") or "MMR").upper()[:3]
    dtype = (data.get("type", "") or "PV").upper()

    pas = (data.get("passport_no", "") or "").upper().replace(" ", "").replace("<", "")
    if not pas:
        pas = gen_passport_no()

    def _parse(s):
        for f in ("%d %b %Y", "%d %B %Y", "%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y"):
            try:
                return datetime.strptime(s, f)
            except Exception:
                pass
        return None

    dob_dt = _parse(data.get("dob", "") or "") or datetime(2000, 2, 6)
    exp_dt = _parse(data.get("expiry_date", "") or "") or datetime(2030, 11, 23)
    sex = (data.get("sex", "") or "F").upper()[:1]
    if sex not in ("M", "F"):
        sex = "F"

    line1 = build_line1(surname, given, dtype, country)
    line2 = build_line2(pas, dob_dt.strftime("%y%m%d"),
                        exp_dt.strftime("%y%m%d"), sex, country)
    return line1, line2


# ===================== 证件照制作 (ICAO 35×45mm @ 300dpi) =====================
# 35mm × 45mm @ 300dpi → 413 × 531 像素
# 头部标准（ICAO 9303）: 头顶到下巴占图高 70~80%；下巴到图底 ≥ 头部高度
# 实际采用：脸高(下巴-发际) ≈ 0.55H；头顶距图顶 ≈ 0.10H；下巴到图底 ≈ 0.35H

ID_W, ID_H = 413, 531


def smart_crop_selfie(source_path, output_path):
    """智能裁切自拍照：包含完整头发+脸+小部分衣领，按 35:45 比例输出 JPG。
    用于左侧预览框显示原图。
    """
    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

    # 用 bisenet 找出头发上边界
    hair_top = y1  # fallback
    try:
        mask = parse_face_only(img, face_bbox=(x1, y1, x2, y2))
        # hair/hat label 13, 14
        hair_rows = np.where((mask[13] > 127) | (mask[14] > 127))[0]
        if len(hair_rows) > 0:
            hair_top = int(hair_rows.min())
        # 衣领下边界（cloth=16 标签）
        cloth_rows = np.where(mask[16] > 127)[0]
        cloth_bottom = y2 + int(fh * 1.0)  # fallback
        if len(cloth_rows) > 0:
            cloth_bottom = int(cloth_rows.max())
    except Exception:
        cloth_bottom = y2 + int(fh * 1.0)

    # 目标：35:45 比例。垂直方向 hair_top → cloth_bottom 包含头发+脸+衣领
    # 加上小余量：上下各放一点空隙
    pad_top = int((cloth_bottom - hair_top) * 0.05)
    pad_bot = int((cloth_bottom - hair_top) * 0.05)
    crop_top = max(0, hair_top - pad_top)
    crop_bot = min(img.shape[0], cloth_bottom + pad_bot)
    crop_h = crop_bot - crop_top
    # 35:45 比例 = 0.7778 ; 宽 = 高 * 35/45
    crop_w = int(crop_h * 35 / 45)
    # 水平以人脸中心为基准
    crop_left = max(0, cx - crop_w // 2)
    crop_right = min(img.shape[1], crop_left + crop_w)
    # 如果右边超出，再左移
    if crop_right - crop_left < crop_w:
        crop_left = max(0, crop_right - crop_w)

    crop = img[crop_top:crop_bot, crop_left:crop_right]
    cv2.imwrite(output_path, crop, [int(cv2.IMWRITE_JPEG_QUALITY), 92])
    return ""


def parse_face_only(image_bgr, face_bbox=None):
    """只返回 19 类的 mask（h, 19, h, w）numpy uint8 0/255。"""
    from bisenet import _get_session, KEEP_LABELS, INPUT_SIZE
    h, w = image_bgr.shape[:2]
    img = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
    img = cv2.resize(img, (INPUT_SIZE, INPUT_SIZE), interpolation=cv2.INTER_LINEAR)
    mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
    std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
    x = (img.astype(np.float32) / 255.0 - mean) / std
    x = x.transpose(2, 0, 1)[None, ...].astype(np.float32)
    sess = _get_session()
    outputs = sess.run(None, {"input": x})[0]
    label_map = outputs[0].argmax(axis=0).astype(np.uint8)
    out = np.zeros((19, INPUT_SIZE, INPUT_SIZE), dtype=np.uint8)
    for i in range(19):
        out[i] = (cv2.resize((label_map == i).astype(np.uint8) * 255,
                              (w, h), interpolation=cv2.INTER_LINEAR) > 127).astype(np.uint8) * 255
    return out


_silueta_session = None

def _get_silueta():
    global _silueta_session
    if _silueta_session is None:
        import onnxruntime as ort
        path = os.path.join(BASE, "models", "silueta.onnx")
        if not os.path.exists(path):
            raise FileNotFoundError(f"silueta 模型不存在: {path}")
        _silueta_session = ort.InferenceSession(path, providers=["CPUExecutionProvider"])
    return _silueta_session


def silueta_mask(image_bgr):
    """使用 silueta 模型做人像抠图，返回 0~1 float mask (h, w)。"""
    h, w = image_bgr.shape[:2]
    sess = _get_silueta()
    img_resized = cv2.resize(image_bgr, (320, 320))
    img_rgb = cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
    mean = np.array([0.5, 0.5, 0.5], dtype=np.float32)
    std = np.array([1.0, 1.0, 1.0], dtype=np.float32)
    img_norm = (img_rgb - mean) / std
    img_t = img_norm.transpose(2, 0, 1)[None, ...].astype(np.float32)
    outs = sess.run(None, {"input.1": img_t})
    mask = outs[0][0, 0]
    mask = (mask - mask.min()) / (mask.max() - mask.min() + 1e-8)
    mask_full = cv2.resize(mask, (w, h))
    return mask_full


_get_box_fn = None  # Hivision U.get_box 懒加载

def _hivision_adjust(bgr, alpha, face_rect, bg_color, head_h, head_top_y, cx):
    """生成标准 35×45mm 证件照 (413×531)。
    缩放: head_h 规一化为 0.70*ID_H (保证脸完整、头比例标准);
    水平: 以脸中心水平居中 (标准证件照脸居中的要求);
    垂直: 头顶固定在 0.10*ID_H, 脸完整置于画布内, 底部留 0.20*ID_H 给颈肩。"""
    bg = np.array(bg_color, dtype=np.float32)
    fg = (alpha[..., None].astype(np.float32) / 255.0)
    bgr_clean = (bgr.astype(np.float32) * fg + bg * (1.0 - fg)).astype(np.uint8)

    h, w = bgr_clean.shape[:2]
    head_h = max(int(head_h), 30)
    scale = (ID_H * 0.70) / head_h
    new_w = max(int(w * scale), 2)
    new_h = max(int(h * scale), 2)
    img_resized = cv2.resize(bgr_clean, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)

    new_head_cx = int(round(cx * scale))
    topm = int(round(head_top_y * scale))
    x1 = new_head_cx - ID_W // 2
    y1 = topm - int(ID_H * 0.10)
    x2 = x1 + ID_W
    y2 = y1 + ID_H

    # 与画布求交
    src_x1 = max(0, x1); src_y1 = max(0, y1)
    src_x2 = min(new_w, x2); src_y2 = min(new_h, y2)
    out = np.full((ID_H, ID_W, 3), bg_color, dtype=np.uint8)
    if src_x2 > src_x1 and src_y2 > src_y1:
        dst_x1 = src_x1 - x1; dst_y1 = src_y1 - y1
        dst_x2 = dst_x1 + (src_x2 - src_x1)
        dst_y2 = dst_y1 + (src_y2 - src_y1)
        out[dst_y1:dst_y2, dst_x1:dst_x2] = img_resized[src_y1:src_y2, src_x1:src_x2]
    return out


def make_id_photo(source_path, output_path, bg_color=(255, 255, 255)):
    """生成标准 35×45mm 证件照 (413×531 像素 @300dpi)。
    流程：
    1) insightface 检测人脸框 + 106 关键点(找 chin/brow 定头部上下)
    2) silueta 模型抠出人像 (alpha)
    3) 缩放: head_h → 0.70*ID_H (保证脸完整、头比例标准)
    4) 水平: 借 Hivision 人像边缘检测, 身体够宽时裁到边缘让肩膀铺满, 否则居中
    5) 垂直: 头顶固定在 0.10*ID_H, 合成到纯色背景输出 413×531
    """
    img = cv2.imread(source_path)
    if img is None:
        return "无法读取源图片"

    faces = _detect_faces(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
    face_rect = (x1, y1, fw, fh)
    cx = (x1 + x2) // 2

    chin_y = y2
    brow_y = y1 + int(fh * 0.3)
    if face.landmark_2d_106 is not None:
        lmk = face.landmark_2d_106
        chin_y = int(lmk[0][1]); brow_y = int(lmk[49][1])
    face_h = max(chin_y - brow_y, 50)
    head_top_y = brow_y - int(face_h * 0.5)
    head_h = chin_y - head_top_y

    # 1) silueta 抠图 → alpha
    mask = silueta_mask(img)
    alpha = (np.clip(mask, 0, 1) * 255).astype(np.uint8)

    try:
        out = _hivision_adjust(img, alpha, face_rect, bg_color, head_h, head_top_y, cx)
    except Exception as e:
        print(f"[make_id_photo] _hivision_adjust failed, fallback: {e!r}", flush=True)
        # 回退：老逻辑（silueta + head_scale 居中裁切）
        mask_f = mask[..., None].astype(np.float32)
        bg = np.array(bg_color, dtype=np.float32)
        img_clean = (img.astype(np.float32) * mask_f + bg * (1.0 - mask_f)).astype(np.uint8)
        h, w = img.shape[:2]
        scale = (ID_H * 0.70) / max(head_h, 30)
        new_w = int(w * scale); new_h = int(h * scale)
        img_resized = cv2.resize(img_clean, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
        new_head_cx = int(cx * scale); new_head_cy = int((head_top_y + chin_y) // 2 * scale)
        crop_x1 = new_head_cx - ID_W // 2; crop_y1 = new_head_cy - ID_H // 2
        out = np.full((ID_H, ID_W, 3), bg_color, dtype=np.uint8)
        src_x1 = max(0, crop_x1); src_y1 = max(0, crop_y1)
        src_x2 = min(new_w, crop_x1 + ID_W); src_y2 = min(new_h, crop_y1 + ID_H)
        if src_x2 > src_x1 and src_y2 > src_y1:
            out[src_y1 - crop_y1:src_y2 - crop_y1, src_x1 - crop_x1:src_x2 - crop_x1] = \
                img_resized[src_y1:src_y2, src_x1:src_x2]

    cv2.imwrite(output_path, out)
    return ""


# ===================== 换脸 =====================

def crop_main_face_from_template(swapped_path, template_path, out_path=None):
    """从已换脸模板中裁切主头像（左下大图）。
    模板 mm_girl.jpg 有两张人脸：左侧大头像 + 右侧小头像。
    通过对比 swapped 和 template 的人脸位置，定位到左侧大头像区域裁切。"""
    if out_path is None:
        out_path = swapped_path.replace(".png", "_face.jpg")
    img_bgr = cv2.imread(swapped_path)
    if img_bgr is None:
        raise RuntimeError("swapped 模板读取失败")
    faces = face_app.get(img_bgr)
    if not faces:
        raise RuntimeError("swapped 模板未检测到人脸")
    h, w = img_bgr.shape[:2]
    # 主头像在左半边，bbox 左上 x < w * 0.4
    left_faces = [f for f in faces if f.bbox[0] < w * 0.4]
    if not left_faces:
        left_faces = faces
    face = max(left_faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
    x1, y1, x2, y2 = face.bbox.astype(int)
    fw, fh = x2 - x1, y2 - y1
    # 上下左右小扩展，保证头像+肩部+头顶空间
    pad_x = int(fw * 0.3)
    pad_top = int(fh * 0.5)
    pad_bot = int(fh * 0.6)
    nx1 = max(0, x1 - pad_x)
    ny1 = max(0, y1 - pad_top)
    nx2 = min(w, x2 + pad_x)
    ny2 = min(h, y2 + pad_bot)
    crop_bgr = img_bgr[ny1:ny2, nx1:nx2]
    cv2.imwrite(out_path, crop_bgr, [int(cv2.IMWRITE_JPEG_QUALITY), 92])
    return out_path


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 = _detect_faces(img_src)
    faces_dst = _detect_faces(img_dst)
    if not faces_src:
        return "源图片未检测到人脸"
    if not faces_dst:
        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 ""


# ===================== 护照图片生成 (以换脸后模板为画布, 叠加字段+MRZ) =====================
# mm_girl.jpg 模板布局 (720×1280 竖版):
#   y=   0- 200  深色头部
#   y= 200- 700  上方内容区 (标题/护照号)
#   y= 720- 980  照片+字段区 (左侧大头像+右侧小头像+文字)
#   y= 980-1160  签名+MRZ区
#   y=1160-1280  深色底部 (MRZ 条)


def generate_passport_image(swapped_path, output_path):
    """只换人像：直接输出换脸后的原版模板，保留模板全部原始文字与布局。"""
    shutil.copy2(swapped_path, output_path)
    return output_path


def _clear_text(img, y0, y1, x0=0, x1=None, thr=90):
    """去文字：把指定 x 范围内的深色文字像素改为该行背景中位数，
    保留其余像素(含模板底纹/渐变)，避免擦除后出现纯色横条。
    必须在创建 draw 之前调用（img 会被替换）。"""
    a = np.asarray(img).copy()
    X1 = x1 if x1 is not None else a.shape[1]
    for y in range(y0, y1):
        row = a[y, x0:X1].copy()
        mask = row.max(axis=1) > thr
        if mask.any():
            bg = np.median(row[mask], axis=0).astype(row.dtype)
            row[~mask] = bg
        a[y, x0:X1] = row
    return Image.fromarray(a)


# mm_girl.jpg 模板字段值区域 (720×1280 竖版)，由 OCR + 像素分析实测：
#   顶部: 姓名号码区 y≈723-748；数据块字段值 y≈758-1003；MRZ 两行 y≈1054-1119
# 值字体高度约 12~15px（对应 DejaVu 约 16~20px）
TOP_NO      = (460, 725, 580, 752)     # 护照号值 (右对齐 x≈565)
TOP_GRP     = (240, 720, 400, 750)     # 顶部 Type/Code 小组
ROW_SURNAME = (240, 755, 430, 776)
ROW_GIVEN   = (240, 774, 430, 794)
ROW_NAT     = (240, 810, 430, 834)
ROW_DOB     = (240, 852, 430, 878)
ROW_SEX     = (228, 893, 268, 920)
ROW_PLACE   = (405, 894, 560, 918)
ROW_ISSUE   = (238, 938, 430, 958)
ROW_AUTH    = (405, 934, 650, 958)
ROW_EXPIRY  = (238, 982, 430, 1006)
ROW_MRZ1    = (40, 1052, 680, 1080)
ROW_MRZ2    = (40, 1090, 680, 1122)
FONT_MONO_MRZ = "/data/port/fonts/DejaVuSansMono.ttf"

# md02.jpg 模板字段位置（基于 OCR 实测标题 bbox + 11px 偏移推算值 y 中线）
MD02_TOP_GRP    = (240, 738, 400, 752)   # Type / Country code 值
MD02_TOP_NO     = (460, 738, 580, 752)   # Passport No 值
MD02_ROW_SURNAME = (240, 780, 430, 796)  # 值 y 中线 787
MD02_ROW_GIVEN  = (240, 820, 430, 836)   # 值 y 中线 827
MD02_ROW_NAT    = (240, 861, 430, 877)   # 值 y 中线 868
MD02_ROW_DOB    = (240, 900, 430, 916)   # 值 y 中线 907
MD02_ROW_SEX    = (228, 900, 268, 916)   # 值 y 中线 907
MD02_ROW_PLACE  = (405, 904, 560, 920)   # 值 y 中线 911
MD02_ROW_ISSUE  = (238, 940, 430, 956)   # 值 y 中线 947
MD02_ROW_AUTH   = (405, 944, 650, 960)   # 值 y 中线 951
MD02_ROW_EXPIRY = (238, 986, 430, 1002)  # 值 y 中线 993
MD02_ROW_MRZ1   = (40, 1052, 680, 1080)
MD02_ROW_MRZ2   = (40, 1090, 680, 1122)


def replace_passport_fields(base_path, data, output_path):
    """步骤2(竖版模板)：以步骤1的结果图(720×1280 mm_girl)为底，
    擦除模板印制的字段值 + MRZ，用前端提交数据重新绘制。"""
    img = Image.open(base_path).convert("RGB")

    # 擦除值区域与 MRZ 区域（逐行去深字，保留模板底纹）
    for x0, y0, x1, y1 in [TOP_GRP, TOP_NO, ROW_SURNAME, ROW_GIVEN, ROW_NAT, ROW_DOB,
                           ROW_SEX, ROW_PLACE, ROW_ISSUE, ROW_AUTH, ROW_EXPIRY,
                           ROW_MRZ1, ROW_MRZ2]:
        img = _clear_text(img, y0, y1, x0=x0, x1=x1)

    W, H = img.size
    draw = ImageDraw.Draw(img)

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

    sf = W / 720
    f_pass  = _font("DejaVuSans-Bold.ttf", max(10, int(13 * sf)))
    f_val   = _font("DejaVuSans-Bold.ttf", max(10, int(12 * sf)))
    f_tiny  = _font("DejaVuSans-Bold.ttf", max(7, int(8 * sf)))
    f_mrz   = _font(FONT_MONO_MRZ, max(12, int(17 * sf)))

    name       = (data.get("name", "") or "").upper()
    surname    = name.split()[0] if name else ""
    given_name = " ".join(name.split()[1:]) if name and len(name.split()) > 1 else ""
    nationality= (data.get("nationality", "") or "MYANMAR").upper()
    dob        = (data.get("dob", "") or "").upper()
    sex        = (data.get("sex", "") or "F").upper()[:1]
    issue      = (data.get("issue_date", "") or "").upper()
    expiry     = (data.get("expiry_date", "") or "").upper()
    birthplace = (data.get("birth_place", "") or "").upper()
    authority  = (data.get("authority", "") or "MOHA, KYAINGTONG").upper()
    pas        = (data.get("passport_no", "") or "MK000000").upper()
    dtype      = (data.get("type", "") or "PV").upper()
    ccode      = (data.get("country_code", "") or "MMR").upper()

    # ── 顶部：Type/Code 小字(PV/MMR) + 护照号(右对齐) ──
    draw.text((263, 740), dtype, fill="#333333", font=f_tiny, anchor="mm")
    draw.text((348, 740), ccode, fill="#333333", font=f_tiny, anchor="mm")
    draw.text((565, 741), pas, fill="#111111", font=f_pass, anchor="rm")

    # ── 数据块字段（值区位置与模板实测一致，值用深色） ──
    def _put(region, text, anchor="lm"):
        x0, y0, x1, y1 = region
        cy = (y0 + y1) // 2
        draw.text((x0 + 1, cy), text, fill="#000000", font=f_val, anchor=anchor)

    _put(ROW_SURNAME, surname)
    _put(ROW_GIVEN, given_name)
    _put(ROW_NAT, nationality)
    _put(ROW_DOB, dob)
    _put(ROW_SEX, sex)
    _put(ROW_PLACE, birthplace)
    _put(ROW_ISSUE, issue)
    _put(ROW_AUTH, authority)
    _put(ROW_EXPIRY, expiry)

    # ── MRZ (两行, 等宽字体) ──
    line1, line2 = build_mrz(data)
    draw.text((W // 2, 1067), line1, fill="#000000", font=f_mrz, anchor="mm")
    draw.text((W // 2, 1107), line2, fill="#000000", font=f_mrz, anchor="mm")

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


def replace_passport_fields_md02(base_path, data, output_path):
    """text 页专用：仅在 md02.jpg 模板的空白值区绘制可编辑字段。
    使用 mm_girl 实测的标题-值偏移定位值位置，仅在检测为空白时才绘制。
    已印刷字段保持模板原样不变。"""
    img = Image.open(base_path).convert("RGB")
    draw = ImageDraw.Draw(img)

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

    W, H = img.size
    sf = W / 720

    # 值区规格：(值 y 中线, 值 x0, 值 x1, 字号, 颜色)
    # 基于 mm_girl 实测偏移 (分支任务_模板字段位置记录.md)，
    # 值 y = md02 标题中心 y + (mm_girl 值中心 - mm_girl 标题中心)
    FIELD_SPECS = {
        "passport_no":  (743.5, 471, 552, 13, "#585450"),   # 右对齐
        "name":         (785.5, 247, 347, 17, "#635A59"),   # 合并姓名
        "nationality":  (867.5, 245, 348, 17, "#474541"),
        "sex":          (907.0, 228, 268, 13, "#52514D"),
        "expiry_date":  (993.0, 241, 360, 20, "#4E4F49"),
    }

    # 检测区域是否为空白（暗像素少于阈值）
    def is_blank(x0, y0, x1, y1):
        gray = img.convert("L").crop((x0, y0, x1, y1))
        dark = sum(1 for p in gray.getdata() if p < 200)
        return dark < 10

    # 擦除并绘制某个字段的值
    def draw_field(field, text):
        spec = FIELD_SPECS[field]
        vy, vx0, vx1, fs, color = spec
        # 检测空白
        if not is_blank(vx0, int(vy) - fs, vx1, int(vy) + fs):
            return  # 已有印刷内容，跳过
        # 擦除
        img = _clear_text(img, int(vy) - fs, int(vy) + fs, x0=vx0, x1=vx1)
        font = _font("DejaVuSans-Bold.ttf", max(10, int(fs * sf)))
        if field == "passport_no":
            draw.text((vx1, int(vy)), text, fill=color, font=font, anchor="rm")
        else:
            draw.text((vx0, int(vy)), text, fill=color, font=font, anchor="lm")

    # 收集要绘制的字段
    to_draw = []
    if (data.get("passport_no") or "").strip():
        to_draw.append(("passport_no", (data["passport_no"] or "MK000000").upper()))
    if (data.get("name") or "").strip():
        to_draw.append(("name", (data["name"] or "").upper()))
    if (data.get("nationality") or "").strip():
        to_draw.append(("nationality", (data["nationality"] or "MYANMAR").upper()))
    sex = (data.get("sex") or "F").upper()[:1]
    if sex in ("M", "F"):
        to_draw.append(("sex", sex))
    if (data.get("expiry_date") or "").strip():
        to_draw.append(("expiry_date", (data["expiry_date"] or "").upper()))

    for field, text in to_draw:
        draw_field(field, text)

    # MRZ（模板中为空）
    line1, line2 = build_mrz(data)
    f_mrz = _font(FONT_MONO_MRZ, max(12, int(17 * sf)))
    draw.text((W // 2, 1067), line1, fill="#000000", font=f_mrz, anchor="mm")
    draw.text((W // 2, 1107), line2, fill="#000000", font=f_mrz, anchor="mm")

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


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

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


@app.route("/text")
def text_page():
    """纯文字修改页面：保留表单/MRZ/同步，不做人像上传/换脸。"""
    return render_template("text.html")


@app.route("/text_generate", methods=["POST"])
def text_generate():
    """纯文字修改（无换脸）：支持 m02/m03 模板 + 文字擦除/重绘。
    数据从 form 取，不接收 source 图片。"""
    data = {}
    for key in ["type", "country_code", "passport_no", "name",
                "nationality", "dob", "sex", "issue_date", "expiry_date",
                "birth_place", "authority"]:
        data[key] = (request.form.get(key, "") or "").strip()
    template = (request.form.get("template", "m03") or "m03").lower()
    if template not in ("m02", "m03"):
        template = "m03"
    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"
    sex = (data.get("sex") or "F").upper()[:1]
    if sex not in ("M", "F"):
        sex = "F"
    data["sex"] = sex
    rnd = random_passport_data(sex=sex)
    for k in ["passport_no", "dob", "birth_place", "issue_date",
              "expiry_date", "name"]:
        if not data[k]:
            data[k] = rnd[k]
    print(f"[TextGen] 表单字段: {data} template={template}", flush=True)

    out_name = f"text_{template}_{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": "text", "template": template}

    def do_gen():
        print(f"[TextGen] {task_id} started", flush=True)
        try:
            template_path = os.path.join(BASE, "模板", f"{template}.png")
            if template in ("m02", "m03"):
                # 两模板共用 M02 几何；固定区/标题已由模板印刷，仅重绘可编辑值+MRZ
                passport_m03.write_passport(template_path, out_path, data,
                                            template="m02", skip_header=True,
                                            skip_fixed=True, no_affine=True)
            else:
                # 回退到 M001 auto_text
                layout = None
                if os.path.exists(TEXT_LAYOUT):
                    import json as _json
                    with open(TEXT_LAYOUT, encoding="utf-8") as _f:
                        layout = _json.load(_f)
                auto_text.render(TEMPLATE_TEXT, out_path, data, layout)
            print(f"[TextGen] {task_id} generated", flush=True)
        except Exception as e:
            import traceback
            print(f"[TextGen] {task_id} ERROR: {e}", flush=True)
            traceback.print_exc()
            with tasks_lock:
                tasks[task_id] = {"status": "error", "error": str(e)}
            return
        with tasks_lock:
            tasks[task_id] = {
                "status": "done",
                "result_url": f"/result/{out_name}",
                "mode": "text",
                "template": template,
            }
        print(f"[TextGen] {task_id} done", flush=True)

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


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


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


@app.route("/random_passport_data")
def random_data():
    sex = (request.args.get("sex") or request.form.get("sex") or "F")
    return jsonify({"ok": True, **random_passport_data(sex)})


@app.route("/compute_mrz", methods=["POST"])
def compute_mrz():
    data = request.get_json(force=True) or {}
    line1, line2 = build_mrz(data)
    return jsonify({"ok": True, "line1": line1, "line2": line2})


SYNC_DIR = os.path.join(BASE, "web", "synced")
os.makedirs(SYNC_DIR, exist_ok=True)


@app.route("/upload_synced", methods=["POST"])
def upload_synced():
    if "file" not in request.files:
        return jsonify({"ok": False, "error": "no file"}), 400
    f = request.files["file"]
    name = datetime.now().strftime("%Y%m%d_%H%M%S_") + uuid.uuid4().hex[:6] + ".png"
    path = os.path.join(SYNC_DIR, name)
    f.save(path)
    print(f"[SYNC] {path}  size={os.path.getsize(path)}", flush=True)
    return jsonify({"ok": True, "path": path, "url": f"/synced/{name}"})


@app.route("/synced/<name>")
def get_synced(name):
    return send_file(os.path.join(SYNC_DIR, name))


@app.route("/preview_results")
def preview_results():
    """结果图预览页面"""
    return render_template("preview_results.html")


@app.route("/mrz_compare")
def mrz_compare():
    """MRZ 区域上下对比页：上方=生成结果，下方=m02 模板"""
    return render_template("mrz_compare.html")


@app.route("/mrz_choose")
def mrz_choose():
    """MRZ 字体选择对比页：m02 原图 vs Sharp(改前) vs OCRBE(改后)"""
    return render_template("mrz_choose.html")


@app.route("/mrz_crop3/<which>")
def mrz_crop3(which):
    """裁出 MRZ 区域用于字体选择对比。
    which = m02(模板) / sharp(改前pout) / ocrbe(OCRBE渲染) / choose(整张拼图)"""
    box = (250, 1558, 1432, 1698)
    _MAP = {
        "m02":    os.path.join(BASE, "模板", "m02.png"),
        "sharp":  os.path.join(BASE, "cmp_sharp.png"),
        "new":    os.path.join(BASE, "cmp_new.png"),
        "ocrbe":  os.path.join(BASE, "cmp_ocrbe.png"),
        "djmono": os.path.join(BASE, "cmp_djmono.png"),
        "choose": os.path.join(BASE, "mrz_compare5.png"),
    }
    src = _MAP.get(which)
    if not src or not os.path.exists(src):
        return "文件不存在", 404
    im = Image.open(src).convert("RGB")
    W, H = im.size
    # 标准护照尺寸(约1672×1840)才裁 MRZ 带；拼接小图直接返回
    if abs(W / 1672.0 - 1) < 0.05 and abs(H / 1840.0 - 1) < 0.05:
        b = (box[0], box[1], box[2], box[3])
        crop = im.crop(b)
    else:
        crop = im
    out = os.path.join("/tmp", f"_mrzchoose_{which}.png")
    crop.save(out)
    return send_file(out, mimetype="image/png")


@app.route("/mrz_crop/<which>")
def mrz_crop(which):
    """裁出 MRZ 区域(仅两行机读码)，用于上下对比。
    which = gen(最新生成结果) / m02(模板) / 或 results 目录下的文件名"""
    # MRZ 区域(1672x1840 下)：x 250-1430, y 1560-1695
    box = (250, 1558, 1432, 1698)
    if which == "m02":
        src = os.path.join(TEMPLATE_DIR, "m02.png")
    else:
        # 指定文件名 或 取最新结果图
        if which == "gen":
            result_dir = app.config["RESULT_FOLDER"]
            cands = [os.path.join(result_dir, f) for f in os.listdir(result_dir)
                     if f.lower().endswith(".png")] if os.path.isdir(result_dir) else []
            if not cands:
                return "无生成结果", 404
            src = max(cands, key=os.path.getmtime)
        else:
            src = os.path.join(app.config["RESULT_FOLDER"], which)
            if not os.path.exists(src):
                src = os.path.join(TEMPLATE_DIR, which)
    if not os.path.exists(src):
        return "文件不存在", 404
    im = Image.open(src).convert("RGB")
    W, H = im.size
    sx, sy = W / 1672.0, H / 1840.0
    b = (int(box[0]*sx), int(box[1]*sy), int(box[2]*sx), int(box[3]*sy))
    crop = im.crop(b)
    out = os.path.join("/tmp", f"_mrzcrop_{which}.png")
    crop.save(out)
    return send_file(out, mimetype="image/png")


@app.route("/api/list_results")
def list_results():
    """列出 results 目录下的所有生成图片"""
    result_dir = app.config["RESULT_FOLDER"]
    if not os.path.exists(result_dir):
        return jsonify({"ok": True, "files": []})
    
    files = []
    for fname in os.listdir(result_dir):
        if fname.lower().endswith(('.png', '.jpg', '.jpeg')):
            fpath = os.path.join(result_dir, fname)
            try:
                stat = os.stat(fpath)
                from PIL import Image
                with Image.open(fpath) as im:
                    w, h = im.size
                files.append({
                    "name": fname,
                    "url": f"/result/{fname}",
                    "thumb_url": f"/result/{fname}",
                    "size_mb": round(stat.st_size / (1024 * 1024), 2),
                    "dimensions": f"{w}×{h}",
                    "mtime": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
                })
            except Exception as e:
                print(f"[list_results] Error reading {fname}: {e}")
    
    # 按修改时间倒序排列
    files.sort(key=lambda x: x["mtime"], reverse=True)
    return jsonify({"ok": True, "files": files})


@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

    # 收集前端提交的 12 个字段(8可编辑 + 4固定)，默认值逻辑参照 /replace_fields
    data = {}
    for key in ["type", "country_code", "passport_no", "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"
    sex = (data.get("sex") or "F").upper()[:1]
    if sex not in ("M", "F"):
        sex = "F"
    data["sex"] = sex
    rnd = random_passport_data(sex=sex)
    for k in ["passport_no", "dob", "birth_place", "issue_date",
              "expiry_date", "name"]:
        if not data[k]:
            data[k] = rnd[k]
    print(f"[Passport] 表单字段: {data}", flush=True)

    # 保存源图
    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 = os.path.join(app.config["PAIRED_PIN"], f"{pair_name}{src_ext}")
    shutil.copy2(src_path, paired_src)
    print(f"[Paired] 源照 → {paired_src}", flush=True)

    # Step 1: 制作 413×531 证件照（BiSeNet 抠图+换底+几何校正）
    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, TEMPLATE_GIRL, swapped_path)
    if err:
        return jsonify({"ok": False, "error": f"换脸失败: {err}"}), 500

    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)
        try:
            # 丢弃固定区：m03 底图已留印刷固定项，仅用表单数据重绘可编辑区+MRZ
            passport_m03.write_passport(swapped_path, out_path, data,
                                        template="m02", skip_header=True,
                                        skip_fixed=True, no_affine=True)
            print(f"[Passport] {task_id} generated (换脸+数据/MRZ重绘)", flush=True)
        except Exception as e:
            import traceback
            print(f"[Passport] {task_id} ERROR: {e}", flush=True)
            traceback.print_exc()
            with tasks_lock:
                tasks[task_id] = {"status": "error", "error": str(e)}
            return
        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)
        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})


TEXT_REGIONS = "0,192,720,278;0,918,720,1002;0,1042,720,1128"
AI_STEPS = 12


def ai_fields_prompt(data):
    pas = (data.get("passport_no") or "").strip()
    dtype = (data.get("type") or "PV").upper()
    ccode = (data.get("country_code") or "MMR").upper()
    nationality = (data.get("nationality") or "MYANMAR").strip()
    dob = (data.get("dob") or "").strip()
    sex = (data.get("sex") or "F").upper()[:1]
    issue = (data.get("issue_date") or "").strip()
    expiry = (data.get("expiry_date") or "").strip()
    place = (data.get("birth_place") or "").strip()
    authority = (data.get("authority") or "MOHA, KYAINGTONG").strip()
    line1, line2 = build_mrz(data)
    parts = [
        "title REPUBLIC OF THE UNION OF MYANMAR, P A S S P O R T",
        f"Passport No {pas}, Type {dtype}, Code {ccode}",
        f"Name {data.get('name','')}",
        f"Nationality {nationality}",
        f"Date of birth {dob}, Sex {sex}",
        f"Place of birth {place}",
        f"Date of issue {issue}, Date of expiry {expiry}",
        f"Authority {authority}",
        f"MRZ line1 {line1}",
        f"MRZ line2 {line2}",
    ]
    return "; ".join(p for p in parts if p)


@app.route("/replace_fields", methods=["POST"])
def replace_fields():
    """步骤2：以步骤1结果图为基础，用 AI(Inpaint 模型)重绘文字区域更换固定值。
    异步执行：模型放独立进程，完成状态经 /status/<task_id> 轮询。"""
    base = (request.form.get("base") or "").strip()
    if not base:
        return jsonify({"ok": False, "error": "缺少步骤1结果图"}), 400

    data = {}
    for key in ["type", "country_code", "passport_no", "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"
    sex = (data.get("sex") or "F").upper()[:1]
    if sex not in ("M", "F"):
        sex = "F"
    data["sex"] = sex

    rnd = random_passport_data(sex=sex)
    for k in ["passport_no", "dob", "birth_place", "issue_date",
              "expiry_date", "name"]:
        if not data[k]:
            data[k] = rnd[k]

    base_path = os.path.join(app.config["RESULT_FOLDER"], os.path.basename(base))
    if not os.path.exists(base_path):
        return jsonify({"ok": False, "error": "找不到步骤1结果图"}), 404

    out_name = f"fields_{uuid.uuid4().hex}.png"
    out_path = os.path.join(app.config["RESULT_FOLDER"], out_name)
    task_id = out_name
    log_path = f"/tmp/fields_{os.path.splitext(out_name)[0]}.log"
    payload = dict(data)
    payload.update({"_base": base_path, "_out": out_path, "_prompt": ai_fields_prompt(data),
                    "_log": log_path})

    with tasks_lock:
        tasks[task_id] = {"status": "processing", "mode": "fields_ai", "log": log_path}

    def do_ai():
        print(f"[FieldsAI] {task_id} started", flush=True)
        import subprocess
        cmd = [sys.executable, "/data/port/edit_text.py",
               payload["_base"], payload["_out"],
               "--regions", TEXT_REGIONS,
               "--text", payload["_prompt"],
               "--steps", str(AI_STEPS),
               "--log", payload["_log"]]
        try:
            r = subprocess.run(cmd, capture_output=False, timeout=1500)
            if os.path.exists(out_path):
                print(f"[FieldsAI] {task_id} done (rc={r.returncode})", flush=True)
                with tasks_lock:
                    tasks[task_id] = {"status": "done", "result_url": f"/result/{out_name}",
                                      "mode": "fields_ai", "filename": out_name}
            else:
                raise RuntimeError(f"AI 未产出结果 rc={r.returncode}")
        except Exception as e:
            print(f"[FieldsAI] {task_id} AI 失败,回退 PIL: {e!r}", flush=True)
            try:
                replace_passport_fields(payload["_base"], payload, payload["_out"])
                with tasks_lock:
                    tasks[task_id] = {"status": "done", "result_url": f"/result/{out_name}",
                                      "mode": "fields_pil", "filename": out_name}
                print(f"[FieldsAI] {task_id} PIL fallback done", flush=True)
            except Exception as e2:
                print(f"[FieldsAI] {task_id} ERROR: {e2}", flush=True)
                with tasks_lock:
                    tasks[task_id] = {"status": "error", "error": str(e2)}

    executor.submit(do_ai)
    print(f"[FieldsAI] {task_id} → {out_name}", flush=True)
    return jsonify({"ok": True, "task_id": task_id})


@app.route("/make_id_photo", methods=["POST"])
def make_id_photo_api():
    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
    # 性别检测（仅用于前端预选下拉框；失败返回 None，不影响证件照）
    _src_img = cv2.imread(src_path)
    gender = detect_gender(_src_img) if _src_img is not None else None
    try: os.remove(src_path)
    except OSError: pass
    return jsonify({"ok": True, "result_url": f"/result/{id_name}",
                    "filename": id_name, "gender": gender})


@app.route("/smart_crop_source", methods=["POST"])
def smart_crop_source():
    """智能裁切自拍照：含完整头发+脸+部分衣领，按 35:45 比例输出。
    备用——当前 UI 改用浏览器 contain 直接显示原图。"""
    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

    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)

    out_name = f"crop_{uuid.uuid4().hex}.jpg"
    out_path = os.path.join(app.config["UPLOAD_FOLDER"], out_name)
    err = smart_crop_selfie(src_path, out_path)
    if err:
        shutil.copy2(src_path, out_path)
    try: os.remove(src_path)
    except OSError: pass
    return jsonify({"ok": True, "result_url": f"/result/{out_name}",
                    "filename": out_name})


def _read_ai_progress(log_path):
    """从 edit_text.py 日志读最近进度。返回 (percent 0-100, stage 文案)。"""
    frac, stage = 0.0, "AI 文字重绘中…"
    try:
        with open(log_path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line.startswith("PROGRESS "):
                    try:
                        frac = float(line.split()[1])
                    except (IndexError, ValueError):
                        pass
                elif line.startswith("STAGE "):
                    stage = line[6:].strip() or stage
    except OSError:
        pass
    return int(round(frac * 100)), stage


@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
    if task.get("status") == "processing" and task.get("mode") == "fields_ai":
        pct, stage = _read_ai_progress(task.get("log", ""))
        return jsonify({"status": "processing", "mode": "fields_ai",
                        "progress": pct, "stage": stage})
    return jsonify(task)


TEMPLATE_DIR = os.path.join(BASE, "模板")

@app.route("/template/<filename>")
def serve_template(filename):
    path = os.path.join(TEMPLATE_DIR, filename)
    if os.path.exists(path):
        mime = "image/png" if filename.lower().endswith(".png") else "image/jpeg"
        return send_file(path, mimetype=mime)
    return "模板不存在", 404


@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)
