#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations

import base64
import json
import random
import secrets
import threading
import time
import uuid
from io import BytesIO

import database
import email_service
from app_config import get_config
from app_logger import get_logger
from app_security import get_rate_limit_ip, require_ip_not_locked, validate_email, validate_password, validate_username
from flask import Blueprint, jsonify, request, session
from flask_login import login_required, login_user, logout_user
from routes.pages import render_app_spa_or_legacy
from services.accounts_service import load_user_accounts
from services.models import User
from services.passkeys import (
    encode_credential_id,
    get_expected_origins,
    get_rp_id,
    is_challenge_valid,
    make_authentication_options,
    normalize_device_name,
    verify_authentication,
)
from services.social_login import parse_providers, provider_label
from services.state import (
    check_ip_request_rate,
    check_email_rate_limit,
    check_login_ip_user_locked,
    check_login_rate_limits,
    check_login_captcha_required,
    clear_login_failures,
    get_login_failure_delay_seconds,
    record_failed_captcha,
    record_login_failure,
    record_login_username_attempt,
    safe_cleanup_expired_captcha,
    safe_delete_captcha,
    safe_set_captcha,
    safe_verify_and_consume_captcha,
    should_send_login_alert,
)

logger = get_logger("app")
config = get_config()

api_auth_bp = Blueprint("api_auth", __name__)

_CAPTCHA_FONT_PATHS = [
    "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
    "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
    "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf",
]
_CAPTCHA_FONT = None
_CAPTCHA_FONT_LOCK = threading.Lock()
_USER_PASSKEY_LOGIN_SESSION_KEY = "user_passkey_login_state"


def _get_json_payload() -> dict:
    data = request.get_json(silent=True)
    return data if isinstance(data, dict) else {}


def _load_captcha_font(image_font_module):
    global _CAPTCHA_FONT

    if _CAPTCHA_FONT is not None:
        return _CAPTCHA_FONT

    with _CAPTCHA_FONT_LOCK:
        if _CAPTCHA_FONT is not None:
            return _CAPTCHA_FONT

        for font_path in _CAPTCHA_FONT_PATHS:
            try:
                _CAPTCHA_FONT = image_font_module.truetype(font_path, 42)
                break
            except Exception:
                continue

        if _CAPTCHA_FONT is None:
            _CAPTCHA_FONT = image_font_module.load_default()

    return _CAPTCHA_FONT


def _generate_captcha_image_data_uri(code: str) -> str:
    from PIL import Image, ImageDraw, ImageFont

    width, height = 160, 60
    image = Image.new("RGB", (width, height), color=(255, 255, 255))
    draw = ImageDraw.Draw(image)

    for _ in range(6):
        x1 = random.randint(0, width)
        y1 = random.randint(0, height)
        x2 = random.randint(0, width)
        y2 = random.randint(0, height)
        draw.line(
            [(x1, y1), (x2, y2)],
            fill=(random.randint(0, 200), random.randint(0, 200), random.randint(0, 200)),
            width=1,
        )

    for _ in range(80):
        x = random.randint(0, width)
        y = random.randint(0, height)
        draw.point((x, y), fill=(random.randint(0, 200), random.randint(0, 200), random.randint(0, 200)))

    font = _load_captcha_font(ImageFont)
    for i, char in enumerate(code):
        x = 12 + i * 35 + random.randint(-3, 3)
        y = random.randint(5, 12)
        color = (random.randint(0, 150), random.randint(0, 150), random.randint(0, 150))
        draw.text((x, y), char, font=font, fill=color)

    buffer = BytesIO()
    image.save(buffer, format="PNG")
    img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
    return f"data:image/png;base64,{img_base64}"


def _with_vip_suffix(message: str, auto_approve_enabled: bool, auto_approve_vip_days: int) -> str:
    if auto_approve_enabled and auto_approve_vip_days > 0:
        return f"{message}，赠送{auto_approve_vip_days}天VIP"
    return message


def _verify_common_captcha(client_ip: str, captcha_session: str, captcha_code: str):
    success, message = safe_verify_and_consume_captcha(captcha_session, captcha_code)
    if success:
        return True, None

    is_locked = record_failed_captcha(client_ip)
    if is_locked:
        return False, (jsonify({"error": "验证码错误次数过多,IP已被锁定1小时"}), 429)
    return False, (jsonify({"error": message}), 400)


