from __future__ import annotations

import asyncio
import os
import re
import shutil
import sys
from pathlib import Path
from typing import Any, Callable, Awaitable

from aicd.cli_encoding import subprocess_env_utf8
from aicd.tools.path_policy import PathPolicy


def resolve_windows_comspec() -> str:
    """``%ComSpec%``，不可用时回退到 PATH 中的 ``cmd.exe``。"""
    for key in ("ComSpec", "COMSPEC"):
        c = os.environ.get(key)
        if c:
            try:
                if Path(c).expanduser().is_file():
                    return str(Path(c).resolve())
            except OSError:
                pass
            return c
    w = shutil.which("cmd.exe")
    return w if w else "cmd.exe"


def windows_cmd_argv(command_line: str) -> list[str]:
    """
    通过 CMD 执行一行命令：``/d`` 禁用 AutoRun；先 ``chcp 65001`` 再执行，与 UTF-8 环境一致。
    ``command_line`` 将传给 ``cmd /c`` 的剩余部分（勿再外包一层引号除非语法需要）。
    """
    comspec = resolve_windows_comspec()
    inner = f"chcp 65001>nul & {command_line}"
    return [comspec, "/d", "/c", inner]


def windows_powershell_argv(command_line: str, *, flavor: str = "powershell") -> list[str]:
    """``flavor``: ``powershell``（Windows PowerShell 5）或 ``pwsh``（PowerShell 7+）。"""
    name = "pwsh.exe" if flavor.lower().strip() == "pwsh" else "powershell.exe"
    exe = shutil.which(name) or shutil.which(name.replace(".exe", "")) or name
    return [
        exe,
        "-NoProfile",
        "-NonInteractive",
        "-ExecutionPolicy",
        "Bypass",
        "-Command",
        command_line,
    ]


def shell_argv_from_user_input(
    *,
    command: str | None = None,
    argv: list[str] | None = None,
    shell: str | None = None,
) -> list[str]:
    """
    - ``argv``：直接 ``execve`` 风格参数列表（不经 CMD/sh 解释），适用于含空格路径、``git``/``python`` 等。
    - ``command``：在 Windows 上经 ``shell``（默认 CMD）执行的一行命令；在 Unix 上为 ``sh -c``。
    """
    if argv is not None:
        if len(argv) == 0:
            raise ValueError("argv must be a non-empty array of strings")
        return [str(x) for x in argv]
    if command is None or not str(command).strip():
        raise ValueError("non-empty command or argv is required")
    line = str(command).strip()
    if sys.platform != "win32":
        return ["/bin/sh", "-c", line]
    sh = (shell or "cmd").strip().lower()
    if sh in ("powershell", "ps"):
        return windows_powershell_argv(line, flavor="powershell")
    if sh == "pwsh":
        return windows_powershell_argv(line, flavor="pwsh")
    return windows_cmd_argv(line)


def default_shell_argv(command: str) -> list[str]:
    """兼容旧调用：等价于 ``shell_argv_from_user_input(command=command)``。"""
    return shell_argv_from_user_input(command=command)


def warn_shell_python_c_unbalanced_quotes(command: str) -> str | None:
    """
    检测一行 shell 里 ``python -c "..."`` 的双引号是否成对（粗略启发式）。
    """
    s = str(command).strip()
    if not re.search(r"(?i)python(?:\.exe)?\b", s):
        return None
    if not re.search(r"(?i)(^|[\s&(])-c(\s+|$)", s):
        return None
    m = re.search(r"(?i)-c\s+(.*)$", s, re.DOTALL)
    if not m:
        return None
    tail = m.group(1).strip()
    if not tail:
        return "python -c 缺少代码参数"
    if tail.startswith("'"):
        return None
    if tail.count('"') % 2 == 1:
        return (
            "检测到 python -c 后的双引号未成对（CMD 下常与 && 混用导致截断，出现 SyntaxError）。"
            "请改用 **script_run**（kind=py）、或 **fs_write_file** 写入 .py 后用 **run_command** 的 "
            "**argv**：[\"python\", \"脚本路径\"]。避免在一条 CMD 链里塞长段 python -c。"
        )
    return None


