#!/usr/bin/env python3
"""
Extraction des règles du Guide Méthodologique MCO 2026 → Q&A raisonnement.

Le Guide Méthodologique est le document de référence ATIH qui définit :
  - Les règles de codage du DP, DR, DAS
  - Les règles CMA et niveaux de sévérité
  - Les règles de codage des actes CCAM
  - Les cas particuliers (séjours multi-unités, transferts, etc.)

Stratégie :
  - Parser le PDF en sections
  - Extraire les règles de codage
  - Générer des Q&A directes (sans LLM) pour les définitions
  - Générer des Q&A de raisonnement via Claude Opus 4.6 pour les règles
  - Focus sur les règles DP/DAS/CMA les plus applicables

Cible : 500 exemples (183 directs + ~320 LLM)

Source : data/raw/guide_methodo_mco_2026.pdf
Nécessite : ANTHROPIC_API_KEY en variable d'environnement

Usage :
  python scripts/16_parse_guide_metho.py [--dry-run] [--max N]
"""

import argparse
import json
import os
import random
import re
import sys
import time
from pathlib import Path

import pdfplumber

random.seed(42)

BASE = Path(__file__).resolve().parent.parent
PDF_PATH = BASE / "data" / "raw" / "guide_methodo_mco_2026.pdf"
OUTPUT = BASE / "data" / "processed" / "guide_metho_chatml.jsonl"
OUTPUT.parent.mkdir(parents=True, exist_ok=True)

MODEL = "claude-opus-4-6"

SYSTEM_PROMPT = (
    "Tu es un médecin DIM expert en codage PMSI. "
    "Tu t'appuies sur le Guide Méthodologique de production des informations "
    "relatives à l'activité MCO (ATIH 2026)."
)

# Patterns pour détecter les règles dans le guide
RULE_PATTERNS = [
    re.compile(r"(?:le\s+)?diagnostic\s+principal\s+(?:est|doit|sera|correspond)", re.IGNORECASE),
    re.compile(r"(?:le\s+)?diagnostic\s+(?:relié|associé)\s+(?:est|doit|sera)", re.IGNORECASE),
    re.compile(r"(?:la\s+)?CMA\s+(?:est|correspond|ne\s+peut)", re.IGNORECASE),
    re.compile(r"(?:le\s+)?DAS\s+(?:est|doit|sera|ne\s+doit\s+pas)", re.IGNORECASE),
    re.compile(r"(?:le\s+)?DP\s+(?:est|doit|sera|ne\s+doit\s+pas)", re.IGNORECASE),
    re.compile(r"(?:le\s+)?DR\s+(?:est|doit|sera)", re.IGNORECASE),
    re.compile(r"(?:on\s+)?code(?:ra)?\s+", re.IGNORECASE),
    re.compile(r"ne\s+(?:pas|doit\s+pas|faut\s+pas)\s+(?:coder|enregistrer|recueillir)", re.IGNORECASE),
    re.compile(r"séjour\s+multi-?uni", re.IGNORECASE),
    re.compile(r"(?:transfert|mutation)\s+", re.IGNORECASE),
    re.compile(r"(?:sévérité|niveau\s+de\s+sévérité|complication)", re.IGNORECASE),
    re.compile(r"(?:ressources?\s+supplémentaires?|consomm)", re.IGNORECASE),
    re.compile(r"(?:groupage|GHM|GHS|CMD)", re.IGNORECASE),
]

GENERATION_PROMPT = """Tu es un formateur DIM. À partir de cette règle du Guide Méthodologique MCO, génère un exercice de raisonnement.

RÈGLE (source : Guide Méthodologique MCO 2026, section « {section} ») :
{rule_text}

Génère un objet JSON avec :
1. "scenario" : un cas clinique réaliste et concis (2-3 phrases) qui illustre l'application de cette règle
2. "reponse" : un objet JSON contenant :
   - "analyse_clinique" : interprétation du cas
   - "regle_pmsi" : la règle du guide méthodologique qui s'applique (citée)
   - "code" : le code CIM-10 ou l'action de codage correcte
   - "confidence" : "high"
   - "justification" : pourquoi cette règle s'applique à ce cas

Réponds UNIQUEMENT avec le JSON, sans texte avant/après."""

# Templates pour les Q&A directes (sans LLM)
DIRECT_QA_TEMPLATES = [
    ("Quelle est la définition du {concept} selon le Guide Méthodologique MCO ?",
     "Selon le Guide Méthodologique MCO 2026 :\n\n{text}"),
    ("Quelles sont les règles de codage pour {concept} selon le Guide Méthodologique ?",
     "Le Guide Méthodologique MCO 2026 précise les règles suivantes pour {concept} :\n\n{text}"),
]


