"""
SSR Bridge — Manages a persistent Bun subprocess for React rendering.

Protocol: newline-delimited JSON-RPC over stdin/stdout.

Request:  {"id": 1, "method": "render", "params": {"file": "/abs/path/Hello.tsx", "props": {...}}}
Response: {"id": 1, "html": "<div>...</div>"}

The subprocess stays alive across requests. It is started on first use
and restarted automatically if it crashes.
"""

from __future__ import annotations

import atexit
import json
import logging
import subprocess
import threading
from dataclasses import dataclass
from typing import Any

logger = logging.getLogger("mizan.ssr")


@dataclass
class RenderResult:
    """Result of an SSR render call."""
    html: str


class SSRBridge:
    """
    Manages a persistent Bun subprocess for server-side rendering.

    Thread-safe. Multiple Django workers can call render() concurrently.
    Request-response matching via message IDs.
    """

    def __init__(self, worker_path: str, timeout: float = 5.0) -> None:
        self._worker_path = worker_path
        self._timeout = timeout
        self._proc: subprocess.Popen | None = None
        self._lock = threading.Lock()
        self._write_lock = threading.Lock()  # Serializes stdin writes
        self._counter = 0
        self._pending: dict[int, threading.Event] = {}
        self._results: dict[int, dict] = {}
        self._reader_thread: threading.Thread | None = None
        self._ready = threading.Event()

        # Ensure cleanup on process exit
        atexit.register(self.shutdown)

    def _ensure_running(self) -> None:
        """Start the Bun subprocess if it's not running."""
        if self._proc is not None and self._proc.poll() is None:
            return

        if self._proc is not None:
            logger.warning("Bun SSR worker died (exit code %s), restarting", self._proc.returncode)

        self._ready.clear()
        self._proc = subprocess.Popen(
            ["bun", "run", self._worker_path],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )

        self._reader_thread = threading.Thread(
            target=self._read_responses, daemon=True, name="mizan-ssr-reader",
        )
        self._reader_thread.start()

        # Wait for the "ready" signal from the worker
        if not self._ready.wait(timeout=self._timeout):
            logger.error("Bun SSR worker failed to start within %ss", self._timeout)
            self.shutdown()
            raise TimeoutError("SSR worker failed to start")

        logger.info("Bun SSR worker started (pid %s)", self._proc.pid)

    def _read_responses(self) -> None:
        """Background thread that reads JSON responses from stdout."""
        try:
            for line in self._proc.stdout:
                if isinstance(line, bytes):
                    line = line.decode("utf-8")
                line = line.strip()
                if not line:
                    continue

                try:
                    msg = json.loads(line)
                except json.JSONDecodeError:
                    logger.warning("Malformed JSON from SSR worker: %s", line[:200])
                    continue

                msg_id = msg.get("id")

                # Ready signal (id=0)
                if msg_id == 0 and msg.get("ready"):
                    self._ready.set()
                    continue

                if msg_id is not None and msg_id in self._pending:
                    self._results[msg_id] = msg
                    self._pending[msg_id].set()
        except Exception:
            logger.warning("SSR reader thread exited", exc_info=True)

    def render(self, file: str, props: dict[str, Any] | None = None) -> RenderResult:
        """
        Render a React component to HTML.

        Args:
            file: Absolute path to the .tsx/.jsx file to render.
            props: Props to pass to the component.

        Returns:
            RenderResult with the HTML string.

        Raises:
            TimeoutError: If the render takes longer than the configured timeout.
            RuntimeError: If the render fails.
        """
        with self._lock:
            self._ensure_running()
            self._counter += 1
            msg_id = self._counter

        event = threading.Event()
        self._pending[msg_id] = event

        request = json.dumps({
            "id": msg_id,
            "method": "render",
            "params": {"file": file, "props": props or {}},
        }) + "\n"

        # Serialize stdin writes to prevent interleaving from concurrent threads
        with self._write_lock:
            try:
                self._proc.stdin.write(request.encode("utf-8"))
                self._proc.stdin.flush()
            except (BrokenPipeError, OSError) as e:
                self._pending.pop(msg_id, None)
                raise RuntimeError(f"SSR worker pipe broken: {e}")

        if not event.wait(self._timeout):
            self._pending.pop(msg_id, None)
            raise TimeoutError(
                f"SSR render of '{file}' timed out after {self._timeout}s"
            )

        self._pending.pop(msg_id, None)
        result = self._results.pop(msg_id)

        if "error" in result:
            raise RuntimeError(f"SSR render failed: {result['error']}")

        return RenderResult(html=result["html"])

    def shutdown(self) -> None:
        """Stop the Bun subprocess."""
        if self._proc is not None:
            try:
                self._proc.stdin.close()
            except Exception:
                pass
            try:
                self._proc.terminate()
                self._proc.wait(timeout=3)
            except Exception:
                try:
                    self._proc.kill()
                except Exception:
                    pass
            self._proc = None
            logger.info("Bun SSR worker stopped")
