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

import json
import os
import stat
import tempfile
import time

import database
from app_config import get_config
from app_logger import get_logger
from app_security import get_rate_limit_ip, require_ip_not_locked
from flask import current_app, jsonify, redirect, request, session, url_for
from routes.admin_api import admin_api_bp
from routes.decorators import admin_required
from services.accounts_service import load_user_accounts
from services.checkpoints import get_checkpoint_mgr
from services.passkeys import (
    MAX_PASSKEYS_PER_OWNER,
    encode_credential_id,
    get_credential_transports,
    get_expected_origins,
    get_rp_id,
    is_challenge_valid,
    make_authentication_options,
    make_registration_options,
    normalize_device_name,
    verify_authentication,
    verify_registration,
)
from services.state import (
    safe_get_user_accounts_snapshot,
    safe_verify_and_consume_captcha,
    check_login_ip_user_locked,
    check_login_rate_limits,
    get_login_failure_delay_seconds,
    record_login_username_attempt,
    check_ip_request_rate,
    check_login_captcha_required,
    clear_login_failures,
    record_login_failure,
)
from services.tasks import submit_account_task

logger = get_logger("app")
config = get_config()
_ADMIN_PASSKEY_LOGIN_SESSION_KEY = "admin_passkey_login_state"
_ADMIN_PASSKEY_REGISTER_SESSION_KEY = "admin_passkey_register_state"


def _admin_reauth_required() -> bool:
    try:
        return time.time() > float(session.get("admin_reauth_until", 0) or 0)
    except Exception:
        return True


def _require_admin_reauth():
    if _admin_reauth_required():
        return jsonify({"error": "需要二次确认", "code": "reauth_required"}), 401
    return None


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


def _truncate_text(value, max_len: int = 300) -> str:
    text = str(value or "").strip()
    if len(text) > max_len:
        return f"{text[:max_len]}..."
    return text


@admin_api_bp.route("/debug-config", methods=["GET"])
@admin_required
def debug_config():
    """调试配置信息（仅管理员可访问，生产环境应禁用）"""
    if not current_app.debug:
        return jsonify({"error": "调试端点已在生产环境禁用"}), 403

    return jsonify(
        {
            "secret_key_set": bool(current_app.secret_key),
            "secret_key_length": len(current_app.secret_key) if current_app.secret_key else 0,
            "session_config": {
                "SESSION_COOKIE_NAME": current_app.config.get("SESSION_COOKIE_NAME"),
                "SESSION_COOKIE_SECURE": current_app.config.get("SESSION_COOKIE_SECURE"),
                "SESSION_COOKIE_HTTPONLY": current_app.config.get("SESSION_COOKIE_HTTPONLY"),
                "SESSION_COOKIE_SAMESITE": current_app.config.get("SESSION_COOKIE_SAMESITE"),
                "PERMANENT_SESSION_LIFETIME": str(current_app.config.get("PERMANENT_SESSION_LIFETIME")),
            },
            "has_session": bool(session),
            "cookies_received": list(request.cookies.keys()),
        }
    )