def _verify_login_captcha_if_needed(
    *,
    captcha_required: bool,
    captcha_session: str,
    captcha_code: str,
    client_ip: str,
    username_key: str,
):
    if not captcha_required:
        return True, None

    if not captcha_session or not captcha_code:
        return False, (jsonify({"error": "请填写验证码", "need_captcha": True}), 400)

    success, message = safe_verify_and_consume_captcha(captcha_session, captcha_code)
    if success:
        return True, None

    record_login_failure(client_ip, username_key)
    return False, (jsonify({"error": message, "need_captcha": True}), 400)


def _send_password_reset_email_if_possible(email: str, username: str, user_id: int) -> None:
    result = email_service.send_password_reset_email(email=email, username=username, user_id=user_id)
    if not result["success"]:
        logger.error(f"密码重置邮件发送失败: {result['error']}")


def _send_login_security_alert_if_needed(user: dict, username: str, client_ip: str) -> None:
    try:
        user_agent = request.headers.get("User-Agent", "")
        context = database.record_login_context(user["id"], client_ip, user_agent)
        if not context or (not context.get("new_ip") and not context.get("new_device")):
            return

        if not config.LOGIN_ALERT_ENABLED:
            return
        if not should_send_login_alert(user["id"], client_ip):
            return
        if not email_service.get_email_settings().get("login_alert_enabled", True):
            return

        user_info = database.get_user_by_id(user["id"]) or {}
        if (not user_info.get("email")) or (not user_info.get("email_verified")):
            return
        if not database.get_user_email_notify(user["id"]):
            return

        email_service.send_security_alert_email(
            email=user_info.get("email"),
            username=user_info.get("username") or username,
            ip_address=client_ip,
            user_agent=user_agent,
            new_ip=context.get("new_ip", False),
            new_device=context.get("new_device", False),
            user_id=user["id"],
        )
    except Exception as e:
        logger.warning(f"发送登录安全提醒失败: user_id={user.get('id')}, error={e}")


def _parse_credential_payload(data: dict) -> dict | None:
    credential = data.get("credential")
    if isinstance(credential, dict):
        return credential
    if isinstance(credential, str):
        try:
            parsed = json.loads(credential)
            return parsed if isinstance(parsed, dict) else None
        except Exception:
            return None
    return None


