"""EpisodeDigestor — reads conversation_log, segments by topic, ingests 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.classifier import GroqClassifier
    from neo_cortex.conversation_log import ConversationLog
    from neo_cortex.cortex import CortexService

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

MAX_EPISODE_CHARS = 8000

# If the last turn is newer than this, the last segment is still "active"
ACTIVE_THRESHOLD_SECS = 60


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_dir = os.path.join(_data_dir, "cortex_db") if os.environ.get("CORTEX_DATA_DIR") else _data_dir
    _log_path = os.path.join(_log_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: segments conversation by topic, ingests completed topics.

    - Every tick: grab ALL undigested turns (regardless of session_id)
    - Ask classifier to segment them by topic
    - Ingest all complete segments
    - Leave last segment if conversation is still active (< 60s since last turn)
    """

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

    async def tick(self) -> None:
        """Called every ~30s. Process all undigested turns by topic."""
        if not self._log:
            return

        entries = self._log.get_undigested(limit=5000)
        if not entries:
            return

        _bl(f"tick: {len(entries)} undigested entries")

        # Pair turns into Q+A compressed strings
        turns = self._prepare_turns(entries)
        if not turns:
            # All noise/empty — mark digested
            self._log.mark_digested([e.id for e in entries])
            _bl("tick: all entries were empty/noise, marked digested")
            return

        # Segment by topic via Groq
        if self._classifier and len(turns) > 1:
            try:
                segments = await self._classifier.segment_topics(
                    [t["text"] for t in turns]
                )
            except Exception as e:
                _bl(f"tick: segment_topics FAILED: {e}, fallback to 1 segment")
                segments = [{"start": 0, "end": len(turns) - 1, "topic": "unknown"}]
        else:
            # No classifier or single turn — treat as 1 segment
            segments = [{"start": 0, "end": len(turns) - 1, "topic": "unknown"}]

        if not segments:
            _bl("tick: no segments returned")
            return

        # Check if last segment is still active
        now = time.time()
        last_entry_ts = max(e.timestamp for e in entries)
        still_active = (now - last_entry_ts) < ACTIVE_THRESHOLD_SECS

        _bl(f"tick: {len(segments)} segments, still_active={still_active}")

        # Process segments
        for i, seg in enumerate(segments):
            is_last = (i == len(segments) - 1)

            if is_last and still_active:
                _bl(f"tick: skipping last segment (active) topic='{seg.get('topic', '?')}'")
                break

            await self._digest_segment(seg, turns, entries)

    async def _digest_segment(
        self,
        seg: dict,
        turns: list[dict],
        entries: list[ConversationEntry],
    ) -> None:
        """Ingest one topic segment as an episode."""
        start = seg.get("start", 0)
        end = seg.get("end", len(turns) - 1)
        topic = seg.get("topic", "unknown")

        # Clamp indices
        start = max(0, min(start, len(turns) - 1))
        end = max(start, min(end, len(turns) - 1))

        # Collect the entry IDs that belong to this segment
        segment_entry_ids = []
        for t in turns[start:end + 1]:
            segment_entry_ids.extend(t["entry_ids"])

        # Build episode text from the turns
        text_parts = []
        for t in turns[start:end + 1]:
            text_parts.append(t["full_text"])

        text = "\n".join(text_parts)
        if len(text) > MAX_EPISODE_CHARS:
            text = text[:MAX_EPISODE_CHARS]

        if not text.strip():
            self._log.mark_digested(segment_entry_ids)
            return

        # Use first entry's session_id for the episode
        session_id = turns[start]["session_id"] if turns[start].get("session_id") else "unknown"

        try:
            mid = await self._cortex.ingest_episode(text, session_id)
            if mid:
                self._total_ingested += 1
                _bl(f"ingested: topic='{topic}' turns={start}-{end} chars={len(text)} → {mid}")
            else:
                _bl(f"filtered: topic='{topic}' turns={start}-{end} (dedup/empty)")
        except Exception as e:
            _bl(f"FAILED: topic='{topic}' turns={start}-{end} error={e}")

        self._log.mark_digested(segment_entry_ids)
        self._total_segments += 1

    def _prepare_turns(self, entries: list[ConversationEntry]) -> list[dict]:
        """Group raw entries into turn dicts with compressed text.

        Returns list of:
            {"text": "User: ... | Assistant: ...",
             "full_text": "User: ...\nAssistant: ...",
             "entry_ids": [id1, id2],
             "session_id": "abc"}
        """
        turns: list[dict] = []
        i = 0
        while i < len(entries):
            e = entries[i]

            if e.role == "user":
                user_text = e.content
                entry_ids = [e.id]
                session_id = e.session_id

                # Look ahead for assistant response
                asst_text = ""
                j = i + 1
                while j < len(entries):
                    nxt = entries[j]
                    if nxt.role == "assistant":
                        content = nxt.content
                        if "<thinking>" in content:
                            content = re.sub(
                                r"<thinking>.*?</thinking>", "", content, flags=re.DOTALL
                            ).strip()
                        if content:
                            asst_text = content
                        entry_ids.append(nxt.id)
                        j += 1
                        break
                    elif nxt.role == "tool_use":
                        entry_ids.append(nxt.id)
                        j += 1
                    elif nxt.role == "tool_result":
                        entry_ids.append(nxt.id)
                        j += 1
                    else:
                        break

                # Compressed version for segmentation prompt
                compressed = f"User: {user_text[:200]}"
                if asst_text:
                    compressed += f" | Assistant: {asst_text[:300]}"

                # Full version for episode text
                full = f"User: {user_text}"
                if asst_text:
                    full += f"\nAssistant: {asst_text}"

                turns.append({
                    "text": compressed,
                    "full_text": full,
                    "entry_ids": entry_ids,
                    "session_id": session_id,
                })
                i = j
            else:
                # Orphan assistant/tool entry — include it
                turns.append({
                    "text": f"{e.role}: {e.content[:200]}",
                    "full_text": f"{e.role}: {e.content}",
                    "entry_ids": [e.id],
                    "session_id": e.session_id,
                })
                i += 1

        return turns
