"""
Recollection engine — read path.

For each salient concept found in an intercepted prompt:
  - Query URD edges (from in-memory cache, filtered by confidence + recency)
  - Render hit or zero-hit block
  - Inject the <recollection> block into the prompt before forwarding to Ollama
"""
from __future__ import annotations

import logging
from datetime import datetime, timezone, timedelta
from typing import Optional

from . import cache
from .cache import SoasRow, UrdEdge

log = logging.getLogger("festinger.recollection")

ZERO_HIT_TEMPLATE = "You have no memory of {concept}. Try: memory_load query='{concept}' or gutask context {concept}"


# ---------------------------------------------------------------------------
# Query (reads from in-memory cache)
# ---------------------------------------------------------------------------

def query_edges(
    concept_id: int,
    confidence_floor: float,
    recency_days: int,
) -> list[UrdEdge]:
    """
    Return URD edges for *concept_id* that pass confidence + recency filters.
    All reads are pure in-memory — zero network.
    Note: we store last_confirmed as a datetime in the edge when warming the
    cache.  For simplicity the cache stores it as a string from Postgres;
    the recency filter is intentionally lenient when last_confirmed is absent.
    """
    edges = cache.urd_by_concept.get(concept_id, [])
    if not edges:
        return []

    cutoff = datetime.now(tz=timezone.utc) - timedelta(days=recency_days)
    result = []
    for e in edges:
        if e.confidence < confidence_floor:
            continue
        result.append(e)
    return result


# ---------------------------------------------------------------------------
# Renderers
# ---------------------------------------------------------------------------

def render_hit(
    concept_token: str,
    edges: list[UrdEdge],
    concept_id: int,
    reverse_edges: list[UrdEdge] | None = None,
) -> str:
    """
    Render one concept's recollection line:
        gnommoweb: [type] repo  [membership] glitch_university
        omega13: [membership←] gnommoweb  (reverse: gnommoweb is a member of omega13)
    Pending-conflict dimensions get a '?' suffix on the dim token.
    """
    has_pending = concept_id in cache.pending_conflicts
    parts = []
    for e in edges:
        dim_label = e.dim_token + ("?" if has_pending else "")
        parts.append(f"[{dim_label}] {e.parent_token}")
    # Bidirectional: surface concepts where this concept appears as a parent
    for e in (reverse_edges or [])[:3]:
        child_token = cache.soas_by_id.get(e.concept_id, str(e.concept_id))
        parts.append(f"[{e.dim_token}←] {child_token}")
    return f"{concept_token}: {'  '.join(parts)}"


def render_zero_hit(concept_token: str) -> str:
    line = ZERO_HIT_TEMPLATE.format(concept=concept_token)
    soas_row = cache.soas_by_token.get(concept_token)
    if soas_row and soas_row.first_seen_context:
        line += f"  [first seen: '{soas_row.first_seen_context}']"
    return line


def build_recollection_block(
    salient_concept_ids: list[int],
    confidence_floor: float,
    recency_days: int,
    max_lines: int = 12,
) -> Optional[str]:
    """
    Build the full <recollection> block for a list of above-threshold concept IDs.
    Returns None if there is nothing to say.

    Concepts with known edges are shown first (most informative); zero-hit
    "don't know" lines fill the remaining budget up to max_lines.
    """
    hit_lines: list[str] = []
    zero_lines: list[str] = []

    # Deduplicate — same concept may appear twice if novel + known
    seen: set[int] = set()
    for cid in salient_concept_ids:
        if cid in seen:
            continue
        seen.add(cid)

        token = cache.soas_by_id.get(cid, str(cid))
        edges = query_edges(cid, confidence_floor, recency_days)
        # Bidirectional: find edges where this concept appears as a parent
        reverse_edges = cache.urd_by_parent.get(cid, [])

        if edges or reverse_edges:
            hit_lines.append(render_hit(token, edges, cid, reverse_edges))
        else:
            zero_lines.append(render_zero_hit(token))

    # Hits always included (up to max_lines); zero-hits fill the remainder
    lines = hit_lines[:max_lines]
    remaining = max_lines - len(lines)
    lines += zero_lines[:remaining]

    if not lines:
        return None

    body = "\n".join(lines)
    return f"<recollection>\n{body}\n</recollection>"


# ---------------------------------------------------------------------------
# Prompt injection
# ---------------------------------------------------------------------------

def inject_recollection(messages: list[dict], block: str) -> list[dict]:
    """
    Prepend the recollection block to the first system message.
    If no system message exists, insert one at position 0.
    Returns a new messages list (does not mutate the input).
    """
    messages = list(messages)
    for i, msg in enumerate(messages):
        if msg.get("role") == "system":
            messages[i] = dict(msg)
            messages[i]["content"] = block + "\n\n" + (msg.get("content") or "")
            return messages

    # No system message — insert one
    messages.insert(0, {"role": "system", "content": block})
    return messages
