#!/usr/bin/env python3
"""
照片分类
"""
import requests
import os
import sys
import json
import argparse
import shutil
import subprocess
import zipfile
from datetime import datetime
import cv2
import numpy as np
import sophnet_tools

# API配置
FACE_API_URL = "https://www.sophnet.com/api/open-apis/projects/detect_and_embed"

# 人脸检测与搜索阈值（不再写入 config.json）
QUERY_THRESHOLD = 0.5
SEARCH_SIMILARITY_THRESHOLD = 0.3

_CONFIG_STRIP_KEYS = ("query_threshold", "search_similarity_threshold")
_REGISTRATION_IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
_DATA_DIR_NAME = ".image-classify"
_PREV_DATA_DIR_NAME = ".references"
_data_dir_ready = False


def convert_to_url(image_path, timeout=10):
    return sophnet_tools.upload_oss(image_path, timeout)


def get_skill_src_dir():
    """skill 源码目录（含 scripts、SKILL.md）。"""
    return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))


def get_data_root():
    """与 images 同级的工作区根目录（通常为 sophclaw-skills）。"""
    return os.path.abspath(os.path.join(get_skill_src_dir(), "..", ".."))


def get_images_path():
    return os.path.join(get_data_root(), "images")


def get_image_classify_dir():
    return os.path.join(get_data_root(), _DATA_DIR_NAME)


def get_config_path():
    return os.path.join(get_image_classify_dir(), "config.json")


def get_prev_data_dir():
    """上一版数据目录名（.references），仅用于迁移。"""
    return os.path.join(get_data_root(), _PREV_DATA_DIR_NAME)


def get_legacy_skill_references_dir():
    """更早期 skill 包内 references 目录（src/references）。"""
    return os.path.join(get_skill_src_dir(), "references")


def _sanitize_config_data(data):
    if not isinstance(data, dict):
        return {"users": []}
    out = {k: v for k, v in data.items() if k not in _CONFIG_STRIP_KEYS}
    if "users" not in out:
        out["users"] = []
    return out


def save_config(config_path, data):
    path = resolve_config_path(config_path)
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        json.dump(_sanitize_config_data(data), f, ensure_ascii=False, indent=2)


def resolve_config_path(config_path=None):
    """解析配置文件路径；兼容旧版 config 路径。"""
    ensure_image_classify_ready()
    if config_path is None:
        return get_config_path()
    norm = config_path.replace("\\", "/")
    legacy_names = (
        "references/config.json",
        ".references/config.json",
        ".image-classify/config.json",
    )
    if norm in legacy_names:
        return get_config_path()
    if any(norm.endswith(f"/{name}") for name in legacy_names):
        return get_config_path()
    legacy = os.path.join(get_legacy_skill_references_dir(), "config.json").replace("\\", "/")
    if os.path.abspath(config_path).replace("\\", "/") == legacy:
        return get_config_path()
    return os.path.abspath(config_path)


def _unique_path(directory, basename):
    stem, suffix = os.path.splitext(basename)
    target = os.path.join(directory, basename)
    counter = 1
    while os.path.exists(target):
        target = os.path.join(directory, f"{stem}_{counter}{suffix}")
        counter += 1
    return target


def store_registration_image(image_path):
    """将注册照复制到 .image-classify 目录，返回最终绝对路径。"""
    data_dir = get_image_classify_dir()
    os.makedirs(data_dir, exist_ok=True)
    src = os.path.realpath(image_path)
    if not os.path.isfile(src):
        return src
    data_root = os.path.realpath(data_dir)
    try:
        if os.path.commonpath([src, data_root]) == data_root:
            return src
    except ValueError:
        pass
    target = _unique_path(data_dir, os.path.basename(src))
    shutil.copy2(src, target)
    return os.path.realpath(target)


def _copy_photo_to_references(file_path, legacy_dir, new_dir):
    if not file_path:
        return None
    candidates = []
    if os.path.isabs(file_path):
        candidates.append(file_path)
    else:
        candidates.append(os.path.join(legacy_dir, file_path))
        candidates.append(os.path.join(new_dir, file_path))
    src = None
    for path in candidates:
        if path and os.path.isfile(path):
            src = os.path.realpath(path)
            break
    if not src:
        return None
    try:
        if os.path.commonpath([src, os.path.realpath(new_dir)]) == os.path.realpath(new_dir):
            return src
    except ValueError:
        pass
    target = _unique_path(new_dir, os.path.basename(src))
    shutil.copy2(src, target)
    return os.path.realpath(target)


def _migrate_user_photos(data, legacy_dir, new_dir):
    for user in data.get("users", []):
        for info in user.get("info", []):
            fp = info.get("file_path", "")
            if not fp:
                continue
            new_fp = _copy_photo_to_references(fp, legacy_dir, new_dir)
            if new_fp:
                info["file_path"] = new_fp


def _copy_legacy_reference_assets(legacy_dir, new_dir, data):
    referenced = set()
    for user in data.get("users", []):
        for info in user.get("info", []):
            fp = info.get("file_path")
            if fp:
                referenced.add(os.path.realpath(fp))
    if not os.path.isdir(legacy_dir):
        return
    for name in os.listdir(legacy_dir):
        if name == "config.json":
            continue
        src = os.path.join(legacy_dir, name)
        if not os.path.isfile(src):
            continue
        if os.path.splitext(name)[1].lower() not in _REGISTRATION_IMAGE_EXTS:
            continue
        dst = os.path.join(new_dir, name)
        if os.path.exists(dst):
            continue
        shutil.copy2(src, dst)


def _cleanup_data_dir(old_dir):
    """迁移成功后删除旧数据目录。"""
    if not os.path.isdir(old_dir):
        return
    try:
        shutil.rmtree(old_dir)
        print(f"[迁移] 已删除旧目录 {old_dir}", file=sys.stderr)
    except OSError as exc:
        print(f"[迁移] 无法删除旧目录 {old_dir}: {exc}", file=sys.stderr)


