"""
Service de parsing : transforme un transcript brut en JSON structuré.

Utilise Groq + Llama 3.3 70B Versatile pour extraire les champs du CV
selon le step actif du VoiceStepper (1-4).

Usage :
    from app.services.parser_service import parser_service
    
    result = await parser_service.parse(
        transcript="J'ai fait l'ENSA Tanger en 2023",
        step=2,
    )
    # result.fields = {"formations": [{"degree": "...", ...}]}
"""

import json
import time
from dataclasses import dataclass

from groq import Groq
from app.core.config import settings


@dataclass
class ParseResult:
    """Résultat d'un parsing LLM."""
    fields: dict
    duration_seconds: float
    model: str
    provider: str


# ============================================
# Prompts par step
# ============================================

STEP_PROMPTS = {
    1: """Tu reçois une transcription orale d'un candidat qui parle de son profil professionnel et de son objectif de carrière.

Ses informations administratives (nom, email, téléphone, adresse) sont DÉJÀ connues du système.
Ignore-les si le candidat les mentionne.

Extrais UNIQUEMENT son objectif professionnel au format JSON STRICT :

{
  "objective": "texte de 2 à 5 phrases incluant TOUS les éléments mentionnés par le candidat : parcours, type de travaux, compétences, objectif professionnel. Ne rien omettre."
}

RÈGLES IMPORTANTES :
- "3andi X 3am" ou "عندي X عام" = âge du candidat → NE PAS inclure dans l'objective
GLOSSAIRE NOMBRES (arabe → chiffre latin) :
واحد=1, جوج/اتنين=2, تلاتة=3, ربعة=4, خمسة=5,
ستة=6, سبعة=7, تمنية=8, تسعود=9, معاشر/عشرة=10,
خمستاش=15, عشرين=20, خمسة وعشرين=25, تلاتين=30
Utilise toujours le chiffre latin correspondant dans la sortie JSON.
Exemple : "بمعاشر سنين" → "10 ans", "خمستاش سنين" → "15 ans"

GLOSSAIRE VOCABULAIRE MÉTIER (pour éviter les confusions de métier) :
- Plomberie : tuyaux, tuyauterie, salle de bain, cuisine, robinetterie,
  sanitaire, évacuation, fuite d'eau
- Électricité : câblage, tableau électrique, installation électrique,
  éclairage, prise, disjoncteur, courant
- Maçonnerie : murs, fondations, béton, briques, coffrage, ferraillage
- Soudure : métaux, arc, MIG, TIG, soudage, acier
- Peinture : façade, ravalement, enduit, revêtement mural, peinture
  intérieure/extérieure
- Carrelage : carreaux, faïence, jointure
Ne jamais attribuer un terme d'un métier à un autre métier. Si le
candidat parle de "tuyaux" ou "salle de bain", c'est de la PLOMBERIE,
jamais de l'électricité — même si le mot "installation" apparaît seul.
Le métier déclaré explicitement par le candidat (ex: "ana plombier")
prévaut toujours sur toute déduction à partir du vocabulaire.

- "khdemt X snin" ou "عندي khibra X snin" = années d'expérience → peut apparaître dans l'objective
- Ne jamais confondre l'âge avec les années d'expérience
- Si le candidat ne mentionne pas d'objectif clair → objective = ""

EXEMPLE avec transcript en arabe :
Transcript : "عندي 32 عام. انا الكتريسيان من اكشر من 10 سنين. الهدف ديالي هو نلقا خدمة قارة فشركة ديال BTP."
Résultat attendu :
{
  "objective": "Électricien avec plus de 10 ans d'expérience dans le BTP. Objectif : trouver un poste stable dans une entreprise BTP."
}

INTERDIT absolu — ne jamais écrire ces phrases génériques si le candidat ne les a pas dites explicitement :
- "offrir des services de qualité"
- "contribuer à la réussite de l'entreprise"
- "mettre en pratique mes compétences"
- "acquérir de nouvelles expériences"
- "développer mes connaissances"
- "relever de nouveaux défis"
L'objective doit contenir UNIQUEMENT les mots et idées exprimés par le candidat dans la transcription.
Si le candidat n'exprime pas d'objectif clair → objective = ""

Le champ "objective" doit être un texte naturel en français, présentable dans un CV.""",

    2: """Tu reçois une transcription orale d'un candidat qui parle de ses formations académiques.

Extrais TOUTES les formations mentionnées au format JSON STRICT :

{
  "formations": [
    {
      "degree": "nom du diplôme",
      "institution": "nom de l'établissement",
      "year_start": "année de début (ex: 2018) ou chaîne vide",
      "year_end": "année de fin (ex: 2023) ou chaîne vide"
    }
  ]
}

Si une seule année est mentionnée (formation courte), mets-la dans year_start ET year_end.

INTERDIT absolu — ne jamais inventer un diplôme, une institution ou une
année qui n'est pas explicitement mentionnée dans la transcription.
Si le transcript est incompréhensible, vague, ou ne mentionne aucune
formation reconnaissable → "formations": []
Ne jamais déduire un nom d'institution à partir d'une description
générique (ex: "j'ai appris sur le terrain", "avec mon oncle" →
ce n'est PAS une institution, formations doit rester vide pour cette entrée).
Ne jamais inventer une plage d'années (year_start/year_end) si elle
n'est pas clairement énoncée dans le transcript.

DÉTECTION DE NÉGATION (règle critique) :
Si le candidat dit explicitement qu'il N'A PAS de diplôme ou de formation
formelle, NE JAMAIS créer une entrée dans "formations" pour ce diplôme nié.
Expressions de négation à reconnaître (français, Darija, arabe) :
- "je n'ai pas de diplôme" / "ma 3andi diplôme" / "ماعنديش ديبلوم"
- "je n'ai même pas" / "maderch hatta" / "مادرتش حتى"
- "sans diplôme" / "bla diplôme" / "بلا ديبلوم"
- "aucune formation" / "ma 3andi formation" / "ماعنديش فورماسيون"
Si une négation de ce type est détectée concernant LE SEUL élément de
formation mentionné dans la transcription → "formations": []
Ne jamais transformer une négation en diplôme positif.

EXEMPLE avec négation :
Transcript : "maderch hatta diplôme f peinture, ta3lamt lmihna 3la lkhdma
directement"
Résultat attendu :
{
  "formations": []
}

GLOSSAIRE NOMBRES (arabe → chiffre latin) :
واحد=1, جوج/اتنين=2, تلاتة=3, ربعة=4, خمسة=5,
ستة=6, سبعة=7, تمنية=8, تسعود=9, معاشر/عشرة=10,
خمستاش=15, عشرين=20, خمسة وعشرين=25, تلاتين=30
Utilise toujours le chiffre latin correspondant pour year_start/year_end.

EXEMPLE avec transcript en arabe déformé :
Transcript : "3andi diplôme dyal soudure mn OFPPT f Fès, dert takwin
mdt sentin, sllit f 2019."
Résultat attendu :
{
  "formations": [
    {
      "degree": "Diplôme de soudure",
      "institution": "OFPPT Fès",
      "year_start": "2017",
      "year_end": "2019"
    }
  ]
}

Si plusieurs formations, ordre chronologique (plus ancienne d'abord).""",

    3: """Tu reçois une transcription orale d'un candidat qui parle de ses expériences professionnelles.

DÉTECTION DE NÉGATION (règle critique) :
Si le candidat indique explicitement n'avoir aucune expérience professionnelle,
NE JAMAIS créer d'entrée dans "experiences" pour l'expérience niée.
Expressions de négation à reconnaître (français, Darija, arabe) :
- "je n'ai pas d'expérience" / "ma khdemtch" / "مخدمتش"
- "c'est ma première fois" / "hadi lawla mrra" / "هادي لاولة مرة"
- "pas d'expérience" / "ma 3andi khibra" / "ماعنديش خبرة"
Si une négation de ce type est détectée concernant LA SEULE expérience
mentionnée dans la transcription → "experiences": []
Ne jamais transformer une négation en expérience positive.

Extrais TOUTES les expériences au format JSON STRICT :

{
    "experiences": [
        {
            "position": "intitulé du poste",
            "company": "nom de l'entreprise EXACTEMENT (en francais)",
            "description": "description courte des missions et responsabilités",
            "year_start": "année de début ou chaîne vide",
            "year_end": "année de fin ou 'actuel' si en cours"
        }
    ]
}

RÈGLES POUR "company" :
- Préserve le nom de l'entreprise EXACTEMENT tel qu'il apparaît dans la transcription
- NE devine PAS un nom d'entreprise si la transcription ne le contient pas → chaîne vide ""
- Les noms d'entreprises courants au Maroc incluent : BatiPro, Constructa, Sigman, OCP, SOMAGEC, TGCC, JET Contractors, Manatra, Addoha, Alliances, Palmeraie Développement, Dolbeau, Bouygues Maroc
- Si la transcription mentionne "le bâtiment" ou "une entreprise" sans nom propre → company = ""

RÈGLES POUR "position" :
- Si un titre de poste explicite est dit (ex: "maçon", "chef d'équipe", "responsable de X", "ouvrier") → utilise ce titre tel quel.
- Si seule une tâche ou une responsabilité est décrite sans titre explicite (ex: "ma responsabilité était l'installation de...", "je m'occupais de...") → NE PAS inventer de titre hiérarchique. Ne mets ni "responsable" ni "ouvrier" par défaut.
- Si aucun titre n'est clairement énoncé dans la transcription → position = ""
- Ne jamais transformer une phrase décrivant une tâche en titre de poste formel non dit.

RÈGLES POUR "description" :
- Chaque description doit provenir UNIQUEMENT du segment de transcript correspondant à CETTE expérience précise.
- Ne jamais réutiliser un mot, un lieu, ou un détail appartenant à la description d'une AUTRE expérience mentionnée dans le même transcript, même si ce détail semble plausible pour cette expérience.
- La paraphrase fidèle est autorisée (reformuler le sens avec d'autres mots), mais aucun terme absent du segment source de cette expérience précise ne doit apparaître dans sa description.

RÈGLES POUR "year_start" / "year_end" :
- INTERDIT absolu : ne jamais calculer ou déduire une année civile si aucune année absolue à 4 chiffres (ex: 2018, 2021) n'apparaît explicitement dans le transcript de cette expérience.
- Une durée relative seule ("5 ans", "5 snin", "4 ans") SANS aucune année absolue mentionnée nulle part dans le transcript → year_start = "" ET year_end = "". Ne jamais transformer une durée en année absolue par déduction.
- Le calcul est autorisé UNIQUEMENT si une année absolue EST présente avec une durée dans le même segment (ex: "j'ai terminé en 2018, ça fait 2 ans" → year_start peut être calculé : 2018 - 2 = 2016, year_end = 2018).
- "actuel" pour year_end est autorisé uniquement si le candidat indique explicitement que cette expérience est en cours ("daba", "toujours", "jusqu'à maintenant").

GLOSSAIRE NOMBRES (arabe → chiffre latin) :
واحد=1, جوج/اتنين=2, تلاتة=3, ربعة=4, خمسة=5,
ستة=6, سبعة=7, تمنية=8, تسعود=9, معاشر/عشرة=10,
خمستاش=15, عشرين=20, خمسة وعشرين=25, تلاتين=30
Utilise toujours le chiffre latin correspondant pour lire les durées et années, mais n'invente jamais une année absolue à partir d'une durée seule.

EXEMPLE positif (année absolue + durée → calcul autorisé) :
Transcript : "khdemt f BatiPro, sllit f 2018, dert temps 2 snin"
Résultat attendu :
{
  "experiences": [
    {
      "position": "",
      "company": "BatiPro",
      "description": "travaux effectués chez BatiPro",
      "year_start": "2016",
      "year_end": "2018"
    }
  ]
}

EXEMPLE négatif (durée seule, aucune année absolue → champs vides) :
Transcript : "khdemt m3a wa7d l'atelier f Marrakech modda 5 snin, konna kandiro les mobiliers"
Résultat attendu :
{
  "experiences": [
    {
      "position": "",
      "company": "",
      "description": "fabrication de mobiliers dans un atelier",
      "year_start": "",
      "year_end": ""
    }
  ]
}

Si plusieurs expériences, ordre chronologique inverse (plus récente d'abord).""",

    4: """Tu reçois une transcription orale d'un candidat qui parle de ses compétences professionnelles et des langues qu'il maîtrise.

Extrais DEUX listes au format JSON STRICT :

{
    "skills": ["compétence 1", "compétence 2", "compétence 3"],
    "languages": [
        { "name": "Français", "level": "advanced" },
        { "name": "Arabe", "level": "native" }
    ]
}

RÈGLES POUR "skills" :
- Chaque compétence doit être une chaîne courte (1-4 mots)
- Normalise les noms (ex: "j'utilise Excel et Word" → ["Microsoft Excel", "Microsoft Word"])
- Ne PAS inclure les langues dans skills (elles vont dans "languages")

INTERDIT absolu — anti-hallucination :
- Si le transcript est vide, incompréhensible ou ne contient aucune compétence mentionnée explicitement → "skills": []
- Ne jamais inventer des compétences BTP génériques (ex: "conduite de camion", "mécanique générale", "entretien véhicule") si le candidat ne les a pas mentionnées dans le transcript
- Chaque compétence dans "skills" doit correspondre à un mot ou groupe de mots effectivement présent dans le transcript

RÈGLES POUR "languages" :
- Chaque langue est un objet avec EXACTEMENT 2 champs : "name" (string en français) et "level" (string parmi 4 valeurs strictes)
- Le champ "level" doit être OBLIGATOIREMENT une de ces 4 valeurs EXACTES (en anglais, en minuscules) :
    * "beginner"     → débutant, notions de base, "kanfham chwiya"
    * "intermediate" → niveau moyen, conversationnel, "mezyan chwiya"
    * "advanced"     → bon niveau, courant, "mezyan bezzaf", "kanhder mezyan"
    * "native"       → langue maternelle, parlée depuis l'enfance, "lougha l'oumm"
- Si le candidat ne précise pas le niveau d'une langue, utilise "intermediate" par défaut
- Si le candidat ne mentionne AUCUNE langue, retourne une liste vide : "languages": []
- Ne JAMAIS inventer des langues que le candidat n'a pas mentionnées
- Normalise les noms de langues en français : "fransawiya" → "Français", "3arabiya" → "Arabe", "anglais" → "Anglais", "darija" → "Darija", "espagnol" → "Espagnol"

INTERDIT absolu — anti-hallucination langues :
- Si le candidat ne mentionne AUCUNE langue → "languages": []
- Ne jamais inventer des langues par défaut (ex: Arabe natif + Français avancé) si le candidat ne les a pas mentionnées explicitement
- La règle "intermediate par défaut" s'applique UNIQUEMENT quand une langue est explicitement nommée par le candidat mais sans précision de niveau — jamais comme valeur générale si aucune langue n'est dite
- Si le transcript est vide ou incompréhensible → "languages": []

EXEMPLES :
- "Kanhder l'fransawiya mezyan w l'3arabiya hiya lougha l'oumm dyali"
    → languages: [
            { "name": "Français", "level": "advanced" },
            { "name": "Arabe", "level": "native" }
        ]

- "Je parle français couramment et un peu d'anglais"
    → languages: [
            { "name": "Français", "level": "advanced" },
            { "name": "Anglais", "level": "beginner" }
        ]
""",
}

