#!/usr/bin/env python3
"""
清除登录防暴破锁定（删除 login_attempts 中近期失败记录）。

锁定规则见 server/application/services/auth_service.py：
  MAX_LOGIN_FAILURES=5，LOCKOUT_MINUTES=15（按 username + ip_address 统计）

用法:
  python scripts/unlock_login_lockout.py
  python scripts/unlock_login_lockout.py --username admin
  python scripts/unlock_login_lockout.py --all-failures   # 删除该用户全部失败记录

需在仓库根目录有 .env（含 NEXUS_DATABASE_URL 或 DATABASE_URL），或在生产机部署目录执行。
"""

from __future__ import annotations

import argparse
import asyncio
import os
import re
import sys
from pathlib import Path
from urllib.parse import unquote, urlparse

ROOT = Path(__file__).resolve().parents[1]


def _load_dotenv() -> None:
    env_path = ROOT / ".env"
    if not env_path.is_file():
        return
    for line in env_path.read_text(encoding="utf-8", errors="replace").splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, _, value = line.partition("=")
        key, value = key.strip(), value.strip().strip("\"'")
        if key and key not in os.environ:
            os.environ[key] = value


def _parse_database_url(url: str) -> dict:
    """Parse mysql+aiomysql://user:pass@host:port/db"""
    u = urlparse(url.replace("mysql+aiomysql://", "mysql://", 1))
    return {
        "host": u.hostname or "127.0.0.1",
        "port": u.port or 3306,
        "user": unquote(u.username or ""),
        "password": unquote(u.password or ""),
        "db": (u.path or "/").lstrip("/") or "nexus",
    }


async def _unlock(username: str, all_failures: bool) -> int:
    import aiomysql

    raw = (
        os.environ.get("NEXUS_DATABASE_URL")
        or os.environ.get("DATABASE_URL")
        or ""
    )
    if not raw:
        print("ERROR: 未找到 NEXUS_DATABASE_URL / DATABASE_URL，请在 .env 中配置或在生产目录执行", file=sys.stderr)
        return 1

    cfg = _parse_database_url(raw)
    conn = await aiomysql.connect(
        host=cfg["host"],
        port=int(cfg["port"]),
        user=cfg["user"],
        password=cfg["password"],
        db=cfg["db"],
        connect_timeout=20,
    )
    try:
        async with conn.cursor() as cur:
            if all_failures:
                await cur.execute(
                    "DELETE FROM login_attempts WHERE username = %s AND success = 0",
                    (username,),
                )
            else:
                await cur.execute(
                    """
                    DELETE FROM login_attempts
                    WHERE username = %s AND success = 0
                      AND attempted_at >= UTC_TIMESTAMP() - INTERVAL 20 MINUTE
                    """,
                    (username,),
                )
            deleted = cur.rowcount
            await conn.commit()
        print(f"OK: 已删除用户「{username}」失败登录记录 {deleted} 条，锁定已解除。")
        return 0
    finally:
        conn.close()


def main() -> int:
    parser = argparse.ArgumentParser(description="解除 Nexus 登录防暴破锁定")
    parser.add_argument("--username", default="admin", help="管理员用户名（默认 admin）")
    parser.add_argument(
        "--all-failures",
        action="store_true",
        help="删除该用户全部历史失败记录（默认仅近 20 分钟）",
    )
    args = parser.parse_args()
    _load_dotenv()
    return asyncio.run(_unlock(args.username, args.all_failures))


if __name__ == "__main__":
    sys.exit(main())
