"""ingest_svc.py

FastAPI-based background worker that:
1. Streams live user-session data (screenshots/events) from Teramind’s REST/WebSocket API.
2. Normalises each payload into the canonical event schema.
3. Pushes events onto a Redis Stream named ``events`` for downstream ML/Rule-Engine workers.

Environment variables expected
-----------------------------
TERAMIND_HOST          e.g. retroindustr.us.teramind.co
TERAMIND_TOKEN         API access token with at least ``sessions:read`` & ``events:read`` scopes.
REDIS_URL              redis://localhost:6379/0

Run with:
    uvicorn ingest_svc:app --port 9000 --reload

Requires Python >=3.11 and:
    pip install fastapi uvicorn httpx redis pydantic[dotenv]
"""
from __future__ import annotations

import asyncio
import base64
import json
import os
from typing import Any, AsyncGenerator, Dict, List

import httpx
import redis.asyncio as aioredis
from fastapi import BackgroundTasks, FastAPI
from pydantic import BaseModel, Field

# ─────────────────────────────────────────────────────────────────────────── Config
TERAMIND_HOST = os.environ.get("TERAMIND_HOST")
TERAMIND_TOKEN = os.environ.get("TERAMIND_TOKEN")
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
STREAM_KEY = "events"
WS_RETRY_DELAY = 5  # seconds

if not TERAMIND_HOST or not TERAMIND_TOKEN:
    raise RuntimeError("TERAMIND_HOST and TERAMIND_TOKEN must be set")

# ───────────────────────────────────────────────────────────────────── Schema model
class RawEvent(BaseModel):
    ts: str
    user_id: str
    device_id: str
    type: str
    payload: Dict[str, Any]

    class Config:
        json_encoders = {bytes: lambda b: base64.b64encode(b).decode()}

# ────────────────────────────────────────────────────────────── Teramind helpers
def make_headers() -> Dict[str, str]:
    return {
        "x-access-token": TERAMIND_TOKEN,
        "Accept": "application/json",
    }

async def fetch_live_events(session: httpx.AsyncClient) -> AsyncGenerator[dict, None]:
    """Yield JSON lines from /events?stream=1 endpoint (long-poll)."""
    url = f"https://{TERAMIND_HOST}/api/v3/events?stream=1"

    while True:
        try:
            async with session.stream("GET", url, headers=make_headers(), timeout=None) as resp:
                async for line in resp.aiter_lines():
                    if not line:
                        continue
                    try:
                        yield json.loads(line)
                    except json.JSONDecodeError:
                        continue
        except (httpx.HTTPError, httpx.ReadError):
            await asyncio.sleep(WS_RETRY_DELAY)
            continue

# ──────────────────────────────────────────────────────────────────── Normaliser
EVENT_TYPE_MAP = {
    1: "WINDOW",
    2: "SCREENSHOT",
    3: "KEYSTROKE",
    4: "URL",
}

def normalise(raw: dict) -> RawEvent | None:
    """Convert Teramind event JSON to canonical RawEvent."""
    etype_code = raw.get("eventTypeId")
    event_type = EVENT_TYPE_MAP.get(etype_code)
    if not event_type:
        return None

    return RawEvent(
        ts=raw.get("timestamp"),
        user_id=str(raw.get("userId")),
        device_id=str(raw.get("computerId")),
        type=event_type,
        payload={
            k: v
            for k, v in raw.items()
            if k not in {"eventTypeId", "userId", "computerId"}
        },
    )

# ───────────────────────────────────────────────────────────────── Redis writer
async def push_to_redis(rds: aioredis.Redis, event: RawEvent) -> None:
    await rds.xadd(STREAM_KEY, {"data": event.json()}, maxlen=100000, approximate=True)

# ───────────────────────────────────────────────────────────────────── Worker
async def worker() -> None:
    redis_client = aioredis.from_url(REDIS_URL, decode_responses=True)
    async with httpx.AsyncClient(timeout=httpx.Timeout(None)) as session:
        async for raw in fetch_live_events(session):
            evt = normalise(raw)
            if evt is None:
                continue
            await push_to_redis(redis_client, evt)

# ───────────────────────────────────────────────────────────────────────── FastAPI
app = FastAPI(title="ingest-svc", version="0.1.0")

@app.on_event("startup")
async def startup_event() -> None:
    bg = BackgroundTasks()
    bg.add_task(worker)
    app.state.bg = bg

@app.get("/healthz")
async def healthcheck() -> dict[str, str]:
    return {"status": "ok"}
