from __future__ import annotations

import json
import re
import unicodedata
from typing import Any

from pydantic import ValidationError

from app.config import Settings, get_settings
from app.models import OEECaseExtraction, OEEInput, OEEInterpretationResponse
from app.oee_calculator import calculate_oee
from app.validators import FIELD_LABELS, validation_messages


SYSTEM_PROMPT = """
Eres un interprete de casos OEE. Tu unica tarea es convertir texto libre en datos
estructurados para una calculadora OEE deterministica. No calcules OEE.

Devuelve solo JSON valido con esta forma exacta:
{
  "estado": "completo|incompleto|inconsistente",
  "confianza": 0.0,
  "tiempo_turno_min": number|null,
  "paradas_planificadas_min": number|null,
  "paradas_no_planificadas_min": number|null,
  "produccion_total": number|null,
  "produccion_buena": number|null,
  "ciclo_ideal_seg": number|null,
  "unidad_produccion": string|null,
  "faltantes": [],
  "advertencias": [],
  "supuestos": []
}

Reglas:
- Convierte horas a minutos y conserva segundos para el ciclo ideal.
- Tiempo de turno es la duracion total programada del turno o jornada.
- Paradas planificadas incluyen refrigerio, almuerzo, mantenimiento programado o limpieza programada.
- Paradas no planificadas incluyen falla, averia, espera, cambio no programado o paro imprevisto.
- Si una parada se menciona como inexistente, usa 0.
- Si el texto no permite inferir un campo requerido, usa null y agregalo en faltantes.
- Si produccion buena es mayor que produccion total, marca estado inconsistente.
- Si hay ambiguedad relevante, agregala en advertencias.
- Si el texto menciona la unidad producida, usa una etiqueta general como unidades, piezas, botellas, kg, lotes o metros.
- No inventes datos. No expliques fuera del JSON.
""".strip()


REQUIRED_FIELDS = (
    "tiempo_turno_min",
    "paradas_planificadas_min",
    "paradas_no_planificadas_min",
    "produccion_total",
    "produccion_buena",
    "ciclo_ideal_seg",
)


def interpret_oee_case(
    text: str,
    settings: Settings | None = None,
) -> OEEInterpretationResponse:
    settings = settings or get_settings()
    clean_text = text.strip()

    if not clean_text:
        extraction = OEECaseExtraction(
            estado="incompleto",
            faltantes=list(REQUIRED_FIELDS),
            advertencias=["Ingrese un caso OEE para interpretar."],
        )
        return OEEInterpretationResponse(texto=text, extraccion=extraction, calculable=False)

    extraction: OEECaseExtraction

    if settings.mistral_api_key:
        try:
            extraction = _call_mistral(clean_text, settings)
        except Exception as exc:  # noqa: BLE001 - external API errors vary by SDK version
            extraction = _basic_case_parser(clean_text)
            extraction.advertencias.append(
                f"No se pudo consultar Mistral; se uso extractor local basico. Detalle: {exc}"
            )
    elif settings.enable_local_fallback:
        extraction = _basic_case_parser(clean_text)
        extraction.advertencias.append(
            "MISTRAL_API_KEY no esta configurada; se uso extractor local basico."
        )
    else:
        extraction = OEECaseExtraction(
            estado="incompleto",
            faltantes=["MISTRAL_API_KEY"],
            advertencias=["Configure MISTRAL_API_KEY para usar el interprete IA."],
        )

    extraction = _normalize_extraction(extraction)
    entrada = _build_input(extraction)

    if entrada is None:
        return OEEInterpretationResponse(
            texto=text,
            extraccion=extraction,
            calculable=False,
        )

    resultado = calculate_oee(entrada)
    return OEEInterpretationResponse(
        texto=text,
        extraccion=extraction,
        calculable=True,
        entrada=entrada,
        resultado=resultado,
    )


def _call_mistral(text: str, settings: Settings) -> OEECaseExtraction:
    try:
        from mistralai import Mistral
    except ImportError:
        try:
            from mistralai.client import Mistral
        except ImportError as exc:
            raise RuntimeError("Instale el paquete mistralai para usar Mistral.") from exc

    client = Mistral(api_key=settings.mistral_api_key)
    response = client.chat.complete(
        model=settings.mistral_model,
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": text},
        ],
        response_format={"type": "json_object"},
        temperature=0,
    )
    content = _extract_message_content(response)
    data = json.loads(content)
    return OEECaseExtraction.model_validate(data)


def _extract_message_content(response: Any) -> str:
    content = response.choices[0].message.content
    if isinstance(content, str):
        return content

    if isinstance(content, list):
        parts: list[str] = []
        for item in content:
            if isinstance(item, dict) and "text" in item:
                parts.append(str(item["text"]))
            elif hasattr(item, "text"):
                parts.append(str(item.text))
        return "".join(parts)

    return str(content)


def _normalize_extraction(extraction: OEECaseExtraction) -> OEECaseExtraction:
    missing = list(extraction.faltantes)
    for field in REQUIRED_FIELDS:
        if getattr(extraction, field) is None and field not in missing:
            missing.append(field)

    if (
        extraction.produccion_buena is not None
        and extraction.produccion_total is not None
        and extraction.produccion_buena > extraction.produccion_total
    ):
        if "produccion_buena mayor que produccion_total" not in extraction.advertencias:
            extraction.advertencias.append("produccion_buena mayor que produccion_total")
        extraction.estado = "inconsistente"

    if missing and extraction.estado != "inconsistente":
        extraction.estado = "incompleto"
    elif not missing and extraction.estado == "incompleto":
        extraction.estado = "completo"

    extraction.faltantes = missing
    return extraction


