"""
ttyd-Reverse-Proxy für die eingebetteten Web-Terminals (Box-Konsole + Hermes-Terminal).

Beide ttyd-Dienste binden NUR an Loopback (--base-path /<name>) und werden hier über
den ohnehin offenen MC2-Port (9001) same-origin durchgereicht — HTTP (index.html/token)
+ WebSocket (/<name>/ws). Vorteil: keine eigene Firewall-Freigabe je Terminal nötig und
alles läuft same-origin zum Dashboard. Reiner Byte-Durchreicher; ttyds `tty`-Subprotokoll
wird auf beiden Seiten ausgehandelt. Der Login-Schutz sitzt IM Terminal (ttyd startet die
Shell über `su` → fragt das Box-Passwort ab), nicht im Proxy.
"""

import asyncio
import logging

import httpx
import websockets
from fastapi import APIRouter, Request, WebSocket
from starlette.responses import Response

from config import BOX_CONSOLE_UPSTREAM

log = logging.getLogger(__name__)
router = APIRouter()


def _register(base: str, upstream: str) -> None:
    """Registriert HTTP- + WS-Proxy-Routen für eine ttyd-Instanz (base-path `/<base>`)."""
    ws_upstream = upstream.replace("http://", "ws://").replace("https://", "wss://")

    @router.websocket(f"/{base}/ws")
    async def _proxy_ws(ws: WebSocket) -> None:  # noqa: ANN001 — Closure je base
        await ws.accept(subprotocol="tty")
        try:
            async with websockets.connect(
                f"{ws_upstream}/{base}/ws", subprotocols=["tty"],
                open_timeout=10, max_size=None, ping_interval=None,
            ) as up:
                async def c2u() -> None:
                    while True:
                        msg = await ws.receive()
                        if msg.get("type") == "websocket.disconnect":
                            return
                        if (t := msg.get("text")) is not None:
                            await up.send(t)
                        elif (b := msg.get("bytes")) is not None:
                            await up.send(b)

                async def u2c() -> None:
                    async for m in up:
                        if isinstance(m, (bytes, bytearray)):
                            await ws.send_bytes(bytes(m))
                        else:
                            await ws.send_text(m)

                _done, pending = await asyncio.wait(
                    [asyncio.create_task(c2u()), asyncio.create_task(u2c())],
                    return_when=asyncio.FIRST_COMPLETED,
                )
                for task in pending:
                    task.cancel()
        except Exception:
            log.debug("ttyd-proxy ws (%s) beendet/fehlgeschlagen", base, exc_info=True)
        finally:
            try:
                await ws.close()
            except Exception:
                pass

    async def _proxy_http(request: Request, path: str = "") -> Response:
        url = f"{upstream}/{base}/{path}"
        try:
            async with httpx.AsyncClient(timeout=10.0) as c:
                r = await c.request(request.method, url, content=await request.body())
            return Response(content=r.content, status_code=r.status_code,
                            media_type=r.headers.get("content-type"))
        except httpx.HTTPError as exc:
            log.debug("ttyd-proxy http (%s) nicht erreichbar: %s", base, exc)
            return Response(content=b"Terminal (ttyd) nicht erreichbar.", status_code=502,
                            media_type="text/plain")

    router.add_api_route(f"/{base}", _proxy_http, methods=["GET"], name=f"{base}_root")
    router.add_api_route(f"/{base}/{{path:path}}", _proxy_http, methods=["GET", "POST"],
                         name=f"{base}_path")


# Box-Konsole (Login-Shell) — Loopback-ttyd. (Das Hermes-Terminal-ttyd ist mit dem
# Umzug auf Hermes Desktop entfallen; die Agent-Oberfläche ist die Desktop-App bzw. /hermes-ui/.)
_register("console", BOX_CONSOLE_UPSTREAM)
