from __future__ import annotations

import os
import subprocess
import time
from pathlib import Path
from typing import Any, Protocol, cast

from cybervisor.adapters.types import AdapterDescriptor, LaunchRequest, PreflightRequirements, RunningProcess
from cybervisor.stream_logging import CanonicalLogEvent, render_canonical_event


class AgentAdapter(Protocol):
    descriptor: AdapterDescriptor

    def start(self, request: LaunchRequest) -> RunningProcess:
        ...

    def parse_output_line(self, line: str) -> list[CanonicalLogEvent]:
        ...

    def flush_pending_events(self) -> list[CanonicalLogEvent]:
        ...

    def should_suppress_event(self, event: CanonicalLogEvent, saw_hook_feedback: bool) -> bool:
        ...

    def preflight_requirements(self) -> PreflightRequirements:
        ...

    def settings_path(self) -> Path | None:
        ...

    def install_hook_settings(self, payload: dict[str, Any], runtime_config_path: Path) -> dict[str, Any]:
        ...

    def remove_hook_settings(self, payload: dict[str, Any], runtime_config_path: Path) -> dict[str, Any]:
        ...


class BaseAdapter:
    def _is_container(self) -> bool:
        """Detect if we are running inside a container."""
        if os.path.exists("/.dockerenv"):
            return True
        try:
            with open("/proc/self/cgroup", "r", encoding="utf-8") as f:
                for line in f:
                    if "docker" in line or "containerd" in line:
                        return True
        except (FileNotFoundError, PermissionError):
            pass
        return False

    def parse_output_line(self, line: str) -> list[CanonicalLogEvent]:
        if not line:
            return []
        return [CanonicalLogEvent(kind="plain_text", text=line)]

    def flush_pending_events(self) -> list[CanonicalLogEvent]:
        return []

    def should_suppress_event(self, event: CanonicalLogEvent, saw_hook_feedback: bool) -> bool:
        del event, saw_hook_feedback
        return False

    def preflight_requirements(self) -> PreflightRequirements:
        return PreflightRequirements()

    def settings_path(self) -> Path | None:
        return None

    def install_hook_settings(self, payload: dict[str, Any], runtime_config_path: Path) -> dict[str, Any]:
        del runtime_config_path
        return payload

    def remove_hook_settings(self, payload: dict[str, Any], runtime_config_path: Path) -> dict[str, Any]:
        del runtime_config_path
        return payload


class CompletedProcessHandle:
    def __init__(self, output: str, exit_code: int = 0, delay_seconds: float = 0.0) -> None:
        self.pid: int | None = None
        self.process_group_id: int | None = None
        self._output = output
        self._exit_code = exit_code
        self._delay_seconds = delay_seconds

    def wait(self) -> tuple[int, str]:
        if self._delay_seconds > 0:
            time.sleep(self._delay_seconds)
        return self._exit_code, self._output


class StreamingSubprocessHandle:
    def __init__(self, process: subprocess.Popen[str], request: LaunchRequest, adapter: AgentAdapter) -> None:
        self._process = process
        self._request = request
        self._adapter = adapter
        self.pid: int | None = process.pid
        self.process_group_id: int | None = (
            os.getpgid(process.pid) if process.pid is not None and hasattr(os, "getpgid") else None
        )

    def wait(self) -> tuple[int, str]:
        transcript_parts: list[str] = []
        saw_hook_feedback = False
        log_path = Path(self._request.log_file)
        log_path.parent.mkdir(parents=True, exist_ok=True)
        assert self._process.stdout is not None

        def _log_events(events: list[CanonicalLogEvent]) -> None:
            nonlocal saw_hook_feedback
            for event in events:
                if event.kind == "hook_feedback":
                    saw_hook_feedback = True
                if self._adapter.should_suppress_event(event, saw_hook_feedback):
                    continue
                rendered = render_canonical_event(event)
                if rendered:
                    self._request.logger.log_to_stderr(
                        f"[{self._request.stage_name}][{self._adapter.descriptor.name}] {rendered}"
                    )

        with log_path.open("w", encoding="utf-8") as log_handle:
            for line in self._process.stdout:
                transcript_parts.append(line)
                log_handle.write(line)
                _log_events(self._adapter.parse_output_line(line.rstrip()))
            _log_events(self._adapter.flush_pending_events())
        exit_code = self._process.wait()
        return exit_code, "".join(transcript_parts).rstrip("\n")


class BaseSubprocessAdapter(BaseAdapter):
    def build_command(self, request: LaunchRequest) -> list[str]:
        raise NotImplementedError

    def start(self, request: LaunchRequest) -> RunningProcess:
        Path(request.log_file).parent.mkdir(parents=True, exist_ok=True)
        command = self.build_command(request)
        if os.name != "nt":
            process = subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1,
                start_new_session=True,
            )
        else:
            process = subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1,
            )
        return StreamingSubprocessHandle(process=process, request=request, adapter=cast(AgentAdapter, self))
