"""Tenant-aware context builder feeding agents safely.

The builder *never* widens the caller's scope — it composes:

* The current request context (tenant + organization node) from
  :func:`simorgh.core.context.current_request_context`.
* Optional semantic entity summaries from the registry.
* Optional vector-store recall using the configured embedder.
* Optional conversation memory.

The result is a frozen :class:`AIContext` ready to be embedded in a prompt
or fed to an agent. The builder refuses to operate without a tenant — that
is the single most important safety property of the AI layer.
"""

from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass, field
from typing import Any

from simorgh.apps.ai.embeddings import get_embedder
from simorgh.apps.ai.memory import MemoryEntry, default_memory
from simorgh.apps.ai.semantic import get_entity, serialize_entity
from simorgh.apps.ai.vector_store import VectorMatch, get_store
from simorgh.apps.tenants.models import Tenant
from simorgh.core.context import current_request_context


class ContextError(RuntimeError):
    """Raised when a context cannot be assembled (e.g. no tenant)."""


@dataclass(frozen=True)
class AIContext:
    tenant_id: int
    tenant_slug: str
    organization_node_id: int | None
    user_id: int | None
    entities: tuple[dict[str, Any], ...] = ()
    recall: tuple[VectorMatch, ...] = ()
    history: tuple[MemoryEntry, ...] = ()
    extra: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "tenant_id": self.tenant_id,
            "tenant_slug": self.tenant_slug,
            "organization_node_id": self.organization_node_id,
            "user_id": self.user_id,
            "entities": list(self.entities),
            "recall": [
                {"id": m.id, "score": m.score, "metadata": m.metadata}
                for m in self.recall
            ],
            "history": [
                {"role": h.role, "content": h.content, "metadata": h.metadata}
                for h in self.history
            ],
            "extra": dict(self.extra),
        }


def build_context(
    *,
    tenant: Tenant | None = None,
    user_id: int | None = None,
    entity_keys: Iterable[str] = (),
    recall_query: str = "",
    recall_collection: str = "",
    recall_top_k: int = 5,
    conversation_id: str = "",
    embedder: str = "hash",
    vector_store: str = "memory",
    extra: dict[str, Any] | None = None,
) -> AIContext:
    """Assemble an :class:`AIContext` honouring tenant scope.

    ``tenant`` overrides the request context — useful in management commands
    or background jobs. In request-handling code, leave it unset so the
    middleware-supplied tenant is used.
    """
    ctx = current_request_context()
    resolved_tenant = tenant or ctx.tenant
    if resolved_tenant is None:
        raise ContextError("tenant required to build AI context")
    org_node_id = next(iter(ctx.org_node_ids), None) if ctx.org_node_ids else None
    resolved_user = user_id if user_id is not None else (
        ctx.actor.pk if ctx.actor is not None else None
    )

    entity_payloads = tuple(serialize_entity(get_entity(k)) for k in entity_keys)

    recall: tuple[VectorMatch, ...] = ()
    if recall_query and recall_collection:
        emb = get_embedder(embedder).embed(recall_query)
        recall = tuple(
            get_store(vector_store).query(
                tenant_id=resolved_tenant.pk,
                collection=recall_collection,
                embedding=emb,
                top_k=recall_top_k,
            ),
        )

    history: tuple[MemoryEntry, ...] = ()
    if conversation_id:
        history = tuple(default_memory().history(conversation_id))

    return AIContext(
        tenant_id=resolved_tenant.pk,
        tenant_slug=resolved_tenant.slug,
        organization_node_id=org_node_id,
        user_id=resolved_user,
        entities=entity_payloads,
        recall=recall,
        history=history,
        extra=dict(extra or {}),
    )


def render_context_block(context: AIContext) -> str:
    """Render a compact textual summary suitable for prompt injection."""
    lines: list[str] = [
        f"tenant: {context.tenant_slug} (#{context.tenant_id})",
    ]
    if context.organization_node_id is not None:
        lines.append(f"organization_node: {context.organization_node_id}")
    if context.user_id is not None:
        lines.append(f"user: {context.user_id}")
    if context.entities:
        names = ", ".join(e["key"] for e in context.entities)
        lines.append(f"entities: {names}")
    if context.recall:
        ids = ", ".join(f"{m.id}({m.score:.2f})" for m in context.recall)
        lines.append(f"recall: {ids}")
    if context.history:
        lines.append(f"history_entries: {len(context.history)}")
    return "\n".join(lines)
