"""Nexus — Sync Engine v2 API Routes (Step 4)

S1: POST /api/sync/files    — rsync file sync
P1: POST /api/sync/preview  — rsync dry-run (方案D: 智能提示预览)
"""

import logging

from fastapi import APIRouter, Depends, HTTPException, Request

from server.api.schemas import (
    SyncFiles,
    SyncBrowse,
    SyncBrowseLocal,
    SyncPreview,
    FileOperation,
    FileClipboardApply,
    LocalFileOperation,
    LocalFilePreview,
    SyncVerify,
    SyncCancel,
    ValidateSourcePath,
    SyncDiagnose,
    FileSyncDiff,
    FileDownload,
    FileRead,
    FileWrite,
    FileChmod,
    FileCompress,
    FileDecompress,
)
from server.api.remote_path_validation import RemotePathError, assert_clipboard_transfer_safe
from server.utils.posix_paths import (
    PosixPathError,
    assert_zip_member_safe,
    ensure_under_nexus_upload,
    is_path_under_prefix,
    normalize_remote_abs_path,
    posix_basename,
    posix_dirname,
    posix_join,
    normalize_remote_dest,
    remote_join,
    resolve_nexus_host_path,
    to_posix,
)
from server.application.services.sync_engine_v2 import SyncEngineV2
from server.infrastructure.database.server_repo import ServerRepositoryImpl
from server.infrastructure.database.sync_log_repo import SyncLogRepositoryImpl
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
from server.infrastructure.database.push_schedule_repo import PushRetryJobRepositoryImpl
from server.api.auth_jwt import get_current_admin
from server.domain.models import Admin, AuditLog

router = APIRouter(prefix="/api/sync", tags=["sync"])

logger = logging.getLogger("nexus.sync_v2")


async def _get_sync_engine(request: Request) -> SyncEngineV2:
    """DI factory for SyncEngineV2"""
    session = request.state.db
    return SyncEngineV2(
        server_repo=ServerRepositoryImpl(session),
        sync_log_repo=SyncLogRepositoryImpl(session),
        audit_repo=AuditLogRepositoryImpl(session),
        retry_repo=PushRetryJobRepositoryImpl(session),
    )


