import asyncio
import os

MAX_OUTPUT = 50_000
INSTALL_TIMEOUT = 120

# Detection priority order: poetry > cargo > npm > pip
_DETECTORS = [
    ("poetry", "pyproject.toml", lambda content: "[tool.poetry]" in content),
    ("cargo", "Cargo.toml", None),
    ("npm", "package.json", None),
    ("pip", "requirements.txt", None),
    ("pip", "pyproject.toml", lambda content: "[project]" in content),
]

_INSTALL_CMDS = {
    "pip": "pip install -r requirements.txt",
    "npm": "npm install",
    "poetry": "poetry install",
    "cargo": "cargo build",
}

_ADD_CMDS = {
    "pip": ["pip", "install"],
    "npm": ["npm", "install"],
    "poetry": ["poetry", "add"],
    "cargo": ["cargo", "add"],
}

_LIST_CMDS = {
    "pip": "pip list",
    "npm": "npm list --depth=0",
    "poetry": "poetry show",
    "cargo": "cargo metadata --format-version=1 --no-deps",
}


def _detect_manager(directory: str) -> str | None:
    """Detect package manager from project files."""
    for name, filename, check_fn in _DETECTORS:
        filepath = os.path.join(directory, filename)
        if os.path.isfile(filepath):
            if check_fn is None:
                return name
            with open(filepath) as f:
                if check_fn(f.read()):
                    return name
    return None


import re

_SAFE_PACKAGE_RE = re.compile(r"^[a-zA-Z0-9._\-\[\],>=<! @/]+$")


async def _run_cmd(cmd: str | list[str], directory: str) -> dict:
    """Run a command and return result dict. Accepts string or arg list."""
    try:
        if isinstance(cmd, list):
            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                cwd=directory,
            )
        else:
            proc = await asyncio.create_subprocess_shell(
                cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                cwd=directory,
            )
        stdout_bytes, stderr_bytes = await asyncio.wait_for(
            proc.communicate(), timeout=INSTALL_TIMEOUT
        )
    except asyncio.TimeoutError:
        proc.kill()
        await proc.communicate()
        return {"output": "", "error": f"Command timed out after {INSTALL_TIMEOUT}s"}

    stdout = stdout_bytes.decode("utf-8", errors="replace")
    stderr = stderr_bytes.decode("utf-8", errors="replace")

    if len(stdout) > MAX_OUTPUT:
        stdout = stdout[:MAX_OUTPUT] + "\n... (truncated)"

    if proc.returncode != 0:
        return {"output": stdout, "error": f"Exit code {proc.returncode}: {stderr}"}
    return {"output": stdout, "error": ""}


async def handler(params: dict) -> dict:
    """Manage project dependencies."""
    action = params["action"]
    directory = params.get("directory") or os.getcwd()

    if action == "detect":
        manager = _detect_manager(directory)
        if manager:
            return {"manager": manager, "output": f"Detected: {manager}", "error": ""}
        return {"manager": "", "output": "", "error": "No package manager detected. No requirements.txt, package.json, pyproject.toml, or Cargo.toml found."}

    # All other actions need a detected manager
    manager = _detect_manager(directory)
    if not manager:
        return {"manager": "", "output": "", "error": "No package manager detected."}

    if action == "install":
        cmd = _INSTALL_CMDS.get(manager)
        if not cmd:
            return {"manager": manager, "output": "", "error": f"No install command for {manager}"}
        result = await _run_cmd(cmd, directory)
        result["manager"] = manager
        return result

    if action == "add":
        package = params.get("package", "")
        if not package:
            return {"manager": manager, "output": "", "error": "Package name required for 'add' action."}
        if not _SAFE_PACKAGE_RE.match(package):
            return {"manager": manager, "output": "", "error": f"Invalid package name: {package!r}. Only alphanumeric, hyphens, dots, brackets, and version specifiers allowed."}
        cmd_base = _ADD_CMDS.get(manager)
        if not cmd_base:
            return {"manager": manager, "output": "", "error": f"No add command for {manager}"}
        result = await _run_cmd(cmd_base + [package], directory)
        result["manager"] = manager
        return result

    if action == "list":
        cmd = _LIST_CMDS.get(manager)
        if not cmd:
            return {"manager": manager, "output": "", "error": f"No list command for {manager}"}
        result = await _run_cmd(cmd, directory)
        result["manager"] = manager
        return result

    return {"manager": "", "output": "", "error": f"Unknown action: {action}. Use: detect, install, add, list."}