@api_auth_bp.route("/api/register", methods=["POST"])
@require_ip_not_locked
def register():
    """用户注册"""
    data = _get_json_payload()
    username = data.get("username", "").strip()
    password = data.get("password", "").strip()
    email = data.get("email", "").strip().lower()
    captcha_session = data.get("captcha_session", "")
    captcha_code = data.get("captcha", "").strip()
    social_bind_token = str(data.get("social_bind_token") or "").strip()

    if not username or not password:
        return jsonify({"error": "用户名和密码不能为空"}), 400

    is_valid, error_msg = validate_username(username)
    if not is_valid:
        return jsonify({"error": error_msg}), 400

    is_valid, error_msg = validate_password(password)
    if not is_valid:
        return jsonify({"error": error_msg}), 400

    client_ip = get_rate_limit_ip()
    allowed, error_msg = check_ip_request_rate(client_ip, "register")
    if not allowed:
        return jsonify({"error": error_msg}), 429

    captcha_ok, captcha_error_response = _verify_common_captcha(client_ip, captcha_session, captcha_code)
    if not captcha_ok:
        return captcha_error_response

    email_settings = email_service.get_email_settings()
    email_verify_enabled = email_settings.get("register_verify_enabled", False) and email_settings.get("enabled", False)

    if email_verify_enabled and not email:
        return jsonify({"error": "启用邮箱验证后，邮箱为必填项"}), 400

    if email:
        is_valid, error_msg = validate_email(email)
        if not is_valid:
            return jsonify({"error": error_msg}), 400

    system_config = database.get_system_config()
    auto_approve_enabled = system_config.get("auto_approve_enabled", 0) == 1
    auto_approve_hourly_limit = system_config.get("auto_approve_hourly_limit", 10)
    auto_approve_vip_days = system_config.get("auto_approve_vip_days", 7)

    hourly_limit = int(auto_approve_hourly_limit) if isinstance(auto_approve_hourly_limit, int) else 10
    if hourly_limit > 0:
        hourly_count = database.get_hourly_registration_count()
        if hourly_count >= hourly_limit:
            return jsonify({"error": f"注册人数过多，请稍后再试（每小时限制{hourly_limit}人）"}), 429

    user_id = database.create_user(username, password, email)
    if user_id:
        social_bind_message = ""
        if social_bind_token:
            pending = database.get_social_pending_bind(social_bind_token)
            if pending:
                provider = str(pending.get("provider") or "").strip().lower()
                social_uid = str(pending.get("social_uid") or "").strip()
                enabled_providers = parse_providers((database.get_system_config() or {}).get("social_login_providers"))
                existing_identity = database.find_social_login_binding(provider, social_uid)
                if provider in enabled_providers and social_uid and not existing_identity:
                    binding = database.upsert_social_login_binding(
                        user_id=user_id,
                        provider=provider,
                        social_uid=social_uid,
                        nickname=pending.get("nickname") or "",
                        avatar_url=pending.get("avatar_url") or "",
                    )
                    if binding:
                        database.delete_social_pending_bind(social_bind_token)
                        social_bind_message = f"，已绑定{provider_label(provider)}"

        if auto_approve_enabled:
            if auto_approve_vip_days > 0:
                database.set_user_vip(user_id, auto_approve_vip_days)

        if email_verify_enabled and email:
            result = email_service.send_register_verification_email(email=email, username=username, user_id=user_id)
            if result["success"]:
                message = _with_vip_suffix(
                    f"注册成功！验证邮件已发送（可直接登录，建议完成邮箱验证）{social_bind_message}",
                    auto_approve_enabled,
                    auto_approve_vip_days,
                )
                return jsonify({"success": True, "message": message, "need_verify": True})

            logger.error(f"注册验证邮件发送失败: {result['error']}")
            message = _with_vip_suffix(
                f"注册成功，但验证邮件发送失败({result['error']})。你仍可直接登录{social_bind_message}",
                auto_approve_enabled,
                auto_approve_vip_days,
            )
            return jsonify({"success": True, "message": message, "need_verify": True})

        message = _with_vip_suffix("注册成功！可直接登录", auto_approve_enabled, auto_approve_vip_days)
        if social_bind_message:
            message = f"{message}{social_bind_message}"
        return jsonify({"success": True, "message": message})
    return jsonify({"error": "用户名已存在"}), 400


@api_auth_bp.route("/api/verify-email/<token>")
def verify_email(token):
    """验证邮箱 - 用户点击邮件中的链接"""
    result = email_service.verify_email_token(token, email_service.EMAIL_TYPE_REGISTER, consume=False)

    if result:
        token_id = result["token_id"]
        user_id = result["user_id"]

        if not database.approve_user(user_id):
            logger.error(f"用户邮箱验证失败: 用户审核更新失败 user_id={user_id}")
            error_message = "验证处理失败，请稍后重试"
            spa_initial_state = {
                "page": "verify_result",
                "success": False,
                "title": "验证失败",
                "error_message": error_message,
                "primary_label": "返回登录",
                "primary_url": "/login",
            }
            return render_app_spa_or_legacy(
                "verify_failed.html",
                legacy_context={"error_message": error_message},
                spa_initial_state=spa_initial_state,
            )

        system_config = database.get_system_config()
        auto_approve_vip_days = system_config.get("auto_approve_vip_days", 7)
        if auto_approve_vip_days > 0:
            database.set_user_vip(user_id, auto_approve_vip_days)

        if not email_service.consume_email_token(token_id):
            logger.warning(f"用户邮箱验证后Token消费失败: user_id={user_id}")

        logger.info(f"用户邮箱验证成功: user_id={user_id}")
        spa_initial_state = {
            "page": "verify_result",
            "success": True,
            "title": "验证成功",
            "message": "您的邮箱已验证成功！账号已激活，现在可以登录使用了。",
            "primary_label": "立即登录",
            "primary_url": "/login",
            "redirect_url": "/login",
            "redirect_seconds": 5,
        }
        return render_app_spa_or_legacy("verify_success.html", spa_initial_state=spa_initial_state)

    logger.warning("邮箱验证失败: token无效或已过期")
    error_message = "验证链接无效或已过期，请重新注册或申请重发验证邮件"
    spa_initial_state = {
        "page": "verify_result",
        "success": False,
        "title": "验证失败",
        "error_message": error_message,
        "primary_label": "重新注册",
        "primary_url": "/register",
        "secondary_label": "返回登录",
        "secondary_url": "/login",
    }
    return render_app_spa_or_legacy(
        "verify_failed.html",
        legacy_context={"error_message": error_message},
        spa_initial_state=spa_initial_state,
    )


