"""Automatización del Ingreso de Licencias Médicas (SIRH La Pintana) con Playwright."""
import os
import difflib
from datetime import datetime
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
from config import (RUT, PASS, LOGIN_URL, FORM_URL, SEL, normaliza,
                    limpiar_num_licencia, partir_nombre_profesional,
                    partir_nombre_completo_profesional, mapear_jornada, es_parcial,
                    rut_valido)
from remuneraciones import extraer_remuneraciones

CAPTURAS_DIR = "capturas"
DOCUMENTOS_DIR = "documentos"
IGNORAR_OPCION = {"-- seleccione --", "sin especificar", ""}


class LicenciaError(Exception):
    pass


class LicenciaBot:
    def __init__(self, headless=True, slow_mo=0):
        self.headless = headless
        self.slow_mo = slow_mo
        self._dialogos = []

    # ---------- ciclo de vida ----------
    def __enter__(self):
        self._pw = sync_playwright().start()
        self.browser = self._pw.chromium.launch(headless=self.headless, slow_mo=self.slow_mo)
        self.ctx = self.browser.new_context(accept_downloads=True)
        # Evita que window.print() del visor de Cobro SIL bloquee la automatización
        self.ctx.add_init_script("window.print = function(){};")
        os.makedirs(CAPTURAS_DIR, exist_ok=True)
        os.makedirs(DOCUMENTOS_DIR, exist_ok=True)
        return self

    def __exit__(self, *a):
        try:
            self.ctx.close(); self.browser.close(); self._pw.stop()
        except Exception:
            pass

    # ---------- login ----------
    def login(self):
        pg = self.ctx.new_page()
        pg.goto(LOGIN_URL, timeout=45000, wait_until="domcontentloaded")
        pg.fill(SEL["login_rut"], RUT)
        pg.fill(SEL["login_pass"], PASS)
        pg.click(SEL["login_btn"])
        try:
            pg.wait_for_selector(SEL["sistema_personal_salud"], timeout=15000)
        except PWTimeout:
            cuerpo = pg.inner_text("body")[:300]
            raise LicenciaError(f"Login falló. Respuesta: {cuerpo!r}")
        # El sistema abre en una pestaña nueva
        with self.ctx.expect_page() as nueva:
            pg.click(SEL["sistema_personal_salud"])
        self.page = nueva.value
        self.page.wait_for_timeout(3000)
        self.page.on("dialog", self._on_dialog)
        return self.page

    def _on_dialog(self, dialog):
        self._dialogos.append((dialog.type, dialog.message))
        try:
            dialog.accept()
        except Exception:
            pass

    # ---------- utilidades ----------
    def _elegir_opcion(self, selector, objetivo, obligatorio=True):
        """Selecciona en un <select> la opción que mejor coincide con `objetivo`
        (comparación sin tildes ni mayúsculas). Devuelve el texto elegido."""
        objn = normaliza(objetivo)
        if not objn:
            if obligatorio:
                raise LicenciaError(f"Valor vacío para {selector}")
            return None
        # Algunos <select> (ej. isapre) se llenan por un postback asíncrono. Esperar a
        # que tengan opciones reales (no vacío ni solo "-- Seleccione --") antes de leer.
        try:
            self.page.wait_for_function(
                "sel => { const e=document.querySelector(sel); if(!e||!e.options) return false;"
                " return [...e.options].some(o => o.value && o.text.trim() &&"
                " !/seleccione|sin especificar/i.test(o.text)); }",
                arg=selector, timeout=10000)
        except PWTimeout:
            pass
        opciones = self.page.eval_on_selector_all(
            selector + " option", "els=>els.map(o=>({v:o.value,t:o.text.trim()}))"
        )
        # 1) coincidencia exacta
        for o in opciones:
            if normaliza(o["t"]) == objn:
                self.page.select_option(selector, value=o["v"])
                return o["t"]
        # 2) contención en cualquier dirección (elige la más parecida en longitud)
        cands = []
        for o in opciones:
            tn = normaliza(o["t"])
            if tn in IGNORAR_OPCION:
                continue
            if objn in tn or tn in objn:
                cands.append((abs(len(tn) - len(objn)), o))
        if cands:
            cands.sort(key=lambda x: x[0])
            elegido = cands[0][1]
            self.page.select_option(selector, value=elegido["v"])
            return elegido["t"]
        # 3) similitud aproximada: tolera palabras extra o de más (ej. "de hijo" vs "hijo").
        #    Compara por conjunto de palabras y por ratio de secuencia; exige alta coincidencia.
        obj_tokens = set(objn.split())
        mejor, mejor_score = None, 0.0
        for o in opciones:
            tn = normaliza(o["t"])
            if tn in IGNORAR_OPCION:
                continue
            tok = set(tn.split())
            jacc = len(obj_tokens & tok) / len(obj_tokens | tok) if (obj_tokens | tok) else 0.0
            ratio = difflib.SequenceMatcher(None, objn, tn).ratio()
            score = max(jacc, ratio)
            if score > mejor_score:
                mejor_score, mejor = score, o
        if mejor and mejor_score >= 0.72:
            self.page.select_option(selector, value=mejor["v"])
            return mejor["t"]
        disponibles = [o["t"] for o in opciones if normaliza(o["t"]) not in IGNORAR_OPCION]
        raise LicenciaError(
            f"No encontré la opción '{objetivo}' en {selector}. Opciones: {disponibles[:15]}"
        )

    def _click_boton(self, selector, timeout=8000):
        """Hace click en un botón. Si un modal/overlay intercepta el click
        (pointer events), cierra el modal y reintenta; como último recurso
        hace click por JS, que dispara el onclick/__doPostBack del botón."""
        pg = self.page
        self._cerrar_bootbox()
        for espera in (timeout, 5000):
            try:
                pg.click(selector, timeout=espera)
                return
            except PWTimeout:
                self._cerrar_bootbox()
        # Último recurso: click directo por JS (evita la intercepción del overlay)
        ok = pg.eval_on_selector(selector, "el => { if(el){ el.click(); return true; } return false; }")
        if not ok:
            raise LicenciaError(
                f"No pude hacer click en {selector}: un modal/overlay intercepta el click.")

    def _fecha_con_postback(self, selector, valor):
        """Escribe una fecha que dispara un postback asíncrono (UpdatePanel) y espera."""
        self.page.fill(selector, valor)
        self.page.dispatch_event(selector, "change")
        try:
            self.page.wait_for_load_state("networkidle", timeout=15000)
        except PWTimeout:
            pass
        self.page.wait_for_timeout(1500)

    def _cerrar_bootbox(self):
        """Cierra un modal (bootbox/Bootstrap) que esté abierto y devuelve su texto.
        Sirve para avisos/validaciones que quedan tapando otros botones (ej. Grabar)."""
        pg = self.page
        try:
            txt = pg.evaluate(
                "() => {"
                "  const ms=[...document.querySelectorAll('.bootbox.modal, .modal')]"
                "    .filter(e=>e.getClientRects().length>0 && (e.classList.contains('in')||e.classList.contains('show')));"
                "  if(!ms.length) return '';"
                "  const m=ms[ms.length-1];"
                "  const t=(m.innerText||'').replace(/\\s+/g,' ').trim();"
                "  const b=m.querySelector('.modal-footer .btn-primary, .modal-footer button, button.btn-primary, .bootbox-accept');"
                "  if(b) b.click();"
                "  return t;"
                "}"
            )
        except Exception:
            txt = ""
        if txt:
            pg.wait_for_timeout(900)
        return txt or ""

    def _cerrar_modal_exito(self):
        """Cierra el modal 'ÉXITO / LICENCIA GRABADA CORRECTAMENTE' (botón Aceptar).
        Devuelve el texto del modal. OJO: los modales son position:fixed, así que su
        offsetParent es null; hay que leerlos con el selector :visible de Playwright."""
        pg = self.page
        txt = ""
        try:
            modal = pg.locator(".modal:visible, [role=dialog]:visible").first
            modal.wait_for(state="visible", timeout=8000)
            txt = " ".join(modal.inner_text().split())
        except Exception:
            txt = ""
        try:
            pg.click('.modal:visible button:has-text("Aceptar"), [role=dialog]:visible button:has-text("Aceptar")', timeout=5000)
        except Exception:
            try:
                pg.evaluate(
                    '()=>{const vis=e=>e.getClientRects().length>0;'
                    'const bs=[...document.querySelectorAll("button,a,input[type=button]")]'
                    '.filter(b=>/aceptar/i.test(b.innerText||b.value||"") && vis(b)); if(bs[0]) bs[0].click();}'
                )
            except Exception:
                pass
        pg.wait_for_timeout(1500)
        return txt

    def _texto_modal_visible(self):
        """Devuelve el texto de un modal visible (ej. mensajes de ERROR), o ''."""
        try:
            m = self.page.locator(".modal:visible, [role=dialog]:visible").first
            if m.count() and m.is_visible():
                return " ".join(m.inner_text().split())
        except Exception:
            pass
        return ""

    def _continuar_prenatal(self):
        """Maneja el modal de aviso Pre/Post Natal ('...se le acortarán los días...
        ¿Desea Continuar?'): presiona 'Continuar' y devuelve el texto del aviso.
        Devuelve '' si no aparece. No confunde con el botón 'Continuar' (B_Ingresar)
        de la cabecera: exige que el contenedor mencione el aviso pre/post natal."""
        pg = self.page
        try:
            txt = pg.evaluate(
                "() => {"
                "  const vis = e => e && e.getClientRects().length>0;"
                "  const re = /pre\\s*natal|post\\s*natal|acortar|desea continuar/i;"
                "  const btns = [...document.querySelectorAll('a,button,input[type=button],input[type=submit]')]"
                "    .filter(b => vis(b) && /^\\s*continuar\\s*$/i.test((b.innerText||b.value||'').trim())"
                "                 && b.id !== 'ContentPlaceHolder1_B_Ingresar');"
                "  for (const b of btns) {"
                "    let n = b;"
                "    for (let i=0; i<6 && n; i++) {"
                "      if (re.test(n.innerText||'')) {"
                "        const t=(n.innerText||'').replace(/\\s+/g,' ').trim();"
                "        b.click();"
                "        return t;"
                "      }"
                "      n = n.parentElement;"
                "    }"
                "  }"
                "  return '';"
                "}"
            )
        except Exception:
            txt = ""
        if txt:
            pg.wait_for_timeout(1800)
            try:
                pg.wait_for_load_state("networkidle", timeout=8000)
            except PWTimeout:
                pass
        return txt or ""

    def _msg_error_modal(self):
        """Si hay un modal de ERROR visible, devuelve su mensaje limpio; si no, ''."""
        t = self._texto_modal_visible()
        tn = normaliza(t)
        if not t or not any(k in tn for k in ("error", "no puede", "honorarios", "no valido", "invalid")):
            return ""
        for junk in ("ERROR", "Aceptar", "×", "✖", "x"):
            t = t.replace(junk, " ")
        return " ".join(t.split())

    def _cobro_sil(self, num_lic, rut):
        """Presiona 'Formu.Cobro SIL' (abre el visor en pestaña nueva), lo guarda
        como PDF 'NroLicencia_RUT.pdf' y extrae las remuneraciones.
        Devuelve (pdf_path, remuneraciones|None)."""
        pdf_nombre = f"{num_lic}_{rut}.pdf"
        pdf_path = os.path.join(DOCUMENTOS_DIR, pdf_nombre)
        try:
            with self.ctx.expect_page(timeout=20000) as nueva:
                self.page.click(SEL["btn_cobro_sil"], timeout=10000)
            visor = nueva.value
        except Exception as e:
            raise LicenciaError(f"No se abrió el visor de Cobro SIL: {repr(e)[:80]}")
        try:
            visor.wait_for_load_state("networkidle", timeout=15000)
        except PWTimeout:
            pass
        visor.wait_for_timeout(2500)
        visor.pdf(path=pdf_path)
        visor.close()
        try:
            remun = extraer_remuneraciones(pdf_path)
        except Exception as e:
            remun = None
            self._dialogos.append(("remuneraciones_error", str(e)))
        return pdf_path, remun

    def _escribir_teclado(self, selector, texto):
        """Escribe en un campo simulando teclado (clic, seleccionar todo, borrar,
        tipear). Útil para campos que no aceptan fill() directo (ej. Primer Nombre
        del profesional cuando trae 'NO ENCONTRADO')."""
        pg = self.page
        pg.click(selector)
        pg.keyboard.press("Control+A")
        pg.keyboard.press("Delete")
        if texto:
            pg.keyboard.type(str(texto), delay=40)

    def _habilitar_rut_profesional(self):
        """Marca 'Selecciona Profesional' y espera a que el campo RUT del profesional
        quede realmente habilitado (el checkbox dispara un postback). Reintenta si no."""
        pg = self.page
        for _ in range(3):
            if not pg.is_checked(SEL["check_sel_profesional"]):
                try:
                    pg.check(SEL["check_sel_profesional"])
                except Exception:
                    pg.evaluate('s=>document.querySelector(s).click()', SEL["check_sel_profesional"])
            try:
                pg.wait_for_load_state("networkidle", timeout=8000)
            except PWTimeout:
                pass
            try:
                pg.wait_for_function(
                    "sel => { const e=document.querySelector(sel); return e && !e.disabled; }",
                    arg=SEL["rut_profesional"], timeout=6000)
                return True
            except PWTimeout:
                # reintento: desmarca y vuelve a marcar
                try:
                    pg.uncheck(SEL["check_sel_profesional"]); pg.wait_for_timeout(1000)
                except Exception:
                    pass
        return False

    # ---------- consulta (solo lectura) ----------
    def consultar_funcionario(self, rut, fecha):
        """Carga el funcionario por RUT+fecha y devuelve el nombre que muestra el
        sistema (sin guardar nada). Devuelve '' si no carga (RUT/período inválido)."""
        pg = self.page
        pg.goto(FORM_URL, timeout=45000, wait_until="domcontentloaded")
        pg.wait_for_selector(SEL["rut"], timeout=15000)
        pg.wait_for_timeout(500)
        pg.fill(SEL["rut"], str(rut))
        pg.fill(SEL["fecha_emision"], str(fecha))
        pg.click(SEL["btn_continuar"])
        try:
            pg.wait_for_function(
                "sel => { const e=document.querySelector(sel); return e && e.value.trim().length>0; }",
                arg=SEL["nombre"], timeout=12000,
            )
        except PWTimeout:
            return ""
        return pg.input_value(SEL["nombre"]).strip()

    # ---------- ingreso de una licencia ----------
    def ingresar_licencia(self, lic, enviar=False, on_progress=None):
        def prog(frac, texto):
            if on_progress:
                on_progress(frac, texto)

        pg = self.page
        self._dialogos.clear()
        prog(0.02, "Abriendo formulario...")
        pg.goto(FORM_URL, timeout=45000, wait_until="domcontentloaded")
        pg.wait_for_selector(SEL["rut"], timeout=15000)
        pg.wait_for_timeout(800)

        # 1) Cargar funcionario: RUT + Fecha Emisión -> Continuar
        prog(0.10, "Cargando funcionario...")
        pg.fill(SEL["rut"], lic["rut"])
        pg.fill(SEL["fecha_emision"], lic["fecha_emision"])
        pg.click(SEL["btn_continuar"])
        try:
            pg.wait_for_function(
                "sel => { const e=document.querySelector(sel); return e && e.value.trim().length>0; }",
                arg=SEL["nombre"], timeout=15000,
            )
        except PWTimeout:
            # Si el sistema mostró un modal de ERROR, usar ese mensaje como la causa.
            m = self._msg_error_modal()
            if m:
                raise LicenciaError(f"RUT {lic['rut']}: {m}")
            raise LicenciaError(
                f"No se cargó el funcionario RUT {lic['rut']}. Causas probables: "
                f"1) ya tiene una licencia ingresada en el período; "
                f"2) la Fecha de Emisión ({lic['fecha_emision']}) cae en un período cerrado; "
                f"3) el RUT no tiene contrato vigente en el período."
            )
        # El nombre cargó, pero puede aparecer un modal de ERROR igual
        # (ej. funcionario de honorarios, que no se puede procesar en esta pantalla).
        pg.wait_for_timeout(700)
        m = self._msg_error_modal()
        if m:
            raise LicenciaError(f"RUT {lic['rut']}: {m}")
        nombre_cargado = pg.input_value(SEL["nombre"]).strip()

        # 2) Datos de la licencia (N° licencia sin guión ni dígito verificador)
        prog(0.30, "Ingresando datos de la licencia...")
        num_lic = limpiar_num_licencia(lic["num_licencia"])
        pg.fill(SEL["num_licencia"], num_lic)
        pg.fill(SEL["dias"], str(lic["dias"]))
        self._fecha_con_postback(SEL["fecha_inicio"], lic["fecha_inicio"])
        # Aviso Pre/Post Natal (el sistema avisa que se acortarán días de otra
        # licencia): confirmamos "Continuar" y lo dejamos registrado.
        avisos_pren = []
        pren = self._continuar_prenatal()
        if pren:
            avisos_pren.append(pren)
        fecha_fin = pg.input_value(SEL["fecha_fin"]).strip()

        # 3) Tipo de licencia
        self._elegir_opcion(SEL["tipo_licencia"], lic["tipo_licencia"])

        # 4) Profesional: validar RUT, habilitar (con reintento), ingresar RUT
        prog(0.55, "Buscando profesional...")
        if not rut_valido(lic.get("rut_profesional")):
            raise LicenciaError(
                f"RUT del profesional inválido: {lic.get('rut_profesional')} "
                f"(dígito verificador incorrecto). Corrígelo en el Excel.")
        if not self._habilitar_rut_profesional():
            raise LicenciaError(
                f"No se habilitó el campo del profesional (checkbox 'Selecciona Profesional' "
                f"no habilitó el RUT). RUT profesional {lic.get('rut_profesional')}")
        pg.fill(SEL["rut_profesional"], lic["rut_profesional"])
        pg.dispatch_event(SEL["rut_profesional"], "blur")
        try:
            pg.wait_for_function(
                "sel => { const e=document.querySelector(sel); return e && e.value.trim().length>0; }",
                arg=SEL["prof_nombre1"], timeout=8000)
        except PWTimeout:
            pass
        pg.wait_for_timeout(500)
        prof_nombre = pg.input_value(SEL["prof_nombre1"]).strip()
        prof_estado = normaliza(prof_nombre)
        # Red de seguridad: si el sistema marca el RUT como inválido, cortar aquí
        # con un mensaje claro (queda en el comentario del Excel).
        if "rut invalido" in prof_estado:
            raise LicenciaError(
                f"RUT del profesional inválido según el sistema: {lic.get('rut_profesional')} "
                f"(RUT INVÁLIDO). Corrígelo en el Excel.")
        # Si el profesional no existe, lo registraremos con los datos del Excel
        # (los campos de nombre se llenan al final, tras los postbacks).
        prof_no_encontrado = (not prof_nombre) or ("no encontrado" in prof_estado)
        if prof_no_encontrado:
            primer_reg, segundo_reg, pat_reg, mat_reg = partir_nombre_completo_profesional(
                lic.get("nombre_profesional"))
            if not (primer_reg or pat_reg):
                raise LicenciaError(
                    f"El profesional {lic.get('rut_profesional')} no existe en el sistema y el Excel "
                    f"no trae 'NOMBRE PROFESIONAL' para registrarlo.")
            prof_nombre = f"{primer_reg} {pat_reg} {mat_reg}".strip() + " (nuevo)"

        # 5) Especialidad
        prog(0.70, "Especialidad, previsión y reposo...")
        esp_elegida = self._elegir_opcion(SEL["especialidad"], lic["especialidad"])

        # 6) Entidad de salud (isapre / FONASA)
        isapre_elegida = self._elegir_opcion(SEL["isapre"], lic["entidad_salud"])

        # 7) Tipo de reposo (TOTAL / PARCIAL)
        reposo_elegido = self._elegir_opcion(SEL["tipo_reposo"], lic["tipo_reposo"])

        # 7b) Si el reposo es PARCIAL, el sistema habilita "Jornada" (tras un postback).
        jornada_elegida = ""
        if es_parcial(reposo_elegido):
            try:
                pg.wait_for_load_state("networkidle", timeout=10000)
            except PWTimeout:
                pass
            pg.wait_for_timeout(1200)
            objetivo = mapear_jornada(lic.get("jornada"))
            if objetivo:
                jornada_elegida = self._elegir_opcion(SEL["jornada"], objetivo)
            else:
                self._dialogos.append(("aviso", "Reposo PARCIAL sin Jornada (AM/PM) en el Excel"))
            # El postback de PARCIAL resetea la Especialidad -> re-seleccionarla
            esp_elegida = self._elegir_opcion(SEL["especialidad"], lic["especialidad"])

        # 8) Calcular Recuperación (obligatorio antes de Grabar; dispara postback)
        prog(0.82, "Calculando recuperación...")
        self._click_boton(SEL["btn_calcular"])
        pg.wait_for_timeout(3500)
        try:
            pg.wait_for_load_state("networkidle", timeout=10000)
        except PWTimeout:
            pass
        pg.wait_for_timeout(1000)

        # 9) Datos del profesional: SE LLENAN AL FINAL, después de todos los postbacks
        #    (si no, el postback de Calcular Recuperación los borra).
        prog(0.90, "Completando datos del profesional...")
        primer, segundo, paterno, materno = partir_nombre_completo_profesional(
            lic.get("nombre_profesional"))
        if prof_no_encontrado:
            # Profesional nuevo: registrar nombre completo (Primer Nombre requiere teclado)
            prog(0.90, "Registrando profesional nuevo...")
            self._escribir_teclado(SEL["prof_nombre1"], primer)
            if segundo:
                pg.fill(SEL["prof_nombre2"], segundo)
        # Apellidos (para profesional nuevo o existente)
        if paterno:
            pg.fill(SEL["prof_apat"], paterno)
        if materno:
            pg.fill(SEL["prof_amat"], materno)

        resumen = {
            "fila_excel": lic.get("_fila_excel"),
            "rut": lic["rut"],
            "nombre_cargado": nombre_cargado,
            "num_licencia": num_lic,
            "dias": lic["dias"],
            "fecha_inicio": lic["fecha_inicio"],
            "fecha_fin": fecha_fin,
            "tipo_licencia": lic["tipo_licencia"],
            "profesional": prof_nombre,
            "prof_apellidos": f"{paterno} {materno}".strip(),
            "especialidad": esp_elegida,
            "entidad_salud": isapre_elegida,
            "tipo_reposo": reposo_elegido,
            "jornada": jornada_elegida,
            "dias_calculados": pg.input_value(SEL["dias_calculados"]).strip(),
            "valor_recuperacion": pg.input_value(SEL["valor_recuperacion"]).strip(),
            "aviso_prenatal": " | ".join(avisos_pren),
            "enviado": False,
            "dialogos": None,
        }

        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        etiqueta = f"fila{lic.get('_fila_excel')}_{lic['rut']}"

        if not enviar:
            prog(0.98, "Tomando captura...")
            ruta = os.path.join(CAPTURAS_DIR, f"revisar_{etiqueta}_{ts}.png")
            pg.screenshot(path=ruta, full_page=True)
            resumen["captura"] = ruta
            resumen["modo"] = "DRY-RUN (no se guardó)"
            prog(1.0, "Listo")
            return resumen

        # 10) Grabar
        prog(0.80, "Grabando...")
        # Si quedó abierto algún modal/aviso (ej. de Calcular Recuperación), cerrarlo
        # primero para que no tape el botón Grabar, y guardar su texto.
        aviso_previo = self._cerrar_bootbox()
        if aviso_previo:
            resumen["aviso_previo"] = aviso_previo
            self._dialogos.append(("aviso_previo", aviso_previo))
        self._dialogos.clear()
        self._click_boton(SEL["btn_grabar"], timeout=15000)
        pg.wait_for_timeout(4000)
        try:
            pg.wait_for_load_state("networkidle", timeout=10000)
        except PWTimeout:
            pass
        # El aviso Pre/Post Natal también puede aparecer al grabar -> Continuar.
        pren2 = self._continuar_prenatal()
        if pren2:
            avisos_pren.append(pren2)
            resumen["aviso_prenatal"] = " | ".join(avisos_pren)
        # cerrar modal de resultado
        modal_txt = self._cerrar_modal_exito()
        m = normaliza(modal_txt)
        resumen["mensaje_grabar"] = modal_txt
        # Solo damos por fallido si el modal dice explícitamente ERROR.
        # Si no pudimos leerlo, NO asumimos fallo: el Cobro SIL lo confirmará
        # (solo funciona con la licencia ya guardada).
        error_evidente = "error" in m and "grabada" not in m
        grabado_ok = ("grabada" in m) or ("exito" in m)

        # 11) Formu. Cobro SIL -> PDF + remuneraciones
        if not error_evidente:
            prog(0.90, "Generando Cobro SIL...")
            try:
                pdf_path, remun = self._cobro_sil(num_lic, lic["rut"])
                resumen["pdf_cobro_sil"] = pdf_path
                resumen["remuneraciones"] = remun
                grabado_ok = True  # el visor solo abre si la licencia se guardó
            except LicenciaError as e:
                resumen["pdf_cobro_sil"] = None
                resumen["remuneraciones"] = None
                resumen["error_cobro_sil"] = str(e)
        resumen["enviado"] = grabado_ok

        prog(0.98, "Tomando captura...")
        ruta = os.path.join(CAPTURAS_DIR, f"grabado_{etiqueta}_{ts}.png")
        try:
            pg.screenshot(path=ruta, full_page=True)
            resumen["captura"] = ruta
        except Exception:
            pass
        resumen["dialogos"] = list(self._dialogos)
        resumen["modo"] = "ENVIADO" if grabado_ok else "NO GRABADO"
        prog(1.0, "Grabado" if grabado_ok else "Revisar")
        return resumen