SYSTEM_PROMPT = """
Tu es un expert en extraction d'informations de CV à partir de transcriptions orales.

CONTEXTE :
- Le candidat est marocain
- Il peut parler en français, arabe, darija marocaine ou mélange
- La transcription peut contenir des erreurs (ASR imparfait)

OBJECTIF PRINCIPAL :
👉 Tout le JSON final DOIT être entièrement en français correct.

RÈGLES STRICTES :
1. La sortie doit être UNIQUEMENT en JSON valide (aucun texte, aucun markdown)
2. TOUT le contenu textuel doit être en FRANÇAIS UNIQUEMENT
   - même si l'audio est en arabe ou darija
   - même les noms de compétences doivent être en français standard si possible
3. Traduis systématiquement :
   - darija → français
   - arabe → français
   - anglais technique → français si équivalent existe
4. Interdiction absolue de laisser des mots en arabe ou darija dans la sortie
5. Ne jamais mélanger langues dans un même champ
6. Si une information est ambiguë ou inconnue → chaîne vide "" obligatoire. Ne jamais inventer de contenu absent de la transcription.
7. Si une information est inconnue → chaîne vide ""

EXEMPLES DE NORMALISATION :
- "khdemt backend" → "développement backend"
- "qrit f ENCG" → "études à l'ENCG"
- "jami3a" → "université"
- "snin" → "années"

IMPORTANT :
👉 Le JSON final doit être prêt pour un CV professionnel en français.
"""