def warn_shell_windows_python_c_with_and_chain(command: str) -> str | None:
    """
    Windows CMD 下 ``cd ... && python -c "..."`` 即使源码里引号成对，也常把 ``-c`` 实参截断，
    表现为 ``SyntaxError: unterminated string literal``（例如只收到 ``"import``）。
    """
    if sys.platform != "win32":
        return None
    s = str(command).strip()
    if "&&" not in s:
        return None
    if not re.search(r"(?i)python(?:\.exe)?\b", s):
        return None
    if not re.search(r"(?i)-c\s+", s):
        return None
    return (
        "在 Windows CMD 中，请勿把 ``cd ... && python -c \"...\"`` 写在同一行（``&&`` 与双引号组合易截断参数，触发 SyntaxError）。"
        "请改用 **script_run**（kind=py）、分两次 **run_command**（先 cd /d，再 python），"
        "或 **fs_write_file** 写入 .py 后用 **argv**：[\"python\", \"脚本路径\"]。"
    )


_FINDSTR_COMMON_NOEXT_FILES = frozenset(
    x.lower()
    for x in (
        "LICENSE",
        "README",
        "COPYING",
        "AUTHORS",
        "CHANGELOG",
        "Makefile",
        "Dockerfile",
        "Procfile",
        "CONTRIBUTING",
        "GITATTRIBUTES",
        "GITIGNORE",
    )
)


def warn_shell_windows_findstr_maybe_dir_operand(command: str) -> str | None:
    """
    Windows ``findstr`` 把「搜索串」后面的实参当作**要打开的文件**；若写成子目录名而无通配符，
    会得到 ``FINDSTR: Cannot open <name>``（并非文件被锁或未释放）。
    """
    if sys.platform != "win32":
        return None
    s = str(command).strip()
    if not re.search(r'(?i)\bfindstr\b', s):
        return None
    if "*" in s:
        return None
    tail = re.split(r"[&|]+", s)[-1].strip()
    toks = tail.split()
    if len(toks) < 3:
        return None
    last = toks[-1].strip('"')
    if not last or last.startswith("/") or last.startswith("-"):
        return None
    if "." in last or last.endswith("\\") or last.endswith("/"):
        return None
    if last.lower() in _FINDSTR_COMMON_NOEXT_FILES:
        return None
    return (
        "Windows **findstr** 将把最后的参数当作**单个文件**；若为**文件夹**会报 *Cannot open*（不是资源锁）。"
        "请改为 `cd /d <dir> && findstr /s /i <pattern> *.*`，或 `findstr /s /i <pattern> <dir>\\\\*.*`，"
        "或改用 **rg** / **fs_search_regex**。"
    )


def warn_shell_risky_python_c(command: str) -> str | None:
    """合并：未成对引号，或 ``&&`` + ``python -c``（仅 Windows）。"""
    return warn_shell_python_c_unbalanced_quotes(
        command
    ) or warn_shell_windows_python_c_with_and_chain(command)


def warn_shell_preflight(command: str) -> str | None:
    """``task_shell`` / ``run_command`` 提交前：Python 引号风险 + Windows findstr 误用提示。"""
    return warn_shell_risky_python_c(command) or warn_shell_windows_findstr_maybe_dir_operand(
        command
    )