def _migrate_data_between_dirs(source_dir, target_dir):
    source_config = os.path.join(source_dir, "config.json")
    if not os.path.isfile(source_config):
        return False
    with open(source_config, "r", encoding="utf-8") as f:
        data = _sanitize_config_data(json.load(f))
    os.makedirs(target_dir, exist_ok=True)
    _migrate_user_photos(data, source_dir, target_dir)
    _copy_legacy_reference_assets(source_dir, target_dir, data)
    save_config(os.path.join(target_dir, "config.json"), data)
    print(f"[迁移] 已从 {source_dir} 迁移到 {target_dir}", file=sys.stderr)
    return True


def ensure_image_classify_ready():
    """首次运行时创建 .image-classify，并按需从 .references 或 skill/references 迁移。"""
    global _data_dir_ready
    if _data_dir_ready:
        return
    _data_dir_ready = True

    new_dir = get_image_classify_dir()
    new_config = get_config_path()
    os.makedirs(new_dir, exist_ok=True)
    if os.path.isfile(new_config):
        return

    prev_config = os.path.join(get_prev_data_dir(), "config.json")
    if os.path.isfile(prev_config):
        if _migrate_data_between_dirs(get_prev_data_dir(), new_dir):
            _cleanup_data_dir(get_prev_data_dir())
        return

    legacy_dir = get_legacy_skill_references_dir()
    legacy_config = os.path.join(legacy_dir, "config.json")
    if os.path.isfile(legacy_config):
        if _migrate_data_between_dirs(legacy_dir, new_dir):
            _cleanup_data_dir(legacy_dir)
        return

    save_config(new_config, {"users": []})

def list_date_folders():
    """列出待搜索目录 images 下所有 YYYYMMDD 子目录，按日期从新到旧排序。
    返回 [{"name": "20260410", "path": "/abs/path/20260410"}, ...]"""
    images_path = get_images_path()
    if not os.path.exists(images_path):
        print(f"[提示] images_path 不存在: {images_path}")
        return []

    if not os.path.isdir(images_path):
        print(f"[提示] images_path 不是目录: {images_path}")
        return []

    valid_dates = []
    invalid_entries = []
    for name in os.listdir(images_path):
        full_path = os.path.join(images_path, name)
        if not os.path.isdir(full_path):
            continue
        try:
            dt = datetime.strptime(name, "%Y%m%d")
            valid_dates.append((dt, name, os.path.abspath(full_path)))
        except ValueError:
            invalid_entries.append(name)

    if invalid_entries:
        print(f"[提示] 以下目录名不是 YYYYMMDD，将忽略: {', '.join(sorted(invalid_entries))}")

    if not valid_dates:
        print(f"[提示] 未找到符合 YYYYMMDD 的日期目录: {images_path}")
        return []

    valid_dates.sort(key=lambda x: x[0], reverse=True)
    return [{"name": name, "path": path} for _, name, path in valid_dates]


def get_last_date_path(count=3):
    folders = list_date_folders()
    return [f["name"] for f in folders[:count]]


def create_today_folder():
    """在待搜索目录 images_path 下创建当天日期目录（YYYYMMDD）。返回当天目录路径，失败返回 None。"""
    images_path = get_images_path()
    if not os.path.exists(images_path):
        os.makedirs(images_path, exist_ok=True)
    elif not os.path.isdir(images_path):
        return None

    today = datetime.now().strftime("%Y%m%d")
    today_path = os.path.join(images_path, today)
    if os.path.exists(today_path):
        if os.path.isdir(today_path):
            return today_path
        return None

    os.makedirs(today_path, exist_ok=False)
    return today_path


def move_image_to_today_folder(file_path, today_folder=None):
    """将文件剪切到待搜索目录的当天日期目录。成功返回目标路径，失败返回 None。"""
    if not file_path:
        return None

    src = os.path.realpath(file_path)
    if not os.path.isfile(src):
        return None

    today_folder = today_folder or create_today_folder()
    if not today_folder:
        return None

    base = os.path.basename(src)
    stem, suffix = os.path.splitext(base)
    target = os.path.join(today_folder, base)
    if os.path.exists(target):
        counter = 1
        while os.path.exists(target):
            target = os.path.join(today_folder, f"{stem}_{counter}{suffix}")
            counter += 1

    moved_path = shutil.move(src, target)
    return os.path.realpath(moved_path)


def move_images_to_today_folder(file_paths):
    """串行将多张图片剪切到待搜索目录的当天日期目录。"""
    if not file_paths:
        return {"success": False, "folder_path": None, "uploaded": [], "failed": []}

    today_folder = create_today_folder()
    if not today_folder:
        return {"success": False, "folder_path": None, "uploaded": [], "failed": list(file_paths)}

    uploaded = []
    failed = []
    total = len(file_paths)
    next_progress = 10

    for index, file_path in enumerate(file_paths, start=1):
        moved_path = move_image_to_today_folder(file_path, today_folder=today_folder)
        if moved_path:
            uploaded.append(moved_path)
        else:
            failed.append(file_path)

        progress = (index * 100) // total
        while progress >= next_progress:
            print(f"[进度] {next_progress}% ({index}/{total})", file=sys.stderr)
            next_progress += 10

    if next_progress == 10:
        print("[进度] 100% (0/0)", file=sys.stderr)

    return {
        "success": not failed,
        "folder_path": os.path.realpath(today_folder),
        "uploaded": uploaded,
        "failed": failed,
    }

def list_images_recursive(folder_path):
    """使用 os.path 递归列出所有图片文件"""
    image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.tif', '.ico', '.svg'}
    folder = os.path.realpath(os.path.abspath(folder_path))
    image_files = []

    try:
        for root, _dirs, files in os.walk(folder):
            for fname in files:
                full = os.path.join(root, fname)
                try:
                    ext = os.path.splitext(fname)[1].lower()
                    if ext in image_extensions and os.path.isfile(full):
                        image_files.append(os.path.abspath(full))
                except PermissionError:
                    print(f"[警告] 无权限访问：{full}")
                    continue
    except PermissionError:
        print(f"[错误] 无权限访问根目录：{folder}")

    return image_files