class ParserService:
    """
    Wrapper autour de Groq + Llama 3.3 70B pour le parsing structuré.
    
    Instance unique partagée via la variable `parser_service` en bas du module.
    """

    def __init__(self):
        if not settings.GROQ_API_KEY:
            raise ValueError(
                "GROQ_API_KEY manquante dans .env. "
                "Récupère-la sur https://console.groq.com/keys"
            )
        self.client = Groq(api_key=settings.GROQ_API_KEY)
        self.model = settings.GROQ_PARSER_MODEL
        self.provider = "groq"

    async def parse(self, transcript: str, step: int) -> ParseResult:
        """
        Parse un transcript en JSON structuré selon le step.
        
        Args:
            transcript : Le texte brut transcrit par Whisper.
            step       : L'étape du VoiceStepper (1=profil, 2=formation, 3=expérience, 4=skills).
        
        Returns:
            ParseResult avec fields (dict), duration, model, provider.
        
        Raises:
            ValueError : si le step est invalide.
            Exception  : si Groq plante ou JSON invalide.
        """
        if step not in STEP_PROMPTS:
            raise ValueError(f"Step invalide : {step}. Doit être 1, 2, 3 ou 4.")

        start_time = time.time()

        # Construire les messages pour le LLM
        step_prompt = STEP_PROMPTS[step]
        user_content = f"{step_prompt}\n\nTRANSCRIPTION :\n\"{transcript}\""

        messages = [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_content},
        ]

        # Appel à Groq
        completion = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            response_format={"type": "json_object"},
            temperature=0.1,
            max_tokens=1024,
        )

        elapsed = time.time() - start_time

        # Parser la réponse JSON
        raw_response = completion.choices[0].message.content
        try:
            fields = json.loads(raw_response)
        except json.JSONDecodeError as e:
            raise Exception(
                f"Llama a renvoyé un JSON invalide : {e}. "
                f"Réponse brute : {raw_response[:200]}..."
            )

        return ParseResult(
            fields=fields,
            duration_seconds=round(elapsed, 2),
            model=self.model,
            provider=self.provider,
        )


# Instance globale unique
parser_service = ParserService()