async def kill_windows_process_tree(
    root_pid: int, *, timeout_sec: float = 15.0
) -> dict[str, Any]:
    """
    结束 ``root_pid`` 及其**所有后代**（``taskkill /T /F``）。

    用于 Windows 上通过 CMD/PowerShell 拉起的 GUI 等子进程：仅 ``terminate`` 外层 ``cmd.exe``
    时，子进程常会残留；整树结束后可一并清理。
    """
    pid = int(root_pid)
    try:
        tk = await asyncio.create_subprocess_exec(
            "taskkill",
            "/PID",
            str(pid),
            "/T",
            "/F",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
    except FileNotFoundError:
        return {"ok": False, "error": "taskkill not found on PATH"}
    try:
        out_b, err_b = await asyncio.wait_for(
            tk.communicate(), timeout=timeout_sec
        )
    except TimeoutError:
        tk.kill()
        return {"ok": False, "error": "taskkill communicate timeout"}
    so = (out_b or b"").decode("utf-8", errors="replace").strip()
    se = (err_b or b"").decode("utf-8", errors="replace").strip()
    rc = tk.returncode if tk.returncode is not None else -1
    low = se.lower()
    # 目标已退出时 taskkill 常非 0，仍视为可接受
    benign = rc == 0 or any(
        x in low
        for x in (
            "not found",
            "could not find",
            "no tasks",
            "no matching",
        )
    ) or ("没有运行" in se and "无法终止" not in se)
    return {
        "ok": benign,
        "returncode": rc,
        "stdout_tail": so[:800],
        "stderr_tail": se[:800],
    }


async def _terminate_subprocess(
    proc: asyncio.subprocess.Process, *, kill_subtree: bool = False
) -> None:
    """取消外部命令时尽量结束子进程；Windows 上可选整树结束（``task_shell`` 取消 GUI 用）。"""
    if proc.returncode is not None:
        return
    pid = proc.pid
    if sys.platform == "win32" and kill_subtree:
        await kill_windows_process_tree(pid)
    else:
        proc.terminate()
        try:
            await asyncio.wait_for(proc.wait(), timeout=5.0)
        except TimeoutError:
            proc.kill()
            try:
                await asyncio.wait_for(proc.wait(), timeout=2.0)
            except TimeoutError:
                pass
    if proc.returncode is None:
        try:
            proc.kill()
        except (ProcessLookupError, OSError):
            return
        try:
            await asyncio.wait_for(proc.wait(), timeout=2.0)
        except TimeoutError:
            pass


class ProcessTool:
    def __init__(self, policy: PathPolicy, cwd: Path | None = None) -> None:
        self._policy = policy
        self._cwd = cwd or Path.cwd()

    async def run_command(
        self,
        argv: list[str],
        *,
        cwd: str | None = None,
        env: dict[str, str] | None = None,
        timeout_sec: float | None = None,
        on_line: Callable[[str], Awaitable[None]] | None = None,
        on_spawned: Callable[[int], Awaitable[None]] | None = None,
        kill_subtree_on_cancel: bool | None = None,
    ) -> dict[str, Any]:
        wd = Path(cwd).resolve() if cwd else self._cwd
        self._policy.check_allowed(wd)
        proc = await asyncio.create_subprocess_exec(
            *argv,
            cwd=str(wd),
            env=subprocess_env_utf8(env),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        if on_spawned is not None:
            await on_spawned(proc.pid)
        subtree = (
            kill_subtree_on_cancel
            if kill_subtree_on_cancel is not None
            else (sys.platform == "win32")
        )
        out_chunks: list[str] = []
        err_chunks: list[str] = []

        async def pump(
            stream: asyncio.StreamReader | None, sink: list[str], prefix: str
        ) -> None:
            if stream is None:
                return
            while True:
                line = await stream.readline()
                if not line:
                    break
                s = line.decode("utf-8", errors="replace").rstrip("\r\n")
                sink.append(s)
                if on_line:
                    await on_line(f"{prefix}{s}")

        async def _pump_all() -> None:
            await asyncio.gather(
                pump(proc.stdout, out_chunks, "[stdout] "),
                pump(proc.stderr, err_chunks, "[stderr] "),
                proc.wait(),
            )

        try:
            if timeout_sec:
                await asyncio.wait_for(_pump_all(), timeout=timeout_sec)
            else:
                await _pump_all()
        except asyncio.CancelledError:
            await _terminate_subprocess(proc, kill_subtree=subtree)
            raise
        except TimeoutError:
            await _terminate_subprocess(proc, kill_subtree=subtree)
            return {
                "ok": False,
                "error": "timeout",
                "pid": proc.pid,
                "stdout": "\n".join(out_chunks[-200:]),
                "stderr": "\n".join(err_chunks[-200:]),
            }
        rc = proc.returncode if proc.returncode is not None else -1
        return {
            "ok": rc == 0,
            "returncode": rc,
            "pid": proc.pid,
            "stdout": "\n".join(out_chunks),
            "stderr": "\n".join(err_chunks),
        }
