"""Classifier — async LLM client for memory classification and query analysis.

Provider-agnostic: takes an LLMClient (Gemini, Groq, etc.) and uses it for
all classification tasks. The Classifier doesn't know which provider it uses.
"""

import json
import logging

from neo_cortex.llm import LLMClient
from neo_cortex.models import Activity, ClassificationResult, QueryAnalysis, RecalledMemory

logger = logging.getLogger(__name__)

from functools import partial
from neo_cortex._log import boot_log
_bl = partial(boot_log, "CLASSIFIER")


class Classifier:
    """LLM-powered classifier for memory operations.

    Provider-agnostic — takes any LLMClient implementation.
    """

    def __init__(self, llm: LLMClient):
        self._llm = llm
        _bl(f"init: provider={llm.name}")

    async def classify(self, question: str, answer: str) -> ClassificationResult:
        """Classify a Q+A turn into project/topic/activity + structured fields."""
        _bl("classify: starting")
        from neo_cortex import config
        projects_hint = ", ".join(config.PROJECT_NAMES)
        text = f"Known projects: {projects_hint}\n\nQ: {question[:300]}\nA: {answer[:500]}"
        try:
            raw = await self._llm.classify(text)
            _bl(f"classify: got response, len={len(raw)}")
            data = json.loads(raw)

            activity_raw = data.get("activity", "discussion")
            try:
                activity = Activity(activity_raw)
            except ValueError:
                activity = Activity.DISCUSSION

            _bl(f"classify: OK → project={data.get('project')}, topic={data.get('topic')}")
            return ClassificationResult(
                project=data.get("project", "general"),
                topic=data.get("topic", "unknown"),
                activity=activity,
                title=data.get("title"),
                summary=data.get("summary"),
                facts=data.get("facts", []),
                concepts=data.get("concepts", []),
                files_touched=data.get("files_touched", []),
            )
        except Exception as e:
            _bl(f"classify: FAILED: {type(e).__name__}: {e}")
            logger.debug("Classification failed: %s", e)
            return ClassificationResult()

    async def is_noise(self, question: str) -> bool:
        """Check if a short message is noise. Defaults to False (keep) on failure."""
        _bl(f"is_noise: checking '{question[:50]}'")
        try:
            raw = await self._llm.check_noise(question[:200])
            data = json.loads(raw)
            result = bool(data.get("noise", False))
            _bl(f"is_noise: result={result}")
            return result
        except Exception as e:
            _bl(f"is_noise: FAILED: {e}")
            return False

    async def analyze_query(self, query: str) -> QueryAnalysis:
        """Analyze a search query to extract filters and refine it."""
        _bl(f"analyze_query: '{query[:50]}'")
        try:
            from neo_cortex import config
            projects_hint = ", ".join(config.PROJECT_NAMES)
            raw = await self._llm.analyze_query(f"Known projects: {projects_hint}\n\n{query}")
            data = json.loads(raw)
            _bl("analyze_query: OK")
            return QueryAnalysis(
                project=data.get("project"),
                topic=data.get("topic"),
                activity_filter=data.get("activity"),
                time_hint=data.get("time_hint", "any"),
                refined_query=data.get("refined_query", query),
            )
        except Exception as e:
            _bl(f"analyze_query: FAILED: {e}")
            return QueryAnalysis(refined_query=query)

    async def aggregate_to_mbel(self, memories: list[RecalledMemory]) -> str:
        """Aggregate recalled memories into compact MBEL notation."""
        if not memories:
            return "@recall::empty{noMemoriesFound}"

        _bl(f"aggregate_to_mbel: {len(memories)} memories")
        parts = []
        for i, mem in enumerate(memories, 1):
            title = mem.record.topic or mem.record.question[:100]
            doc = mem.record.document or mem.record.answer_preview
            parts.append(f"[{i}] {title}\n{doc}")

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

        try:
            raw = await self._llm.aggregate(combined)
            lines = [line for line in raw.split("\n") if line.strip()][:10]
            _bl(f"aggregate_to_mbel: OK, {len(lines)} lines")
            return "\n".join(lines)
        except Exception as e:
            _bl(f"aggregate_to_mbel: FAILED: {e}")
            return "\n".join(f"@recall::{m.record.question[:80]}" for m in memories[:5])

    async def resolve_conflict(
        self,
        new_text: str,
        existing_memories: list[tuple],
    ) -> dict:
        """Compare incoming memory against existing similar memories.

        Returns:
            {"action": "add"|"update"|"noop", "target_id": str|None, "reason": str}
        """
        _bl(f"resolve_conflict: checking against {len(existing_memories)} existing")

        for record, similarity, sf in existing_memories:
            title = sf.title if sf else record.question[:80]
            summary = sf.summary if sf else record.answer_preview[:200]
            facts = json.dumps(sf.facts) if sf and sf.facts else "[]"

            try:
                raw = await self._llm.resolve_conflict(new_text[:500], title, summary, facts)
                data = json.loads(raw)
                action = data.get("action", "add")
                reason = data.get("reason", "")
                _bl(f"resolve_conflict: vs {record.id} → {action} ({reason})")

                if action in ("update", "noop"):
                    return {"action": action, "target_id": record.id, "reason": reason}
            except Exception as e:
                _bl(f"resolve_conflict: FAILED for {record.id}: {e}")
                continue

        _bl("resolve_conflict: no conflict found → add")
        return {"action": "add", "target_id": None, "reason": "no conflict"}

    async def close(self) -> None:
        await self._llm.close()
