"""Recall/read-path cluster for HonchoSessionManager: context, cards, search, conclusions, dialectic."""

from __future__ import annotations

import logging
import re
from typing import Any, Callable

from plugins.memory.honcho.session_auth import HonchoAuthError

logger = logging.getLogger("plugins.memory.honcho.session")

_FAILED = object()  # sentinel: a guarded call raised (distinct from a legitimately empty/None result)

# Reasoning-channel markers a summarizer model can leave inside a persisted session summary.
_THINK_BLOCK_RE = re.compile(
    r"<\s*(?:think|thinking|reasoning|thought|reasoning_scratchpad)\s*>.*?"
    r"<\s*/\s*(?:think|thinking|reasoning|thought|reasoning_scratchpad)\s*>",
    re.IGNORECASE | re.DOTALL,
)
_THINK_CLOSE_RE = re.compile(r"</\s*think\s*>", re.IGNORECASE)
_PLANNING_HEAD_RE = re.compile(
    r"^(?:i need to create a thorough|the instruction says to focus on capturing key facts|first, let me review)",
    re.IGNORECASE,
)


def usable_honcho_summary(text: object) -> str | None:
    """A session summary safe to inject, or None to omit it. Honcho summarizers can persist the
    model's planning text and a trailing ``</think>`` as the summary body; the text after that
    close survives, planning-only or still-tagged text is dropped."""
    raw = "" if text is None else str(text)
    if not raw.strip():
        return None
    # The observed payload has no opening tag: planning prose, then ``</think>``, then the summary.
    close = _THINK_CLOSE_RE.search(raw)
    if close:
        raw = raw[close.end():]
    cleaned = _THINK_BLOCK_RE.sub("", raw).strip()
    lowered = cleaned.lower()
    if not cleaned or "<think" in lowered or "</think" in lowered or _PLANNING_HEAD_RE.search(cleaned):
        return None
    return cleaned