def detect_faces(image_path, soph_api_key):
    """调用API检测人脸"""
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"图片文件不存在: {image_path}")
    
    ost_img = cv2.imread(image_path)
    if ost_img is None:
        raise ValueError(f"无法读取图片文件: {image_path}")
    
    # 确定图片的 MIME 类型
    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')
    
    try:
        with open(image_path, 'rb') as f:
            files = {'file': (os.path.basename(image_path), f, mime_type)}
            headers = {"Authorization": f"Bearer {soph_api_key}"}
            # 设置较短的超时时间，避免Node.js环境中的超时溢出
            response = requests.post(FACE_API_URL, files=files, headers=headers, timeout=10)
    except requests.exceptions.Timeout:
        raise RuntimeError(f"API请求超时: 图片 {image_path} 处理时间过长")
    except requests.exceptions.RequestException as e:
        raise RuntimeError(f"API请求异常: {str(e)}")
    
    if response.status_code != 200:
        raise RuntimeError(f"API请求失败: {response.status_code} - {response.text[:200]}")
    
    return response.json().get('result', {})

def get_largest_face(faces):
    """从检测到的人脸中找出尺寸最大的人脸"""
    if not faces:
        return None
    
    largest_face = None
    largest_area = 0
    
    for face in faces:
        box = face.get('box', [])
        if len(box) >= 4:
            x1, y1, x2, y2 = box[0], box[1], box[2], box[3]
            area = (x2 - x1) * (y2 - y1)
            if area > largest_area:
                largest_area = area
                largest_face = face
    
    return largest_face


def get_baseface_embedding(image_path, det_thr=0.7):
    """获取查询图片的最大人脸embedding"""
    try:
        result = detect_faces(image_path, sophnet_tools.get_api_key())
        faces_count = result.get("faces_count", 0)
        faces = result.get("output", [])
        faces = [face for face in faces if face.get('det_score', 0) >= det_thr]
        faces_count = len(faces)
        
        if faces_count > 0:
            largest_face = get_largest_face(faces)
            return largest_face.get('embedding', [])
        else:
            return None
    
    except Exception as e:
        print(f"错误: {e}", file=sys.stderr)
        return None
    
def get_config(config_path=None):
    """从 .image-classify/config.json 读取用户与搜索结果。"""
    path = resolve_config_path(config_path)
    try:
        with open(path, "r", encoding="utf-8") as f:
            return _sanitize_config_data(json.load(f))
    except FileNotFoundError:
        return {"users": []}


def _user_friend_id(user):
    """用户条目中的好友 ID（虾友号）。新字段 `friendId`，兼容旧字段 `xia_you_hao`。"""
    if not isinstance(user, dict):
        return None
    v = user.get("friendId")
    if v is not None:
        return v
    return user.get("xia_you_hao")


def _user_friend_label(user):
    """DM 展示名。新字段 `friendLabel`，兼容旧字段 `xia_you_label`。"""
    if not isinstance(user, dict):
        return None
    v = user.get("friendLabel")
    if v is not None:
        return v
    return user.get("xia_you_label")
    
def name_in_config(name, config_path=None):
    """检查名字是否在配置文件的用户列表中"""
    basic_config = get_config(config_path)
    user_list = basic_config.get("users", [])
    for user in user_list:
        if user.get("name") == name:
            return True
    return False

def replace_name_config(ost_name, new_name, config_path=None):
    """替换配置文件中用户列表中的名字"""
    basic_config = get_config(config_path)
    user_list = basic_config.get("users", [])
    for user in user_list:
        if user.get("name") == ost_name:
            user["name"] = new_name
            break
    basic_config["users"] = user_list
    save_config(config_path, basic_config)


def delete_user_config(ost_name, config_path=None):
    """删除配置文件中用户列表中的用户信息"""
    basic_config = get_config(config_path)
    user_list = basic_config.get("users", [])
    for user in user_list:
        if user.get("name") == ost_name:
            user_list.remove(user)
            break
    basic_config["users"] = user_list
    save_config(config_path, basic_config)

def add_user_config(
    name,
    image_path,
    config_path=None,
    friend_id=None,
    friend_label=None,
):
    """添加用户到 config。friendId 为可选好友 userId（正整数）；friendLabel 为可选展示名。"""
    basic_config = get_config(config_path)
    user_list = basic_config.get("users", [])
    stored_path = store_registration_image(image_path)
    face_embedding = get_baseface_embedding(stored_path, det_thr=QUERY_THRESHOLD)
    if face_embedding is None:
        return False, "未检测到有效的人脸信息"

    abs_image_path = os.path.realpath(stored_path)
    entry = {"name": name, "info": [{"file_path": abs_image_path, "embedding": face_embedding}]}
    if friend_id is not None:
        entry["friendId"] = int(friend_id)
    if friend_label is not None and str(friend_label).strip():
        entry["friendLabel"] = str(friend_label).strip()
    user_list.append(entry)
    basic_config["users"] = user_list
    save_config(config_path, basic_config)

    return True, "添加成功"


def replace_user_embeding_config(name, new_image_path, config_path=None):
    """根据用户名替换其所有照片和embedding信息，并删除原始照片"""
    basic_config = get_config(config_path)
    user_list = basic_config.get("users", [])
    stored_path = store_registration_image(new_image_path)
    face_embedding = get_baseface_embedding(stored_path, det_thr=QUERY_THRESHOLD)
    if face_embedding is None:
        return False, "未检测到有效的人脸信息"

    abs_new_path = os.path.realpath(stored_path)
    for user in user_list:
        if user.get("name") == name:
            for info in user.get("info", []):
                old_path = info.get("file_path", "")
                if old_path and old_path != abs_new_path and os.path.exists(old_path):
                    os.remove(old_path)
            user["info"] = [{"file_path": abs_new_path, "embedding": face_embedding}]
            break
    basic_config["users"] = user_list
    save_config(config_path, basic_config)

    return True, "替换成功"

def append_user_embeding_config(name, image_path, config_path=None):
    """新增用户embedding信息"""
    basic_config = get_config(config_path)
    user_list = basic_config.get("users", [])
    stored_path = store_registration_image(image_path)
    face_embedding = get_baseface_embedding(stored_path, det_thr=QUERY_THRESHOLD)
    if face_embedding is None:
        return False, "未检测到有效的人脸信息"

    for user in user_list:
        if user.get("name") == name:
            abs_image_path = os.path.realpath(stored_path)
            user.get("info", []).append({"file_path": abs_image_path, "embedding": face_embedding})
            break
    basic_config["users"] = user_list
    save_config(config_path, basic_config)

    return True, "添加成功"

