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

import json
import logging
import sys
from typing import Optional

import httpx

from neo_cortex import config
from neo_cortex.models import Activity, ClassificationResult, QueryAnalysis, RecalledMemory

logger = logging.getLogger(__name__)

# uv-managed Python on Windows has broken OpenSSL (ssl.create_default_context() crashes).
_VERIFY_SSL = sys.platform != "win32"


def _strip_code_fences(text: str) -> str:
    """Strip markdown code fences from LLM output."""
    text = text.strip()
    if text.startswith("```"):
        text = "\n".join(text.split("\n")[1:])
    if text.endswith("```"):
        text = "\n".join(text.split("\n")[:-1])
    return text.strip()


CLASSIFY_PROMPT = """Classify and extract structured info from this conversation turn.
Return ONLY valid JSON, no explanation.

{
  "project": "project name or 'general' or 'soul'",
  "topic": "1-3 word topic",
  "activity": "one of: bugfix|feature|debug|config|deploy|research|discussion|learning|identity|behavior|growth",
  "title": "concise 5-10 word title describing what happened",
  "summary": "one sentence summary",
  "facts": ["concrete fact 1", "concrete fact 2"],
  "concepts": ["abstract-concept-1", "abstract-concept-2"],
  "files_touched": ["path/to/file.py"]
}

Extract facts = concrete things learned/done.
Extract concepts = abstract topics (e.g. "testing", "deployment").
Extract files only if explicitly mentioned.
If uncertain about a field, omit it.

Known projects: neo-console, neo-reloaded, neo-cortex, ai2ai, memory-bridge, openclaw, moltbook, soul
Project "soul" is for: identity, personality, behavior, lessons, growth, self-reflection, values.

Q+A:
"""

NOISE_PROMPT = """You are a noise filter for a developer's memory system.

NOISE (skip) = no lasting knowledge:
- Pure greetings: "ciao", "hey", "hello"
- Acknowledgments: "ok", "si", "grazie", "perfetto"
- System commands: "/clear", "kill", "esci"

SIGNAL (keep) = real work or knowledge:
- Questions about code, bugs, architecture
- Instructions to do something specific
- Observations, decisions, opinions with substance

KEY RULE: if the message is an instruction to DO something (even informal/typos), it is SIGNAL.

Return ONLY: {"noise": true/false, "why": "2-3 words"}

Message: """

QUERY_ANALYZE_PROMPT = """Analyze this search query. Return ONLY valid JSON, no explanation.

{
  "project": "project name or null",
  "topic": "topic keyword or null",
  "activity": "activity type or null",
  "time_hint": "recent|old|any",
  "refined_query": "rewritten query optimized for semantic search"
}

Known projects: neo-console, neo-reloaded, neo-cortex, ai2ai, memory-bridge, openclaw, moltbook, soul

Query: """

AGGREGATE_PROMPT = """Compress these memory results into ONE compact MBEL block (max 10 lines).

MBEL Grammar:
- > = past/did, @ = present/is, ? = future/todo
- :: = defines/is, -> = causes, <- = because
- [] = section, {} = metadata, () = note
- CamelCase for multi-word, no articles

Memories to aggregate:
"""


class GroqClassifier:
    """Async Groq LLM client for classification, noise filtering, query analysis."""

    def __init__(self, api_key: str):
        self._api_key = api_key
        self._client = httpx.AsyncClient(timeout=15.0, verify=_VERIFY_SSL)

    async def classify(self, question: str, answer: str) -> ClassificationResult:
        """Classify a Q+A turn into project/topic/activity + structured fields."""
        text = f"Q: {question[:300]}\nA: {answer[:500]}"
        try:
            raw = await self._call(CLASSIFY_PROMPT, text, max_tokens=300)
            raw = _strip_code_fences(raw)
            data = json.loads(raw)

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

            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:
            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."""
        try:
            raw = await self._call(NOISE_PROMPT, question[:200], max_tokens=40, temperature=0.3)
            raw = _strip_code_fences(raw)
            data = json.loads(raw)
            return bool(data.get("noise", False))
        except Exception:
            return False

    async def analyze_query(self, query: str) -> QueryAnalysis:
        """Analyze a search query to extract filters and refine it."""
        try:
            raw = await self._call(QUERY_ANALYZE_PROMPT, query, max_tokens=150)
            raw = _strip_code_fences(raw)
            data = json.loads(raw)
            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:
            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}"

        parts = []
        for i, mem in enumerate(memories, 1):
            q = mem.record.question[:200]
            a = mem.record.answer_preview[:300]
            parts.append(f"[{i}] Q: {q}\nA: {a}")

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

        try:
            raw = await self._call(AGGREGATE_PROMPT, combined, max_tokens=400)
            lines = [line for line in raw.split("\n") if line.strip()][:10]
            return "\n".join(lines)
        except Exception:
            return "\n".join(f"@recall::{m.record.question[:80]}" for m in memories[:5])

    async def _call(
        self,
        prompt: str,
        text: str,
        max_tokens: int = 250,
        temperature: float = 0.1,
    ) -> str:
        """Call Groq API."""
        resp = await self._client.post(
            config.GROQ_URL,
            headers={
                "Authorization": f"Bearer {self._api_key}",
                "Content-Type": "application/json",
            },
            json={
                "model": config.GROQ_MODEL,
                "messages": [
                    {"role": "user", "content": f'{prompt}"{text}"'},
                ],
                "temperature": temperature,
                "max_tokens": max_tokens,
            },
        )
        resp.raise_for_status()
        data = resp.json()
        return data["choices"][0]["message"]["content"].strip()

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