"""M-21 Persistent Shell — 상태 유지 셸 유틸리티.

subprocess.Popen으로 단일 셸 프로세스를 유지하며 CWD/환경변수 상태를 이어갑니다.
sentinels 패턴(echo "SENTINEL"$?)으로 exit code를 감지합니다.

Usage:
    from utils.persistent_shell import PersistentShell

    with PersistentShell() as sh:
        sh.run("cd /tmp")
        sh.run("export MY_VAR=hello")
        out, code = sh.run("echo $MY_VAR")  # ("hello", 0)
        cwd = sh.get_cwd()
"""

import os
import select
import subprocess
import time
import uuid
from types import TracebackType
from typing import Optional

# sentinel 태그 — 명령 종료/exit code 감지용
_SENTINEL_PREFIX = "__SHELL_DONE__"
_SENTINEL_TPL = f"{_SENTINEL_PREFIX}_{{tag}}_EXIT_"


class PersistentShell:
    """단일 셸 프로세스를 유지하며 run() 호출마다 상태(CWD, 환경변수)가 이어지는 클래스."""

    def __init__(self, shell: str = "/bin/bash", timeout: float = 30.0) -> None:
        self.shell = shell
        self.timeout = timeout
        self._proc: Optional[subprocess.Popen[bytes]] = None
        self._closed = False
        self._proc = subprocess.Popen(
            [self.shell],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            bufsize=0,
            env=os.environ.copy(),
        )

    def _write(self, data: str) -> None:
        if self._proc is None or self._proc.stdin is None:
            raise RuntimeError("Shell process is not running")
        self._proc.stdin.write(data.encode("utf-8"))
        self._proc.stdin.flush()

    def _read_until(self, sentinel: str, timeout: float) -> str:
        """sentinel 문자열이 나타날 때까지 stdout을 읽어 반환."""
        if self._proc is None or self._proc.stdout is None:
            return ""
        buf = b""
        deadline = time.monotonic() + timeout
        sentinel_bytes = sentinel.encode("utf-8")
        while True:
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                break
            rlist, _, _ = select.select([self._proc.stdout], [], [], min(remaining, 0.1))
            if rlist:
                chunk = os.read(self._proc.stdout.fileno(), 4096)
                if not chunk:
                    break
                buf += chunk
                if sentinel_bytes in buf:
                    break
        return buf.decode("utf-8", errors="replace")

    def run(self, cmd: str, timeout: Optional[float] = None) -> tuple[str, int]:
        """명령을 실행하고 (출력, exit_code) 튜플을 반환합니다."""
        if self._closed or self._proc is None:
            return ("", -1)

        eff_timeout = timeout if timeout is not None else self.timeout

        # exit 명령: 프로세스가 종료되므로 별도 처리
        stripped = cmd.strip()
        if stripped.startswith("exit"):
            parts = stripped.split(maxsplit=1)
            try:
                code = int(parts[1]) if len(parts) > 1 else 0
            except ValueError:
                code = 0
            try:
                self._write(f"{cmd}\n")
            except Exception:
                pass
            self._closed = True
            return ("", code)

        sentinel = _SENTINEL_TPL.format(tag=uuid.uuid4().hex)
        try:
            self._write(f"{cmd}\necho '{sentinel}'$?\n")
        except BrokenPipeError:
            self._closed = True
            return ("", -1)

        raw = self._read_until(sentinel, eff_timeout)

        exit_code = -1
        output_lines: list[str] = []
        for line in raw.splitlines():
            if sentinel in line:
                idx = line.index(sentinel)
                prefix = line[:idx].strip()
                if prefix:
                    output_lines.append(prefix)
                try:
                    exit_code = int(line[idx + len(sentinel) :].strip())
                except ValueError:
                    exit_code = 0
                break
            output_lines.append(line)

        return ("\n".join(output_lines).strip(), exit_code)

    def get_cwd(self) -> str:
        """현재 작업 디렉토리를 반환합니다."""
        out, _ = self.run("pwd")
        return out.strip()

    def get_env(self, key: str) -> Optional[str]:
        """환경변수 값을 조회합니다. 미설정 시 None 반환."""
        sentinel_key = f"__ENV_SENTINEL_{uuid.uuid4().hex}__"
        out, _ = self.run(f'echo "${{{key}:-{sentinel_key}}}"')
        value = out.strip()
        if value == sentinel_key or value == "":
            check, _ = self.run(f'[ -z "${{{key}+x}}" ] && echo "UNSET" || echo "SET"')
            return None if "UNSET" in check else ""
        return value

    def close(self) -> None:
        """셸 프로세스를 종료합니다."""
        if self._closed:
            return
        self._closed = True
        if self._proc is not None:
            try:
                if self._proc.stdin:
                    self._proc.stdin.close()
            except Exception:
                pass
            try:
                self._proc.terminate()
                self._proc.wait(timeout=3.0)
            except Exception:
                try:
                    self._proc.kill()
                except Exception:
                    pass

    def __enter__(self) -> "PersistentShell":
        return self

    def __exit__(
        self,
        exc_type: Optional[type[BaseException]],
        exc_val: Optional[BaseException],
        exc_tb: Optional[TracebackType],
    ) -> None:
        self.close()
