# retoor <retoor@molodetz.nl>

from __future__ import annotations

import logging
import re
import time
from typing import Optional

from devplacepy.services.base import BaseService
from devplacepy.services.manager import service_manager
from devplacepy.services.pubsub import publish as pubsub_publish
from devplacepy.services.pubsub.hub import pubsub

logger = logging.getLogger(__name__)

_SEGMENT = r"[A-Za-z0-9_-]+"
LOG_TAIL = 400


def _instance_project(inst: dict) -> dict:
    from devplacepy.database import get_table

    return get_table("projects").find_one(uid=inst["project_uid"]) or {}


def _instance_is_broadcastable(inst: dict) -> bool:
    project = _instance_project(inst)
    return bool(project) and not project.get("is_private")


async def _container_list(_match: re.Match) -> dict:
    from devplacepy.routers.admin.containers import _decorate
    from devplacepy.services.containers import store

    return {"instances": _decorate(store.all_instances()), "partial": True}


async def _project_containers(match: re.Match) -> Optional[dict]:
    from devplacepy.database import get_table, resolve_by_slug
    from devplacepy.services.containers import store

    project = resolve_by_slug(get_table("projects"), match.group("slug"))
    if not project or project.get("is_private"):
        return None
    return {"instances": store.list_instances(project["uid"])}


async def _container_detail(match: re.Match) -> Optional[dict]:
    from devplacepy.services.containers import api, store

    uid = match.group("uid")
    inst = store.get_instance(uid)
    if not inst or not _instance_is_broadcastable(inst):
        return None
    return {
        "instance": inst,
        "events": store.list_events(uid),
        "schedules": store.list_schedules(uid),
        "stats": api.instance_stats(uid),
        "runtime": await api.instance_runtime(inst),
    }


async def _container_logs(match: re.Match) -> Optional[dict]:
    from devplacepy.services.containers import store
    from devplacepy.services.containers.runtime import get_backend

    uid = match.group("uid")
    inst = store.get_instance(uid)
    if not inst or not _instance_is_broadcastable(inst):
        return None
    if not inst.get("container_id"):
        return {"logs": ""}
    lines: list = []

    async def collect(line: str) -> None:
        lines.append(line)

    await get_backend().logs(
        inst["container_id"], follow=False, tail=LOG_TAIL, on_log=collect
    )
    return {"logs": "\n".join(lines)}


async def _bots(_match: re.Match) -> dict:
    from devplacepy.routers.admin.bots import _frames_payload

    return _frames_payload()


async def _services(_match: re.Match) -> dict:
    return {"services": service_manager.describe_all()}


async def _service_detail(match: re.Match) -> Optional[dict]:
    svc = service_manager.get_service(match.group("name"))
    if svc is None:
        return None
    return {"service": svc.describe()}


async def _ai_usage(match: re.Match) -> dict:
    from devplacepy.services.openai_gateway.analytics import build_analytics
    from devplacepy.services.openai_gateway.usage import pricing_from_cfg

    hours = int(match.group("hours"))
    svc = service_manager.get_service("openai")
    pricing = pricing_from_cfg(svc.get_config()) if svc is not None else None
    return build_analytics(hours, top_n=10, pricing=pricing)


async def _backups(_match: re.Match) -> dict:
    from devplacepy.routers.admin.backups import _dashboard

    return _dashboard(can_download=False)


async def _workspace_detail(match: re.Match) -> Optional[dict]:
    from devplacepy.services.containers import store
    from devplacepy.services.containers.workspace import provision

    inst = store.get_instance(match.group("uid"))
    if not inst or not inst.get("is_workspace"):
        return None
    if inst.get("workspace_owner_uid") != match.group("owner"):
        return None
    project = _instance_project(inst)
    slug = project.get("slug") or project.get("uid") or ""
    return {
        "workspace": await provision.view(inst),
        "editor_url": (
            f"/projects/{slug}/containers/instances/{inst['uid']}/code/" if slug else ""
        ),
    }


VIEWS = [
    (re.compile(r"^container\.list$"), _container_list, 4.0),
    (re.compile(rf"^project\.(?P<slug>{_SEGMENT})\.containers$"), _project_containers, 3.0),
    (re.compile(rf"^container\.(?P<uid>{_SEGMENT})\.detail$"), _container_detail, 4.0),
    (re.compile(rf"^container\.(?P<uid>{_SEGMENT})\.logs$"), _container_logs, 3.0),
    (re.compile(r"^fleet\.bots$"), _bots, 2.0),
    (re.compile(r"^admin\.services$"), _services, 5.0),
    (re.compile(rf"^admin\.services\.(?P<name>{_SEGMENT})$"), _service_detail, 5.0),
    (re.compile(r"^admin\.ai-usage\.(?P<hours>\d+)$"), _ai_usage, 15.0),
    (re.compile(r"^admin\.backups$"), _backups, 8.0),
    (
        re.compile(rf"^user\.(?P<owner>{_SEGMENT})\.workspace\.(?P<uid>{_SEGMENT})$"),
        _workspace_detail,
        3.0,
    ),
]


class LiveViewRelayService(BaseService):
    title = "Live view relay"
    description = (
        "Pushes admin live-view snapshots (container list and instances, bot fleet, "
        "background services, AI usage, backups) onto the pub/sub bus, computed only for topics "
        "that currently have subscribers. Runs on the service lock owner where pub/sub "
        "subscribers converge, replacing per-client HTTP polling with server push."
    )
    default_enabled = True

    def __init__(self):
        super().__init__(name="live_view_relay", interval_seconds=1)
        self._last: dict[str, float] = {}

    def _handler_for(self, topic: str):
        for pattern, compute, interval in VIEWS:
            match = pattern.match(topic)
            if match is not None:
                return compute, interval, match
        return None

    async def run_once(self) -> None:
        now = time.monotonic()
        active: set[str] = set()
        published = 0
        for entry in pubsub.topics():
            topic = entry["topic"]
            if not entry["subscribers"] or "*" in topic:
                continue
            handler = self._handler_for(topic)
            if handler is None:
                continue
            active.add(topic)
            compute, interval, match = handler
            if now - self._last.get(topic, 0.0) < interval:
                continue
            self._last[topic] = now
            try:
                payload = await compute(match)
            except Exception:
                logger.exception("live view relay failed for %s", topic)
                continue
            if payload is None:
                continue
            published += await pubsub_publish(topic, payload)
        self._last = {topic: ts for topic, ts in self._last.items() if topic in active}
        if published:
            self.log(f"pushed {published} live-view frame(s)")

    def collect_metrics(self) -> dict:
        return {"tracked_topics": len(self._last)}
