"""LLM-based knowledge extraction from conversations."""

import json
import logging

from anthropic import AsyncAnthropic
from openai import AsyncOpenAI

from sulci_core.config import SulciConfig
from sulci_core.models import AtomType, KnowledgeAtom
from sulci_core.store import KnowledgeStore

logger = logging.getLogger(__name__)

EXTRACTION_PROMPT = """\
You are a knowledge extraction engine. Analyze the following conversation and extract \
structured knowledge atoms. Each atom should be a discrete, reusable piece of knowledge.

Extract these types:
- fact: Objective information discussed or established
- decision: Choices made or conclusions reached
- preference: User preferences, likes, dislikes, or opinions
- entity: People, projects, tools, or organizations mentioned
- relationship: Connections between entities
- context: Situational information about what the user is working on
- instruction: Guidelines, rules, or conventions the user wants followed

For each atom, provide:
- atom_type: one of the types above
- content: human-readable description (1-2 sentences)
- subject: the main entity or topic (short string)
- predicate: the property or relationship (short string, optional)
- object: the value or target (short string, optional)
- confidence: 0.0-1.0 how certain this knowledge is
- tags: relevant tags (list of strings)
- projects: list of project names this knowledge belongs to. Use [] (empty) for universal \
knowledge (preferences, instructions, conventions that apply everywhere). Use ["project-name"] \
for project-specific knowledge (facts about a specific codebase, decisions for a particular project).

IMPORTANT: Do NOT include PII (emails, phone numbers, physical addresses, SSNs, credit card numbers) \
in extracted atoms. Extract the knowledge without the personal data. For example, instead of \
"John's email is john@example.com", write "John has a contact email on file".

Respond with ONLY a JSON array of atoms. No markdown, no explanation.

Example output:
[
  {
    "atom_type": "preference",
    "content": "User prefers Python for backend development",
    "subject": "user",
    "predicate": "prefers_language",
    "object": "Python",
    "confidence": 0.9,
    "tags": ["programming", "preferences"],
    "projects": []
  }
]

Conversation to analyze:
"""

INTEGRATION_EXTRACTION_PROMPT = """\
You are a knowledge extraction engine processing data from an automated integration sync.
ONLY extract knowledge that is:
- Durable and reusable (will still be relevant in a week)
- Significant decisions, commitments, or action items
- Key facts about people, projects, or organizations
- Preferences or instructions expressed by people

DO NOT extract:
- Routine/transient info (meeting reminders, status updates, "joining the call")
- Scheduling logistics (unless it reveals a preference or commitment)
- Social pleasantries, small talk, automated notifications
- Information that's only relevant for a few hours

Be highly selective — extract 0-3 atoms maximum per batch. It's better to extract nothing \
than to store noise.

For each atom, provide:
- atom_type: one of fact, decision, preference, entity, relationship, context, instruction
- content: human-readable description (1-2 sentences)
- subject: the main entity or topic (short string)
- predicate: the property or relationship (short string, optional)
- object: the value or target (short string, optional)
- confidence: 0.0-1.0 how certain this knowledge is
- tags: relevant tags (list of strings)
- projects: list of project names this knowledge belongs to. Use [] for universal knowledge.

IMPORTANT: Do NOT include PII (emails, phone numbers, physical addresses, SSNs, credit card numbers).

Respond with ONLY a JSON array of atoms. No markdown, no explanation. Return [] if nothing \
is worth extracting.

Data to analyze:
"""


