"""Memory extractor — post-conversation LLM pass to capture knowledge."""

import json
import re
from datetime import date
from pathlib import Path

import structlog

from fliiq.runtime.llm.providers import BaseLLM
from fliiq.runtime.memory.manager import MemoryManager

log = structlog.get_logger()

_EXTRACTION_PROMPT = """\
You are a memory extraction system. Analyze this conversation and extract \
facts worth remembering for future conversations.

## What to extract:
- People: names, roles, relationships, preferences, contact info, notable traits
- Topics: ongoing projects, recurring interests, goals, decisions made
- Preferences: user likes/dislikes, constraints, communication style, standing instructions
- Decisions: non-trivial choices made with reasoning

## What NOT to extract:
- One-time ephemeral tasks ("send this email now", "set a timer")
- Information already obvious from the conversation medium (e.g. the user's own name if already known)
- Vague or low-confidence observations ("seemed busy")
- Implementation details with no lasting value
- Trivial facts that won't affect future interactions

## Output format:
Return ONLY a JSON object (no markdown fences, no explanation):
{
  "people": [
    {
      "name": "slug-name",
      "display_name": "Full Name",
      "facts": ["fact 1", "fact 2"]
    }
  ],
  "topics": [
    {
      "name": "slug-name",
      "display_name": "Topic Title",
      "facts": ["fact 1", "fact 2"]
    }
  ],
  "decisions": [
    {
      "topic": "slug-name-of-related-topic-or-person",
      "decision": "What was decided",
      "rationale": "Why (1 sentence)"
    }
  ]
}

If nothing worth remembering, return: {"people": [], "topics": [], "decisions": []}
"""

_MAX_SUMMARY_CHARS = 8000


def _build_conversation_summary(messages: list[dict]) -> str:
    """Extract text content from message history for the extraction LLM."""
    parts: list[str] = []
    for msg in messages:
        role = msg.get("role", "unknown")
        content = msg.get("content", "")
        if isinstance(content, str) and content.strip():
            parts.append(f"{role}: {content[:2000]}")
        elif isinstance(content, list):
            for block in content:
                if isinstance(block, dict) and block.get("type") == "text":
                    text = block.get("text", "")[:1000]
                    if text.strip():
                        parts.append(f"{role}: {text}")
    joined = "\n\n".join(parts)
    return joined[:_MAX_SUMMARY_CHARS]


def _parse_extraction(raw: str) -> dict:
    """Parse JSON from LLM response. Tolerant of markdown fences."""
    text = raw.strip()
    # Strip markdown code fences if present
    if text.startswith("```"):
        text = text.split("\n", 1)[-1]
        if text.endswith("```"):
            text = text[:-3].rstrip()
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return {"people": [], "topics": [], "decisions": []}


def _slugify(name: str) -> str:
    """Convert a display name to a filesystem-safe slug."""
    slug = re.sub(r"[^\w\s-]", "", name.lower().strip())
    slug = re.sub(r"[\s_]+", "-", slug)
    return slug.strip("-") or "unknown"


def _merge_entity_file(
    manager: MemoryManager,
    category: str,
    slug: str,
    display_name: str,
    new_facts: list[str],
    decisions: list[dict] | None = None,
) -> None:
    """Merge new facts into an existing entity file, or create it."""
    today = date.today().isoformat()
    existing = manager.load_entity(category, slug)

    if existing:
        existing_lower = existing.lower()
        unique_facts = [f for f in new_facts if f.lower() not in existing_lower]
        unique_decisions = []
        if decisions:
            for d in decisions:
                if d.get("decision", "").lower() not in existing_lower:
                    unique_decisions.append(d)

        if not unique_facts and not unique_decisions:
            return

        addition = f"\n\n## Updated {today}\n"
        for fact in unique_facts:
            addition += f"- {fact}\n"
        for d in unique_decisions:
            addition += f"- Decision: {d['decision']}"
            if d.get("rationale"):
                addition += f" (Reason: {d['rationale']})"
            addition += "\n"

        # Update the 'updated' date in frontmatter if present
        updated_content = existing + addition
        updated_content = re.sub(
            r"^(updated:\s*)\d{4}-\d{2}-\d{2}",
            f"\\g<1>{today}",
            updated_content,
            count=1,
            flags=re.MULTILINE,
        )
        manager.save_entity(category, slug, updated_content)
    else:
        lines = [
            "---",
            f"name: {display_name}",
            f"created: {today}",
            f"updated: {today}",
            "---",
            f"# {display_name}",
            "",
        ]
        for fact in new_facts:
            lines.append(f"- {fact}")
        if decisions:
            lines.append("")
            lines.append("## Decisions")
            for d in decisions:
                line = f"- {d['decision']}"
                if d.get("rationale"):
                    line += f" (Reason: {d['rationale']})"
                lines.append(line)
        manager.save_entity(category, slug, "\n".join(lines) + "\n")


async def extract_and_save_memories(
    llm: BaseLLM,
    messages: list[dict],
    project_root: Path,
) -> dict:
    """Run post-conversation extraction and persist results.

    Returns the extraction dict for logging/testing. Non-fatal — exceptions are caught.
    """
    try:
        summary = _build_conversation_summary(messages)
        if len(summary) < 50:
            return {"people": [], "topics": [], "decisions": []}

        response = await llm.generate(
            messages=[{"role": "user", "content": summary}],
            system=_EXTRACTION_PROMPT,
        )
        extracted = _parse_extraction(response.content)

        manager = MemoryManager.from_project_root(project_root)
        manager.ensure_dirs()

        # Index extracted people/topic slugs for decision scoping
        people_slugs = set()
        topic_slugs = set()

        for person in extracted.get("people", []):
            raw_name = person.get("name", "").strip()
            if not raw_name:
                continue
            slug = _slugify(raw_name)
            people_slugs.add(slug)
            display_name = person.get("display_name", raw_name)
            facts = person.get("facts", [])
            person_decisions = [
                d for d in extracted.get("decisions", [])
                if _slugify(d.get("topic", "")) == slug
            ]
            _merge_entity_file(manager, "people", slug, display_name, facts, person_decisions)

        for topic in extracted.get("topics", []):
            raw_name = topic.get("name", "").strip()
            if not raw_name:
                continue
            slug = _slugify(raw_name)
            topic_slugs.add(slug)
            display_name = topic.get("display_name", raw_name)
            facts = topic.get("facts", [])
            topic_decisions = [
                d for d in extracted.get("decisions", [])
                if _slugify(d.get("topic", "")) == slug
            ]
            _merge_entity_file(manager, "topics", slug, display_name, facts, topic_decisions)

        # Unscoped decisions go to a general decisions log
        all_scoped = people_slugs | topic_slugs
        unscoped = [
            d for d in extracted.get("decisions", [])
            if _slugify(d.get("topic", "")) not in all_scoped
        ]
        if unscoped:
            _merge_entity_file(manager, "topics", "decisions", "Decisions Log", [], unscoped)

        log.debug(
            "memory_extraction_complete",
            people=len(extracted.get("people", [])),
            topics=len(extracted.get("topics", [])),
            decisions=len(extracted.get("decisions", [])),
        )
        return extracted

    except Exception as e:
        log.debug("memory_extraction_failed", error=str(e))
        return {"people": [], "topics": [], "decisions": []}