@api_auth_bp.route("/api/resend-verify-email", methods=["POST"])
@require_ip_not_locked
def resend_verify_email():
    """重发验证邮件"""
    data = _get_json_payload()
    email = data.get("email", "").strip().lower()
    captcha_session = data.get("captcha_session", "")
    captcha_code = data.get("captcha", "").strip()

    if not email:
        return jsonify({"error": "请输入邮箱"}), 400

    is_valid, error_msg = validate_email(email)
    if not is_valid:
        return jsonify({"error": error_msg}), 400

    client_ip = get_rate_limit_ip()
    allowed, error_msg = check_ip_request_rate(client_ip, "email")
    if not allowed:
        return jsonify({"error": error_msg}), 429
    allowed, error_msg = check_email_rate_limit(email, "resend_verify")
    if not allowed:
        return jsonify({"error": error_msg}), 429

    captcha_ok, captcha_error_response = _verify_common_captcha(client_ip, captcha_session, captcha_code)
    if not captcha_ok:
        return captcha_error_response

    user = database.get_user_by_email(email)
    if not user:
        return jsonify({"error": "该邮箱未注册"}), 404

    if user["status"] == "approved":
        return jsonify({"error": "该账号已验证通过，请直接登录"}), 400

    result = email_service.resend_register_verification_email(user_id=user["id"], email=email, username=user["username"])

    if result["success"]:
        return jsonify({"success": True, "message": "验证邮件已重新发送，请查收"})
    return jsonify({"error": result["error"]}), 500


@api_auth_bp.route("/api/email/verify-status")
def get_email_verify_status():
    """获取邮箱验证功能状态（公开API）"""
    try:
        settings = email_service.get_email_settings()
        return jsonify(
            {
                "email_enabled": settings.get("enabled", False),
                "register_verify_enabled": settings.get("register_verify_enabled", False) and settings.get("enabled", False),
            }
        )
    except Exception:
        return jsonify({"email_enabled": False, "register_verify_enabled": False})


@api_auth_bp.route("/api/forgot-password", methods=["POST"])
@require_ip_not_locked
def forgot_password():
    """发送密码重置邮件"""
    data = _get_json_payload()
    email = data.get("email", "").strip().lower()
    username = data.get("username", "").strip()
    captcha_session = data.get("captcha_session", "")
    captcha_code = data.get("captcha", "").strip()

    if not email and not username:
        return jsonify({"error": "请输入邮箱或用户名"}), 400

    if username:
        is_valid, error_msg = validate_username(username)
        if not is_valid:
            return jsonify({"error": error_msg}), 400

    if email:
        is_valid, error_msg = validate_email(email)
        if not is_valid:
            return jsonify({"error": error_msg}), 400

    client_ip = get_rate_limit_ip()
    allowed, error_msg = check_ip_request_rate(client_ip, "email")
    if not allowed:
        return jsonify({"error": error_msg}), 429
    if email:
        allowed, error_msg = check_email_rate_limit(email, "forgot_password")
        if not allowed:
            return jsonify({"error": error_msg}), 429

    captcha_ok, captcha_error_response = _verify_common_captcha(client_ip, captcha_session, captcha_code)
    if not captcha_ok:
        return captcha_error_response

    email_settings = email_service.get_email_settings()
    if not email_settings.get("enabled", False):
        return jsonify({"error": "邮件功能未启用，请联系管理员"}), 400

    if username:
        user = database.get_user_by_username(username)
        if user and user.get("status") == "approved":
            bound_email = (user.get("email") or "").strip()
            if not bound_email:
                return (
                    jsonify(
                        {
                            "error": "您尚未绑定邮箱，无法通过邮箱找回密码。请联系管理员重置密码。",
                            "code": "email_not_bound",
                        }
                    ),
                    400,
                )

            allowed, error_msg = check_email_rate_limit(bound_email, "forgot_password")
            if not allowed:
                return jsonify({"error": error_msg}), 429

            _send_password_reset_email_if_possible(
                email=bound_email,
                username=user["username"],
                user_id=user["id"],
            )
        return jsonify({"success": True, "message": "如果该账号已绑定邮箱，您将收到密码重置邮件"})

    user = database.get_user_by_email(email)
    if user and user.get("status") == "approved":
        _send_password_reset_email_if_possible(email=email, username=user["username"], user_id=user["id"])

    return jsonify({"success": True, "message": "如果该邮箱已注册，您将收到密码重置邮件"})


