#!/usr/bin/env python3
"""Batch detect target_path via SSH (workerman.bat under /www/wwwroot).

Targets servers with「未设路径」(empty or ``/www/wwwroot`` placeholder) by default.

Usage:
  python scripts/batch_detect_target_path.py --unset-only
  python scripts/batch_detect_target_path.py --ids 245,246
  python scripts/batch_detect_target_path.py --name 温胜夜 --dry-run
"""

from __future__ import annotations

import argparse
import asyncio
import os
import sys
from pathlib import Path
from typing import Any

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


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.startswith("NEXUS_") and key[6:] not in os.environ:
            os.environ[key[6:]] = value
        elif key and key not in os.environ:
            os.environ[key] = value


async def _fetch_servers(
    session: Any,
    *,
    ids: list[int] | None,
    name_like: str | None,
    unset_only: bool,
    limit: int,
) -> list[Any]:
    from sqlalchemy import or_, select

    from server.domain.models import Server
    from server.infrastructure.database.server_repo import _target_path_unset_filter

    q = select(Server).order_by(Server.id)
    if ids:
        q = q.where(Server.id.in_(ids))
    if name_like:
        q = q.where(or_(Server.name.like(f"%{name_like}%"), Server.domain.like(f"%{name_like}%")))
    if unset_only:
        q = q.where(_target_path_unset_filter(True))
    result = await session.execute(q)
    servers = list(result.scalars().all())
    if limit > 0:
        servers = servers[:limit]
    return servers


async def _detect_one_server(server: Any) -> dict[str, Any]:
    from server.application.server_batch_common import detect_and_save_target_path
    from server.application.services.server_batch_service import result_item_dict

    sid = server.id
    label = server.name or str(sid)
    try:
        outcome = await detect_and_save_target_path(server)
        if outcome.get("success"):
            return result_item_dict(
                server_id=sid,
                server_name=label,
                success=True,
                stdout=str(outcome.get("stdout") or ""),
            )
        return result_item_dict(
            server_id=sid,
            server_name=label,
            success=False,
            error=str(outcome.get("error") or "探测失败"),
        )
    except Exception as e:
        return result_item_dict(server_id=sid, server_name=label, success=False, error=str(e)[:300])


async def run_detect(args: argparse.Namespace) -> list[dict[str, Any]]:
    from server.config import settings
    from server.infrastructure.database.session import AsyncSessionLocal, init_db
    from server.infrastructure.redis.client import close_redis, init_redis

    await init_db()
    await init_redis(settings.REDIS_URL)

    ids: list[int] | None = None
    if args.ids:
        ids = [int(x.strip()) for x in args.ids.split(",") if x.strip()]

    async with AsyncSessionLocal() as session:
        servers = await _fetch_servers(
            session,
            ids=ids,
            name_like=args.name,
            unset_only=args.unset_only,
            limit=args.limit,
        )

    if not servers:
        print("No servers matched.", file=sys.stderr)
        await close_redis()
        return []

    if args.dry_run:
        for s in servers:
            print(f"[DRY] #{s.id} {s.name} target_path={s.target_path!r}")
        print(f"\nWould detect: {len(servers)} servers")
        await close_redis()
        return [{"server_id": s.id, "dry_run": True} for s in servers]

    sem = asyncio.Semaphore(max(1, getattr(args, "concurrency", 8)))

    async def _one(server: Any) -> dict[str, Any]:
        async with sem:
            return await _detect_one_server(server)

    results = await asyncio.gather(*[_one(s) for s in servers])
    await close_redis()

    ok = sum(1 for r in results if r.get("success"))
    fail = len(results) - ok
    print(f"\nDetect: {ok} ok / {fail} fail / {len(results)} total")
    for r in results:
        status = "OK" if r.get("success") else "FAIL"
        print(
            f"[{status}] #{r.get('server_id')} {r.get('server_name')}: "
            f"{r.get('stdout') or r.get('error') or ''}"
        )
    return list(results)


def main() -> int:
    _load_dotenv()
    parser = argparse.ArgumentParser(description="Batch detect server target_path")
    parser.add_argument("--ids", type=str, help="Comma-separated server ids")
    parser.add_argument("--name", type=str, help="Filter name/domain LIKE")
    parser.add_argument("--unset-only", action="store_true", help="Only「未设路径」servers")
    parser.add_argument("--limit", type=int, default=0)
    parser.add_argument("--concurrency", type=int, default=8)
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()
    if not args.ids and not args.name and not args.unset_only:
        parser.error("Specify --ids, --name, or --unset-only")
    results = asyncio.run(run_detect(args))
    if args.dry_run:
        return 0 if results else 2
    return 0 if results and all(r.get("success") for r in results) else 1


if __name__ == "__main__":
    raise SystemExit(main())
