"""GeminiDistiller — distills raw conversation turns into clean knowledge entries.

Uses Gemini 2.0 Flash to extract factual, standalone knowledge from noisy
conversation logs. Produces structured KnowledgeEntry objects ready for
embedding and storage.
"""

import json
import logging
import sys
import time

import httpx
from pydantic import BaseModel, Field

from neo_cortex import config

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

_VERIFY_SSL = sys.platform != "win32"


def _bl(msg: str) -> None:
    """Boot log — same pattern as other modules."""
    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')} [DISTILLER] {msg}\n"
    try:
        with open(_log_path, "a", encoding="utf-8") as f:
            f.write(line)
    except Exception:
        pass


DISTILL_PROMPT = """You are a KNOWLEDGE DISTILLER for a software development project.

Extract factual knowledge entries from this conversation log between Neo (Italian-speaking developer) and an AI assistant.

OUTPUT: JSON array only. No markdown code blocks, no explanation, no preamble.

Each entry:
{
  "topic": "short title (3-8 words, English)",
  "content": "2-4 sentence factual description. Standalone. English only.",
  "type": "decision|architecture|bugfix|feature|config|lesson|preference",
  "project": "neo-cortex|neo-reloaded|neo-vscode|neo-ram|general",
  "concepts": ["technical-concept-1", "technical-concept-2"]
}

concepts = specific technical terms, tools, libraries, patterns, protocols mentioned.
Examples: "ChromaDB", "FTS5", "SimHash", "Gemini", "conflict-resolution", "embeddings", "digestor", "MBEL", "SignalR", "BBC-architecture", "LLMClient".
NOT abstract categories like "bugfix" or "feature" — those go in "type".
2-5 concepts per entry.

EXAMPLE:
{"topic": "MIN_SIMILARITY filter for recall", "content": "MIN_SIMILARITY=0.2 threshold added to _smart_recall() in cortex.py. ChromaDB previously returned N results regardless of similarity. When best vector < 0.4, FTS5 full-text results blended as fallback via _fts_blend().", "type": "bugfix", "project": "neo-cortex", "concepts": ["ChromaDB", "FTS5", "similarity-threshold", "recall-pipeline"]}

RULES:
- Extract ALL significant knowledge — do NOT limit the number of entries
- ONE FACT = ONE ENTRY. Do NOT merge multiple facts into one entry, even if related
- Specific > vague. Include file paths, function names, versions, errors, exact numbers
- Each entry standalone — no references to other entries
- English, third person factual
- EVERY entry MUST have "concepts" with 2-5 specific technical terms — NEVER leave it empty

SKIP individual turns that are:
- Greetings, yes/no, interruptions, "ok", "done", confirmations, complaints, frustration
- Tool output, XML tags, git status, raw logs, command output
- Internal tool operations from Claude Code: TaskCreate, TaskUpdate, TaskList, task descriptions,
  task priorities, task status changes, todo items. These are ephemeral workflow artifacts, NOT
  project knowledge. Example noise: "Add priority field to Task model", "Task description persistence"
- Configuration values alone (API keys, env vars, paths) without meaningful technical context

IMPORTANT: Even if many turns are noise, ALWAYS extract knowledge from the meaningful turns.
A batch may contain 80% noise (interruptions, frustration, debugging chatter) and 20% real technical
content — you MUST still extract the technical knowledge from that 20%. Only return an empty array
if there is TRULY zero technical content in the entire batch.

Conversation log:

"""


class KnowledgeEntry(BaseModel):
    """A single distilled knowledge entry from conversation."""
    topic: str
    content: str
    type: str = "feature"
    project: str = "general"
    concepts: list[str] = Field(default_factory=list)


class GeminiDistiller:
    """Distills raw conversation turns into clean knowledge entries via Gemini."""

    def __init__(self, api_key: str):
        self._api_key = api_key
        self._client = httpx.AsyncClient(timeout=60, verify=_VERIFY_SSL)
        self._model = config.GEMINI_MODEL
        _bl(f"init: model={self._model}")

    async def distill(self, turns: list[str]) -> list[KnowledgeEntry]:
        """Send conversation turns to Gemini, return structured knowledge entries."""
        if not turns:
            return []

        text = "\n\n".join(turns)
        _bl(f"distill: {len(turns)} turns, {len(text)} chars")

        try:
            resp = await self._client.post(
                f"{config.GEMINI_URL}/models/{self._model}:generateContent?key={self._api_key}",
                json={
                    "contents": [{"parts": [{"text": f"{DISTILL_PROMPT}{text}"}]}],
                    "generationConfig": {
                        "temperature": 0.2,
                        "maxOutputTokens": 16000,
                    },
                },
            )
            resp.raise_for_status()
        except Exception as e:
            _bl(f"distill: API error: {e}")
            logger.error("Gemini API error: %s", e)
            return []

        try:
            data = resp.json()
            content = data["candidates"][0]["content"]["parts"][0]["text"]
        except (KeyError, IndexError) as e:
            _bl(f"distill: response parse error: {e}")
            return []

        return self._parse_entries(content)

    def _parse_entries(self, raw: str) -> list[KnowledgeEntry]:
        """Parse JSON array from Gemini response into KnowledgeEntry list."""
        start = raw.find("[")
        end = raw.rfind("]")
        if start < 0 or end < start:
            _bl(f"distill: no JSON array in response ({len(raw)} chars)")
            return []

        try:
            items = json.loads(raw[start:end + 1])
        except json.JSONDecodeError as e:
            _bl(f"distill: JSON parse error: {e}")
            return []

        entries = []
        for item in items:
            if not isinstance(item, dict):
                continue
            topic = item.get("topic", "").strip()
            content = item.get("content", "").strip()
            if not topic or not content:
                continue
            entries.append(KnowledgeEntry(
                topic=topic,
                content=content,
                type=item.get("type", "feature"),
                project=item.get("project", "general"),
                concepts=item.get("concepts", []),
            ))

        _bl(f"distill: parsed {len(entries)} entries from {len(items)} items")
        return entries
