"""Local bot process manager - a simplified version of host.py for local development."""

import json
import os
import select
import signal
import subprocess
import sys
import threading
import time
from typing import Callable, Optional


class BotRunner:
    """Manages a local bot process, similar to how host.py manages remote bots."""

    def __init__(
        self,
        project_dir: str,
        on_log: Callable[[str, str], None],
        on_error: Callable[[str, str], None],
    ):
        self.project_dir = project_dir
        self.on_log = on_log
        self.on_error = on_error
        self.process: Optional[subprocess.Popen] = None
        self.last_activity = 0.0
        self._stdout_thread: Optional[threading.Thread] = None
        self._stderr_thread: Optional[threading.Thread] = None

    def is_alive(self) -> bool:
        return self.process is not None and self.process.poll() is None

    def execute(
        self,
        func: str,
        params: dict,
        token: str,
        context: dict,
        server_host: str,
        server_scheme: str = "https",
    ) -> bool:
        """Execute a function on the bot process.

        If the process is already running, writes to stdin.
        Otherwise, spawns a new process first.
        """
        self.last_activity = time.time()

        if not self.is_alive():
            if not self._spawn(token, context, server_host, server_scheme):
                return False

        # Write function call to stdin
        try:
            if self.process and self.process.stdin:
                call_data = json.dumps({"func": func, "params": params}) + "\n"
                self.process.stdin.write(call_data)
                self.process.stdin.flush()
                self.on_log("info", f"[DEV] Executing: {func}")
                return True
        except (BrokenPipeError, OSError) as e:
            self.on_error("error", f"[DEV] Failed to write to bot stdin: {e}")
            self.kill()
            return False

        return False

    def kill(self) -> None:
        """Kill the running bot process."""
        if self.process is not None:
            try:
                self.process.terminate()
                self.process.wait(timeout=3)
            except subprocess.TimeoutExpired:
                self.process.kill()
                self.process.wait(timeout=3)
            except Exception:
                pass
            self.process = None
            self.on_log("info", "[DEV] Bot process stopped")

    def _spawn(self, token: str, context: dict, server_host: str, server_scheme: str) -> bool:
        """Spawn a new bot process."""
        python_exec = sys.executable

        main_py = os.path.join(self.project_dir, "main.py")
        if not os.path.exists(main_py):
            self.on_error("error", f"[DEV] main.py not found in {self.project_dir}")
            return False

        # Use a random port for the bot's HTTP server (not used in dev mode but required by SDK)
        import random
        port = random.randint(10000, 60000)

        context_json = json.dumps(context)
        env = os.environ.copy()
        env["APP_HOST"] = server_host
        env["APP_SCHEME"] = server_scheme
        env["UGLY_BOT_DEV"] = "1"

        self.on_log("info", f"[DEV] Spawning bot process (port={port})...")

        try:
            self.process = subprocess.Popen(
                [python_exec, "-u", "main.py", "bot", str(port), token, context_json],
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                cwd=self.project_dir,
                text=True,
                bufsize=1,
                env=env,
            )
        except Exception as e:
            self.on_error("error", f"[DEV] Failed to spawn bot: {e}")
            return False

        # Wait for "[BOT] initialized" on stdout
        if not self._wait_for_init():
            self.on_error("error", "[DEV] Bot failed to initialize (timeout)")
            self.kill()
            return False

        # Start background threads to read stdout/stderr
        self._start_log_readers()

        self.on_log("info", "[DEV] Bot initialized successfully")
        return True

    def _wait_for_init(self, timeout: float = 30.0) -> bool:
        """Wait for the bot to print '[BOT] initialized' to stdout."""
        if not self.process or not self.process.stdout:
            return False

        streams = [self.process.stdout]
        if self.process.stderr:
            streams.append(self.process.stderr)

        start = time.time()
        while time.time() - start < timeout:
            if self.process.poll() is not None:
                # Process exited — drain remaining output
                for s in streams:
                    try:
                        remaining = s.read()
                        if remaining:
                            label = "STDERR" if s == self.process.stderr else "STDOUT"
                            for line in remaining.strip().splitlines():
                                self.on_error("error", f"[DEV] {label}: {line}")
                    except Exception:
                        pass
                return False

            # Non-blocking read from both stdout and stderr
            ready, _, _ = select.select(streams, [], [], 0.1)
            for s in ready:
                line = s.readline()
                if not line:
                    continue
                line = line.strip()
                if s == self.process.stderr:
                    self.on_error("error", f"[DEV] {line}")
                else:
                    self.on_log("info", f"[DEV] {line}")
                    if "[BOT] initialized" in line:
                        return True

        return False

    def _start_log_readers(self) -> None:
        """Start background threads to read stdout/stderr and forward logs."""
        if self.process and self.process.stdout:
            self._stdout_thread = threading.Thread(
                target=self._read_stream,
                args=(self.process.stdout, "log"),
                daemon=True,
            )
            self._stdout_thread.start()

        if self.process and self.process.stderr:
            self._stderr_thread = threading.Thread(
                target=self._read_stream,
                args=(self.process.stderr, "error"),
                daemon=True,
            )
            self._stderr_thread.start()

    def _read_stream(self, stream, log_type: str) -> None:
        """Read lines from a stream and forward as logs."""
        try:
            for line in stream:
                line = line.rstrip("\n")
                if line:
                    if log_type == "error":
                        self.on_error(log_type, line)
                    else:
                        self.on_log(log_type, line)
        except Exception:
            pass