@api_auth_bp.route("/reset-password/<token>")
def reset_password_page(token):
    """密码重置页面"""
    result = email_service.verify_password_reset_token(token)
    valid = bool(result)
    error_message = "" if valid else "重置链接无效或已过期，请重新申请密码重置"

    legacy_context = {"token": token, "valid": valid, "error_message": error_message}
    spa_initial_state = {"page": "reset_password", "token": token, "valid": valid, "error_message": error_message}

    return render_app_spa_or_legacy(
        "reset_password.html",
        legacy_context=legacy_context,
        spa_initial_state=spa_initial_state,
    )


@api_auth_bp.route("/api/reset-password-confirm", methods=["POST"])
def reset_password_confirm():
    """确认密码重置"""
    data = _get_json_payload()
    token = data.get("token", "").strip()
    new_password = data.get("new_password", "").strip()

    if not token or not new_password:
        return jsonify({"error": "参数不完整"}), 400

    is_valid, error_msg = validate_password(new_password)
    if not is_valid:
        return jsonify({"error": error_msg}), 400

    result = email_service.confirm_password_reset(token)
    if not result:
        return jsonify({"error": "重置链接无效或已过期"}), 400

    user_id = result["user_id"]
    if database.admin_reset_user_password(user_id, new_password):
        logger.info(f"用户密码重置成功: user_id={user_id}")
        return jsonify({"success": True, "message": "密码重置成功"})
    return jsonify({"error": "密码重置失败"}), 500


@api_auth_bp.route("/api/generate_captcha", methods=["POST"])
def generate_captcha():
    """生成4位数字验证码图片"""
    client_ip = get_rate_limit_ip()
    allowed, error_msg = check_ip_request_rate(client_ip, "login")
    if not allowed:
        return jsonify({"error": error_msg}), 429

    session_id = str(uuid.uuid4())
    code = "".join(str(secrets.randbelow(10)) for _ in range(4))

    safe_set_captcha(session_id, {"code": code, "expire_time": time.time() + 300, "failed_attempts": 0})
    safe_cleanup_expired_captcha()

    try:
        captcha_image = _generate_captcha_image_data_uri(code)
        return jsonify({"session_id": session_id, "captcha_image": captcha_image})
    except ImportError as e:
        logger.error(f"PIL库未安装，验证码功能不可用: {e}")
        safe_delete_captcha(session_id)
        return jsonify({"error": "验证码服务暂不可用，请联系管理员安装PIL库"}), 503


