#!/usr/bin/env python3
"""Import servers from Excel (.xls) via Nexus API (name + IP columns).

Usage:
  NEXUS_TEST_BASE=https://api.synaglobal.vip \\
  NEXUS_TEST_ADMIN_USER=admin \\
  NEXUS_TEST_ADMIN_PASSWORD='***' \\
  python scripts/import_servers_from_xls.py /path/to/servers.xls

Options:
  --dry-run     Parse only, do not call API
  --batch       Use /add-by-ips-batch (requires server with name-tab support)
  --chunk N     Batch chunk size (default 200)
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))


def _load_test_credentials() -> tuple[str, str, str]:
    sys.path.insert(0, str(ROOT / "tests"))
    import test_api  # noqa: E402

    test_api._load_credentials_from_env_file()
    base = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600").rstrip("/")
    user = os.environ.get("NEXUS_TEST_ADMIN_USER", "admin")
    password = os.environ.get("NEXUS_TEST_ADMIN_PASSWORD", "admin")
    return base, user, password


def _api_json(method: str, url: str, token: str | None = None, body: dict | None = None, timeout: int = 300):
    data = None
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    if body is not None:
        data = json.dumps(body).encode()
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read()
            return resp.status, json.loads(raw) if raw else {}
    except urllib.error.HTTPError as e:
        raw = e.read()
        try:
            payload = json.loads(raw) if raw else {"detail": e.reason}
        except json.JSONDecodeError:
            payload = {"detail": raw.decode(errors="replace")}
        return e.code, payload


def _login(base: str, user: str, password: str) -> str:
    code, data = _api_json("POST", f"{base}/api/auth/login", body={"username": user, "password": password})
    if code != 200 or not data.get("access_token"):
        detail = data.get("detail", data)
        raise SystemExit(f"登录失败 ({code}): {detail}")
    return data["access_token"]


def _read_xls(path: Path) -> list[tuple[str, str]]:
    try:
        import xlrd
    except ImportError as e:
        raise SystemExit("需要 xlrd: pip install xlrd") from e

    wb = xlrd.open_workbook(str(path))
    sh = wb.sheet_by_index(0)
    rows: list[tuple[str, str]] = []
    for r in range(sh.nrows):
        if sh.ncols < 2:
            continue
        name = str(sh.cell_value(r, 0)).strip()
        ip = str(sh.cell_value(r, 1)).strip()
        if not ip:
            continue
        rows.append((name or ip, ip))
    return rows


def _dedupe_rows(rows: list[tuple[str, str]]) -> tuple[list[tuple[str, str]], int]:
    seen: set[str] = set()
    out: list[tuple[str, str]] = []
    removed = 0
    for name, ip in rows:
        key = ip.casefold()
        if key in seen:
            removed += 1
            continue
        seen.add(key)
        out.append((name, ip))
    return out, removed


def _import_single(base: str, token: str, rows: list[tuple[str, str]], port: int, agent_port: int) -> dict:
    stats = {"created": 0, "pending": 0, "skipped": 0, "errors": 0}
    total = len(rows)
    for i, (name, ip) in enumerate(rows, 1):
        code, data = _api_json(
            "POST",
            f"{base}/api/servers/add-by-ip",
            token=token,
            body={
                "domain": ip,
                "name": name,
                "port": port,
                "agent_port": agent_port,
            },
            timeout=300,
        )
        if code == 200 and data.get("success"):
            stats["created"] += 1
            status = "created"
        elif code == 409:
            stats["skipped"] += 1
            status = "skipped"
        elif code == 200 and not data.get("success"):
            stats["pending"] += 1
            status = "pending"
        else:
            stats["errors"] += 1
            status = f"error:{code}"
        print(f"[{i}/{total}] {status} {name} ({ip})", flush=True)
    return stats


def _import_batch(
    base: str,
    token: str,
    rows: list[tuple[str, str]],
    port: int,
    agent_port: int,
    chunk: int,
) -> dict:
    stats = {"created": 0, "pending": 0, "skipped": 0, "errors": 0}
    for start in range(0, len(rows), chunk):
        part = rows[start : start + chunk]
        text = "\n".join(f"{name}\t{ip}" for name, ip in part)
        code, data = _api_json(
            "POST",
            f"{base}/api/servers/add-by-ips-batch",
            token=token,
            body={"text": text, "port": port, "agent_port": agent_port},
            timeout=3600,
        )
        if code != 200:
            stats["errors"] += len(part)
            print(f"batch {start // chunk + 1} failed ({code}): {data.get('detail', data)}", flush=True)
            continue
        stats["created"] += int(data.get("created", 0))
        stats["pending"] += int(data.get("pending", 0))
        stats["skipped"] += int(data.get("skipped", 0))
        print(
            f"batch {start // chunk + 1}: created={data.get('created')} "
            f"pending={data.get('pending')} skipped={data.get('skipped')}",
            flush=True,
        )
    return stats


def main() -> None:
    parser = argparse.ArgumentParser(description="Import servers from Excel via Nexus API")
    parser.add_argument("xls", type=Path, help="Excel file (.xls), column A=name, B=IP")
    parser.add_argument("--dry-run", action="store_true", help="Parse only")
    parser.add_argument("--batch", action="store_true", help="Use batch API instead of single add-by-ip")
    parser.add_argument("--chunk", type=int, default=200, help="Batch chunk size")
    parser.add_argument("--port", type=int, default=22)
    parser.add_argument("--agent-port", type=int, default=8601)
    args = parser.parse_args()

    if not args.xls.is_file():
        raise SystemExit(f"文件不存在: {args.xls}")

    rows = _read_xls(args.xls)
    rows, dup_removed = _dedupe_rows(rows)
    print(f"读取 {args.xls.name}: {len(rows)} 台（去重忽略 {dup_removed} 行重复 IP）")
    if args.dry_run:
        for name, ip in rows[:5]:
            print(f"  {name}\t{ip}")
        if len(rows) > 5:
            print(f"  ... 共 {len(rows)} 行")
        return

    base, user, password = _load_test_credentials()
    print(f"目标 API: {base}  用户: {user}")
    token = _login(base, user, password)
    started = time.time()
    if args.batch:
        stats = _import_batch(base, token, rows, args.port, args.agent_port, args.chunk)
    else:
        stats = _import_single(base, token, rows, args.port, args.agent_port)
    elapsed = int(time.time() - started)
    print(
        f"完成 ({elapsed}s): 成功 {stats['created']}，待连接 {stats['pending']}，"
        f"跳过 {stats['skipped']}，错误 {stats['errors']}"
    )


if __name__ == "__main__":
    main()