def save_embeddings(image_path, faces, json_path):
    """保存多个人脸embedding到json文件"""
    data = {
        "image_path": str(image_path),
        "embeddings": [face.get('embedding', []) for face in faces],
        "boxes": [face.get('box', []) for face in faces],
        "det_scores": [face.get('det_score', 0) for face in faces]
    }
    
    with open(json_path, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    
    return json_path

def embeddings_from_images(image_list, det_thr=0.5):
    """获取图片列表的所有人脸embedding信息，并保存到对应的json文件中"""
    json_list = []
    
    for image_path in image_list:
        try:
            abs_img = os.path.abspath(image_path)
            dir_name = os.path.dirname(abs_img)
            stem = os.path.splitext(os.path.basename(abs_img))[0]

            save_path = os.path.join(dir_name, ".embedding")
            os.makedirs(save_path, exist_ok=True)
            json_path = os.path.join(save_path, f"{stem}.json")
            
            if os.path.exists(json_path):
                print(f"跳过已存在的embedding文件: {json_path}")
                json_list.append(str(json_path))
                continue
            
            print(f"开始处理图片: {image_path}")
            result = detect_faces(image_path, sophnet_tools.get_api_key())
            faces = result.get("output", [])
            faces = [face for face in faces if face.get('det_score', 0) >= det_thr]
            
            save_embeddings(image_path, faces, json_path)
            json_list.append(str(json_path))
        
        except Exception as e:
            print(f"处理 {image_path} 时出错: {e}", file=sys.stderr)
    
    return json_list

def embeddings_from_imagepath(image_path, det_thr=0.5):
    image_list = list_images_recursive(image_path)
    return embeddings_from_images(image_list, det_thr)

def get_searchface_embeddings_from_json(json_paths):
    """从json文件列表加载搜索人脸embeddings"""
    all_embeddings = {}
    for json_path in json_paths:
        with open(json_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
            embeddings = data.get("embeddings", [])
            image_name = data.get("image_path", "")
            all_embeddings[image_name] = np.array(embeddings).astype(np.float32)
    return all_embeddings

def cosine_similarity(vec1, vec2):
    """计算余弦相似度"""
    dot_product = np.dot(vec1, vec2)
    norm1 = np.linalg.norm(vec1)
    norm2 = np.linalg.norm(vec2)
    if norm1 == 0 or norm2 == 0:
        return 0
    return dot_product / (norm1 * norm2)

def search_similar_faces(name, search_path, config_path=None):
    """搜索相似人脸"""
    basic_config = get_config(config_path)
    user_list = basic_config.get("users", [])
    
    embedding_list = []
    for user in user_list:
        if user.get("name") == name:
            for info in user.get("info", []):
                embedding = info.get("embedding")
                if embedding:
                    embedding_list.append(embedding)

    json_list = embeddings_from_imagepath(search_path, det_thr=QUERY_THRESHOLD)
    search_embeddings = get_searchface_embeddings_from_json(json_list)

    
    results = []
    for face_embedding in embedding_list:
        for image_name, embeddings in search_embeddings.items():
            for idx, embedding in enumerate(embeddings):
                similarity = cosine_similarity(face_embedding, embedding)
                if similarity >= SEARCH_SIMILARITY_THRESHOLD:
                    existing = None
                    for result in results:
                        if result.get("image_path") == image_name:
                            existing = result
                            break
                    if existing is None:
                        results.append({
                            "image_path": image_name,
                            "face_index": idx,
                            "similarity": float(similarity)
                        })
                    elif existing["similarity"] < float(similarity):
                        existing["face_index"] = idx
                        existing["similarity"] = float(similarity)
    results.sort(key=lambda x: x["similarity"], reverse=True)
    for user in user_list:
        if user.get("name") == name:
            user["search_result"] = results
    save_config(config_path, basic_config)
    return results


def quick_search(image_path, search_path, config_path=None):
    """直接用一张照片搜索相似人脸，结果缓存到 config.json 的 quick_search_result 字段，
    供后续 copy/pack 命令使用。
    """
    basic_config = get_config(config_path)

    face_embedding = get_baseface_embedding(image_path, det_thr=QUERY_THRESHOLD)
    if face_embedding is None:
        return None, "照片中未发现有效的人脸信息"

    json_list = embeddings_from_imagepath(search_path, det_thr=QUERY_THRESHOLD)
    search_embeddings = get_searchface_embeddings_from_json(json_list)

    results = []
    for img_name, embeddings in search_embeddings.items():
        for idx, embedding in enumerate(embeddings):
            similarity = cosine_similarity(face_embedding, embedding)
            if similarity >= SEARCH_SIMILARITY_THRESHOLD:
                existing = None
                for result in results:
                    if result.get("image_path") == img_name:
                        existing = result
                        break
                if existing is None:
                    results.append({
                        "image_path": img_name,
                        "face_index": idx,
                        "similarity": float(similarity)
                    })
                elif existing["similarity"] < float(similarity):
                    existing["face_index"] = idx
                    existing["similarity"] = float(similarity)

    results.sort(key=lambda x: x["similarity"], reverse=True)

    basic_config["quick_search_result"] = results
    save_config(config_path, basic_config)

    return results, "搜索完成"


def classify_all_users(search_path, config_path=None):
    """一键分类：遍历所有注册用户，搜索所有图片并返回每个用户的匹配结果"""
    basic_config = get_config(config_path)
    user_list = basic_config.get("users", [])

    if not user_list:
        return {}

    json_list = embeddings_from_imagepath(search_path, det_thr=QUERY_THRESHOLD)
    search_embeddings = get_searchface_embeddings_from_json(json_list)

    all_results = {}
    for user in user_list:
        name = user.get("name", "")
        embedding_list = []
        for info in user.get("info", []):
            embedding = info.get("embedding")
            if embedding:
                embedding_list.append(embedding)

        if not embedding_list:
            continue

        results = []
        for face_embedding in embedding_list:
            for image_name, embeddings in search_embeddings.items():
                for idx, embedding in enumerate(embeddings):
                    similarity = cosine_similarity(face_embedding, embedding)
                    if similarity >= SEARCH_SIMILARITY_THRESHOLD:
                        existing = None
                        for result in results:
                            if result.get("image_path") == image_name:
                                existing = result
                                break
                        if existing is None:
                            results.append({
                                "image_path": image_name,
                                "face_index": idx,
                                "similarity": float(similarity)
                            })
                        elif existing["similarity"] < float(similarity):
                            existing["face_index"] = idx
                            existing["similarity"] = float(similarity)

        results.sort(key=lambda x: x["similarity"], reverse=True)
        user["search_result"] = results
        all_results[name] = results

    save_config(config_path, basic_config)

    return all_results


def _format_classify_dm_message(name: str, items: list, max_lines: int = 30, max_chars: int = 3500) -> str:
    """将单用户分类结果格式化为私信正文（控制长度）。"""
    n = len(items)
    lines = [f"【照片分类结果】👤 {name}", f"匹配共 {n} 张。"]
    if n == 0:
        lines.append("本轮目录中暂无匹配照片。")
        return "\n".join(lines)
    for i, item in enumerate(items[:max_lines]):
        path = item.get("image_path", "")
        sim = item.get("similarity", 0)
        try:
            sim_s = f"{float(sim):.4f}"
        except (TypeError, ValueError):
            sim_s = str(sim)
        lines.append(f"- {path} · 相似度 {sim_s}")
    if n > max_lines:
        lines.append(f"… 另有 {n - max_lines} 张未列出，请在本机打包下载查看完整列表。")
    text = "\n".join(lines)
    if len(text) > max_chars:
        text = text[: max_chars - 20] + "\n…（正文过长已截断）"
    return text


def _format_pack_link_dm_message(name: str, url: str) -> str:
    """pack 成功后发给 friendId 的私信正文（含下载链接）。"""
    return (
        f"✨「{name}」的照片已为您送达\n\n"
        f"{url}\n\n"
        f"请在 24 小时内下载保存；逾期链接可能失效，请及时转存。"
    )


def _find_user_by_name(config_path: str, name: str):
    if not name:
        return None
    for u in get_config(config_path).get("users", []):
        if u.get("name") == name:
            return u
    return None


def _send_classify_dm(friend_id: int, message: str, timeout: int = 90) -> tuple:
    """调用同目录 send_dm_message.py 发送私信（--user-id 即 friendId）。返回 (success: bool, detail: str)。"""
    script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "send_dm_message.py")
    if not os.path.isfile(script):
        return False, f"未找到 DM 脚本: {script}"
    try:
        proc = subprocess.run(
            [sys.executable, script, "--user-id", str(int(friend_id)), "-m", message],
            capture_output=True,
            text=True,
            timeout=timeout,
        )
        if proc.returncode == 0:
            return True, (proc.stdout or "").strip() or "ok"
        err = (proc.stderr or proc.stdout or "").strip() or f"exit {proc.returncode}"
        return False, err[:2000]
    except subprocess.TimeoutExpired:
        return False, "发送私信超时"
    except Exception as e:
        return False, str(e)


def notify_classify_dm(config_path: str, all_results: dict) -> dict:
    """对配置了 friendId 的用户推送 DM。返回 {用户名: {success, friendId, friendLabel?, detail?}}。"""
    basic_config = get_config(config_path)
    out = {}
    for user in basic_config.get("users", []):
        xid = _user_friend_id(user)
        name = user.get("name") or ""
        if xid is None:
            continue
        try:
            xid_int = int(xid)
        except (TypeError, ValueError):
            bad = {"success": False, "detail": f"无效的 friendId: {xid!r}"}
            lab_bad = _user_friend_label(user)
            if isinstance(lab_bad, str) and lab_bad.strip():
                bad["friendLabel"] = lab_bad.strip()
            out[name if name else f"invalid_friend_id:{xid}"] = bad
            continue
        if xid_int <= 0:
            fail_e = {"success": False, "detail": "friendId 须为正整数", "friendId": xid_int}
            lab0 = _user_friend_label(user)
            if isinstance(lab0, str) and lab0.strip():
                fail_e["friendLabel"] = lab0.strip()
            out[name] = fail_e
            continue
        lab = _user_friend_label(user)
        if isinstance(lab, str) and lab.strip():
            label_s = lab.strip()
        else:
            label_s = None
        items = all_results.get(name, [])
        msg = _format_classify_dm_message(name, items)
        ok, detail = _send_classify_dm(xid_int, msg)
        entry = {"success": ok, "friendId": xid_int}
        if label_s:
            entry["friendLabel"] = label_s
        if not ok:
            entry["detail"] = detail
        out[name] = entry
    return out


def notify_pack_link_dm(config_path: str, name_to_url: dict) -> dict:
    """pack 成功后，向配置了 friendId 的用户私信下载链接。name_to_url: {本地用户名: url}。"""
    out = {}
    for name, url in (name_to_url or {}).items():
        if not name or name == "quick_search":
            continue
        if not url:
            continue
        user = _find_user_by_name(config_path, name)
        if not user:
            continue
        xid = _user_friend_id(user)
        if xid is None:
            continue
        try:
            xid_int = int(xid)
        except (TypeError, ValueError):
            bad = {"success": False, "detail": f"无效的 friendId: {xid!r}", "url": url}
            lab_bad = _user_friend_label(user)
            if isinstance(lab_bad, str) and lab_bad.strip():
                bad["friendLabel"] = lab_bad.strip()
            out[name] = bad
            continue
        if xid_int <= 0:
            fail_e = {"success": False, "detail": "friendId 须为正整数", "friendId": xid_int, "url": url}
            lab0 = _user_friend_label(user)
            if isinstance(lab0, str) and lab0.strip():
                fail_e["friendLabel"] = lab0.strip()
            out[name] = fail_e
            continue
        lab = _user_friend_label(user)
        label_s = lab.strip() if isinstance(lab, str) and lab.strip() else None
        msg = _format_pack_link_dm_message(name, url)
        ok, detail = _send_classify_dm(xid_int, msg)
        entry = {"success": ok, "friendId": xid_int, "url": url}
        if label_s:
            entry["friendLabel"] = label_s
        if not ok:
            entry["detail"] = detail
        out[name] = entry
    return out


def _fmt_dm_line_ok(disp: str) -> str:
    """dm_lines：私信发送成功（展示虾友昵称或 friendId）。"""
    return f"✅ 💌 已向虾友「{disp}」发送成功"


def _fmt_dm_line_fail(disp: str, detail: str) -> str:
    """dm_lines：私信发送失败。"""
    return f"❌ 💢 虾友「{disp}」发送失败：{detail}"


def _fmt_dm_line_no_friend(name: str) -> str:
    """dm_lines：未配置 friendId。"""
    return f"📭 「{name}」暂未绑定虾友，跳过私信"


def _fmt_pack_line_fail(name: str, err: str) -> str:
    """dm_lines：pack 打包失败。"""
    return f"📦❌ 「{name}」下载包生成失败：{err}"


def build_pack_dm_status_lines(
    config_path: str,
    results_dict: dict,
    user_urls: dict,
    errors: dict | None,
    pack_dm_out: dict,
) -> list:
    """pack 后的虾友发送状态短句（与 classify 的 dm_lines 同一套 emoji 模板）。"""
    lines = []
    errors = errors or {}
    pack_dm_out = pack_dm_out or {}
    for name in results_dict.keys():
        if name == "quick_search":
            continue
        if name in errors and name not in user_urls:
            lines.append(_fmt_pack_line_fail(name, str(errors[name])))
            continue
        if name not in user_urls:
            continue
        u = _find_user_by_name(config_path, name) or {}
        xid = _user_friend_id(u)
        if xid is None:
            lines.append(_fmt_dm_line_no_friend(name))
            continue

        def _disp(uu) -> str:
            lab = _user_friend_label(uu)
            if isinstance(lab, str) and lab.strip():
                return lab.strip()
            try:
                return str(int(_user_friend_id(uu)))
            except (TypeError, ValueError):
                return str(_user_friend_id(uu))

        try:
            xid_int = int(xid)
        except (TypeError, ValueError):
            disp = _disp(u)
            info = pack_dm_out.get(name)
            if info and not info.get("success"):
                det = (info.get("detail") or "未知错误").strip()
                lines.append(_fmt_dm_line_fail(disp, det))
            else:
                lines.append(_fmt_dm_line_fail(disp, "friendId 无效"))
            continue

        if xid_int <= 0:
            disp = _disp(u)
            info = pack_dm_out.get(name)
            if info and not info.get("success"):
                det = (info.get("detail") or "未知错误").strip()
                lines.append(_fmt_dm_line_fail(disp, det))
            else:
                lines.append(_fmt_dm_line_fail(disp, "friendId 须为正整数"))
            continue

        disp = _disp(u)
        info = pack_dm_out.get(name)
        if info and info.get("success"):
            lines.append(_fmt_dm_line_ok(disp))
        elif info:
            det = (info.get("detail") or "未知错误").strip()
            lines.append(_fmt_dm_line_fail(disp, det))
        else:
            lines.append(_fmt_dm_line_fail(disp, "未返回状态"))
    return lines


def build_dm_status_lines(config_path: str, all_results: dict, dm_out: dict) -> list:
    """按本轮参与分类的用户顺序，生成最终提示用短句（含 emoji，见 _fmt_dm_line_*）。"""
    basic_config = get_config(config_path)
    name_to_user = {u.get("name"): u for u in basic_config.get("users", []) if u.get("name")}
    lines = []
    for name in all_results.keys():
        u = name_to_user.get(name, {})
        xid = _user_friend_id(u)
        if xid is None:
            lines.append(_fmt_dm_line_no_friend(name))
            continue

        def _disp_for_xid(raw, fallback_int=None) -> str:
            lab = _user_friend_label(u)
            if isinstance(lab, str) and lab.strip():
                return lab.strip()
            if fallback_int is not None:
                return str(fallback_int)
            return str(raw)

        try:
            xid_int = int(xid)
        except (TypeError, ValueError):
            disp = _disp_for_xid(xid, None)
            info = dm_out.get(name) if dm_out else None
            if info and not info.get("success"):
                det = (info.get("detail") or "未知错误").strip()
                lines.append(_fmt_dm_line_fail(disp, det))
            else:
                lines.append(_fmt_dm_line_fail(disp, "friendId 无效"))
            continue

        if xid_int <= 0:
            disp = _disp_for_xid(xid, xid_int)
            info = dm_out.get(name) if dm_out else None
            if info and not info.get("success"):
                det = (info.get("detail") or "未知错误").strip()
                lines.append(_fmt_dm_line_fail(disp, det))
            else:
                lines.append(_fmt_dm_line_fail(disp, "friendId 须为正整数"))
            continue

        disp = _disp_for_xid(xid, xid_int)
        info = dm_out.get(name) if dm_out else None
        if info and info.get("success"):
            lines.append(_fmt_dm_line_ok(disp))
        elif info:
            det = (info.get("detail") or "未知错误").strip()
            lines.append(_fmt_dm_line_fail(disp, det))
        else:
            lines.append(_fmt_dm_line_fail(disp, "未返回状态"))
    return lines


def copy_results_to_folder(results, target_folder, user_name=None):
    """将搜索结果中的照片复制到指定文件夹。
    如果指定了 user_name，会在 target_folder 下创建以用户名命名的子目录。
    results 可以是列表（单用户）或字典（多用户一键分类结果）。
    返回 (成功数, 失败数, 目标目录路径)。
    """
    if isinstance(results, dict):
        total_ok, total_fail = 0, 0
        base = os.path.realpath(target_folder)
        os.makedirs(base, exist_ok=True)
        for name, items in results.items():
            ok, fail, _ = copy_results_to_folder(items, os.path.join(base, name))
            total_ok += ok
            total_fail += fail
        return total_ok, total_fail, base

    dest = os.path.realpath(target_folder)
    if user_name:
        dest = os.path.join(dest, user_name)
    os.makedirs(dest, exist_ok=True)

    ok_count, fail_count = 0, 0
    for item in results:
        src = item.get("image_path", "")
        if not src or not os.path.exists(src):
            fail_count += 1
            continue
        base_name = os.path.basename(src)
        dst = os.path.join(dest, base_name)
        if os.path.exists(dst):
            stem, ext = os.path.splitext(base_name)
            counter = 1
            while os.path.exists(dst):
                dst = os.path.join(dest, f"{stem}_{counter}{ext}")
                counter += 1
        try:
            shutil.copy2(src, dst)
            ok_count += 1
        except Exception as e:
            print(f"复制失败 {src}: {e}", file=sys.stderr)
            fail_count += 1

    return ok_count, fail_count, str(dest)


def pack_results_to_url(results, archive_name="search_results", upload_timeout=120):
    """将搜索结果中的照片压缩为 zip 并上传获取下载链接。
    results 可以是列表（单用户）或字典（多用户一键分类结果）。
    upload_timeout 用于控制上传超时时间（秒），压缩文件较大时应适当增大。
    返回 (下载链接, zip文件路径) 或 (None, 错误信息)。
    """
    import tempfile
    tmp_dir = tempfile.mkdtemp()
    zip_path = os.path.join(tmp_dir, f"{archive_name}.zip")

    try:
        with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
            file_count = 0
            if isinstance(results, dict):
                for name, items in results.items():
                    for item in items:
                        src = item.get("image_path", "")
                        if not src or not os.path.exists(src):
                            continue
                        arcname = f"{name}/{os.path.basename(src)}"
                        zf.write(src, arcname)
                        file_count += 1
            else:
                for item in results:
                    src = item.get("image_path", "")
                    if not src or not os.path.exists(src):
                        continue
                    zf.write(src, os.path.basename(src))
                    file_count += 1

            if file_count == 0:
                return None, "没有可打包的文件"

        url = convert_to_url(zip_path, timeout=upload_timeout)
        return url, zip_path

    except Exception as e:
        return None, str(e)


def _json_output(data):
    """统一 JSON 输出到 stdout"""
    print(json.dumps(data, ensure_ascii=False, indent=2))


def main():
    parser = argparse.ArgumentParser(description="📸 照片分类器 CLI")
    parser.add_argument(
        "-c",
        "--config",
        default=None,
        help="配置文件路径 (默认: 与 images 同级的 .image-classify/config.json)",
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    # check — 检查用户名是否存在
    sp = subparsers.add_parser("check", help="检查用户名是否已注册")
    sp.add_argument("name", help="用户名")

    # add — 注册新用户
    sp = subparsers.add_parser("add", help="注册新用户")
    sp.add_argument("name", help="用户名")
    sp.add_argument("image", help="照片路径")
    sp.add_argument(
        "--friend-id",
        type=int,
        default=None,
        dest="friend_id",
        metavar="ID",
        help="可选：好友 userId（friendId）。设置后，一键 classify 会将该用户分类结果自动 DM 到此账号",
    )
    sp.add_argument(
        "--friend-label",
        default=None,
        dest="friend_label",
        metavar="NAME",
        help="可选：DM 展示名（与自然语言「虾友：xxx」对应，用于最终提示括号内文案）",
    )

    # replace — 替换用户照片
    sp = subparsers.add_parser("replace", help="替换用户照片（删除旧照片）")
    sp.add_argument("name", help="用户名")
    sp.add_argument("image", help="新照片路径")

    # append — 追加照片
    sp = subparsers.add_parser("append", help="为已有用户追加一张照片")
    sp.add_argument("name", help="用户名")
    sp.add_argument("image", help="照片路径")

    # rename — 修改用户名
    sp = subparsers.add_parser("rename", help="修改用户名")
    sp.add_argument("old_name", help="原用户名")
    sp.add_argument("new_name", help="新用户名")

    # delete — 删除用户
    sp = subparsers.add_parser("delete", help="删除用户")
    sp.add_argument("name", help="用户名")

    # search — 搜索某用户的相似人脸
    sp = subparsers.add_parser("search", help="在指定目录搜索某用户的相似人脸")
    sp.add_argument("name", help="用户名")
    sp.add_argument("search_path", help="搜索目录路径")

    # quick-search — 直接用照片搜索，不写入 config
    sp = subparsers.add_parser("quick-search", help="直接用一张照片搜索相似人脸（不注册）")
    sp.add_argument("image", help="查询照片路径")
    sp.add_argument("search_path", help="搜索目录路径")

    # classify — 一键分类
    sp = subparsers.add_parser("classify", help="遍历所有注册用户，搜索所有照片")
    sp.add_argument("search_path", help="搜索目录路径")

    # copy — 将上次搜索结果复制到文件夹
    sp = subparsers.add_parser("copy", help="将搜索结果中的照片复制到指定文件夹")
    sp.add_argument("target_folder", help="目标文件夹路径")
    sp.add_argument("--name", default=None, help="指定用户名（仅复制该用户的结果）；不指定则复制所有用户")

    # pack — 打包搜索结果并上传
    sp = subparsers.add_parser("pack", help="将搜索结果压缩为 zip 并上传获取下载链接")
    sp.add_argument("--name", default=None, help="指定用户名；不指定则打包所有用户")
    sp.add_argument("--archive-name", default="search_results", help="压缩包名称 (默认: search_results)")
    sp.add_argument("--timeout", type=int, default=120, help="上传超时秒数 (默认: 120)")

    # upload — 上传文件获取 URL
    sp = subparsers.add_parser("upload", help="上传本地文件获取 URL")
    sp.add_argument("file_path", help="文件路径")
    sp.add_argument("--timeout", type=int, default=10, help="上传超时秒数 (默认: 10)")

    # init-today-folder — 在待搜索目录下按当前日期创建子目录
    subparsers.add_parser("init-today-folder", help="在待搜索目录 images 下创建当天日期文件夹(YYYYMMDD)")

    # list-date-folders — 列出待搜索目录下日期子目录（供未指定搜索目录时选择）
    subparsers.add_parser(
        "list-date-folders",
        help="列出 images 下 YYYYMMDD 日期文件夹（从新到旧），JSON 含 folders 与 preview_n",
    )

    # upload-image — 将图片剪切到待搜索目录的当天日期目录
    sp = subparsers.add_parser("upload-image", help="将图片串行剪切到待搜索目录 images/当天日期目录")
    sp.add_argument("file_paths", nargs="+", help="一个或多个图片文件路径")

    args = parser.parse_args()
    cfg = resolve_config_path(args.config)

    if args.command == "check":
        exists = name_in_config(args.name, cfg)
        _json_output({"exists": exists, "name": args.name})

    elif args.command == "add":
        friend_id = getattr(args, "friend_id", None)
        raw_label = getattr(args, "friend_label", None)
        friend_label = str(raw_label).strip() if raw_label is not None and str(raw_label).strip() else None
        if friend_id is not None and friend_id <= 0:
            _json_output(
                {
                    "success": False,
                    "message": "friendId 须为正整数",
                    "name": args.name,
                }
            )
        else:
            ok, msg = add_user_config(
                args.name,
                args.image,
                cfg,
                friend_id=friend_id,
                friend_label=friend_label,
            )
            result = {"success": ok, "message": msg, "name": args.name}
            if friend_id is not None:
                result["friendId"] = friend_id
            if friend_label:
                result["friendLabel"] = friend_label
            if ok:
                try:
                    result["image_url"] = convert_to_url(args.image, timeout=30)
                except Exception:
                    result["image_url"] = None
            _json_output(result)

    elif args.command == "replace":
        ok, msg = replace_user_embeding_config(args.name, args.image, cfg)
        result = {"success": ok, "message": msg, "name": args.name}
        if ok:
            try:
                result["image_url"] = convert_to_url(args.image, timeout=30)
            except Exception:
                result["image_url"] = None
        _json_output(result)

    elif args.command == "append":
        ok, msg = append_user_embeding_config(args.name, args.image, cfg)
        result = {"success": ok, "message": msg, "name": args.name}
        if ok:
            try:
                result["image_url"] = convert_to_url(args.image, timeout=30)
            except Exception:
                result["image_url"] = None
        _json_output(result)

    elif args.command == "rename":
        replace_name_config(args.old_name, args.new_name, cfg)
        _json_output({"success": True, "old_name": args.old_name, "new_name": args.new_name})

    elif args.command == "delete":
        delete_user_config(args.name, cfg)
        _json_output({"success": True, "name": args.name})

    elif args.command == "search":
        results = search_similar_faces(args.name, args.search_path, cfg)
        _json_output({"name": args.name, "count": len(results), "results": results})

    elif args.command == "quick-search":
        results, msg = quick_search(args.image, args.search_path, cfg)
        if results is None:
            _json_output({"success": False, "message": msg})
        else:
            _json_output({"success": True, "count": len(results), "results": results})

    elif args.command == "classify":
        all_results = classify_all_users(args.search_path, cfg)
        summary = {}
        for name, items in all_results.items():
            summary[name] = {"count": len(items), "results": items}
        payload = {"user_count": len(all_results), "users": summary}
        dm_status = notify_classify_dm(cfg, all_results)
        if dm_status:
            payload["dm"] = dm_status
        if all_results:
            payload["dm_lines"] = build_dm_status_lines(cfg, all_results, dm_status or {})
        _json_output(payload)

    elif args.command == "copy":
        results = _load_search_results(cfg, args.name)
        if results is None:
            _json_output({"success": False, "message": "未找到搜索结果，请先执行 search 或 classify"})
            return
        ok, fail, dest = copy_results_to_folder(results, args.target_folder)
        _json_output({"success": True, "copied": ok, "failed": fail, "target": dest})

    elif args.command == "pack":
        results = _load_search_results(cfg, args.name)
        if results is None:
            _json_output({"success": False, "message": "未找到搜索结果，请先执行 search 或 classify"})
            return
        if isinstance(results, dict) and not args.name:
            user_urls = {}
            errors = {}
            for user_name, user_results in results.items():
                url, info = pack_results_to_url(user_results, f"search_results_{user_name}", args.timeout)
                if url:
                    user_urls[user_name] = url
                else:
                    errors[user_name] = info
            pack_dm = notify_pack_link_dm(cfg, user_urls)
            dm_lines = build_pack_dm_status_lines(cfg, results, user_urls, errors, pack_dm)
            payload = {
                "success": bool(user_urls),
                "user_urls": user_urls,
                "errors": errors if errors else None,
            }
            if pack_dm:
                payload["dm"] = pack_dm
            if dm_lines:
                payload["dm_lines"] = dm_lines
            _json_output(payload)
        else:
            url, info = pack_results_to_url(results, args.archive_name, args.timeout)
            if url:
                payload = {"success": True, "url": url, "zip_path": info}
                if args.name:
                    pack_dm = notify_pack_link_dm(cfg, {args.name: url})
                    dm_lines = build_pack_dm_status_lines(
                        cfg, {args.name: results}, {args.name: url}, None, pack_dm
                    )
                    if pack_dm:
                        payload["dm"] = pack_dm
                    if dm_lines:
                        payload["dm_lines"] = dm_lines
                _json_output(payload)
            else:
                _json_output({"success": False, "message": info})

    elif args.command == "upload":
        url = convert_to_url(args.file_path, timeout=args.timeout)
        _json_output({"success": True, "url": url})

    elif args.command == "init-today-folder":
        _json_output({"folder_path": create_today_folder()})

    elif args.command == "list-date-folders":
        folders = list_date_folders()
        preview_n = 3
        _json_output({
            "images_base": get_images_path(),
            "image_classify_dir": get_image_classify_dir(),
            "config_path": get_config_path(),
            "folders": folders,
            "total": len(folders),
            "preview_n": preview_n,
            "preview": folders[:preview_n],
        })

    elif args.command == "upload-image":
        result = move_images_to_today_folder(args.file_paths)
        if len(args.file_paths) == 1:
            _json_output({
                "success": result["success"],
                "folder_path": result["folder_path"],
                "file_path": result["uploaded"][0] if result["uploaded"] else None,
                "failed": result["failed"],
            })
        else:
            _json_output(result)


def _load_search_results(config_path, name=None):
    """从 config.json 加载上次的搜索结果。
    name=None 时返回所有用户的结果（字典）或 quick-search 的结果（列表）。
    指定 name 时返回该用户的结果（列表）。
    """
    basic_config = get_config(config_path)
    user_list = basic_config.get("users", [])
    quick_results = basic_config.get("quick_search_result")

    if name:
        for user in user_list:
            if user.get("name") == name:
                sr = user.get("search_result")
                return sr if sr else None
        return None

    all_results = {}
    for user in user_list:
        sr = user.get("search_result")
        if sr:
            all_results[user.get("name", "")] = sr

    if all_results and quick_results:
        all_results["quick_search"] = quick_results
        return all_results
    if all_results:
        return all_results
    if quick_results:
        return quick_results
    return None


if __name__ == "__main__":
    main()