@router.post("/files")
async def sync_files(
    payload: SyncFiles,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """S1: File sync via rsync over SSH (with parallel execution)"""
    engine = await _get_sync_engine(request)
    return await engine.sync_files(
        server_ids=payload.server_ids,
        source_path=payload.source_path,
        target_path=payload.target_path,
        sync_mode=payload.sync_mode,
        operator=admin.username,
        batch_size=payload.batch_size,
        concurrency=payload.concurrency,
    )


@router.post("/preview")
async def preview_sync(
    payload: SyncPreview,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """P1: Dry-run rsync --stats against one representative server.

    No data is transferred. Returns file-change statistics and optional
    per-file list (verbose=true, capped at 200 lines).

    Full-sync mode (--delete) shows files_deleted — use this to confirm
    before running a destructive full sync.
    """
    engine = await _get_sync_engine(request)
    result = await engine.preview_sync(
        server_id=payload.server_id,
        source_path=payload.source_path,
        target_path=payload.target_path,
        sync_mode=payload.sync_mode,
        verbose=payload.verbose,
    )
    if "error" in result:
        raise HTTPException(status_code=400, detail=result["error"])

    await _audit_sync(
        "sync_preview", "server", payload.server_id,
        f"Dry-run preview: {payload.source_path} → server #{payload.server_id} ({payload.sync_mode})",
        admin.username, request,
    )
    return result


@router.post("/file-ops")
async def file_operation(
    payload: FileOperation,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Remote file operation (delete / rename / mkdir) via SSH exec.

    All paths are shlex-quoted server-side. Dirs use rm -rf; confirm
    client-side before sending delete on a directory. Audit-logged.
    """
    import shlex
    from server.infrastructure.database.server_repo import ServerRepositoryImpl
    from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback

    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="Server not found")

    op = payload.operation
    try:
        p = normalize_remote_abs_path(payload.path)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    if op == "delete":
        # rm -rf covers both files and directories
        cmd = f"rm -rf {shlex.quote(p)}"
        audit_detail = f"删除: {p}"
    elif op == "rename":
        if not payload.new_path:
            raise HTTPException(status_code=400, detail="new_path required for rename")
        try:
            np = normalize_remote_abs_path(payload.new_path)
        except PosixPathError as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc
        cmd = f"mv {shlex.quote(p)} {shlex.quote(np)}"
        audit_detail = f"重命名: {p} → {np}"
    elif op == "mkdir":
        cmd = f"mkdir -p {shlex.quote(p)}"
        audit_detail = f"新建目录: {p}"
    else:
        raise HTTPException(status_code=400, detail="Invalid operation")

    result = await exec_ssh_command_with_fallback(server, cmd, timeout=30)
    if result["exit_code"] != 0:
        err = (result.get("stderr") or result.get("stdout") or "")[:300]
        raise HTTPException(
            status_code=403 if "permission denied" in err.lower() else 400,
            detail=err or f"Command failed (exit {result['exit_code']})",
        )

    await _audit_sync(
        f"file_{op}", "server", payload.server_id,
        f"{audit_detail} 于 {server.name}", admin.username, request,
    )
    return {"success": True, "operation": op, "path": p}


@router.post("/file-clipboard")
async def apply_file_clipboard(
    payload: FileClipboardApply,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Batch copy (cp -r) or move/cut (mv -t) remote paths into dest_dir."""
    import shlex
    from server.infrastructure.database.server_repo import ServerRepositoryImpl
    from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
    from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback

    try:
        sources, dest_dir = assert_clipboard_transfer_safe(payload.sources, payload.dest_dir)
    except RemotePathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="服务器不存在")

    dest_q = shlex.quote(dest_dir)
    check = await exec_ssh_command(server, f"test -d {dest_q}", timeout=15)
    if check["exit_code"] != 0:
        raise HTTPException(status_code=400, detail="目标目录不存在")

    quoted_sources = " ".join(shlex.quote(s) for s in sources)
    if payload.mode == "copy":
        cmd = f"cp -r {quoted_sources} {dest_q}"
        audit_action = "file_copy"
        audit_detail = f"复制 {len(sources)} 项 → {dest_dir}"
    else:
        cmd = f"mv -t {dest_q} {quoted_sources}"
        audit_action = "file_move"
        audit_detail = f"移动 {len(sources)} 项 → {dest_dir}"

    result = await exec_ssh_command_with_fallback(server, cmd, timeout=120)
    if result["exit_code"] != 0:
        raise HTTPException(
            status_code=502,
            detail=(result["stderr"] or result["stdout"] or "传输失败")[:300],
        )

    await _audit_sync(
        audit_action,
        "server",
        payload.server_id,
        f"{audit_detail} 于 {server.name}",
        admin.username,
        request,
    )
    return {
        "success": True,
        "mode": payload.mode,
        "dest_dir": dest_dir,
        "count": len(sources),
    }


@router.post("/browse")
async def browse_directory(
    payload: SyncBrowse,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Browse remote directory listing via SSH"""
    from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
    from server.infrastructure.database.server_repo import ServerRepositoryImpl

    session = request.state.db
    server_id = payload.server_id
    path = payload.path

    if not server_id:
        raise HTTPException(status_code=400, detail="server_id required")

    repo = ServerRepositoryImpl(session)
    server = await repo.get_by_id(server_id)
    if not server:
        raise HTTPException(status_code=404, detail="Server not found")

    try:
        path = normalize_remote_abs_path(path)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    import shlex
    from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback
    from server.utils.unix_ls import parse_ls_la_line, perms_to_mode_octal

    path_q = shlex.quote(path)
    # 与写文件/chmod 一致：非 root SSH 在 Permission denied 时按 files_elevation 尝试 sudo -n
    result = await exec_ssh_command_with_fallback(
        server,
        f"ls -la --time-style=long-iso {path_q}",
        timeout=10,
    )
    if result["exit_code"] != 0:
        result = await exec_ssh_command_with_fallback(
            server, f"ls -la {path_q}", timeout=10
        )

    await _audit_sync("browse_directory", "server", server_id, f"Browse {server.name}:{path}", admin.username, request)

    if result["exit_code"] != 0:
        return {"path": path, "items": [], "error": (result.get("stderr") or "")[:500]}

    raw_entries = []
    for line in result["stdout"].split("\n"):
        parsed = parse_ls_la_line(line)
        if parsed:
            raw_entries.append(parsed)

    items = []
    for e in raw_entries:
        is_dir = bool(e["is_dir"])
        is_link = bool(e["is_link"])
        try:
            size = int(e["size"]) if not is_dir and not is_link else 0
        except (TypeError, ValueError):
            size = 0
        if is_dir:
            ftype = "directory"
        elif is_link:
            ftype = "symlink"
        else:
            ftype = "file"
        mode_octal = perms_to_mode_octal(e["perms"]) or ""
        items.append({
            "name": e["name"],
            "type": ftype,
            "size": size,
            "modified": e["modified"],
            "permissions": e["perms"],
            "owner": e.get("owner") or "",
            "group": e.get("group") or "",
            "mode_octal": mode_octal,
            "link_target": e.get("link_target"),
        })

    return {"path": path, "items": items}


@router.post("/browse-local")
async def browse_local_directory(
    payload: SyncBrowseLocal,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Browse a local directory on the Nexus server (for upload file manager).

    Only allows browsing under /tmp/nexus_upload_* directories.
    Returns entries with name, is_dir, size.
    """
    import os

    try:
        real_path = ensure_under_nexus_upload(payload.path)
    except PosixPathError as exc:
        raise HTTPException(status_code=403, detail=str(exc)) from exc

    if not os.path.isdir(real_path):
        raise HTTPException(status_code=404, detail="目录不存在")

    entries = []
    try:
        for entry in sorted(os.scandir(real_path), key=lambda e: (not e.is_dir(), e.name.lower())):
            try:
                stat = entry.stat()
                entries.append({
                    "name": entry.name,
                    "is_dir": entry.is_dir(),
                    "size": stat.st_size if not entry.is_dir() else 0,
                })
            except OSError:
                continue
    except PermissionError:
        raise HTTPException(status_code=403, detail="无权限访问该目录") from None

    return {"path": real_path, "entries": entries}


@router.post("/local-file-ops")
async def local_file_operation(
    payload: LocalFileOperation,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Delete or rename a file/directory in the local upload staging area.

    Only allows operations under /tmp/nexus_upload_* directories.
    """
    import os
    import shutil

    try:
        real_path = ensure_under_nexus_upload(payload.path)
    except PosixPathError as exc:
        raise HTTPException(status_code=403, detail=str(exc)) from exc

    path = to_posix(real_path)

    if payload.operation == "delete":
        if not os.path.exists(real_path):
            raise HTTPException(status_code=404, detail="文件或目录不存在")
        try:
            if os.path.isdir(real_path):
                shutil.rmtree(real_path)
            else:
                os.unlink(real_path)
        except OSError as e:
            raise HTTPException(status_code=500, detail=f"删除失败: {e}") from e

        await _audit_sync(
            "local_file_delete", "sync", 0,
            f"删除: {path}", admin.username, request,
        )
        return {"success": True, "operation": "delete", "path": path}

    elif payload.operation == "rename":
        if not payload.new_name:
            raise HTTPException(status_code=400, detail="new_name 必填")
        # Sanitize: strip slashes and path traversal
        safe_name = payload.new_name.replace("/", "_").replace("\\", "_").strip()
        if not safe_name or safe_name == "." or safe_name == "..":
            raise HTTPException(status_code=400, detail="无效的新名称")

        if not os.path.exists(real_path):
            raise HTTPException(status_code=404, detail="文件或目录不存在")

        new_path = posix_join(posix_dirname(real_path), safe_name)

        try:
            ensure_under_nexus_upload(new_path)
        except PosixPathError as exc:
            raise HTTPException(status_code=403, detail=str(exc)) from exc
        new_path = resolve_nexus_host_path(new_path)

        if os.path.exists(new_path):
            raise HTTPException(status_code=400, detail="目标名称已存在")

        try:
            os.rename(real_path, new_path)
        except OSError as e:
            raise HTTPException(status_code=500, detail=f"重命名失败: {e}") from e

        await _audit_sync(
            "local_file_rename", "sync", 0,
            f"重命名: {path} → {safe_name}", admin.username, request,
        )
        return {"success": True, "operation": "rename", "path": path, "new_name": safe_name}


@router.post("/local-file-preview")
async def local_file_preview(
    payload: LocalFilePreview,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Preview file content in the local upload staging area.

    Only allows files under /tmp/nexus_upload_* directories.
    Returns up to 4KB of file content, base64-encoded.
    """
    import os
    import base64

    try:
        real_path = ensure_under_nexus_upload(payload.path)
    except PosixPathError as exc:
        raise HTTPException(status_code=403, detail=str(exc)) from exc

    path = to_posix(real_path)

    if not os.path.isfile(real_path):
        raise HTTPException(status_code=404, detail="文件不存在或不是普通文件")

    # Size check: skip files over 1MB
    file_size = os.path.getsize(real_path)
    if file_size > 1_048_576:
        raise HTTPException(status_code=400, detail="文件过大，不支持预览（>1MB）")

    # Read up to 4KB
    max_read = 4096
    try:
        with open(real_path, "rb") as f:
            raw = f.read(max_read)
    except OSError as e:
        raise HTTPException(status_code=500, detail=f"读取失败: {e}") from None

    truncated = file_size > max_read

    # Detect encoding: try UTF-8 decode
    encoding_hint = "binary"
    try:
        raw.decode("utf-8")
        encoding_hint = "text"
    except (UnicodeDecodeError, ValueError):
        pass

    name = posix_basename(real_path)

    await _audit_sync(
        "local_file_preview", "sync", 0,
        f"预览: {path}", admin.username, request,
    )

    return {
        "name": name,
        "size": file_size,
        "content_base64": base64.b64encode(raw).decode("ascii"),
        "truncated": truncated,
        "encoding_hint": encoding_hint,
    }


async def _audit_sync(action: str, target_type: str, target_id: int, detail: str, operator: str, request: Request):
    """Create audit log entry for sync operations"""
    try:
        session = request.state.db
        audit_repo = AuditLogRepositoryImpl(session)
        await audit_repo.create(AuditLog(
            admin_username=operator,
            action=action,
            target_type=target_type,
            target_id=target_id,
            detail=detail,
            ip_address=request.client.host if request.client else "",
        ))
    except Exception:
        logger.warning(f"Audit log write failed for {action} on {target_type}/{target_id}", exc_info=True)


@router.post("/cancel")
async def cancel_sync(
    payload: SyncCancel,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Cancel an in-progress push by setting a Redis flag.

    The sync engine checks this flag before each rsync execution.
    Already-completed servers retain their results; pending ones are skipped.
    """
    try:
        from server.infrastructure.redis.client import get_redis
        redis = get_redis()
        await redis.set(f"sync:cancel:{payload.batch_id}", "1", ex=3600)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"取消操作失败: {e}") from e

    await _audit_sync(
        "sync_cancel", "sync", 0,
        f"取消推送: batch_id={payload.batch_id}", admin.username, request,
    )

    return {"cancelled": True, "batch_id": payload.batch_id}


# ── H5: Validate Source Path ──

_FORBIDDEN_PATH_PREFIXES = ("/etc", "/root", "/home", "/var", "/boot", "/dev", "/proc", "/sys", "/run", "/srv")


@router.post("/validate-source-path")
async def validate_source_path(
    payload: ValidateSourcePath,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Validate a local directory path on the Nexus server as a push source.

    Security: rejects sensitive system paths (/etc, /root, /home, etc.)
    and paths with traversal components.
    """
    import os

    path = payload.path.strip()

    try:
        real_path = resolve_nexus_host_path(path)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    if ".." in to_posix(path).split("/"):
        raise HTTPException(status_code=400, detail="路径不允许包含 '..'")

    # Reject sensitive paths
    for prefix in _FORBIDDEN_PATH_PREFIXES:
        if real_path == prefix or real_path.startswith(prefix + "/"):
            raise HTTPException(status_code=403, detail=f"不允许使用系统路径: {prefix}")

    if not os.path.exists(real_path):
        raise HTTPException(status_code=404, detail="路径不存在")

    if not os.path.isdir(real_path):
        raise HTTPException(status_code=400, detail="路径不是目录")

    # Count files and total size
    file_count = 0
    total_size = 0
    for root, _dirs, fnames in os.walk(real_path):
        for fn in fnames:
            file_count += 1
            try:
                total_size += os.path.getsize(posix_join(root, fn))
            except OSError:
                pass
            if file_count >= 10000:
                break
        if file_count >= 10000:
            break

    await _audit_sync(
        "validate_source_path", "sync", 0,
        f"验证源路径: {path} ({file_count} 文件)",
        admin.username, request,
    )

    return {
        "valid": True,
        "is_dir": True,
        "path": real_path,
        "file_count": file_count,
        "size_bytes": total_size,
        "file_count_truncated": file_count >= 10000,
    }


# ── H4: Diagnose Push Failure ──

@router.post("/diagnose")
async def diagnose_push(
    payload: SyncDiagnose,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Diagnose why a push failed for a specific server.

    Checks SSH connectivity, disk space, target path permissions, and write access.
    """
    import shlex
    from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
    from server.infrastructure.database.server_repo import ServerRepositoryImpl

    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="服务器不存在")

    result = {
        "server_id": payload.server_id,
        "server_name": server.name,
        "ssh_ok": False,
        "disk_avail": None,
        "disk_used_pct": None,
        "path_exists": None,
        "path_perms": None,
        "path_writable": None,
        "errors": [],
    }

    # 1. SSH connectivity
    try:
        r = await exec_ssh_command(server, "echo ok", timeout=10)
        if r["exit_code"] == 0 and "ok" in r["stdout"]:
            result["ssh_ok"] = True
        else:
            result["errors"].append(f"SSH 连接异常: {(r.get('stderr') or '')[:200] or 'exit code ' + str(r['exit_code'])}")
            # Cannot continue if SSH fails
            await _audit_sync("diagnose", "server", payload.server_id, "诊断: SSH不通", admin.username, request)
            return result
    except Exception as e:
        result["errors"].append(f"SSH 连接失败: {e}")
        await _audit_sync("diagnose", "server", payload.server_id, "诊断: SSH失败", admin.username, request)
        return result

    try:
        dest = normalize_remote_dest(payload.target_path or server.target_path)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    safe_dest = shlex.quote(dest)

    # 2. Disk space
    try:
        r = await exec_ssh_command(server, f"df -h {safe_dest} | tail -1", timeout=10)
        if r["exit_code"] == 0:
            parts = r["stdout"].strip().split()
            if len(parts) >= 6:
                result["disk_avail"] = parts[3]  # e.g. "50G"
                try:
                    result["disk_used_pct"] = int(parts[4].replace("%", ""))  # e.g. 72
                except ValueError:
                    pass
    except Exception as e:
        logger.debug(f"Diagnose disk check failed for server {payload.server_id}: {e}")
    try:
        r = await exec_ssh_command(server, f"ls -ld {safe_dest} 2>&1", timeout=10)
        if r["exit_code"] == 0:
            result["path_exists"] = True
            parts = r["stdout"].strip().split()
            if parts:
                result["path_perms"] = parts[0]  # e.g. "drwxr-xr-x"
        else:
            result["path_exists"] = False
            result["errors"].append(f"目标路径不存在: {dest}")
    except Exception as e:
        logger.debug(f"Diagnose path check failed for server {payload.server_id}: {e}")

    # 4. Write test
    if result["path_exists"]:
        try:
            test_file = remote_join(dest, ".nexus_diag_test_$$")
            r = await exec_ssh_command(
                server,
                f"touch {shlex.quote(test_file)} && rm -f {shlex.quote(test_file)}",
                timeout=10,
            )
            result["path_writable"] = r["exit_code"] == 0
            if r["exit_code"] != 0:
                result["errors"].append(f"目标路径不可写: {(r.get('stderr') or '')[:200]}")
        except Exception as e:
            result["errors"].append(f"写入测试失败: {e}")
            result["path_writable"] = False
    else:
        result["path_writable"] = False

    await _audit_sync(
        "diagnose", "server", payload.server_id,
        f"诊断: SSH={'✓' if result['ssh_ok'] else '✗'} 路径={'✓' if result['path_exists'] else '✗'} 写入={'✓' if result['path_writable'] else '✗'}",
        admin.username, request,
    )

    return result


# ── H3: File Diff View ──

_MAX_DIFF_SIZE = 102_400  # 100 KB


@router.post("/file-diff")
async def file_diff(
    payload: FileSyncDiff,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Compare a local file with its remote counterpart to show push diff.

    Security: only allows diffing files under /tmp/nexus_upload_* source paths.
    Reads up to 100KB from each side. Returns unified diff lines.
    """
    import os
    import difflib
    import shlex
    from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
    from server.infrastructure.database.server_repo import ServerRepositoryImpl

    try:
        real_source = ensure_under_nexus_upload(payload.source_path)
    except PosixPathError as exc:
        raise HTTPException(status_code=403, detail=str(exc)) from exc

    rel = to_posix(payload.relative_path).lstrip("/")
    if not rel or ".." in rel.split("/"):
        raise HTTPException(status_code=400, detail="相对路径无效")

    local_file = resolve_nexus_host_path(posix_join(real_source, rel))
    if not is_path_under_prefix(local_file, real_source):
        raise HTTPException(status_code=403, detail="文件路径越界")

    if not os.path.isfile(local_file):
        return {
            "file_name": posix_basename(rel),
            "local_exists": False,
            "remote_exists": None,
            "local_size": 0,
            "remote_size": None,
            "diff_lines": [],
            "truncated": False,
            "error": "本地文件不存在",
        }

    # Read local file
    local_size = os.path.getsize(local_file)
    try:
        with open(local_file, "r", encoding="utf-8", errors="replace") as f:
            local_text = f.read(_MAX_DIFF_SIZE)
    except OSError as e:
        raise HTTPException(status_code=500, detail=f"读取本地文件失败: {e}") from None

    local_lines = local_text.splitlines(keepends=True)
    local_truncated = local_size > _MAX_DIFF_SIZE

    # Get server + target path
    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="服务器不存在")

    try:
        dest = normalize_remote_dest(payload.target_path or server.target_path)
        remote_file = remote_join(dest, rel)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    # Read remote file
    remote_exists = False
    remote_size = None
    remote_lines = []
    remote_truncated = False

    try:
        r = await exec_ssh_command(
            server,
            f"wc -c {shlex.quote(remote_file)} 2>/dev/null && cat {shlex.quote(remote_file)}",
            timeout=15,
        )
        if r["exit_code"] == 0:
            remote_exists = True
            output = r["stdout"]
            # First line is wc -c output: "  12345 /path/file"
            first_newline = output.index("\n") if "\n" in output else 0
            wc_line = output[:first_newline].strip()
            try:
                remote_size = int(wc_line.split()[0])
            except (ValueError, IndexError):
                remote_size = None
            remote_text = output[first_newline + 1:]
            if remote_size is not None and remote_size > _MAX_DIFF_SIZE:
                remote_truncated = True
            remote_lines = remote_text.splitlines(keepends=True)
        else:
            # File doesn't exist on remote — treat as empty (all local lines are additions)
            remote_exists = False
    except Exception as e:
        # SSH failed — report error
        logger.warning(f"File diff remote read failed for server {payload.server_id}: {e}")
        remote_exists = None

    # Compute unified diff
    diff_lines = []
    for line in difflib.unified_diff(
        remote_lines, local_lines,
        fromfile=f"remote/{payload.relative_path}",
        tofile=f"local/{payload.relative_path}",
        lineterm="",
    ):
        if line.startswith("+") and not line.startswith("+++"):
            diff_lines.append({"type": "add", "content": line[1:]})
        elif line.startswith("-") and not line.startswith("---"):
            diff_lines.append({"type": "del", "content": line[1:]})
        elif line.startswith("@@"):
            diff_lines.append({"type": "header", "content": line})
        elif line.startswith(" "):
            diff_lines.append({"type": "ctx", "content": line[1:]})

    await _audit_sync(
        "file_diff", "server", payload.server_id,
        f"文件对比: {payload.relative_path}",
        admin.username, request,
    )

    return {
        "file_name": posix_basename(rel),
        "local_exists": True,
        "remote_exists": remote_exists,
        "local_size": local_size,
        "remote_size": remote_size,
        "diff_lines": diff_lines,
        "truncated": local_truncated or remote_truncated,
    }


# ── File Upload via SFTP ──

MAX_UPLOAD_FILE_SIZE = 104_857_600  # 100 MB


@router.post("/upload")
async def upload_file(
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Upload a file to a remote server via SFTP.

    Multipart form fields:
      - server_id: int (required)
      - remote_path: str (target directory, required)
      - file: UploadFile (required)

    The file is uploaded to {remote_path}/{filename} on the target server.
    If the file already exists, it will be overwritten.
    """
    import asyncssh
    from server.infrastructure.ssh.asyncssh_pool import remote_write_bytes, ssh_pool

    form = await request.form()
    server_id_raw = form.get("server_id")
    remote_path_raw = form.get("remote_path", "/tmp")
    file = form.get("file")

    if not server_id_raw:
        raise HTTPException(status_code=400, detail="server_id 必填")
    try:
        server_id = int(server_id_raw)
    except (ValueError, TypeError):
        raise HTTPException(status_code=400, detail="server_id 必须为数字") from None

    if not file or not hasattr(file, "filename") or not file.filename:
        raise HTTPException(status_code=400, detail="请选择要上传的文件")

    # Read file content with size check
    content = await file.read()
    if len(content) > MAX_UPLOAD_FILE_SIZE:
        raise HTTPException(status_code=400, detail=f"文件大小超过限制 ({MAX_UPLOAD_FILE_SIZE // (1024*1024)}MB)")

    remote_path = str(remote_path_raw).strip()
    filename = file.filename

    try:
        remote_dir = normalize_remote_abs_path(remote_path)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    # Sanitize filename — strip slashes to prevent path injection
    safe_filename = filename.replace("/", "_").replace("\\", "_").strip()
    if not safe_filename:
        raise HTTPException(status_code=400, detail="文件名无效")

    # Get server from DB
    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(server_id)
    if not server:
        raise HTTPException(status_code=404, detail="服务器不存在")

    try:
        full_remote_path = remote_join(remote_dir, safe_filename)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    from server.utils.files_elevation import get_files_elevation

    try:
        conn = await ssh_pool.acquire(server)
        try:
            await remote_write_bytes(
                conn,
                full_remote_path,
                content,
                elevation=get_files_elevation(server),
            )
        finally:
            await ssh_pool.release(server.id)
    except asyncssh.PermissionDenied:
        raise HTTPException(status_code=403, detail=f"SSH 权限不足，无法写入 {full_remote_path}") from None
    except asyncssh.SFTPError as e:
        raise HTTPException(status_code=400, detail=f"SFTP 上传失败: {e}") from e
    except Exception as e:
        raise HTTPException(status_code=502, detail=f"SSH 连接失败: {e}") from e

    # Audit
    await _audit_sync(
        "file_upload", "server", server_id,
        f"上传 {safe_filename} ({len(content)} bytes) → {server.name}:{full_remote_path}",
        admin.username, request,
    )

    return {
        "success": True,
        "server_id": server_id,
        "remote_path": full_remote_path,
        "filename": safe_filename,
        "size": len(content),
    }


# ── ZIP Upload & Extract (for push workflow) ──

MAX_ZIP_FILE_SIZE = 524_288_000  # 500 MB


@router.post("/upload-zip")
async def upload_zip(
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Upload a ZIP file to Nexus, extract it, return the source path for rsync push.

    Multipart form fields:
      - file: UploadFile (required, must be .zip)

    Returns {"source_path": "/tmp/nexus_upload_xxx/", "file_count": N}
    """
    import os
    import posixpath
    import uuid
    import zipfile
    import shutil

    form = await request.form()
    file = form.get("file")

    if not file or not hasattr(file, "filename") or not file.filename:
        raise HTTPException(status_code=400, detail="请选择 ZIP 文件")

    filename = file.filename
    if not filename.lower().endswith(".zip"):
        raise HTTPException(status_code=400, detail="仅支持 .zip 格式")

    # Read with size check
    content = await file.read()
    if len(content) > MAX_ZIP_FILE_SIZE:
        raise HTTPException(status_code=400, detail=f"文件大小超过限制 ({MAX_ZIP_FILE_SIZE // (1024*1024)}MB)")
    if len(content) == 0:
        raise HTTPException(status_code=400, detail="文件为空")

    # Create temp directory
    upload_id = uuid.uuid4().hex[:12]
    extract_dir = f"/tmp/nexus_upload_{upload_id}"
    zip_path = f"{extract_dir}.zip"

    try:
        os.makedirs(extract_dir, exist_ok=True)

        # Save ZIP
        with open(zip_path, "wb") as f:
            f.write(content)

        # Validate and extract with zip slip protection
        file_count = 0
        with zipfile.ZipFile(zip_path, "r") as zf:
            extract_base = resolve_nexus_host_path(extract_dir)
            for info in zf.infolist():
                try:
                    member_path = assert_zip_member_safe(extract_dir, info.filename)
                except PosixPathError as exc:
                    raise HTTPException(status_code=400, detail=str(exc)) from exc
                real_path = resolve_nexus_host_path(member_path)
                if not is_path_under_prefix(real_path, extract_base):
                    raise HTTPException(status_code=400, detail=f"ZIP 包含非法路径: {info.filename}")
                if info.is_dir():
                    continue
                file_count += 1

            zf.extractall(extract_dir)

        # Clean up ZIP file
        os.unlink(zip_path)

        if file_count == 0:
            shutil.rmtree(extract_dir, ignore_errors=True)
            raise HTTPException(status_code=400, detail="ZIP 文件为空（无文件）")

        # Build file listing (relative paths, capped at 500)
        files = []
        for root, _dirs, fnames in os.walk(extract_dir):
            for fn in fnames:
                rel = posixpath.relpath(posix_join(root, fn), extract_dir)
                files.append(rel)
                if len(files) >= 500:
                    break
            if len(files) >= 500:
                break
        files_truncated = file_count > len(files)

        # Audit
        await _audit_sync(
            "upload_zip", "sync", 0,
            f"上传 ZIP: {filename} ({len(content)} bytes) → {extract_dir} ({file_count} 个文件)",
            admin.username, request,
        )

        return {
            "success": True,
            "source_path": extract_dir,
            "file_count": file_count,
            "files": files,
            "files_truncated": files_truncated,
            "filename": filename,
            "size": len(content),
        }

    except HTTPException:
        shutil.rmtree(extract_dir, ignore_errors=True)
        try:
            os.unlink(zip_path)
        except OSError:
            pass
        raise
    except zipfile.BadZipFile:
        shutil.rmtree(extract_dir, ignore_errors=True)
        try:
            os.unlink(zip_path)
        except OSError:
            pass
        raise HTTPException(status_code=400, detail="无效的 ZIP 文件") from None
    except Exception as e:
        shutil.rmtree(extract_dir, ignore_errors=True)
        try:
            os.unlink(zip_path)
        except OSError:
            pass
        raise HTTPException(status_code=500, detail=f"解压失败: {e}") from e


# ── Post-Push Verification (sha256sum comparison) ──

@router.post("/verify")
async def verify_sync(
    payload: SyncVerify,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Verify pushed files by comparing sha256sums between source and target servers.

    Walks the local source directory and computes sha256 for each file,
    then runs sha256sum on each target server via SSH and compares.
    Returns per-server results: matched / missing / mismatched file lists.
    """
    import asyncio
    import hashlib
    import os
    import posixpath
    import shlex
    from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
    from server.infrastructure.database.server_repo import ServerRepositoryImpl

    session = request.state.db

    if not os.path.isdir(payload.source_path):
        raise HTTPException(status_code=400, detail=f"源路径不存在: {payload.source_path}")

    # Build local file manifest: {relative_path: sha256hex}
    local_files = {}
    for root, _dirs, fnames in os.walk(payload.source_path):
        for fn in fnames:
            full = posix_join(root, fn)
            rel = posixpath.relpath(full, to_posix(payload.source_path))
            if len(local_files) >= payload.max_files:
                break
            try:
                h = hashlib.sha256()
                with open(full, "rb") as f:
                    for chunk in iter(lambda: f.read(8192), b""):
                        h.update(chunk)
                local_files[rel] = h.hexdigest()
            except OSError:
                continue
        if len(local_files) >= payload.max_files:
            break

    if not local_files:
        raise HTTPException(status_code=400, detail="源目录无文件可校验")

    # Verify against each server concurrently
    sem = asyncio.Semaphore(5)
    results = {}

    async def _verify_one(sid: int):
        async with sem:
            server = await ServerRepositoryImpl(session).get_by_id(sid)
            if not server:
                results[sid] = {"server_id": sid, "server_name": "", "error": "服务器不存在"}
                return

            try:
                dest = normalize_remote_dest(payload.target_path or server.target_path)
            except PosixPathError as exc:
                results[sid] = {
                    "server_id": sid,
                    "server_name": server.name,
                    "error": str(exc),
                }
                return
            # Run sha256sum on remote server for the same relative paths
            # Build a file list for sha256sum to check
            file_list = " ".join(shlex.quote(f) for f in local_files.keys())
            cmd = f"cd {shlex.quote(dest)} && sha256sum {file_list} 2>/dev/null"

            try:
                r = await exec_ssh_command(server, cmd, timeout=60)
            except Exception as e:
                results[sid] = {
                    "server_id": sid, "server_name": server.name,
                    "error": f"SSH 连接失败: {e}",
                }
                return

            # Parse sha256sum output: "hash  filename"
            remote_files = {}
            if r["exit_code"] == 0:
                for line in r["stdout"].strip().split("\n"):
                    parts = line.split(None, 1)
                    if len(parts) == 2:
                        remote_files[parts[1]] = parts[0]

            matched = []
            missing = []
            mismatched = []
            for rel, local_hash in local_files.items():
                if rel not in remote_files:
                    missing.append(rel)
                elif remote_files[rel] != local_hash:
                    mismatched.append(rel)
                else:
                    matched.append(rel)

            results[sid] = {
                "server_id": sid,
                "server_name": server.name,
                "matched": len(matched),
                "missing": len(missing),
                "mismatched": len(mismatched),
                "missing_files": missing[:50],
                "mismatched_files": mismatched[:50],
            }

    await asyncio.gather(*[_verify_one(sid) for sid in payload.server_ids])

    # Audit
    await _audit_sync(
        "sync_verify", "sync", 0,
        f"推送校验: {payload.source_path} → {len(payload.server_ids)} 台服务器",
        admin.username, request,
    )

    return {"results": results, "total_local_files": len(local_files)}


# ── File Download / Read / Write ──

MAX_READ_FILE_SIZE = 1_000_000  # 1MB


@router.post("/download")
async def download_file(
    payload: FileDownload,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """P2-7: Download a file from a remote server via SFTP."""
    from fastapi.responses import StreamingResponse
    from server.infrastructure.database.server_repo import ServerRepositoryImpl
    from server.infrastructure.ssh.asyncssh_pool import sftp_session, ssh_pool

    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="服务器不存在")

    try:
        remote_path = normalize_remote_abs_path(payload.path)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    conn = await ssh_pool.acquire(server)
    # NOTE: connection is released INSIDE file_generator (after stream finishes),
    # not in a finally block here — releasing before the generator runs would close
    # the SFTP channel mid-download.
    try:
        async with sftp_session(conn) as sftp:
            # Check file exists and size
            try:
                stat = await sftp.stat(remote_path)
            except FileNotFoundError:
                await ssh_pool.release(server.id)
                raise HTTPException(status_code=404, detail="文件不存在") from None
            except Exception as e:
                await ssh_pool.release(server.id)
                raise HTTPException(status_code=502, detail=f"无法访问文件: {e}") from e

            if stat.type not in ("regular file", "unknown"):
                await ssh_pool.release(server.id)
                raise HTTPException(status_code=400, detail="只能下载文件，不能下载目录")

            # H-15: Download file size limit (100MB)
            MAX_DOWNLOAD_SIZE = 100 * 1024 * 1024
            if hasattr(stat, "size") and stat.size and stat.size > MAX_DOWNLOAD_SIZE:
                await ssh_pool.release(server.id)
                raise HTTPException(status_code=413, detail=f"文件大小 {stat.size} 字节超过下载限制 100MB")

            # H-02: Sanitize filename for Content-Disposition (RFC 6266)
            import re as _re
            filename = _re.sub(r'[^\w.\-]', '_', posix_basename(remote_path)) or "download"

            async def file_generator():
                try:
                    async with sftp.open(remote_path, "rb") as f:
                        while chunk := await f.read(65536):
                            yield chunk
                finally:
                    # Release after stream is fully consumed (or on client disconnect)
                    await ssh_pool.release(server.id)

            await _audit_sync(
                "file_download", "server", payload.server_id,
                f"下载文件: {remote_path}",
                admin.username, request,
            )

            return StreamingResponse(
                file_generator(),
                media_type="application/octet-stream",
                headers={"Content-Disposition": f'attachment; filename="{filename}"'},
            )
    except HTTPException:
        raise
    except Exception as e:
        await ssh_pool.release(server.id)
        raise HTTPException(status_code=502, detail=f"下载失败: {e}") from e


@router.post("/read-file")
async def read_file(
    payload: FileRead,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """P2-8: Read text file content from a remote server."""
    import shlex
    from server.infrastructure.database.server_repo import ServerRepositoryImpl
    from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback

    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="服务器不存在")

    try:
        remote_path = normalize_remote_abs_path(payload.path)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    path_q = shlex.quote(remote_path)
    size_result = await exec_ssh_command_with_fallback(
        server, f"stat -c%s {path_q}", timeout=5,
    )
    if size_result["exit_code"] != 0:
        raise HTTPException(status_code=404, detail="文件不存在或无法访问")

    try:
        file_size = int(size_result["stdout"].strip())
    except ValueError:
        raise HTTPException(status_code=502, detail="无法获取文件大小") from None

    if file_size > payload.max_size:
        raise HTTPException(
            status_code=413,
            detail=f"文件大小 {file_size} 字节超过限制 {payload.max_size} 字节",
        )

    result = await exec_ssh_command_with_fallback(server, f"cat {path_q}", timeout=15)
    if result["exit_code"] != 0:
        err = ((result.get("stderr") or "") + (result.get("stdout") or ""))[:200]
        raise HTTPException(status_code=502, detail=f"读取失败: {err}")

    await _audit_sync(
        "file_read", "server", payload.server_id,
        f"读取文件: {remote_path} ({file_size} bytes)",
        admin.username, request,
    )

    return {"content": result["stdout"], "size": file_size, "path": remote_path}


@router.post("/write-file")
async def write_file(
    payload: FileWrite,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Write text file content to a remote server (SFTP + SSH tee/mv/sudo fallback)."""
    import asyncssh
    from server.infrastructure.database.server_repo import ServerRepositoryImpl
    from server.infrastructure.ssh.asyncssh_pool import remote_write_bytes, ssh_pool

    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="服务器不存在")

    try:
        remote_path = normalize_remote_abs_path(payload.path)
    except PosixPathError as e:
        raise HTTPException(status_code=400, detail=str(e)) from e

    from server.utils.files_elevation import get_files_elevation

    content_bytes = payload.content.encode("utf-8")
    if len(content_bytes) > 5_000_000:
        raise HTTPException(status_code=413, detail="文件内容超过 5MB 限制")

    try:
        conn = await ssh_pool.acquire(server)
        try:
            await remote_write_bytes(
                conn,
                remote_path,
                content_bytes,
                elevation=get_files_elevation(server),
            )
        finally:
            await ssh_pool.release(server.id)
    except asyncssh.PermissionDenied:
        raise HTTPException(
            status_code=403,
            detail=(
                f"无写入权限: {remote_path}。"
                "请确认 SSH 用户对目录有写权限，或已配置免密 sudo（tee/mv）。"
            ),
        ) from None
    except FileNotFoundError:
        raise HTTPException(status_code=404, detail="目标路径不存在") from None
    except asyncssh.SFTPError as e:
        raise HTTPException(
            status_code=403,
            detail=(
                f"写入失败: {e}。"
                f"路径 {remote_path} — 若文件属主为 root，请为 SSH 用户配置免密 sudo 或修改文件权限。"
            ),
        ) from e
    except Exception as e:
        raise HTTPException(status_code=502, detail=f"SSH 连接失败: {e}") from e

    await _audit_sync(
        "file_write", "server", payload.server_id,
        f"写入文件: {remote_path} ({len(content_bytes)} bytes)",
        admin.username, request,
    )

    return {"success": True, "path": remote_path, "size": len(content_bytes)}


@router.post("/chmod")
async def chmod_file(
    payload: FileChmod,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Change file permissions (and optional owner) on a remote server."""
    import shlex
    from server.infrastructure.database.server_repo import ServerRepositoryImpl
    from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback

    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="服务器不存在")

    try:
        remote_path = normalize_remote_abs_path(payload.path)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
    from server.utils.files_chmod_policy import (
        RECURSIVE_CHMOD_TIMEOUT_SEC,
        SINGLE_CHMOD_TIMEOUT_SEC,
        assert_recursive_chmod_allowed,
    )

    path_q = shlex.quote(remote_path)
    recursive = bool(payload.recursive)
    if recursive:
        try:
            assert_recursive_chmod_allowed(remote_path)
        except ValueError as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc
        dir_check = await exec_ssh_command(server, f"test -d {path_q}", timeout=10)
        if dir_check.get("exit_code") != 0:
            raise HTTPException(status_code=400, detail="递归 chmod 仅适用于目录")

    chmod_prefix = "chmod -R " if recursive else "chmod "
    chown_prefix = "chown -R " if recursive else "chown "
    cmd_parts = [f"{chmod_prefix}{shlex.quote(payload.mode)} {path_q}"]
    if payload.owner:
        group = (payload.group or payload.owner).strip()
        owner_spec = shlex.quote(f"{payload.owner}:{group}")
        cmd_parts.append(f"{chown_prefix}{owner_spec} {path_q}")

    timeout = RECURSIVE_CHMOD_TIMEOUT_SEC if recursive else SINGLE_CHMOD_TIMEOUT_SEC
    result = await exec_ssh_command_with_fallback(server, " && ".join(cmd_parts), timeout=timeout)
    if result["exit_code"] != 0:
        err = ((result.get("stderr") or "") + (result.get("stdout") or ""))[:300]
        raise HTTPException(
            status_code=403 if "permission denied" in err.lower() else 502,
            detail=err or "chmod 失败",
        )

    audit_bits = [f"chmod {'-R ' if recursive else ''}{payload.mode}"]
    if payload.owner:
        audit_bits.append(
            f"chown {'-R ' if recursive else ''}{payload.owner}:{payload.group or payload.owner}"
        )
    await _audit_sync(
        "file_permission_change",
        "server",
        payload.server_id,
        f"{' '.join(audit_bits)} {remote_path}",
        admin.username,
        request,
    )

    return {
        "success": True,
        "path": remote_path,
        "mode": payload.mode,
        "owner": payload.owner,
        "group": payload.group,
        "recursive": recursive,
        "elevated": bool(result.get("elevated")),
    }


@router.post("/compress")
async def compress_files(
    payload: FileCompress,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Compress files into tar.gz or zip archive."""
    from server.infrastructure.database.server_repo import ServerRepositoryImpl
    from server.infrastructure.ssh.remote_archive import run_remote_compress

    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="服务器不存在")

    try:
        dest = normalize_remote_abs_path(payload.dest)
        paths = [normalize_remote_abs_path(p) for p in payload.paths]
    except PosixPathError as e:
        raise HTTPException(status_code=400, detail=str(e)) from e

    result = await run_remote_compress(
        server, paths, dest, payload.format, timeout=120,
    )
    if result["exit_code"] != 0:
        err = ((result.get("stderr") or "") + (result.get("stdout") or ""))[:400]
        raise HTTPException(
            status_code=403 if "permission denied" in err.lower() else 502,
            detail=(
                f"压缩失败: {err}"
                if err.strip()
                else "压缩失败。请确认对目标目录有写权限，或配置免密 sudo。"
            ),
        )

    await _audit_sync(
        "file_compress", "server", payload.server_id,
        f"压缩 {len(payload.paths)} 个文件 → {dest}",
        admin.username, request,
    )

    return {"success": True, "dest": dest, "format": payload.format}


@router.post("/decompress")
async def decompress_file(
    payload: FileDecompress,
    request: Request,
    admin: Admin = Depends(get_current_admin),
):
    """Decompress a tar.gz or zip archive."""
    import shlex
    from server.infrastructure.database.server_repo import ServerRepositoryImpl
    from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback

    session = request.state.db
    server = await ServerRepositoryImpl(session).get_by_id(payload.server_id)
    if not server:
        raise HTTPException(status_code=404, detail="服务器不存在")

    try:
        archive_path = normalize_remote_abs_path(payload.path)
        dest_dir = normalize_remote_abs_path(payload.dest)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    path = shlex.quote(archive_path)
    dest = shlex.quote(dest_dir)

    if archive_path.endswith(".tar.gz") or archive_path.endswith(".tgz"):
        cmd = f"tar xzf {path} -C {dest}"
    elif archive_path.endswith(".zip"):
        cmd = f"unzip -o {path} -d {dest}"
    else:
        raise HTTPException(status_code=400, detail="仅支持 .tar.gz/.tgz 和 .zip 格式")

    result = await exec_ssh_command_with_fallback(server, cmd, timeout=120)
    if result["exit_code"] != 0:
        raise HTTPException(status_code=502, detail=f"解压失败: {(result.get('stderr') or '')[:300]}")

    await _audit_sync(
        "file_decompress", "server", payload.server_id,
        f"解压 {archive_path} → {dest_dir}",
        admin.username, request,
    )

    return {"success": True, "path": archive_path, "dest": dest_dir}