def extract_sections(pdf_path: Path) -> list[tuple[str, str]]:
    """Extraire les sections du Guide Méthodologique."""
    with pdfplumber.open(str(pdf_path)) as pdf:
        full_text = ""
        for page in pdf.pages:
            text = page.extract_text() or ""
            full_text += text + "\n\n"

    lines = []
    for line in full_text.split("\n"):
        stripped = line.strip()
        if re.match(r"^\s*\d{1,3}\s*$", stripped):
            continue
        if re.match(r"^(Guide méthodologique|ATIH|Page\s+\d)", stripped, re.IGNORECASE):
            continue
        if "...." in stripped:
            continue
        lines.append(line)

    clean_text = "\n".join(lines)

    section_pattern = re.compile(
        r"^(\d+(?:\.\d+)*\.?)\s+([A-ZÀÂÉÈÊËÎÏÔÙÛÜÇ].*)",
        re.MULTILINE
    )

    sections = []
    matches = list(section_pattern.finditer(clean_text))

    for i, match in enumerate(matches):
        num = match.group(1).rstrip(".")
        title = match.group(2).strip()
        start = match.end()
        end = matches[i + 1].start() if i + 1 < len(matches) else len(clean_text)
        body = clean_text[start:end].strip()

        if len(body) > 80:
            section_title = f"{num}. {title}"
            sections.append((section_title, body))

    return sections


def extract_rule_paragraphs(sections: list[tuple[str, str]]) -> list[tuple[str, str]]:
    """Extraire les paragraphes contenant des règles de codage."""
    rules = []

    for section_title, body in sections:
        paragraphs = re.split(r"\n\s*\n", body)

        for para in paragraphs:
            para = para.strip()
            if len(para) < 80:
                continue

            matches = sum(1 for pat in RULE_PATTERNS if pat.search(para))
            if matches >= 1:
                text = para[:1500] if len(para) > 1500 else para
                rules.append((section_title, text))

    return rules


def call_claude(client, prompt: str, max_retries: int = 2) -> str | None:
    """Appel Claude Opus 4.6 via API Anthropic avec retry."""
    for attempt in range(max_retries + 1):
        try:
            response = client.messages.create(
                model=MODEL,
                max_tokens=2048,
                temperature=0.7,
                messages=[{"role": "user", "content": prompt}],
            )
            return response.content[0].text
        except Exception as e:
            if attempt < max_retries:
                wait = 2 ** (attempt + 1)
                print(f"    Retry in {wait}s: {e}")
                time.sleep(wait)
            else:
                print(f"  Claude error: {e}")
                return None


def parse_llm_response(response_text: str) -> dict | None:
    """Parse la réponse JSON du LLM."""
    if not response_text:
        return None
    text = response_text.strip()
    if "```json" in text:
        text = text.split("```json", 1)[1].split("```", 1)[0].strip()
    elif "```" in text:
        text = text.split("```", 1)[1].split("```", 1)[0].strip()

    try:
        data = json.loads(text)
        if "scenario" in data and "reponse" in data:
            return data
    except json.JSONDecodeError:
        pass

    # Fallback
    brace_start = text.find("{")
    if brace_start >= 0:
        depth = 0
        for i in range(brace_start, len(text)):
            if text[i] == "{":
                depth += 1
            elif text[i] == "}":
                depth -= 1
                if depth == 0:
                    try:
                        data = json.loads(text[brace_start:i+1])
                        if "scenario" in data:
                            return data
                    except json.JSONDecodeError:
                        break
    return None


def make_chatml(system: str, user: str, assistant: str) -> dict:
    return {
        "messages": [
            {"role": "system", "content": system},
            {"role": "user", "content": user},
            {"role": "assistant", "content": assistant},
        ]
    }