class SessionContextMixin:
    """Peer context / card / search / conclusion / dialectic operations against Honcho."""

    def _guarded(self, fn: Callable[[], Any], default: Any, level: int, msg: str, *args: Any) -> Any:
        """Run ``fn``; re-raise HonchoAuthError, log (exc appended to args) and return default otherwise."""
        try:
            return fn()
        except HonchoAuthError:
            raise
        except Exception as e:
            logger.log(level, msg, *args, e)
            return default

    def _guarded_authed(self, label: str, fn: Callable[[], Any], default: Any, level: int, msg: str, *args: Any) -> Any:
        """``_guarded`` around ``_authed_call(label, fn)`` (401 -> forced refresh + one retry)."""
        return self._guarded(lambda: self._authed_call(label, fn), default, level, msg, *args)

    def _guarded_session(
        self, session_key: str, fn: Callable[[Any], Any], default: Any, level: int, msg: str, *args: Any,
    ) -> Any:
        """``_guarded`` over ``fn(session)`` for the cached session; ``default`` when no session is cached."""
        session = self._cached_session(session_key)
        return self._guarded(lambda: fn(session), default, level, msg, *args) if session else default

    @staticmethod
    def _normalize_card(card: Any) -> list[str]:
        """Normalize Honcho card payloads into a plain list of strings."""
        return ([str(item) for item in card if item] if isinstance(card, list) else [str(card)]) if card else []

    @staticmethod
    def _target_kwargs(target: str | None) -> dict[str, str]:
        """SDK peer getters take ``target=`` only when observing another peer."""
        return {} if target is None else {"target": target}

    def _fetch_peer_card(self, peer_id: str, *, target: str | None = None) -> list[str]:
        """Fetch a peer card from the peer object (session.context() can return an empty card)."""
        def _get_card() -> Any:
            peer = self._get_or_create_peer(peer_id)
            getters = (getattr(peer, n, None) for n in ("get_card", "card"))  # "card" is the legacy SDK getter
            getter = next((g for g in getters if callable(g)), None)
            return getter(**self._target_kwargs(target)) if getter else None
        return self._normalize_card(self._authed_call("peer card fetch", _get_card))

    def _fetch_peer_context(
        self, peer_id: str, search_query: str | None = None, *, target: str | None = None,
    ) -> dict[str, Any]:
        """Fetch representation + peer card from a peer object; when peer.context() leaves either
        empty, fall back to the dedicated representation / card getters. Raises HonchoAuthError when
        auth is dead or a 401 survives the forced refresh; the fallback chain would just repeat it."""
        context_kwargs: dict[str, Any] = self._target_kwargs(target)
        if search_query is not None:
            context_kwargs["search_query"] = search_query
        peer = lambda: self._get_or_create_peer(peer_id)  # noqa: E731
        failed = "Direct %s failed for '%%s': %%s"
        ctx = self._guarded_authed(
            "peer context fetch", lambda: peer().context(**context_kwargs), None, logging.DEBUG,
            failed % "peer.context()", peer_id,
        )
        representation = getattr(ctx, "representation", None) or getattr(ctx, "peer_representation", None) or ""
        card = self._normalize_card(getattr(ctx, "peer_card", None))
        if not representation:
            representation = self._guarded_authed(
                "peer representation fetch", lambda: peer().representation(**self._target_kwargs(target)),
                "", logging.DEBUG, failed % "peer.representation()", peer_id,
            ) or ""
        if not card:
            card = self._guarded(
                lambda: self._fetch_peer_card(peer_id, target=target), [], logging.DEBUG,
                failed % "peer card fetch", peer_id,
            )
        return {"representation": representation, "card": card}

    def _peer_context_strings(self, peer_id: str, search_query: str | None = None, *, target: str | None = None):
        """``_fetch_peer_context`` flattened to ``(representation, newline-joined card)`` for prompt injection."""
        ctx = self._fetch_peer_context(peer_id, search_query, target=target)
        return ctx["representation"], "\n".join(ctx["card"])

    def get_prefetch_context(
        self, session_key: str, user_message: str | None = None, *, current_query_only: bool = False,
    ) -> dict[str, str]:
        """Pre-fetch user + AI peer context (representation, card) plus the session summary.
        ``user_message`` is passed as search_query so Honcho returns topic-relevant conclusions.
        Stops early (returning what it has) once auth is dead."""
        session = self._cached_session(session_key)
        if not session:
            return {}
        result: dict[str, str] = {}

        def _summary() -> None:
            if session.honcho_session_id not in self._sessions_cache:
                return
            ctx = self._authed_call(
                "session summary fetch",
                lambda: self._sdk_session(session.honcho_session_id).context(summary=True, tokens=self._context_tokens),
            )
            if ctx.summary and (summary := usable_honcho_summary(getattr(ctx.summary, "content", None))):
                result["summary"] = summary

        def _user() -> None:
            observer_peer_id, target_peer_id = self._resolve_observer_target(session, "user")
            if current_query_only:
                # The legacy fallback getters do not accept search_query. An empty
                # or failed current-query lookup must not silently become generic recall.
                ctx = self._authed_call("peer context fetch", lambda: self._get_or_create_peer(observer_peer_id).context(
                    search_query=user_message, target=target_peer_id or session.user_peer_id,
                ))
                result["representation"] = getattr(ctx, "representation", None) or getattr(ctx, "peer_representation", None) or ""
                result["card"] = "\n".join(self._normalize_card(getattr(ctx, "peer_card", None)))
                return
            result["representation"], result["card"] = self._peer_context_strings(
                observer_peer_id, search_query=user_message or None, target=target_peer_id or session.user_peer_id,
            )

        def _ai() -> None:
            result["ai_representation"], result["ai_card"] = self._peer_context_strings(
                session.assistant_peer_id, target=session.assistant_peer_id,
            )
        for step, level, msg in (
            (_summary, logging.DEBUG, "Failed to fetch session summary from Honcho: %s"),
            (_user, logging.WARNING, "Failed to fetch user context from Honcho: %s"),
            (_ai, logging.DEBUG, "Failed to fetch AI peer context from Honcho: %s"),
        ):
            try:
                step()
            except HonchoAuthError:
                if current_query_only:
                    raise
                break  # Auth is dead; the pop_auth_notice path tells the model why context is missing.
            except Exception as e:
                if current_query_only:
                    raise
                logger.log(level, msg, e)
        return result

    def get_session_context(self, session_key: str, peer: str = "user") -> dict[str, Any]:
        """Fetch session-level context (summary, representation, card, recent messages).
        Raises HonchoAuthError so callers can tell rejected credentials from no context."""
        session = self._cached_session(session_key)
        if not session:
            return {}
        if session.honcho_session_id not in self._sessions_cache:
            # Fall back to peer-level context, respecting the requested peer.
            peer_id = self._resolve_peer_id(session, peer)
            return self._fetch_peer_context(peer_id, target=peer_id)

        def _fetch() -> dict[str, Any]:
            observer_peer_id, target_peer_id = self._resolve_observer_target(session, peer)
            ctx = self._authed_call(
                "session context fetch",
                lambda: self._sdk_session(session.honcho_session_id).context(
                    summary=True, tokens=self._context_tokens,
                    peer_target=target_peer_id or observer_peer_id, peer_perspective=observer_peer_id,
                ),
            )
            result: dict[str, Any] = {}
            if ctx.summary and (summary := usable_honcho_summary(getattr(ctx.summary, "content", ctx.summary))):
                result["summary"] = summary
            if ctx.peer_representation:
                result["representation"] = ctx.peer_representation
            if ctx.peer_card:
                result["card"] = "\n".join(ctx.peer_card)
            if ctx.messages:
                result["recent_messages"] = [
                    {"role": getattr(m, "peer_id", "unknown"), "content": (m.content or "")[:500]}
                    for m in ctx.messages[-10:]
                ]
            return result
        return self._guarded(_fetch, {}, logging.DEBUG, "Session context fetch failed: %s")

    def get_peer_card(self, session_key: str, peer: str = "user") -> list[str]:
        """Fetch a peer card (curated facts, no LLM). [] if unavailable; raises HonchoAuthError."""
        def _fetch(session: Any) -> list[str]:
            observer_peer_id, target_peer_id = self._resolve_observer_target(session, peer)
            card = self._fetch_peer_card(observer_peer_id, target=target_peer_id)
            # Some backends store cards on the target peer, not the observer-target slot.
            return card or (self._fetch_peer_card(target_peer_id) if target_peer_id else [])
        return self._guarded_session(session_key, _fetch, [], logging.DEBUG, "Failed to fetch peer card from Honcho: %s")

    def search_context(self, session_key: str, query: str, max_tokens: int = 800, peer: str = "user") -> str:
        """Hybrid search over raw messages visible from ``peer``'s perspective, all sessions. Snippets
        accumulate until ``max_tokens`` (~4 chars/token) is exhausted. Returns "" when nothing matches;
        raises HonchoAuthError on rejected credentials."""
        session = self._cached_session(session_key)
        q = (query or "").strip()[:4000]  # Honcho caps query length for the embedding model.
        if not session or not q:
            return ""
        peer_id = self._resolve_peer_id(session, peer)
        char_budget = max(200, int(max_tokens) * 4)
        limit = max(3, min(20, char_budget // 300))
        messages = self._guarded_authed(
            "message search", lambda: self.honcho.search(q, filters={"peer_perspective": peer_id}, limit=limit),
            _FAILED, logging.DEBUG, "Honcho message search failed (peer_perspective=%s): %s", peer_id,
        )
        if messages is _FAILED:
            # Older Honcho versions lack the perspective filter; fall back to peer-authored search.
            messages = self._guarded_authed(
                "peer search", lambda: self._get_or_create_peer(peer_id).search(q, limit=limit),
                None, logging.DEBUG, "Honcho peer search fallback also failed: %s",
            )
        if not messages:
            return ""
        # Author labels distinguish user-stated facts from assistant-derived ones.
        lines: list[str] = []
        for m in messages:
            content = (getattr(m, "content", "") or "").strip()
            if not content:
                continue
            author = getattr(m, "peer_id", "") or "unknown"
            who = "assistant" if author == session.assistant_peer_id else author
            sess = getattr(m, "session_id", "") or ""
            entry = f"[{who}{f' · {sess}' if sess else ''}] {content[:1200]}"
            # Budget left after the joined snippets so far plus the separator this entry would need.
            remaining = char_budget - len("\n\n".join(lines)) - (2 if lines else 0)
            if remaining <= 0:
                break
            truncated = len(entry) > remaining
            entry = entry[:remaining].rstrip()
            if not entry:
                break
            lines.append(entry)
            if truncated:
                break
        return "\n\n".join(lines)

    def _conclusions_scope(self, session: Any, target_peer_id: str) -> Any:
        """ConclusionScope for observing target_peer_id; shared by create/delete/list."""
        ai_observes = target_peer_id == session.assistant_peer_id or self._ai_observes_others(session)
        observer = self._get_or_create_peer(session.assistant_peer_id if ai_observes else target_peer_id)
        return observer.conclusions_of(target_peer_id)

    def create_conclusion(self, session_key: str, content: str, peer: str = "user") -> bool:
        """Write a conclusion (durable fact) about ``peer`` back to Honcho."""
        if not content or not content.strip():
            return False
        session = self._cached_session(session_key)
        if not session:
            logger.warning("No session cached for '%s', skipping conclusion", session_key)
            return False

        def _create() -> bool:
            target_peer_id = self._resolve_peer_id(session, peer)
            if target_peer_id is None:
                logger.warning("Could not resolve conclusion peer '%s' for session '%s'", peer, session_key)
                return False
            payload = [{"content": content.strip(), "session_id": session.honcho_session_id}]
            self._authed_call("conclusion create", lambda: self._conclusions_scope(session, target_peer_id).create(payload))
            logger.info("Created conclusion about %s for %s: %s", target_peer_id, session_key, content[:80])
            return True
        return self._guarded(_create, False, logging.ERROR, "Failed to create conclusion: %s")

    def delete_conclusion(self, session_key: str, conclusion_id: str, peer: str = "user") -> bool:
        """Delete a conclusion by ID. Use only for PII removal."""
        def _delete(session: Any) -> bool:
            target_peer_id = self._resolve_peer_id(session, peer)
            self._authed_call(
                "conclusion delete", lambda: self._conclusions_scope(session, target_peer_id).delete(conclusion_id),
            )
            logger.info("Deleted conclusion %s for %s", conclusion_id, session_key)
            return True
        return self._guarded_session(
            session_key, _delete, False, logging.ERROR, "Failed to delete conclusion %s: %s", conclusion_id,
        )

    def list_conclusions(self, session_key: str, query: str | None = None, peer: str = "user", limit: int = 20):
        """List (or semantically search with ``query``) conclusions as {"id", "content"} dicts."""
        def _list(session: Any) -> list[dict]:
            target_peer_id = self._resolve_peer_id(session, peer)
            if target_peer_id is None:
                return []

            def _fetch() -> Any:
                scope = self._conclusions_scope(session, target_peer_id)
                return scope.query(query, top_k=limit) if query else scope.list(size=limit).items
            return [{"id": c.id, "content": c.content} for c in self._authed_call("conclusion list", _fetch)]
        return self._guarded_session(session_key, _list, [], logging.DEBUG, "Honcho list_conclusions failed: %s")

    def set_peer_card(self, session_key: str, card: list[str], peer: str = "user") -> list[str] | None:
        """Replace a peer's card. Returns the updated card, or None on failure."""
        def _update(session: Any) -> list[str] | None:
            observer_peer_id, target_peer_id = self._resolve_observer_target(session, peer)
            if observer_peer_id is None:
                logger.warning("Could not resolve peer '%s' for set_peer_card in session '%s'", peer, session_key)
                return None
            result = self._authed_call(
                "peer card update",
                lambda: self._get_or_create_peer(observer_peer_id).set_card(card, **self._target_kwargs(target_peer_id)),
            )
            logger.info("Updated peer card observer=%s target=%s (%d facts)",
                        observer_peer_id, target_peer_id or observer_peer_id, len(card))
            return result
        return self._guarded_session(session_key, _update, None, logging.ERROR, "Failed to set peer card: %s")

    def seed_ai_identity(self, session_key: str, content: str, source: str = "manual") -> bool:
        """Seed the AI peer's representation from text (SOUL.md, exported chats, ...), sent as an
        assistant-peer message so Honcho's reasoning model incorporates it. Unlike the other
        operations, auth failures are logged and swallowed here too."""
        if not content or not content.strip():
            return False
        session = self._cached_session(session_key)
        if not session:
            logger.warning("No session cached for '%s', skipping AI seed", session_key)
            return False
        if session.honcho_session_id not in self._sessions_cache:
            logger.warning("No Honcho session cached for '%s', skipping AI seed", session_key)
            return False
        wrapped = f"<ai_identity_seed>\n<source>{source}</source>\n\n{content.strip()}\n</ai_identity_seed>"

        def _seed() -> bool:
            assistant_peer = self._get_or_create_peer(session.assistant_peer_id)
            self._sdk_session(session.honcho_session_id).add_messages([assistant_peer.message(wrapped)])
            logger.info("Seeded AI identity from '%s' into %s", source, session_key)
            return True
        try:
            return self._authed_call("identity seed", _seed)
        except Exception as e:
            logger.error("Failed to seed AI identity: %s", e)
            return False

    def get_ai_representation(self, session_key: str) -> dict[str, str]:
        """Fetch the AI peer's representation + card ("" values if unavailable)."""
        def _fetch(session: Any) -> dict[str, str]:
            rep, card = self._peer_context_strings(session.assistant_peer_id, target=session.assistant_peer_id)
            return {"representation": rep, "card": card}
        return self._guarded_session(
            session_key, _fetch, {"representation": "", "card": ""}, logging.DEBUG,
            "Failed to fetch AI representation: %s",
        )

    def dialectic_query(
        self, session_key: str, query: str, reasoning_level: str | None = None, peer: str = "user",
        apply_injection_cap: bool = True, raise_errors: bool = False,
    ) -> str:
        """Ask Honcho's dialectic endpoint about a peer (LLM on the backend; run off-thread).
        ``reasoning_level`` is honored only when dialecticDynamic is true. ``apply_injection_cap``
        clips to ``dialecticMaxChars`` (automatic injection only). ``raise_errors`` re-raises backend
        failures instead of returning "" so explicit tool calls can tell a timeout from an empty answer.
        Raises HonchoAuthError when credentials are rejected after a forced refresh and one retry.

        Args: session_key: The session key to query against. query: Natural language question.
        reasoning_level: Override the configured default (dialecticReasoningLevel). If None or
        dialecticDynamic is false, uses the configured default. peer: Which peer to query — "user" (default)
        or "ai". apply_injection_cap: Clip automatic injections to ``dialecticMaxChars``. Explicit
        ``honcho_reasoning`` calls pass False because Honcho already bounds their output. raise_errors:
        Re-raise backend failures instead of returning "". Explicit tool calls pass True so a timeout or
        server error surfaces as an error, not as "no result" (#36098 issue 4: collapsing failures to ""
        made auth errors, timeouts, and genuinely-empty answers indistinguishable).
        """
        session = self._cached_session(session_key)
        target_peer_id = self._resolve_peer_id(session, peer) if session else None
        if target_peer_id is None:
            return ""
        if len(query) > self._dialectic_max_input_chars:
            query = query[:self._dialectic_max_input_chars].rsplit(" ", 1)[0]
        level = reasoning_level if (self._dialectic_dynamic and reasoning_level) else self._dialectic_reasoning_level

        def _chat_once() -> str:
            # The AI peer observes others when allowed; otherwise each peer queries its own context.
            if self._ai_observes_others(session) and target_peer_id != session.assistant_peer_id:
                observer = self._get_or_create_peer(session.assistant_peer_id)
                return observer.chat(query, target=target_peer_id, reasoning_level=level) or ""
            return self._get_or_create_peer(target_peer_id).chat(query, reasoning_level=level) or ""
        try:
            result = self._authed_call("dialectic query", _chat_once)
            cap = self._dialectic_max_chars if apply_injection_cap else 0
            return result[:cap].rsplit(" ", 1)[0] + " …" if result and cap and len(result) > cap else result
        except HonchoAuthError:
            raise
        except Exception as e:
            logger.warning("Honcho dialectic query failed: %s", e)
            if raise_errors:
                raise
            return ""
