"""Blueprint Flask del módulo de evaluación RA2 CDT1.

Adaptador web (capa infraestructura): valida entradas con allow-list y límites
de longitud, gestiona sesiones por token, registra incidentes de trampa y
delega toda la lógica de calificación al dominio puro (ra2_grader).

Rutas:
    GET  /ra2-cdt1           Página de la evaluación.
    POST /iniciar-ra2        Crea sesión (token) tras identificar al estudiante.
    POST /incidente-ra2      Registra un incidente anti-trampa (-5 pts c/u).
    POST /guardar-ra2        Califica server-side y guarda el CSV final.
"""

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

from flask import Blueprint, jsonify, render_template, request

import ra2_grader

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

DIR_ACTUAL = os.path.dirname(os.path.abspath(__file__))
CSV_RESULTADOS = os.path.join(DIR_ACTUAL, "resultados_ra2_cdt1.csv")
CSV_INCIDENCIAS = os.path.join(DIR_ACTUAL, "incidencias_ra2_cdt1.csv")
ENCABEZADO_RESULTADOS = ["No", "Clave", "Apellidos", "Nombres", "Carrera",
                         "Nota", "Incidentes", "Descuento"]
ENCABEZADO_INCIDENCIAS = ["fecha", "token", "estudiante", "tipo"]

CARRERAS_PERMITIDAS = ("Mecánica", "Electrónica", "Bachillerato")
TIPOS_INCIDENTE_VALIDOS = (
    "pantalla_completa", "pestana_oculta", "ventana_ajena",
    "copiar", "pegar", "cortar", "atajo_teclado", "devtools", "clic_derecho",
)

MAX_LONGITUD_TEXTO = 100
MAX_LONGITUD_CLAVE = 50
MAX_LONGITUD_RESPUESTA = 200

_sesiones = {}
_bloqueo = threading.Lock()


def _texto_valido(valor, maximo):
    """Devuelve texto saneado o None si viola tipo/longitud."""
    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


# ---------------------------------------------------------------------------
# Escritura segura de CSV (UTF-8 BOM + protección contra inyección de fórmulas)
# ---------------------------------------------------------------------------

def _escapar_celda(valor):
    """Neutraliza inyección de fórmulas CSV (=,+,-,@ inicial) y saltos."""
    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():
    """No = número de filas de datos ya presentes (encabezado excluido)."""
    if not os.path.exists(CSV_RESULTADOS):
        return 1
    with open(CSV_RESULTADOS, encoding="utf-8-sig") as archivo:
        filas = [linea for linea in archivo if linea.strip()]
        return max(1, len(filas))


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

@bp.route("/ra2-cdt1")
def pagina_ra2():
    return render_template("ra2-cdt1-iones.html")


@bp.route("/iniciar-ra2", methods=["POST"])
def iniciar_ra2():
    datos = request.get_json(silent=True) or {}
    apellidos = _texto_valido(datos.get("apellidos"), MAX_LONGITUD_TEXTO)
    nombres = _texto_valido(datos.get("nombres"), MAX_LONGITUD_TEXTO)
    clave = _texto_valido(datos.get("clave"), MAX_LONGITUD_CLAVE)
    carrera = datos.get("carrera")

    if not apellidos or not nombres or not clave:
        return jsonify({"ok": False, "error": "Identificación 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,
            "clave": clave,
            "carrera": carrera,
            "incidentes": [],
            "enviado": False,
        }
        return jsonify({"ok": True, "token": token})


@bp.route("/incidente-ra2", methods=["POST"])
def incidente_ra2():
    datos = request.get_json(silent=True) or {}
    sesion = _obtener_sesion(datos.get("token"))
    if sesion is None:
        return jsonify({"ok": False, "error": "Sesión inválida."}), 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})


# ---------------------------------------------------------------------------
# Validación de respuestas (defense in depth: allow-list en el boundary)
# ---------------------------------------------------------------------------

def _sanear_respuestas(datos):
    """Construye {indice: valor} saneado con límite de longitud."""
    r = datos.get("respuestas") or {}
    if not isinstance(r, dict):
        return {}
    limpio = {}
    for i in range(ra2_grader.TOTAL_PREGUNTAS):
        clave = str(i)
        valor = r.get(clave)
        if isinstance(valor, str) and len(valor.strip()) <= MAX_LONGITUD_RESPUESTA:
            limpio[clave] = valor.strip()
    return limpio


@bp.route("/guardar-ra2", methods=["POST"])
def guardar_ra2():
    datos = request.get_json(silent=True) or {}
    sesion = _obtener_sesion(datos.get("token"))
    if sesion is None:
        return jsonify({"ok": False, "error": "Sesión inválida."}), 400
    with _bloqueo:
        if sesion["enviado"]:
            return jsonify({"ok": False,
                            "error": "Esta evaluación ya fue enviada."}), 409

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

    with _bloqueo:
        numero = _siguiente_correlativo()
        sesion["enviado"] = True
    _agregar_fila(
        CSV_RESULTADOS, ENCABEZADO_RESULTADOS,
        [numero, sesion["clave"], sesion["apellidos"], sesion["nombres"],
         sesion["carrera"], resultado["nota_final"],
         resultado["incidentes_trampa"], resultado["descuento_trampa"]],
    )
    return jsonify({"ok": True, "resultado": resultado})