def generate_direct_qa(sections: list[tuple[str, str]]) -> list[dict]:
    """Générer des Q&A directes depuis les sections (sans LLM)."""
    examples = []

    concept_keywords = {
        "diagnostic principal": ["DP", "diagnostic principal"],
        "diagnostic relié": ["DR", "diagnostic relié"],
        "diagnostic associé significatif": ["DAS", "diagnostic associé"],
        "complication ou morbidité associée": ["CMA", "complication", "sévérité"],
        "séjour multi-unités": ["multi-unité", "transfert", "mutation"],
        "groupage GHM": ["GHM", "GHS", "groupage", "CMD"],
        "actes CCAM": ["CCAM", "acte", "procédure"],
    }

    for section_title, body in sections:
        body_lower = body.lower()

        for concept, keywords in concept_keywords.items():
            if any(kw.lower() in body_lower for kw in keywords):
                text = body[:2000] if len(body) > 2000 else body

                tmpl_q, tmpl_a = random.choice(DIRECT_QA_TEMPLATES)
                user = tmpl_q.format(concept=f"{concept} (section {section_title})")
                assistant = tmpl_a.format(concept=concept, text=text)

                examples.append(make_chatml(SYSTEM_PROMPT, user, assistant))
                break

    return examples


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--dry-run", action="store_true", help="Pas d'appel LLM")
    parser.add_argument("--max", type=int, default=500, help="Max exemples à générer via LLM")
    args = parser.parse_args()

    print("=" * 60)
    print("Parsing du Guide Méthodologique MCO 2026")
    print(f"Modèle : {MODEL}")
    print("=" * 60)

    if not PDF_PATH.exists():
        print(f"PDF non trouvé : {PDF_PATH}")
        return

    # Vérifier la clé API
    if not args.dry_run:
        api_key = os.environ.get("ANTHROPIC_API_KEY")
        if not api_key:
            print("Erreur: ANTHROPIC_API_KEY non définie.")
            print("  export ANTHROPIC_API_KEY='sk-ant-...'")
            sys.exit(1)
        import anthropic
        client = anthropic.Anthropic(api_key=api_key)
    else:
        client = None

    print(f"\nParsing de {PDF_PATH.name} ({PDF_PATH.stat().st_size / 1024 / 1024:.1f} Mo)...")
    sections = extract_sections(PDF_PATH)
    print(f"  Sections extraites : {len(sections)}")

    # Q&A directes (sans LLM)
    print("\nGénération des Q&A directes (sans LLM)...")
    direct_examples = generate_direct_qa(sections)
    print(f"  → {len(direct_examples)} exemples directs")

    # Extraire les paragraphes de règles
    print("\nExtraction des règles de codage...")
    rules = extract_rule_paragraphs(sections)
    print(f"  Règles détectées : {len(rules)}")

    if args.dry_run:
        for i, (section, rule) in enumerate(rules[:20]):
            print(f"  [{i+1}] [{section}] {rule[:100]}...")
        print(f"\n[DRY RUN] {len(rules)} règles à traiter. Relancez sans --dry-run.")
        return

    # Limiter le nombre de règles pour LLM
    if len(rules) > args.max:
        random.shuffle(rules)
        rules = rules[:args.max]

    # Générer les Q&A via Claude
    print(f"\nGénération via Claude ({len(rules)} règles)...")
    llm_examples = []
    n_ok = 0
    n_fail = 0

    for i, (section_title, rule_text) in enumerate(rules):
        prompt = GENERATION_PROMPT.format(section=section_title, rule_text=rule_text)
        response_text = call_claude(client, prompt)
        parsed = parse_llm_response(response_text)

        if parsed and "scenario" in parsed and "reponse" in parsed:
            user_content = (
                f"Cas clinique :\n{parsed['scenario']}\n\n"
                f"Applique les règles du Guide Méthodologique MCO (section « {section_title} »)."
            )
            assistant_content = json.dumps(parsed["reponse"], ensure_ascii=False)
            llm_examples.append(make_chatml(SYSTEM_PROMPT, user_content, assistant_content))
            n_ok += 1
        else:
            n_fail += 1

        if (i + 1) % 50 == 0:
            print(f"  Progression : {i+1}/{len(rules)} (ok={n_ok}, fail={n_fail})")

    print(f"  Résultat LLM : {n_ok} exemples, {n_fail} échecs")

    # Fusionner
    all_examples = direct_examples + llm_examples
    random.shuffle(all_examples)

    with open(OUTPUT, "w") as f:
        for ex in all_examples:
            f.write(json.dumps(ex, ensure_ascii=False) + "\n")

    print(f"\n{'='*60}")
    print(f"Total : {len(all_examples)} exemples → {OUTPUT}")
    print(f"  Directs (sans LLM) : {len(direct_examples)}")
    print(f"  LLM (Claude)       : {len(llm_examples)}")
    print(f"Taille : {OUTPUT.stat().st_size / 1024:.0f} Ko")


if __name__ == "__main__":
    main()
