#!/usr/bin/env python3
"""Verify rsync push elevation for selected servers (diagnose + dry-run + optional probe push).

Usage:
  python scripts/verify_push_elevation.py --ids 313,314
  python scripts/verify_push_elevation.py --name 温胜夜 --push
"""

from __future__ import annotations

import argparse
import asyncio
import os
import secrets
import sys
import tempfile
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,
    non_root_only: bool = False,
) -> list[Any]:
    from sqlalchemy import or_, select

    from server.domain.models import Server

    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 non_root_only:
        q = q.where(Server.username.isnot(None)).where(Server.username != "root")
    result = await session.execute(q)
    return list(result.scalars().all())


async def _diagnose_write(server: Any, dest: str) -> dict[str, Any]:
    import shlex

    from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback
    from server.utils.posix_paths import remote_join

    test_file = remote_join(dest, ".nexus_verify_probe")
    cmd = f"touch {shlex.quote(test_file)} && rm -f {shlex.quote(test_file)}"
    r = await exec_ssh_command_with_fallback(server, cmd, timeout=15)
    return {
        "path_writable": r.get("exit_code") == 0,
        "elevated": bool(r.get("elevated")),
        "stderr": (r.get("stderr") or "")[:200],
    }


async def run_verify(args: argparse.Namespace) -> list[dict[str, Any]]:
    from server.application.services.files_sudoers_service import probe_rsync_sudo
    from server.application.services.sync_engine_v2 import _rsync_push
    from server.config import settings
    from server.infrastructure.database.session import AsyncSessionLocal, init_db
    from server.infrastructure.redis.client import close_redis, init_redis
    from server.utils.posix_paths import UPLOAD_STAGING_PREFIX, normalize_remote_abs_path, resolve_push_target_path

    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, non_root_only=args.non_root_only
        )

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

    staging = f"{UPLOAD_STAGING_PREFIX}{secrets.token_hex(6)}"
    os.makedirs(staging, exist_ok=True)
    probe_name = ".nexus_push_probe.txt"
    probe_path = os.path.join(staging, probe_name)
    with open(probe_path, "w", encoding="utf-8") as fh:
        fh.write("nexus push elevation verify\n")

    results: list[dict[str, Any]] = []
    sem = asyncio.Semaphore(max(1, getattr(args, "concurrency", 8)))

    async def _verify_one(server: Any) -> None:
        async with sem:
            dest = resolve_push_target_path(server.target_path)
            row: dict[str, Any] = {
                "server_id": server.id,
                "server_name": server.name,
                "ssh_user": server.username,
                "target_path": dest,
            }

            row["rsync_sudo"] = await probe_rsync_sudo(server)
            write = await _diagnose_write(server, dest)
            row["write_check"] = write

            preview = await _rsync_push(
                server, staging, dest, sync_mode="incremental", dry_run=True
            )
            row["dry_run_exit"] = preview.get("exit_code")
            row["dry_run_ok"] = preview.get("exit_code") in (0, 23)
            if not row["dry_run_ok"]:
                row["dry_run_stderr"] = (preview.get("stderr") or "")[:300]

            if args.push and row["dry_run_ok"]:
                push = await _rsync_push(server, staging, dest, sync_mode="incremental")
                row["push_exit"] = push.get("exit_code")
                row["push_ok"] = push.get("exit_code") == 0
                row["push_elevated"] = bool(push.get("elevated"))
                if not row["push_ok"]:
                    row["push_stderr"] = (push.get("stderr") or "")[:300]

            ok = (
                row.get("rsync_sudo")
                and write.get("path_writable")
                and row.get("dry_run_ok")
                and (not args.push or row.get("push_ok"))
            )
            row["ok"] = ok
            results.append(row)

            status = "OK" if ok else "FAIL"
            print(
                f"[{status}] #{server.id} {server.name} user={server.username} dest={dest}\n"
                f"  rsync_sudo={row['rsync_sudo']} write={write.get('path_writable')} "
                f"elevated={write.get('elevated')} dry_run={row.get('dry_run_exit')}"
                + (
                    f" push={row.get('push_exit')} push_elevated={row.get('push_elevated')}"
                    if args.push
                    else ""
                ),
                flush=True,
            )
            if row.get("dry_run_stderr"):
                print(f"  dry_run_err: {row['dry_run_stderr']}", flush=True)
            if row.get("push_stderr"):
                print(f"  push_err: {row['push_stderr']}", flush=True)

    try:
        await asyncio.gather(*[_verify_one(s) for s in servers])
    finally:
        import shutil

        shutil.rmtree(staging, ignore_errors=True)
        await close_redis()

    ok_n = sum(1 for r in results if r.get("ok"))
    print(f"\nVerify: {ok_n}/{len(results)} passed")
    return results


def main() -> int:
    _load_dotenv()
    parser = argparse.ArgumentParser(description="Verify push elevation on servers")
    parser.add_argument("--ids", type=str, help="Comma-separated server ids")
    parser.add_argument("--name", type=str, help="Filter name/domain")
    parser.add_argument("--non-root-only", action="store_true", help="All servers except username=root")
    parser.add_argument("--concurrency", type=int, default=8, help="Parallel servers (default 8)")
    parser.add_argument("--push", action="store_true", help="Run real probe file push after dry-run")
    args = parser.parse_args()
    if not args.ids and not args.name and not args.non_root_only:
        parser.error("Specify --ids, --name, or --non-root-only")
    results = asyncio.run(run_verify(args))
    return 0 if results and all(r.get("ok") for r in results) else 1


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