@admin_api_bp.route("/passkeys/login/options", methods=["POST"])
@require_ip_not_locked
def admin_passkey_login_options():
    """管理员 Passkey 登录：获取 assertion challenge。"""
    data = request.get_json(silent=True) or {}
    username = str(data.get("username", "") or "").strip()

    client_ip = get_rate_limit_ip()
    mode = "named" if username else "discoverable"
    username_key = f"admin-passkey:{username}" if username else "admin-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

    admin_id = 0
    allow_credential_ids = []
    if mode == "named":
        admin_row = database.get_admin_by_username(username)
        if not admin_row:
            record_login_failure(client_ip, username_key)
            return jsonify({"error": "账号或Passkey不可用"}), 400

        admin_id = int(admin_row["id"])
        passkeys = database.list_passkeys("admin", admin_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] 管理员登录 options 失败(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[_ADMIN_PASSKEY_LOGIN_SESSION_KEY] = {
        "mode": mode,
        "username": username,
        "admin_id": int(admin_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})


@admin_api_bp.route("/passkeys/login/verify", methods=["POST"])
@require_ip_not_locked
def admin_passkey_login_verify():
    """管理员 Passkey 登录：校验 assertion 并登录。"""
    data = request.get_json(silent=True) or {}
    request_username = str(data.get("username", "") or "").strip()
    credential = _parse_credential_payload(data)
    if not credential:
        return jsonify({"error": "Passkey参数缺失"}), 400

    state = session.get(_ADMIN_PASSKEY_LOGIN_SESSION_KEY) or {}
    if not state:
        return jsonify({"error": "Passkey挑战不存在或已过期，请重试"}), 400
    if not is_challenge_valid(state.get("created_at")):
        session.pop(_ADMIN_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(_ADMIN_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(_ADMIN_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"admin-passkey:{expected_username}" if mode == "named" else "admin-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 "") != "admin":
        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("admin_id") or 0):
        record_login_failure(client_ip, username_key)
        return jsonify({"error": "Passkey与管理员账号不匹配"}), 401

    try:
        _, 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

    admin_id = int(passkey.get("owner_id") or 0)
    admin_row = database.get_admin_by_id(admin_id)
    if not admin_row:
        return jsonify({"error": "管理员账号不存在"}), 401
    admin_username = str(admin_row.get("username") or "").strip() or username or f"admin-{admin_id}"

    database.update_passkey_usage(int(passkey["id"]), int(verified.new_sign_count))
    clear_login_failures(client_ip, username_key)
    admin_login_key = f"admin-passkey:{admin_username}"
    if admin_login_key and admin_login_key != username_key:
        clear_login_failures(client_ip, admin_login_key)

    session.pop(_ADMIN_PASSKEY_LOGIN_SESSION_KEY, None)
    session.pop("admin_id", None)
    session.pop("admin_username", None)
    session["admin_id"] = admin_id
    session["admin_username"] = admin_username
    session["admin_reauth_until"] = time.time() + int(config.ADMIN_REAUTH_WINDOW_SECONDS)
    session.permanent = True
    session.modified = True
    return jsonify({"success": True, "redirect": "/yuyx/admin", "username": admin_username})


@admin_api_bp.route("/login", methods=["POST"])
@require_ip_not_locked
def admin_login():
    """管理员登录（支持JSON和form-data两种格式）"""
    if request.is_json:
        data = request.json or {}
    else:
        data = request.form

    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}秒"
        if request.is_json:
            return jsonify({"error": f"账号短时锁定，请{wait_hint}后再试", "need_captcha": True}), 429
        return redirect(url_for("pages.admin_login_page"))

    allowed, error_msg = check_ip_request_rate(client_ip, "login")
    if not allowed:
        if request.is_json:
            return jsonify({"error": error_msg, "need_captcha": True}), 429
        return redirect(url_for("pages.admin_login_page"))

    allowed, error_msg = check_login_rate_limits(client_ip, username_key)
    if not allowed:
        if request.is_json:
            return jsonify({"error": error_msg, "need_captcha": True}), 429
        return redirect(url_for("pages.admin_login_page"))

    captcha_required = check_login_captcha_required(client_ip, username_key) or scan_locked or bool(need_captcha)
    if captcha_required:
        if not captcha_session or not captcha_code:
            if request.is_json:
                return jsonify({"error": "请填写验证码", "need_captcha": True}), 400
            return redirect(url_for("pages.admin_login_page"))
        success, message = safe_verify_and_consume_captcha(captcha_session, captcha_code)
        if not success:
            record_login_failure(client_ip, username_key)
            if request.is_json:
                return jsonify({"error": message, "need_captcha": True}), 400
            return redirect(url_for("pages.admin_login_page"))

    admin = database.verify_admin(username, password)
    if admin:
        clear_login_failures(client_ip, username_key)
        session.pop("admin_id", None)
        session.pop("admin_username", None)
        session["admin_id"] = admin["id"]
        session["admin_username"] = admin["username"]
        session["admin_reauth_until"] = time.time() + int(config.ADMIN_REAUTH_WINDOW_SECONDS)
        session.permanent = True
        session.modified = True

        logger.info(f"[admin_login] 管理员 {username} 登录成功")

        if request.is_json:
            return jsonify({"success": True, "redirect": "/yuyx/admin"})
        return redirect(url_for("pages.admin_page"))

    record_login_failure(client_ip, username_key)
    delay = get_login_failure_delay_seconds(client_ip, username_key)
    if delay > 0:
        time.sleep(delay)
    logger.warning(f"[admin_login] 管理员 {username} 登录失败 - 用户名或密码错误")
    if request.is_json:
        return jsonify({"error": "管理员用户名或密码错误", "need_captcha": check_login_captcha_required(client_ip)}), 401
    return redirect(url_for("pages.admin_login_page"))


@admin_api_bp.route("/logout", methods=["POST"])
@admin_required
def admin_logout():
    """管理员登出"""
    session.pop("admin_id", None)
    session.pop("admin_username", None)
    session.pop("admin_reauth_until", None)
    return jsonify({"success": True})


@admin_api_bp.route("/admin/passkeys", methods=["GET"])
@admin_required
def list_admin_passkeys():
    admin_id = int(session.get("admin_id") or 0)
    rows = database.list_passkeys("admin", admin_id)
    items = []
    for row in rows:
        credential_id = str(row.get("credential_id") or "")
        preview = f"{credential_id[:8]}...{credential_id[-6:]}" if len(credential_id) > 16 else credential_id
        items.append(
            {
                "id": int(row.get("id")),
                "device_name": str(row.get("device_name") or ""),
                "credential_id_preview": preview,
                "created_at": row.get("created_at"),
                "last_used_at": row.get("last_used_at"),
                "transports": str(row.get("transports") or ""),
            }
        )
    return jsonify({"items": items, "limit": MAX_PASSKEYS_PER_OWNER})


@admin_api_bp.route("/admin/passkeys/register/options", methods=["POST"])
@admin_required
def admin_passkey_register_options():
    admin_id = int(session.get("admin_id") or 0)
    admin_username = str(session.get("admin_username") or "").strip() or f"admin-{admin_id}"

    count = database.count_passkeys("admin", admin_id)
    if count >= MAX_PASSKEYS_PER_OWNER:
        return jsonify({"error": f"最多可绑定{MAX_PASSKEYS_PER_OWNER}台设备"}), 400

    data = request.get_json(silent=True) or {}
    device_name = normalize_device_name(data.get("device_name"))

    existing = database.list_passkeys("admin", admin_id)
    exclude_credential_ids = [str(item.get("credential_id") or "").strip() for item in existing if item.get("credential_id")]

    try:
        rp_id = get_rp_id(request)
        expected_origins = get_expected_origins(request)
        options = make_registration_options(
            rp_id=rp_id,
            rp_name="知识管理平台",
            user_name=admin_username,
            user_display_name=admin_username,
            user_id_bytes=f"admin:{admin_id}".encode("utf-8"),
            exclude_credential_ids=exclude_credential_ids,
        )
    except Exception as e:
        logger.warning(f"[passkey] 管理员注册 options 失败(admin_id={admin_id}): {e}")
        return jsonify({"error": "生成Passkey挑战失败"}), 500

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

    session[_ADMIN_PASSKEY_REGISTER_SESSION_KEY] = {
        "admin_id": admin_id,
        "challenge": challenge,
        "rp_id": rp_id,
        "expected_origins": expected_origins,
        "device_name": device_name,
        "created_at": time.time(),
    }
    session.modified = True
    return jsonify({"publicKey": options, "limit": MAX_PASSKEYS_PER_OWNER})


@admin_api_bp.route("/admin/passkeys/register/verify", methods=["POST"])
@admin_required
def admin_passkey_register_verify():
    admin_id = int(session.get("admin_id") or 0)
    state = session.get(_ADMIN_PASSKEY_REGISTER_SESSION_KEY) or {}
    if not state:
        return jsonify({"error": "Passkey挑战不存在或已过期，请重试"}), 400
    if int(state.get("admin_id") or 0) != admin_id:
        return jsonify({"error": "Passkey挑战与当前管理员不匹配"}), 400
    if not is_challenge_valid(state.get("created_at")):
        session.pop(_ADMIN_PASSKEY_REGISTER_SESSION_KEY, None)
        return jsonify({"error": "Passkey挑战已过期，请重试"}), 400

    data = request.get_json(silent=True) or {}
    credential = _parse_credential_payload(data)
    if not credential:
        return jsonify({"error": "Passkey参数缺失"}), 400

    count = database.count_passkeys("admin", admin_id)
    if count >= MAX_PASSKEYS_PER_OWNER:
        session.pop(_ADMIN_PASSKEY_REGISTER_SESSION_KEY, None)
        return jsonify({"error": f"最多可绑定{MAX_PASSKEYS_PER_OWNER}台设备"}), 400

    try:
        verified = verify_registration(
            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 []),
        )
    except Exception as e:
        logger.warning(f"[passkey] 管理员注册验签失败(admin_id={admin_id}): {e}")
        return jsonify({"error": "Passkey验证失败，请重试"}), 400

    device_name = normalize_device_name(data.get("device_name") if "device_name" in data else state.get("device_name"))

    created_id = database.create_passkey(
        "admin",
        admin_id,
        credential_id=encode_credential_id(verified.credential_id),
        public_key=encode_credential_id(verified.credential_public_key),
        sign_count=int(verified.sign_count or 0),
        device_name=device_name,
        transports=get_credential_transports(credential),
        aaguid=str(verified.aaguid or ""),
    )
    if not created_id:
        return jsonify({"error": "该Passkey已绑定，或保存失败"}), 400

    session.pop(_ADMIN_PASSKEY_REGISTER_SESSION_KEY, None)
    return jsonify({"success": True, "id": int(created_id)})


@admin_api_bp.route("/admin/passkeys/<int:passkey_id>", methods=["DELETE"])
@admin_required
def delete_admin_passkey(passkey_id):
    admin_id = int(session.get("admin_id") or 0)
    ok = database.delete_passkey("admin", admin_id, int(passkey_id))
    if ok:
        return jsonify({"success": True})
    return jsonify({"error": "设备不存在或已删除"}), 404


@admin_api_bp.route("/admin/passkeys/client-error", methods=["POST"])
@admin_required
def report_admin_passkey_client_error():
    """上报管理员端浏览器 Passkey 失败详情，便于排查兼容性问题。"""
    data = request.get_json(silent=True) or {}
    error_name = _truncate_text(data.get("name"), 120)
    error_message = _truncate_text(data.get("message"), 400)
    error_code = _truncate_text(data.get("code"), 120)
    ua = _truncate_text(data.get("user_agent") or request.headers.get("User-Agent", ""), 300)
    stage = _truncate_text(data.get("stage"), 80)
    source = _truncate_text(data.get("source"), 80)
    admin_id = int(session.get("admin_id") or 0)

    logger.warning(
        "[passkey][client-error][admin] admin_id=%s stage=%s source=%s name=%s code=%s message=%s ua=%s",
        admin_id,
        stage or "-",
        source or "-",
        error_name or "-",
        error_code or "-",
        error_message or "-",
        ua or "-",
    )
    return jsonify({"success": True})


@admin_api_bp.route("/admin/reauth", methods=["POST"])
@admin_required
def admin_reauth():
    """管理员敏感操作二次确认"""
    data = request.json or {}
    password = (data.get("password") or "").strip()
    if not password:
        return jsonify({"error": "密码不能为空"}), 400

    username = session.get("admin_username")
    if not username:
        return jsonify({"error": "未登录"}), 401

    admin = database.verify_admin(username, password)
    if not admin:
        return jsonify({"error": "密码错误"}), 401

    session["admin_reauth_until"] = time.time() + int(config.ADMIN_REAUTH_WINDOW_SECONDS)
    session.modified = True
    return jsonify({"success": True, "expires_in": int(config.ADMIN_REAUTH_WINDOW_SECONDS)})

@admin_api_bp.route("/docker/restart", methods=["POST"])
@admin_required
def restart_docker_container():
    """重启Docker容器"""
    import subprocess

    try:
        reauth_response = _require_admin_reauth()
        if reauth_response:
            return reauth_response
        if not os.path.exists("/.dockerenv"):
            return jsonify({"error": "当前不在Docker容器中运行"}), 400

        logger.info("[系统] 管理员触发Docker容器重启")

        restart_script = """
import os
import time

time.sleep(3)
os._exit(0)
"""

        with tempfile.NamedTemporaryFile("w", suffix=".py", prefix="restart_container_", delete=False) as temp_file:
            temp_file.write(restart_script)
            script_path = temp_file.name

        os.chmod(script_path, stat.S_IRUSR | stat.S_IWUSR)

        subprocess.Popen(
            ["python3", script_path],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            start_new_session=True,
        )

        return jsonify({"success": True, "message": "容器将在3秒后重启，请稍后刷新页面"})
    except Exception as e:
        logger.error(f"[系统] Docker容器重启失败: {str(e)}")
        return jsonify({"error": f"重启失败: {str(e)}"}), 500


# ==================== 断点续传（管理员） ====================


@admin_api_bp.route("/checkpoint/paused")
@admin_required
def checkpoint_get_paused():
    try:
        user_id = request.args.get("user_id", type=int)
        tasks = get_checkpoint_mgr().get_paused_tasks(user_id=user_id)
        return jsonify({"success": True, "tasks": tasks})
    except Exception as e:
        logger.error(f"获取暂停任务失败: {e}")
        return jsonify({"success": False, "message": str(e)}), 500


@admin_api_bp.route("/checkpoint/<task_id>/resume", methods=["POST"])
@admin_required
def checkpoint_resume(task_id):
    try:
        checkpoint_mgr = get_checkpoint_mgr()
        checkpoint = checkpoint_mgr.get_checkpoint(task_id)
        if not checkpoint:
            return jsonify({"success": False, "message": "任务不存在"}), 404
        if checkpoint["status"] != "paused":
            return jsonify({"success": False, "message": "任务未暂停"}), 400
        if checkpoint_mgr.resume_task(task_id):
            user_id = checkpoint["user_id"]
            if not safe_get_user_accounts_snapshot(user_id):
                load_user_accounts(user_id)
            ok, msg = submit_account_task(
                user_id=user_id,
                account_id=checkpoint["account_id"],
                browse_type=checkpoint["browse_type"],
                enable_screenshot=True,
                source="resumed",
            )
            if not ok:
                return jsonify({"success": False, "message": msg}), 400
            return jsonify({"success": True})
        return jsonify({"success": False}), 500
    except Exception as e:
        logger.error(f"恢复任务失败: {e}")
        return jsonify({"success": False, "message": str(e)}), 500


@admin_api_bp.route("/checkpoint/<task_id>/abandon", methods=["POST"])
@admin_required
def checkpoint_abandon(task_id):
    try:
        if get_checkpoint_mgr().abandon_task(task_id):
            return jsonify({"success": True})
        return jsonify({"success": False}), 404
    except Exception as e:
        return jsonify({"success": False, "message": str(e)}), 500
