"""Blueprint Flask del modulo "Estequiometria" (4.1.5-4.1.7) - actividad a libro abierto.

Rutas:
    GET  /estequiometria          Pagina del modulo (teoria + evaluacion).
    POST /iniciar-estequiometria  Crea sesion (token) tras identificar al estudiante.
    POST /incidente-estequiometria Registra una incidencia anti-trampa.
    POST /guardar-estequiometria  Califica en servidor y guarda 1 fila (nota unificada).
"""

import csv
import os
import secrets
import threading
from datetime import datetime

from flask import Blueprint, jsonify, render_template, request

import estequiometria_grader
from estequiometria_config import (
    CARRERAS_PERMITIDAS,
    MAX_APELLIDOS,
    MAX_NOMBRES,
    MAX_TEXTO_CORTO,
    TEMAS,
    TIPOS_INCIDENTE_VALIDOS,
)

bp = Blueprint("estequiometria", __name__, template_folder="templates")

DIR_ACTUAL = os.path.dirname(os.path.abspath(__file__))
CSV_RESULTADOS = os.path.join(DIR_ACTUAL, "estequiometria_resultados.csv")
CSV_INCIDENCIAS = os.path.join(DIR_ACTUAL, "estequiometria_incidencias.csv")

ENCABEZADO_RESULTADOS = ["No", "Apellidos", "Nombres", "Carrera",
                         "Puntos", "Posibles", "Nota"]
ENCABEZADO_INCIDENCIAS = ["fecha", "token", "estudiante", "tipo"]

_sesiones = {}
_bloqueo = threading.Lock()


def _texto_valido(valor, maximo):
    if not isinstance(valor, str):
        return None
    limpio = valor.strip()
    if not limpio or len(limpio) > maximo:
        return None
    return limpio


def _obtener_sesion(token):
    with _bloqueo:
        return _sesiones.get(token) if isinstance(token, str) else None


def _escapar_celda(valor):
    """Neutraliza inyeccion de formulas CSV (=,+,-,@) y saltos de linea."""
    texto = str(valor)
    texto = texto.replace("\n", " ").replace("\r", " ")
    if texto.startswith(("=", "+", "-", "@")):
        texto = "'" + texto
    return texto


def _agregar_fila(ruta_csv, encabezado, fila):
    existe = os.path.exists(ruta_csv)
    with open(ruta_csv, "a", newline="", encoding="utf-8-sig") as archivo:
        escritor = csv.writer(archivo)
        if not existe:
            escritor.writerow(encabezado)
        escritor.writerow([_escapar_celda(celda) for celda in fila])


def _siguiente_correlativo():
    """Correlativo `No` por ESTUDIANTE = numero de estudiantes previos + 1."""
    if not os.path.exists(CSV_RESULTADOS):
        return 1
    with open(CSV_RESULTADOS, encoding="utf-8-sig") as archivo:
        lector = csv.reader(archivo)
        next(lector, None)  # encabezado
        estudiantes = sum(1 for fila in lector if fila)
    return estudiantes + 1


def _sanear_respuestas_por_tema(datos):
    """Construye un payload confiable {tema: {indice: valor}}."""
    r = datos.get("respuestas") or {}
    if not isinstance(r, dict):
        return {}
    permitidos = {
        "4.1.5": ("A", "B", "C", "D"),
        "4.1.6": None,  # texto libre (coeficientes)
        "4.1.7": None,  # texto libre (numeros)
        "ACT": ("A", "B", "C", "D"),
    }
    sano = {}
    for tema in TEMAS:
        bloque = r.get(tema)
        if not isinstance(bloque, dict):
            sano[tema] = {}
            continue
        limpio = {}
        for indice, valor in bloque.items():
            if not isinstance(valor, str):
                continue
            if permitidos[tema] is not None:
                if valor in permitidos[tema]:
                    limpio[indice] = valor
            else:
                limpio[indice] = valor[:MAX_TEXTO_CORTO]
        sano[tema] = limpio
    return sano


# ---------------------------------------------------------------------------
# Rutas
# ---------------------------------------------------------------------------

@bp.route("/estequiometria")
def pagina_estequiometria():
    return render_template("estequiometria.html")


@bp.route("/iniciar-estequiometria", methods=["POST"])
def iniciar_estequiometria():
    datos = request.get_json(silent=True) or {}
    apellidos = _texto_valido(datos.get("apellidos"), MAX_APELLIDOS)
    nombres = _texto_valido(datos.get("nombres"), MAX_NOMBRES)
    carrera = datos.get("carrera")
    if not apellidos or not nombres:
        return jsonify({"ok": False, "error": "Identificacion incompleta."}), 400
    if carrera not in CARRERAS_PERMITIDAS:
        return jsonify({"ok": False, "error": "Carrera no permitida."}), 400

    token = secrets.token_hex(16)
    with _bloqueo:
        _sesiones[token] = {
            "apellidos": apellidos,
            "nombres": nombres,
            "carrera": carrera,
            "incidentes": [],
            "enviado": False,
        }
    return jsonify({"ok": True, "token": token})


@bp.route("/incidente-estequiometria", methods=["POST"])
def incidente_estequiometria():
    datos = request.get_json(silent=True) or {}
    sesion = _obtener_sesion(datos.get("token"))
    if sesion is None:
        return jsonify({"ok": False, "error": "Sesion invalida."}), 400
    tipo = datos.get("tipo")
    if tipo not in TIPOS_INCIDENTE_VALIDOS:
        return jsonify({"ok": False, "error": "Tipo de incidente desconocido."}), 400

    with _bloqueo:
        sesion["incidentes"].append(tipo)
        total = len(sesion["incidentes"])
        estudiante = f"{sesion['apellidos']}, {sesion['nombres']}"
    _agregar_fila(
        CSV_INCIDENCIAS, ENCABEZADO_INCIDENCIAS,
        [datetime.now().strftime("%Y-%m-%d %H:%M:%S"), str(datos.get("token"))[:8],
         estudiante, tipo],
    )
    return jsonify({"ok": True, "incidentes": total,
                    "descuento": 5 * total})


@bp.route("/guardar-estequiometria", methods=["POST"])
def guardar_estequiometria():
    datos = request.get_json(silent=True) or {}
    sesion = _obtener_sesion(datos.get("token"))
    if sesion is None:
        return jsonify({"ok": False, "error": "Sesion invalida."}), 400
    with _bloqueo:
        if sesion["enviado"]:
            return jsonify({"ok": False,
                            "error": "Esta actividad ya fue enviada."}), 409

    respuestas = _sanear_respuestas_por_tema(datos)
    with _bloqueo:
        n_incidentes = len(sesion["incidentes"])
    resultado = estequiometria_grader.calificar_examen(respuestas, n_incidentes)

    with _bloqueo:
        numero = _siguiente_correlativo()
        sesion["enviado"] = True

    # Calcular puntos totales obtenidos y posibles
    total_puntos = 0
    total_posibles = 0
    for tema in TEMAS:
        resumen = estequiometria_grader.calificar_tema(tema, respuestas[tema])
        total_puntos += resumen["puntos"]
        total_posibles += resumen["posibles"]

    # Una sola fila por estudiante con nota unificada
    _agregar_fila(
        CSV_RESULTADOS, ENCABEZADO_RESULTADOS,
        [numero, sesion["apellidos"], sesion["nombres"],
         sesion["carrera"], total_puntos, total_posibles,
         resultado["nota_final"]],
    )
    return jsonify({"ok": True, "resultado": resultado})