from __future__ import annotations

import subprocess
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Awaitable, Callable

from aicd.tools.process_tool import ProcessTool, windows_cmd_argv
from aicd.tools.script_backup import archive_script_to_bak, format_script_run_filename


class ScriptTool:
    def __init__(
        self,
        scripts_dir: Path,
        process: ProcessTool,
    ) -> None:
        self._dir = scripts_dir
        self._dir.mkdir(parents=True, exist_ok=True)
        self._process = process

    def _path_for_run(
        self, kind: str, ext: str, related_doc: str | None
    ) -> Path:
        u = uuid.uuid4().hex[:8]
        name = format_script_run_filename(
            datetime.now(timezone.utc), kind, ext, related_doc, u
        )
        return self._dir / name

    async def _run_with_backup(
        self,
        script_path: Path,
        argv: list[str],
        *,
        on_line: Callable[[str], Awaitable[None]] | None,
        timeout_sec: float | None,
        backup_dir: Path | None,
        policy_check: Callable[[Path], None] | None,
    ) -> dict[str, Any]:
        meta: dict[str, Any] = {"script_exec_path": str(script_path.resolve())}
        out = await self._process.run_command(
            argv,
            timeout_sec=timeout_sec,
            on_line=on_line,
        )
        merged: dict[str, Any] = (
            {**out, **meta} if isinstance(out, dict) else {**meta, "raw": out}
        )
        if backup_dir is not None and policy_check is not None:
            arch = archive_script_to_bak(script_path, backup_dir, policy_check)
            merged = {**merged, **arch}
        return merged

    async def run_python(
        self,
        source: str,
        *,
        on_line: Callable[[str], Awaitable[None]] | None = None,
        timeout_sec: float | None = 300,
        related_doc: str | None = None,
        backup_dir: Path | None = None,
        policy_check: Callable[[Path], None] | None = None,
    ) -> dict[str, Any]:
        p = self._path_for_run("py", ".py", related_doc)
        p.write_text(source, encoding="utf-8")
        py = sys.executable
        return await self._run_with_backup(
            p,
            [py, str(p)],
            on_line=on_line,
            timeout_sec=timeout_sec,
            backup_dir=backup_dir,
            policy_check=policy_check,
        )

    async def run_powershell(
        self,
        source: str,
        *,
        on_line: Callable[[str], Awaitable[None]] | None = None,
        timeout_sec: float | None = 300,
        related_doc: str | None = None,
        backup_dir: Path | None = None,
        policy_check: Callable[[Path], None] | None = None,
    ) -> dict[str, Any]:
        if sys.platform != "win32":
            return {"ok": False, "error": "PowerShell runner only on Windows"}
        p = self._path_for_run("ps1", ".ps1", related_doc)
        p.write_text(source, encoding="utf-8")
        return await self._run_with_backup(
            p,
            [
                "powershell.exe",
                "-NoProfile",
                "-ExecutionPolicy",
                "Bypass",
                "-File",
                str(p),
            ],
            on_line=on_line,
            timeout_sec=timeout_sec,
            backup_dir=backup_dir,
            policy_check=policy_check,
        )

    async def run_cmd(
        self,
        batch: str,
        *,
        on_line: Callable[[str], Awaitable[None]] | None = None,
        timeout_sec: float | None = 300,
        related_doc: str | None = None,
        backup_dir: Path | None = None,
        policy_check: Callable[[Path], None] | None = None,
    ) -> dict[str, Any]:
        if sys.platform != "win32":
            return {"ok": False, "error": "CMD runner only on Windows"}
        p = self._path_for_run("cmd", ".cmd", related_doc)
        p.write_text(batch, encoding="utf-8")
        quoted = subprocess.list2cmdline([str(p)])
        return await self._run_with_backup(
            p,
            windows_cmd_argv(quoted),
            on_line=on_line,
            timeout_sec=timeout_sec,
            backup_dir=backup_dir,
            policy_check=policy_check,
        )

    async def run_bash(
        self,
        source: str,
        *,
        bash_path: str = "bash",
        on_line: Callable[[str], Awaitable[None]] | None = None,
        timeout_sec: float | None = 300,
        related_doc: str | None = None,
        backup_dir: Path | None = None,
        policy_check: Callable[[Path], None] | None = None,
    ) -> dict[str, Any]:
        p = self._path_for_run("bash", ".sh", related_doc)
        p.write_text(source, encoding="utf-8")
        return await self._run_with_backup(
            p,
            [bash_path, str(p)],
            on_line=on_line,
            timeout_sec=timeout_sec,
            backup_dir=backup_dir,
            policy_check=policy_check,
        )