class KnowledgeExtractor:
    """Extract structured knowledge from conversations using LLMs."""

    def __init__(self, config: SulciConfig, store: KnowledgeStore) -> None:
        self.config = config
        self.store = store

    async def extract_from_messages(
        self,
        messages: list[dict],
        source_tool: str,
        project: str | None = None,
        integration_mode: bool = False,
    ) -> list[KnowledgeAtom]:
        """Extract knowledge atoms from a conversation.

        When integration_mode=True, uses a selective prompt that filters out
        routine/transient information from automated integration syncs.
        """
        # Format conversation for the LLM
        conversation_text = self._format_messages(messages)
        if project:
            conversation_text = f"[Project context: {project}]\n\n" + conversation_text
        base_prompt = INTEGRATION_EXTRACTION_PROMPT if integration_mode else EXTRACTION_PROMPT
        prompt = base_prompt + conversation_text

        # Call LLM
        raw_json = await self._call_llm(prompt)

        # Parse response
        atoms = self._parse_atoms(raw_json, source_tool, project=project)

        # Auto-redact minor PII that slipped through
        from sulci_core.pii import PIIScanner

        pii_scanner = PIIScanner()
        for atom in atoms:
            if pii_scanner.has_pii(atom.content):
                atom.content = pii_scanner.redact(atom.content)
            if atom.subject and pii_scanner.has_pii(atom.subject):
                atom.subject = pii_scanner.redact(atom.subject)
            if atom.object and pii_scanner.has_pii(atom.object):
                atom.object = pii_scanner.redact(atom.object)

        # Deduplicate against existing store
        atoms = await self._deduplicate(atoms)

        # Store new atoms with conflict detection
        stored = []
        for atom in atoms:
            stored_atom, _conflicts = await self.store.add_atom_with_conflict_check(atom)
            stored.append(stored_atom)

        return stored

    async def _call_llm(self, prompt: str) -> str:
        """Call the configured LLM provider."""
        if self.config.llm_provider == "anthropic":
            return await self._call_anthropic(prompt)
        return await self._call_openai(prompt)

    async def _call_openai(self, prompt: str) -> str:
        client = AsyncOpenAI(api_key=self.config.openai_api_key)
        response = await client.chat.completions.create(
            model=self.config.openai_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1,
            response_format={"type": "json_object"},
        )
        return response.choices[0].message.content or "[]"

    async def _call_anthropic(self, prompt: str) -> str:
        client = AsyncAnthropic(api_key=self.config.anthropic_api_key)
        response = await client.messages.create(
            model=self.config.anthropic_model,
            max_tokens=4096,
            messages=[{"role": "user", "content": prompt}],
        )
        return response.content[0].text

    def _format_messages(self, messages: list[dict]) -> str:
        """Format conversation messages into readable text."""
        lines = []
        for msg in messages:
            role = msg.get("role", "unknown")
            content = msg.get("content", "")
            if isinstance(content, list):
                # Handle multimodal content
                text_parts = [p.get("text", "") for p in content if p.get("type") == "text"]
                content = " ".join(text_parts)
            lines.append(f"{role}: {content}")
        return "\n".join(lines)

    def _parse_atoms(self, raw_json: str, source_tool: str, project: str | None = None) -> list[KnowledgeAtom]:
        """Parse LLM output into KnowledgeAtom objects."""
        try:
            # Strip markdown code fences (Claude sometimes wraps JSON)
            text = raw_json.strip()
            if text.startswith("```"):
                lines = text.split("\n")
                # Remove first line (```json) and last line (```)
                lines = [line for line in lines if not line.strip().startswith("```")]
                text = "\n".join(lines)

            data = json.loads(text)
            # Handle both raw array and {"atoms": [...]} wrapper
            if isinstance(data, dict):
                data = data.get("atoms", data.get("results", []))
            if not isinstance(data, list):
                logger.warning("LLM returned non-list: %s", type(data))
                return []
        except json.JSONDecodeError:
            logger.warning("Failed to parse LLM JSON output: %.200s", raw_json)
            return []

        atoms = []
        for item in data:
            try:
                # Read projects from LLM output, fallback to project hint
                projects = item.get("projects", [])
                if not isinstance(projects, list):
                    projects = []
                # If LLM didn't set projects and we have a hint, use it for non-global types
                if not projects and project:
                    atom_type = item.get("atom_type", "")
                    if atom_type not in ("preference", "instruction"):
                        projects = [project]

                atom = KnowledgeAtom(
                    atom_type=AtomType(item["atom_type"]),
                    content=item["content"],
                    subject=item.get("subject"),
                    predicate=item.get("predicate"),
                    object=item.get("object"),
                    confidence=float(item.get("confidence", 0.7)),
                    source_tool=source_tool,
                    tags=item.get("tags", []),
                    projects=projects,
                )
                atoms.append(atom)
            except (KeyError, ValueError) as e:
                logger.warning("Skipping malformed atom: %s", e)
                continue

        return atoms

    async def _deduplicate(self, atoms: list[KnowledgeAtom]) -> list[KnowledgeAtom]:
        """Remove atoms that duplicate existing knowledge, merging projects."""
        unique = []
        for atom in atoms:
            # Search for similar existing atoms
            results = await self.store.search_atoms(atom.content, limit=3)
            is_duplicate = False
            for result in results:
                if result.vector_similarity > 0.92:
                    # Very similar — treat as duplicate
                    logger.debug(
                        "Duplicate detected: '%s' ~ '%s' (%.2f)",
                        atom.content,
                        result.atom.content,
                        result.vector_similarity,
                    )
                    is_duplicate = True
                    # Merge projects and bump confidence on the existing atom
                    updated = False
                    if atom.confidence > result.atom.confidence:
                        result.atom.confidence = atom.confidence
                        updated = True
                    for proj in atom.projects:
                        if proj and proj not in result.atom.projects:
                            result.atom.projects.append(proj)
                            updated = True
                    if updated:
                        await self.store.update_atom(result.atom)
                    break
            if not is_duplicate:
                unique.append(atom)

        return unique