@api_auth_bp.route("/api/passkeys/login/options", methods=["POST"])
@require_ip_not_locked
def user_passkey_login_options():
    """用户 Passkey 登录：获取 assertion challenge。"""
    data = _get_json_payload()
    username = str(data.get("username", "") or "").strip()
    client_ip = get_rate_limit_ip()
    mode = "named" if username else "discoverable"
    username_key = f"passkey:{username}" if username else "passkey:discoverable"

    is_locked, remaining = check_login_ip_user_locked(client_ip, username_key)
    if is_locked:
        wait_hint = f"{remaining // 60 + 1}分钟" if remaining >= 60 else f"{remaining}秒"
        return jsonify({"error": f"账号短时锁定，请{wait_hint}后再试"}), 429

    allowed, error_msg = check_ip_request_rate(client_ip, "login")
    if not allowed:
        return jsonify({"error": error_msg}), 429

    allowed, error_msg = check_login_rate_limits(client_ip, username_key)
    if not allowed:
        return jsonify({"error": error_msg}), 429

    user_id = 0
    allow_credential_ids = []
    if mode == "named":
        user = database.get_user_by_username(username)
        if not user or user.get("status") != "approved":
            record_login_failure(client_ip, username_key)
            return jsonify({"error": "账号或Passkey不可用"}), 400

        user_id = int(user["id"])
        passkeys = database.list_passkeys("user", user_id)
        if not passkeys:
            record_login_failure(client_ip, username_key)
            return jsonify({"error": "该账号尚未绑定Passkey"}), 400
        allow_credential_ids = [str(item.get("credential_id") or "").strip() for item in passkeys if item.get("credential_id")]

    try:
        rp_id = get_rp_id(request)
        expected_origins = get_expected_origins(request)
    except Exception as e:
        logger.warning(f"[passkey] 生成登录 challenge 失败(mode={mode}, username={username or '-'}) : {e}")
        return jsonify({"error": "Passkey配置异常，请联系管理员"}), 500

    options = make_authentication_options(rp_id=rp_id, allow_credential_ids=allow_credential_ids)
    challenge = str(options.get("challenge") or "").strip()
    if not challenge:
        return jsonify({"error": "生成Passkey挑战失败"}), 500

    session[_USER_PASSKEY_LOGIN_SESSION_KEY] = {
        "mode": mode,
        "username": username,
        "user_id": int(user_id),
        "challenge": challenge,
        "rp_id": rp_id,
        "expected_origins": expected_origins,
        "username_key": username_key,
        "created_at": time.time(),
    }
    session.modified = True
    return jsonify({"publicKey": options})


@api_auth_bp.route("/api/passkeys/login/verify", methods=["POST"])
@require_ip_not_locked
def user_passkey_login_verify():
    """用户 Passkey 登录：校验 assertion 并登录。"""
    data = _get_json_payload()
    request_username = str(data.get("username", "") or "").strip()
    credential = _parse_credential_payload(data)
    if not credential:
        return jsonify({"error": "Passkey参数缺失"}), 400

    state = session.get(_USER_PASSKEY_LOGIN_SESSION_KEY) or {}
    if not state:
        return jsonify({"error": "Passkey挑战不存在或已过期，请重试"}), 400
    if not is_challenge_valid(state.get("created_at")):
        session.pop(_USER_PASSKEY_LOGIN_SESSION_KEY, None)
        return jsonify({"error": "Passkey挑战已过期，请重试"}), 400

    mode = str(state.get("mode") or "named")
    if mode not in {"named", "discoverable"}:
        session.pop(_USER_PASSKEY_LOGIN_SESSION_KEY, None)
        return jsonify({"error": "Passkey状态异常，请重试"}), 400

    expected_username = str(state.get("username") or "").strip()
    username = expected_username
    if mode == "named":
        if not expected_username:
            session.pop(_USER_PASSKEY_LOGIN_SESSION_KEY, None)
            return jsonify({"error": "Passkey状态异常，请重试"}), 400
        if request_username and request_username != expected_username:
            return jsonify({"error": "用户名与挑战不匹配，请重试"}), 400
    else:
        username = request_username

    client_ip = get_rate_limit_ip()
    username_key = str(state.get("username_key") or "").strip() or (
        f"passkey:{expected_username}" if mode == "named" else "passkey:discoverable"
    )

    is_locked, remaining = check_login_ip_user_locked(client_ip, username_key)
    if is_locked:
        wait_hint = f"{remaining // 60 + 1}分钟" if remaining >= 60 else f"{remaining}秒"
        return jsonify({"error": f"账号短时锁定，请{wait_hint}后再试"}), 429

    credential_id = str(credential.get("id") or credential.get("rawId") or "").strip()
    if not credential_id:
        return jsonify({"error": "Passkey参数无效"}), 400

    passkey = database.get_passkey_by_credential_id(credential_id)
    if not passkey:
        record_login_failure(client_ip, username_key)
        return jsonify({"error": "Passkey不存在或已删除"}), 401
    if str(passkey.get("owner_type") or "") != "user":
        record_login_failure(client_ip, username_key)
        return jsonify({"error": "Passkey不属于用户账号"}), 401
    if mode == "named" and int(passkey.get("owner_id") or 0) != int(state.get("user_id") or 0):
        record_login_failure(client_ip, username_key)
        return jsonify({"error": "Passkey与账号不匹配"}), 401

    try:
        parsed_credential, verified = verify_authentication(
            credential=credential,
            expected_challenge=str(state.get("challenge") or ""),
            expected_rp_id=str(state.get("rp_id") or ""),
            expected_origins=list(state.get("expected_origins") or []),
            credential_public_key=str(passkey.get("public_key") or ""),
            credential_current_sign_count=int(passkey.get("sign_count") or 0),
        )
        verified_credential_id = encode_credential_id(verified.credential_id)
        if verified_credential_id != str(passkey.get("credential_id") or ""):
            raise ValueError("credential_id mismatch")
    except Exception as e:
        logger.warning(f"[passkey] 用户登录验签失败(mode={mode}, username={expected_username or request_username or '-'}) : {e}")
        record_login_failure(client_ip, username_key)
        return jsonify({"error": "Passkey验证失败"}), 401

    user_id = int(passkey.get("owner_id") or 0)
    user = database.get_user_by_id(user_id)
    if not user or user.get("status") != "approved":
        return jsonify({"error": "账号不可用"}), 401

    database.update_passkey_usage(int(passkey["id"]), int(verified.new_sign_count))
    clear_login_failures(client_ip, username_key)
    user_login_key = f"passkey:{str(user.get('username') or '').strip()}"
    if user_login_key and user_login_key != username_key:
        clear_login_failures(client_ip, user_login_key)
    session.pop(_USER_PASSKEY_LOGIN_SESSION_KEY, None)

    user_obj = User(user_id)
    login_user(user_obj)
    load_user_accounts(user_id)

    resolved_username = str(user.get("username") or "").strip() or username or f"user-{user_id}"
    _send_login_security_alert_if_needed(user=user, username=resolved_username, client_ip=client_ip)
    return jsonify({"success": True, "credential_id": parsed_credential.id, "username": resolved_username})