def _build_input(extraction: OEECaseExtraction) -> OEEInput | None:
    if extraction.estado != "completo":
        return None

    data = {field: getattr(extraction, field) for field in REQUIRED_FIELDS}
    data["unidad_produccion"] = extraction.unidad_produccion or "unidades"
    try:
        return OEEInput.model_validate(data)
    except ValidationError as exc:
        extraction.estado = "inconsistente"
        extraction.advertencias.extend(validation_messages(exc))
        return None


def _basic_case_parser(text: str) -> OEECaseExtraction:
    normalized = _normalize_text(text)
    extraction = OEECaseExtraction(confianza=0.35)

    extraction.tiempo_turno_min = _find_time_near(
        normalized,
        ["turno", "jornada", "trabajo", "trabajo", "linea trabajo", "laboro"],
    )
    extraction.paradas_planificadas_min = _find_time_near(
        normalized,
        ["refrigerio", "almuerzo", "planificada", "programada", "mantenimiento programado"],
    )
    extraction.paradas_no_planificadas_min = _find_time_near(
        normalized,
        ["falla", "averia", "imprevista", "no planificada", "paro", "parada por"],
    )
    extraction.produccion_total = _find_number_after(
        normalized,
        ["produjo", "produccion total", "piezas totales", "total"],
    )
    extraction.produccion_buena = _find_good_units(normalized)
    extraction.ciclo_ideal_seg = _find_cycle_seconds(normalized)
    extraction.unidad_produccion = _find_production_unit(normalized)

    if extraction.paradas_planificadas_min is None and _mentions_no_planned_stops(normalized):
        extraction.paradas_planificadas_min = 0
    if extraction.paradas_no_planificadas_min is None and _mentions_no_unplanned_stops(normalized):
        extraction.paradas_no_planificadas_min = 0

    extraction.supuestos.append("Extractor local basico; revise los campos detectados.")
    return extraction


def _normalize_text(text: str) -> str:
    text = unicodedata.normalize("NFD", text)
    text = "".join(char for char in text if unicodedata.category(char) != "Mn")
    return text.lower()


def _find_time_near(text: str, keywords: list[str]) -> float | None:
    time_pattern = re.compile(
        r"(?P<value>\d+(?:[\.,]\d+)?)\s*(?P<unit>horas|hora|hrs|hr|h|minutos|minuto|mins|min|m)\b"
    )
    matches = list(time_pattern.finditer(text))
    best: tuple[int, re.Match[str]] | None = None

    for match in matches:
        for keyword in keywords:
            for keyword_match in re.finditer(re.escape(keyword), text):
                distance = min(
                    abs(match.start() - keyword_match.end()),
                    abs(match.end() - keyword_match.start()),
                )
                score = distance + (10 if match.start() > keyword_match.end() else 0)
                if distance <= 80 and (best is None or score < best[0]):
                    best = (score, match)

    if best is None:
        return None

    match = best[1]
    return _time_to_minutes(match.group("value"), match.group("unit"))


def _time_to_minutes(value: str, unit: str) -> float:
    number = float(value.replace(",", "."))
    if unit in {"horas", "hora", "hrs", "hr", "h"}:
        return number * 60
    return number


def _find_number_after(text: str, keywords: list[str]) -> float | None:
    for keyword in keywords:
        pattern = re.compile(rf"{re.escape(keyword)}[^\d]{{0,30}}(?P<value>\d+(?:[\.,]\d+)?)")
        match = pattern.search(text)
        if match:
            return float(match.group("value").replace(",", "."))
    return None


def _find_good_units(text: str) -> float | None:
    patterns = [
        r"(?P<value>\d+(?:[\.,]\d+)?)\s*(?:piezas\s*)?(?:buenas|buenos|conformes|ok)",
        r"(?:produccion buena|buenas|buenos|conformes)[^\d]{0,30}(?P<value>\d+(?:[\.,]\d+)?)",
    ]
    for pattern in patterns:
        match = re.search(pattern, text)
        if match:
            return float(match.group("value").replace(",", "."))
    return None


def _find_cycle_seconds(text: str) -> float | None:
    pattern = re.compile(
        r"(?:ciclo ideal|tiempo de ciclo|ciclo|takt)[^\d]{0,30}"
        r"(?P<value>\d+(?:[\.,]\d+)?)\s*(?:segundos|seg|s)\b"
    )
    match = pattern.search(text)
    if not match:
        return None
    return float(match.group("value").replace(",", "."))


def _find_production_unit(text: str) -> str | None:
    match = re.search(
        r"(?:produjo|produccion total)[^\d]{0,30}\d+(?:[\.,]\d+)?\s+"
        r"(?P<unit>unidades|piezas|productos|botellas|lotes|kg|kilogramos|metros|cajas|litros|toneladas)",
        text,
    )
    if not match:
        return None
    return match.group("unit")


def _mentions_no_planned_stops(text: str) -> bool:
    return any(
        phrase in text
        for phrase in [
            "sin paradas planificadas",
            "no hubo paradas planificadas",
            "sin parada planificada",
        ]
    )


def _mentions_no_unplanned_stops(text: str) -> bool:
    return any(
        phrase in text
        for phrase in [
            "sin paradas no planificadas",
            "no hubo paradas no planificadas",
            "sin fallas",
            "sin averias",
        ]
    )


def extraction_form_values(extraction: OEECaseExtraction) -> dict[str, str]:
    values: dict[str, str] = {}
    for field in REQUIRED_FIELDS:
        value = getattr(extraction, field)
        values[field] = "" if value is None else str(value)
    values["unidad_produccion"] = extraction.unidad_produccion or "unidades"
    return values


def missing_labels(extraction: OEECaseExtraction) -> list[str]:
    return [FIELD_LABELS.get(field, field) for field in extraction.faltantes]
