"""CortexService — orchestrator for ingest, recall, dream, stats, timeline."""

import logging
import time

from neo_cortex.classifier import GroqClassifier
from neo_cortex.embedder import JinaEmbedder
from neo_cortex.memory_index import MemoryIndex
from neo_cortex.models import (
    Activity,
    DreamResult,
    IngestRequest,
    MemoryRecord,
    RecalledMemory,
    RecallResult,
    StructuredFields,
    TimelineResult,
)
from neo_cortex.store import MemoryStore

logger = logging.getLogger(__name__)

# Static noise patterns — tier 1 filter (free, instant)
NOISE_PATTERNS = frozenset({
    "ciao", "/clear", "eccoti", "kill", "kill?", "esci", "killa ti",
    "puoi killare", "restarta ti", "indovina", "laco",
    "hey", "hello", "hi", "buongiorno", "buonasera", "salve",
    "ok", "si", "no", "va bene", "grazie", "thanks", "yes", "yep",
    "nope", "nah", "sure", "done", "fatto", "perfetto", "bene",
    "vai", "avanti", "continua", "stop",
})


class CortexService:
    """Orchestrates memory ingestion, recall, and lifecycle."""

    def __init__(
        self,
        store: MemoryStore,
        embedder: JinaEmbedder,
        classifier: GroqClassifier,
        index: MemoryIndex | None = None,
    ):
        self._store = store
        self._embedder = embedder
        self._classifier = classifier
        self._index = index
        self._dream_count = 0

    async def ingest(self, request: IngestRequest) -> str | None:
        """Ingest a conversation turn.

        Flow: noise check → embed → classify → store.
        Returns memory ID or None if filtered as noise.
        """
        # Tier 1: static noise filter
        q = request.question.strip().lower()
        if q.startswith("[system]"):
            return None
        if q.rstrip("!?.,;:") in NOISE_PATTERNS:
            return None

        # Tier 2: Groq noise filter for short ambiguous messages
        if len(q) <= 50:
            if await self._classifier.is_noise(request.question):
                return None

        # Build combined text
        combined = f"Q: {request.question}\nA: {request.answer}"
        if len(combined) > 8000:
            combined = combined[:8000]

        # Embed
        embedding = await self._embedder.embed_passage(combined)

        # Classify
        classification = await self._classifier.classify(
            request.question, request.answer
        )

        # Build record
        ts = time.time()
        memory_id = f"v3_{request.session_id[:8]}_{request.turn_number}_{int(ts) % 100000}"

        record = MemoryRecord(
            id=memory_id,
            session_id=request.session_id,
            timestamp=ts,
            turn_number=request.turn_number,
            question=request.question[:500],
            answer_preview=request.answer[:500],
            document=combined,
            project=classification.project,
            topic=classification.topic,
            activity=classification.activity,
            energy=1.0,
            model=request.model,
            source=request.source,
            tools_used=request.tools_used,
        )

        # Store in ChromaDB
        self._store.insert(record, embedding)

        # Dual-write to SQLite index
        if self._index:
            structured = StructuredFields(
                title=classification.title,
                summary=classification.summary,
                facts=classification.facts,
                concepts=classification.concepts,
                files_touched=classification.files_touched,
            )
            self._index.insert(record, structured)

        logger.info("Ingested memory %s [%s/%s]", memory_id, classification.project, classification.topic)
        return memory_id

    async def recall(self, query: str, n: int = 5, smart: bool = True) -> RecallResult:
        """Recall memories relevant to a query.

        Smart mode: analyze query → embed refined → search with filters → aggregate.
        Basic mode: embed query → vector search → return.
        """
        if smart:
            return await self._smart_recall(query, n)
        return await self._basic_recall(query, n)

    async def _basic_recall(self, query: str, n: int) -> RecallResult:
        try:
            embedding = await self._embedder.embed_query(query)
            results = self._store.query(embedding, n=n)
            memories = [RecalledMemory(record=r, similarity=s) for r, s in results]
            return RecallResult(query=query, count=len(memories), memories=memories)
        except Exception as e:
            logger.warning("Embedding failed, falling back to FTS: %s", e)
            return self._fts_recall(query, n)

    def _fts_recall(self, query: str, n: int) -> RecallResult:
        if not self._index:
            return RecallResult(query=query, count=0, memories=[])
        compact = self._index.search_fts(query, n=n)
        ids = [c.id for c in compact]
        records = self._index.get_by_ids(ids)
        memories = [RecalledMemory(record=r, similarity=0.5) for r in records]
        return RecallResult(query=query, count=len(memories), memories=memories)

    async def _smart_recall(self, query: str, n: int) -> RecallResult:
        # Analyze query
        analysis = await self._classifier.analyze_query(query)

        try:
            # Embed refined query
            embedding = await self._embedder.embed_query(analysis.refined_query)

            # Build where filters
            where = self._build_where(analysis)

            # Fetch 3x candidates for re-ranking
            fetch_n = min(n * 3, 20)
            results = self._store.query(embedding, n=fetch_n, where=where)

            # If filtered returns too few, retry without filter
            if len(results) < 2 and where:
                results = self._store.query(embedding, n=fetch_n)

            memories = [RecalledMemory(record=r, similarity=s) for r, s in results]
        except Exception as e:
            logger.warning("Smart recall embedding failed, falling back to FTS: %s", e)
            fts_result = self._fts_recall(query, n)
            memories = fts_result.memories

        # Aggregate to MBEL
        mbel = await self._classifier.aggregate_to_mbel(memories[:n])

        return RecallResult(
            query=query,
            count=len(memories[:n]),
            memories=memories[:n],
            mbel=mbel,
        )

    def _build_where(self, analysis: 'QueryAnalysis') -> dict | None:
        from neo_cortex.models import QueryAnalysis as _QA  # noqa: avoid circular at module level

        conditions = []
        if analysis.project:
            conditions.append({"project": {"$eq": analysis.project}})
        if analysis.activity_filter:
            conditions.append({"activity": {"$eq": analysis.activity_filter}})
        if analysis.time_hint == "recent":
            cutoff = time.time() - (7 * 24 * 3600)
            conditions.append({"timestamp": {"$gte": cutoff}})
        elif analysis.time_hint == "old":
            cutoff = time.time() - (7 * 24 * 3600)
            conditions.append({"timestamp": {"$lt": cutoff}})

        if not conditions:
            return None
        if len(conditions) == 1:
            return conditions[0]
        return {"$and": conditions}

    async def dream(self) -> DreamResult:
        """Run one dream cycle: boost high-energy cluster, decay others."""
        all_meta = self._store.get_all_metadata()
        if len(all_meta) < 2:
            return DreamResult(status="skipped", dream_count=self._dream_count, cluster=[])

        # Find highest energy memory
        seed = max(all_meta, key=lambda m: m.get("energy", 0))
        seed_id = seed["id"]

        # Find top 3 most similar to seed (approximate cluster)
        # For simplicity, use energy-based neighborhood
        high_energy = {
            m["id"] for m in all_meta
            if m.get("energy", 0) >= seed.get("energy", 1.0) * 0.8
        }
        cluster = high_energy | {seed_id}

        # Boost cluster, decay rest
        for meta in all_meta:
            mid = meta["id"]
            energy = meta.get("energy", 1.0)
            if mid in cluster:
                new_energy = min(1.0, energy + 0.1)
            else:
                new_energy = max(0.05, energy - 0.03)
            if abs(new_energy - energy) > 0.001:
                self._store.update_energy(mid, new_energy)

        self._dream_count += 1
        return DreamResult(
            status="completed",
            dream_count=self._dream_count,
            cluster=list(cluster),
        )

    def stats(self) -> 'CortexStats':
        from neo_cortex.models import CortexStats as _CS  # noqa
        return self._store.stats(self._dream_count)

    async def timeline(self, n: int = 10, project: str | None = None, smart: bool = False) -> TimelineResult:
        memories = self._store.timeline(limit=n, project=project)
        mbel = None
        if smart and memories:
            recalled = [RecalledMemory(record=m, similarity=1.0) for m in memories]
            mbel = await self._classifier.aggregate_to_mbel(recalled)
        return TimelineResult(count=len(memories), memories=memories, mbel=mbel)