@api_auth_bp.route("/api/login", methods=["POST"])
@require_ip_not_locked
def login():
    """用户登录"""
    data = _get_json_payload()
    username = data.get("username", "").strip()
    password = data.get("password", "").strip()
    captcha_session = data.get("captcha_session", "")
    captcha_code = data.get("captcha", "").strip()
    need_captcha = data.get("need_captcha", False)

    client_ip = get_rate_limit_ip()
    username_key = username

    scan_locked = record_login_username_attempt(client_ip, username_key)
    is_locked, remaining = check_login_ip_user_locked(client_ip, username_key)
    if is_locked:
        wait_hint = f"{remaining // 60 + 1}分钟" if remaining >= 60 else f"{remaining}秒"
        return jsonify({"error": f"账号短时锁定，请{wait_hint}后再试", "need_captcha": True}), 429

    allowed, error_msg = check_ip_request_rate(client_ip, "login")
    if not allowed:
        return jsonify({"error": error_msg, "need_captcha": True}), 429

    allowed, error_msg = check_login_rate_limits(client_ip, username_key)
    if not allowed:
        return jsonify({"error": error_msg, "need_captcha": True}), 429

    captcha_required = check_login_captcha_required(client_ip, username_key) or scan_locked or bool(need_captcha)
    captcha_ok, captcha_error_response = _verify_login_captcha_if_needed(
        captcha_required=captcha_required,
        captcha_session=captcha_session,
        captcha_code=captcha_code,
        client_ip=client_ip,
        username_key=username_key,
    )
    if not captcha_ok:
        return captcha_error_response

    user = database.verify_user(username, password)
    if not user:
        record_login_failure(client_ip, username_key)
        delay = get_login_failure_delay_seconds(client_ip, username_key)
        if delay > 0:
            time.sleep(delay)
        return jsonify({"error": "用户名或密码错误", "need_captcha": check_login_captcha_required(client_ip, username_key)}), 401

    if user["status"] != "approved":
        return jsonify({"error": "账号未审核，请等待管理员审核", "need_captcha": False}), 401

    clear_login_failures(client_ip, username_key)
    user_obj = User(user["id"])
    login_user(user_obj)
    load_user_accounts(user["id"])

    _send_login_security_alert_if_needed(user=user, username=username, client_ip=client_ip)
    return jsonify({"success": True})


@api_auth_bp.route("/api/logout", methods=["POST"])
@login_required
def logout():
    """用户登出"""
    logout_user()
    session.pop("admin_id", None)
    session.pop("admin_username", None)
    session.pop("admin_reauth_until", None)
    return jsonify({"success": True})
