#!/usr/bin/env python3
"""
Filesystem helpers for the workflow scripts.

Keep this module generic and side-effect minimal (no business logic / patterns).
"""

from __future__ import annotations

import shutil
import subprocess
import sys
from pathlib import Path
from typing import List, Optional


def run(cmd: List[str], cwd: Optional[Path] = None):
    print(f"[RUN] {' '.join(cmd)}")
    result = subprocess.run(
        cmd,
        cwd=str(cwd) if cwd else None,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
    )
    sys.stdout.write(result.stdout)
    if result.returncode != 0:
        raise RuntimeError(f"命令执行失败: {' '.join(cmd)}\n{result.stdout}")
    return result


def ensure_dir(p: Path) -> Path:
    p.mkdir(parents=True, exist_ok=True)
    return p


def find_named_ancestor(path: Path, name: str) -> Optional[Path]:
    try:
        resolved = path.resolve()
    except Exception:
        resolved = path
    for candidate in (resolved, *resolved.parents):
        if candidate.name == name:
            return candidate
    return None


def find_prefixed_ancestor(path: Path, prefix: str) -> Optional[Path]:
    try:
        resolved = path.resolve()
    except Exception:
        resolved = path
    for candidate in (resolved, *resolved.parents):
        if candidate.name.startswith(prefix):
            return candidate
    return None


def unique_dst_path(dst: Path) -> Path:
    if not dst.exists():
        return dst
    for i in range(1, 1000):
        if dst.suffix:
            cand = dst.with_name(f"{dst.stem}_{i}{dst.suffix}")
        else:
            cand = dst.with_name(f"{dst.name}_{i}")
        if not cand.exists():
            return cand
    raise RuntimeError(f"无法生成唯一目标路径: {dst}")


def move_to_path(src: Path, dst: Path) -> Optional[Path]:
    if not src.exists():
        return None
    ensure_dir(dst.parent)
    if src.resolve() == dst.resolve():
        return dst
    dst = unique_dst_path(dst)
    shutil.move(str(src), str(dst))
    return dst


def move(src: Path, dst_dir: Path) -> Optional[Path]:
    if not src.exists():
        return None
    ensure_dir(dst_dir)
    return move_to_path(src, dst_dir / src.name)


def move_preserve(src: Path, src_root: Path, dst_root: Path) -> Optional[Path]:
    """Move src under dst_root keeping its relative path to src_root."""
    if not src.exists():
        return None
    rel = src.relative_to(src_root)
    return move_to_path(src, dst_root / rel)


def copy_to_path(src: Path, dst: Path) -> Optional[Path]:
    if not src.exists():
        return None
    ensure_dir(dst.parent)
    if src.resolve() == dst.resolve():
        return dst
    dst = unique_dst_path(dst)
    if src.is_dir():
        shutil.copytree(str(src), str(dst))
    else:
        shutil.copy2(str(src), str(dst))
    return dst


def copy(src: Path, dst_dir: Path) -> Optional[Path]:
    if not src.exists():
        return None
    ensure_dir(dst_dir)
    return copy_to_path(src, dst_dir / src.name)


def copy_preserve(src: Path, src_root: Path, dst_root: Path) -> Optional[Path]:
    if not src.exists():
        return None
    rel = src.relative_to(src_root)
    return copy_to_path(src, dst_root / rel)


def prune_empty_dirs(root: Path) -> List[Path]:
    """Remove empty directories under root, deepest paths first."""
    if not root.exists() or not root.is_dir():
        return []

    removed: List[Path] = []
    dirs = sorted(
        (p for p in root.rglob("*") if p.is_dir()),
        key=lambda p: len(p.parts),
        reverse=True,
    )
    for d in dirs:
        try:
            d.rmdir()
        except OSError:
            continue
        removed.append(d)
    return removed
