"""EpisodeDigestor — reads conversation_log, digests closed sessions into episodes."""

import json
import logging
import re
import time
from typing import TYPE_CHECKING

from neo_cortex.models import ConversationEntry

if TYPE_CHECKING:
    from neo_cortex.conversation_log import ConversationLog
    from neo_cortex.cortex import CortexService

logger = logging.getLogger("neo-cortex-digestor")

# A session is "closed" if its last entry is older than this
SESSION_CLOSED_SECS = 300  # 5 minutes

MAX_EPISODE_CHARS = 8000


def _bl(msg: str) -> None:
    """Boot log — same pattern as mcp.py for consistent logging."""
    import os
    _data_dir = os.environ.get("CORTEX_DATA_DIR", os.getcwd())
    _log_path = os.path.join(_data_dir, "neo-cortex.log")
    line = f"{time.strftime('%Y-%m-%d %H:%M:%S')} [DIGESTOR] {msg}\n"
    try:
        with open(_log_path, "a", encoding="utf-8") as f:
            f.write(line)
    except Exception:
        pass


class EpisodeDigestor:
    """Background worker: wakes up, digests all closed sessions, sleeps.

    - Closed session: last entry > SESSION_CLOSED_SECS ago → digest entire session
    - Open session: still being written to → leave it alone
    """

    def __init__(
        self,
        log: 'ConversationLog | None',
        cortex: 'CortexService',
    ):
        self._log = log
        self._cortex = cortex
        self._total_sessions = 0
        self._total_ingested = 0

    async def tick(self) -> None:
        """Called periodically. Drains ALL closed sessions in one go."""
        if not self._log:
            return

        now = time.time()
        processed_any = True

        # Loop until nothing left — get_undigested has a limit,
        # so we keep fetching until no closed sessions remain
        while processed_any:
            processed_any = False
            grouped = self._log.get_undigested_by_session(limit=5000)
            if not grouped:
                break

            closed = 0
            skipped = 0
            for sid, entries in grouped.items():
                last_ts = max(e.timestamp for e in entries)

                if now - last_ts < SESSION_CLOSED_SECS:
                    # Session still open — leave it
                    skipped += 1
                    continue

                # Session closed — digest the whole thing
                await self._digest_session(sid, entries)
                processed_any = True
                closed += 1

            if closed > 0 or skipped > 0:
                _bl(f"tick: closed={closed} digested, open={skipped} skipped")

    async def _digest_session(self, session_id: str, entries: list[ConversationEntry]) -> None:
        """Digest an entire closed session as one episode."""
        text = self._combine_entries(entries)
        if not text:
            # Mark as digested anyway (empty/noise)
            self._log.mark_digested([e.id for e in entries])
            return

        try:
            mid = await self._cortex.ingest_episode(text, session_id)
            if mid:
                self._total_ingested += 1
                _bl(f"ingested: session={session_id[:12]} turns={len(entries)} chars={len(text)} → {mid}")
            else:
                _bl(f"filtered: session={session_id[:12]} turns={len(entries)} (dedup/empty)")
        except Exception as e:
            _bl(f"FAILED: session={session_id[:12]} turns={len(entries)} error={e}")

        # Mark all entries as digested regardless of ingest result
        self._log.mark_digested([e.id for e in entries])
        self._total_sessions += 1

    def _combine_entries(self, entries: list[ConversationEntry]) -> str:
        """Assemble entries into episode text."""
        if not entries:
            return ""

        parts: list[str] = []
        for e in entries:
            if e.role == "user":
                parts.append(f"User: {e.content}")
            elif e.role == "assistant":
                content = e.content
                if "<thinking>" in content:
                    content = re.sub(
                        r"<thinking>.*?</thinking>", "", content, flags=re.DOTALL
                    ).strip()
                if content:
                    parts.append(f"Assistant: {content}")
            elif e.role == "tool_use":
                try:
                    data = json.loads(e.content)
                    tool = data.get("tool", "unknown")
                    parts.append(f"[Tool: {tool}]")
                except (json.JSONDecodeError, TypeError):
                    parts.append("[Tool call]")
            # tool_result: skip (too verbose)

        combined = "\n".join(parts)
        if len(combined) > MAX_EPISODE_CHARS:
            combined = combined[:MAX_EPISODE_CHARS]
        return combined
