# -*- coding: utf-8 -*-
"""
TEITAN MASTER RESCUE FINALIZER — V5.2 (SECURE KINGSTON HOTFIX)
Ubicacion: E:\\TEITAN_RESCUE_OUTPUT\\TOOLS\\finalize_master_v52.py

Cumplimiento estricto de las 6 correcciones de seguridad de la orden V5.2:
1. AISLAMIENTO FISICO ESTRICTO:
   - Validacion y normalizacion de staged_path contra STAGED_COPIES_ROOT.
   - Rechazo riguroso de rutas fuera de Kingston E:, rutas relativas, symlinks o junctions.
   - Validacion de arcnames en ZIP para evitar directory traversal (.. o barras iniciales).
2. HASH REAL Y TAMANO EN STREAMING DE AR88:
   - Durante la lectura por streaming para empaquetar en ZIP, calculo en vuelo del SHA-256 de los bytes efectivamente leidos.
   - Comparacion contra el SHA-256 registrado en SQLite y contra el tamano esperado.
   - Si cualquier archivo difiere en hash o tamano, ABORTA inmediatamente sin publicar el maestro.
3. INTEGRIDAD EXACTA DEL NUCLEO (teitan_core.zip):
   - Verificacion del SHA-256 historico exacto:
     40DA5AB6568DEABF9E60D70EAF96321632FBDBF4B9B646CA392BDC3BEF1EC199.
   - Validacion estricta de las 147,903 entradas unicas en el ZIP contra las fuentes legitimas del nucleo en SQLite.
4. RESOLUCION EXACTA DEL REGISTRO FICTICIO:
   - Identificacion exacta de 'c:\\disco_roto_no_existe_fake.dat' (sin subcadenas ambiguas).
   - Verificacion de que comparte exactamente staged_path con 'C:\\xampp\\htdocs\\Digital Optimus\\.gitignore' (733 bytes, hash identico).
   - Validacion de que no existen otras colisiones sin resolver. Exactamente 150,290 archivos legitimos resultantes.
5. CONTRATO DE NO-SOBRESCRITURA DE RESPALDOS:
   - Si 'teitan.zip' ya existe, BLOQUEA sin modificarlo ni renombrarlo.
   - Si 'FINAL_MASTER_REPORT.json' ya existe, BLOQUEA sin modificarlo ni sobrescribirlo.
   - Publicacion no-destructiva (os.replace solo si destino no preexiste).
6. CALCULO DINAMICO CONSERVADOR DE ESPACIO:
   - Evalua: tamano real teitan_core.zip + tamano total AR88 + sobrecarga ZIP + 15 GiB de margen de seguridad.
   - Bloquea si Kingston no cuenta con el espacio necesario antes de iniciar.
"""

import os
import sys
import time
import shutil
import zipfile
import hashlib
import json
import subprocess
import uuid
import sqlite3

EXPECTED_KINGSTON_SERIAL_SUBSTR = "0026_B728_3F45_1265"
TARGET_DRIVE = "E:\\"
RESCUE_ROOT = os.path.join(TARGET_DRIVE, "TEITAN_RESCUE_OUTPUT")
STAGE_ROOT = os.path.join(RESCUE_ROOT, "STAGED_COPIES")
CHECKPOINT_DB_PATH = os.path.join(RESCUE_ROOT, "stage_checkpoint.sqlite")
CORE_ZIP_PATH = os.path.join(RESCUE_ROOT, "teitan_core.zip")
MASTER_ZIP_PATH = os.path.join(RESCUE_ROOT, "teitan.zip")
FINAL_REPORT_PATH = os.path.join(RESCUE_ROOT, "FINAL_MASTER_REPORT.json")

EXPECTED_CORE_COUNT = 147903
EXPECTED_AR88_COUNT = 2387
EXPECTED_TOTAL_COUNT = 150290

EXPECTED_CORE_SHA256 = "40DA5AB6568DEABF9E60D70EAF96321632FBDBF4B9B646CA392BDC3BEF1EC199"
EXACT_FAKE_NORM_SOURCE = "c:\\disco_roto_no_existe_fake.dat"
EXACT_COLLIDING_NORM_SOURCE = "c:\\xampp\\htdocs\\digital optimus\\.gitignore"

SAFETY_MARGIN_BYTES = 15 * 1024 * 1024 * 1024  # 15 GiB
SAFE_FALLBACK_DATE_TIME = (2026, 10, 29, 12, 0, 0)

def verify_kingston_hardware():
    """Valida fisicamente que E: resida en KINGSTON SNV3S1000G con el serial esperado."""
    ps_cmd = "Get-Partition -DriveLetter E | Get-Disk | Select-Object FriendlyName, SerialNumber, Number | ConvertTo-Json"
    try:
        proc = subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd], capture_output=True, text=True, timeout=10)
        if proc.returncode == 0 and proc.stdout.strip():
            disk_info = json.loads(proc.stdout)
            friendly = disk_info.get("FriendlyName", "")
            serial = disk_info.get("SerialNumber", "")
            if "KINGSTON" not in friendly or EXPECTED_KINGSTON_SERIAL_SUBSTR not in serial:
                return False, f"El volumen E: reside en {friendly} (S/N: {serial}), NO en el Kingston esperado."
            return True, f"Hardware Kingston verificado ({friendly}, S/N: {serial})"
        else:
            return False, "No fue posible consultar la procedencia fisica del volumen E:."
    except Exception as e:
        return False, f"Error validando hardware: {e}"

def validate_staged_path_isolation(staged_path, allowed_stage_root=None):
    """
    Aislamiento fisico estricto:
    - Comprueba que la ruta este dentro de allowed_stage_root (por defecto STAGE_ROOT).
    - Resuelve enlaces simbolicos con realpath.
    - Rechaza cualquier ruta que intente escapar o leer de C:.
    """
    if allowed_stage_root is None:
        allowed_stage_root = STAGE_ROOT

    norm_root = os.path.normcase(os.path.realpath(allowed_stage_root))
    norm_target = os.path.normcase(os.path.realpath(staged_path))

    if not norm_target.startswith(norm_root + os.sep) and norm_target != norm_root:
        return False, f"Ruta fuera de staging autorizado: {staged_path} -> {norm_target}"

    # Rechazo explicito de acceso a C:
    if norm_target.startswith("c:"):
        return False, f"Intento critico de lectura hacia SSD C:: {staged_path}"

    return True, "OK"

def validate_zip_arcname(arcname):
    """Valida que el nombre de archivo dentro del ZIP sea seguro y libre de directory traversal."""
    if os.path.isabs(arcname):
        return False, f"Ruta absoluta en ZIP no permitida: {arcname}"
    parts = arcname.replace('\\', '/').split('/')
    if '..' in parts or '.' in parts:
        return False, f"Directory traversal en ZIP no permitido: {arcname}"
    if arcname.startswith('/') or arcname.startswith('\\'):
        return False, f"Barra inicial en ZIP no permitida: {arcname}"
    return True, "OK"

def get_sanitized_zip_date_time(mtime_timestamp):
    """
    Convierte mtime a tupla ZIP (Y, M, D, H, M, S) de forma segura.
    Si falla time.localtime() por desbordamiento en Windows o queda fuera de 1980..2107,
    retorna SAFE_FALLBACK_DATE_TIME y registra el motivo.
    """
    try:
        dt = time.localtime(mtime_timestamp)
        if dt.tm_year < 1980 or dt.tm_year > 2107:
            return SAFE_FALLBACK_DATE_TIME, f"Year out of bounds ({dt.tm_year})"
        return (dt.tm_year, dt.tm_mon, dt.tm_mday, dt.tm_hour, dt.tm_min, dt.tm_sec), None
    except Exception as e:
        return SAFE_FALLBACK_DATE_TIME, f"time.localtime failed: {e}"

def calculate_sha256(filepath):
    sha = hashlib.sha256()
    with open(filepath, "rb") as f:
        while chunk := f.read(4 * 1024 * 1024):
            sha.update(chunk)
    return sha.hexdigest().upper()

def run_preflight(db_path=None, core_zip_path=None, target_drive=None, stage_root=None, master_zip_path=None, final_report_path=None):
    """
    Modo Preflight completo de V5.2.
    Permite inyectar rutas para pruebas aisladas sin tocar los datos de produccion.
    """
    if db_path is None: db_path = CHECKPOINT_DB_PATH
    if core_zip_path is None: core_zip_path = CORE_ZIP_PATH
    if target_drive is None: target_drive = TARGET_DRIVE
    if stage_root is None: stage_root = STAGE_ROOT
    if master_zip_path is None: master_zip_path = MASTER_ZIP_PATH
    if final_report_path is None: final_report_path = FINAL_REPORT_PATH

    print("=" * 68)
    print("  TEITAN FINALIZER V5.2 — MODO PREFLIGHT (AUDITORIA DE SEGURIDAD)")
    print("=" * 68)

    # 1. Comprobacion de destino maestro no preexistente (Correccion 5)
    if os.path.exists(master_zip_path):
        print(f"[FAIL] teitan.zip ya existe en destino ({master_zip_path}). Prohibido sobrescribir.")
        return False
    if os.path.exists(final_report_path):
        print(f"[FAIL] FINAL_MASTER_REPORT.json ya existe en destino ({final_report_path}). Prohibido sobrescribir.")
        return False
    print("[PASS] 1. Contrato de no-sobrescritura: teitan.zip y reporte final no preexisten.")

    # 2. Hardware Kingston (solo en ejecucion sobre E:)
    if target_drive.upper().startswith("E:"):
        ok_hw, msg_hw = verify_kingston_hardware()
        if not ok_hw:
            print(f"[FAIL] {msg_hw}")
            return False
        print(f"[PASS] 2. {msg_hw}")
    else:
        print("[INFO] 2. Modo sandbox / prueba: omitiendo verificacion directa de hardware Kingston.")

    # 3. Checkpoint SQLite integridad
    if not os.path.exists(db_path):
        print(f"[FAIL] No existe base de checkpoint: {db_path}")
        return False

    uri_db = f"file:{os.path.abspath(db_path).replace(os.sep, '/')}?mode=ro"
    conn = sqlite3.connect(uri_db, uri=True)
    cur = conn.cursor()
    cur.execute("PRAGMA quick_check;")
    qc = cur.fetchone()[0]
    if qc != "ok":
        print(f"[FAIL] Checkpoint corrupto: {qc}")
        conn.close()
        return False
    print(f"[PASS] 3. Checkpoint SQLite integro (PRAGMA quick_check = ok).")

    # 4. Resolucion exacta del registro ficticio (Correccion 4)
    # Validar coincidencia exacta de 'c:\disco_roto_no_existe_fake.dat'
    cur.execute("SELECT norm_source, original_source, staged_path, bytes, sha256 FROM file_checkpoints WHERE norm_source = ?", (EXACT_FAKE_NORM_SOURCE,))
    fake_rows = cur.fetchall()
    if len(fake_rows) != 1:
        print(f"[FAIL] Se esperaba exactamente 1 registro ficticio con norm_source='{EXACT_FAKE_NORM_SOURCE}', encontrados: {len(fake_rows)}")
        conn.close()
        return False
    fake_rec = fake_rows[0]

    # Validar que comparte staged_path exclusivamente con .gitignore
    cur.execute("SELECT norm_source, original_source, staged_path, bytes, sha256 FROM file_checkpoints WHERE staged_path = ?", (fake_rec[2],))
    colliding_rows = cur.fetchall()
    if len(colliding_rows) != 2:
        print(f"[FAIL] El staged_path del registro ficticio no tiene exactamente 2 entradas: {len(colliding_rows)}")
        conn.close()
        return False

    norm_sources_in_collision = set(r[0] for r in colliding_rows)
    if norm_sources_in_collision != {EXACT_FAKE_NORM_SOURCE, EXACT_COLLIDING_NORM_SOURCE}:
        print(f"[FAIL] Colision en staged_path involucra registros no autorizados: {norm_sources_in_collision}")
        conn.close()
        return False

    # Validar que ambos tienen 733 bytes y mismo sha256
    if colliding_rows[0][3] != 733 or colliding_rows[1][3] != 733 or colliding_rows[0][4] != colliding_rows[1][4]:
        print(f"[FAIL] Metadatos de la colision no coinciden con los 733 bytes o hash documentado.")
        conn.close()
        return False

    # Validar que la copia fisica corresponde al archivo legitimo y su hash
    if not os.path.exists(fake_rec[2]) or os.path.getsize(fake_rec[2]) != 733:
        print(f"[FAIL] El archivo fisico en staging no existe o no tiene 733 bytes: {fake_rec[2]}")
        conn.close()
        return False

    actual_git_hash = calculate_sha256(fake_rec[2])
    if actual_git_hash != fake_rec[4]:
        print(f"[FAIL] Hash real de .gitignore en staging difiere del checkpoint.")
        conn.close()
        return False

    # Validar que NO existen otros duplicados en staged_path en toda la base
    cur.execute("SELECT staged_path, count(*) as c FROM file_checkpoints GROUP BY staged_path HAVING c > 1")
    all_dupes = cur.fetchall()
    if len(all_dupes) != 1:
        print(f"[FAIL] Existen otras colisiones de staged_path en SQLite no resueltas: {all_dupes}")
        conn.close()
        return False
    print(f"[PASS] 4. Registro ficticio resuelto por evidencia exacta (1 duplicado identificado y auditado).")

    # 5. Integridad exacta del nucleo teitan_core.zip (Correccion 3)
    if not os.path.exists(core_zip_path):
        print(f"[FAIL] No existe teitan_core.zip: {core_zip_path}")
        conn.close()
        return False

    actual_core_sha256 = calculate_sha256(core_zip_path)
    # En sandbox el hash puede ser diferente si se usa un zip mock; si es el archivo real, debe coincidir exactamente
    if core_zip_path == CORE_ZIP_PATH:
        if actual_core_sha256 != EXPECTED_CORE_SHA256:
            print(f"[FAIL] Hash de teitan_core.zip difiere del esperado: {actual_core_sha256} != {EXPECTED_CORE_SHA256}")
            conn.close()
            return False
        print(f"[PASS] 5. SHA-256 de teitan_core.zip coincide al 100% ({actual_core_sha256}).")
    else:
        print(f"[INFO] 5. teitan_core.zip (sandbox) SHA-256: {actual_core_sha256}")

    core_zf = zipfile.ZipFile(core_zip_path, 'r')
    core_namelist = core_zf.namelist()
    core_count = len(core_namelist)
    core_unique = len(set(core_namelist))

    expected_core_c = EXPECTED_CORE_COUNT if core_zip_path == CORE_ZIP_PATH else core_count
    if core_count != expected_core_c or core_unique != expected_core_c:
        print(f"[FAIL] Entradas en teitan_core.zip: {core_count} (unicas: {core_unique}), esperadas: {expected_core_c}")
        core_zf.close()
        conn.close()
        return False

    # Validar correspondencia de nombres normalizados contra SQLite (excluyendo el fake y AR88)
    cur.execute("SELECT original_source, bytes FROM file_checkpoints WHERE norm_source NOT LIKE ? AND norm_source != ?", (r'%ar88_sandbox%', EXACT_FAKE_NORM_SOURCE))
    db_core_rows = cur.fetchall()

    if core_zip_path == CORE_ZIP_PATH:
        if len(db_core_rows) != EXPECTED_CORE_COUNT:
            print(f"[FAIL] SQLite registros legitimos de nucleo ({len(db_core_rows)}) != {EXPECTED_CORE_COUNT}")
            core_zf.close()
            conn.close()
            return False

        db_core_names = set(os.path.splitdrive(r[0])[1].lstrip('\\/').replace('\\', '/') for r in db_core_rows)
        core_zip_set = set(core_namelist)
        if core_zip_set != db_core_names:
            diff_zip = len(core_zip_set - db_core_names)
            diff_db = len(db_core_names - core_zip_set)
            print(f"[FAIL] Inconsistencia entre entradas del ZIP del nucleo y SQLite (diff_zip={diff_zip}, diff_db={diff_db})")
            core_zf.close()
            conn.close()
            return False
        print(f"[PASS] 6. Cobertura del nucleo: 147,903 entradas del ZIP corresponden exactamente con SQLite.")

    core_zf.close()

    # 6. Aislamiento fisico y comprobacion de AR88 en SQLite y Kingston (Correcciones 1 y 2)
    cur.execute("SELECT norm_source, original_source, staged_path, bytes, sha256 FROM file_checkpoints WHERE norm_source LIKE ? AND status = 'VERIFIED' ORDER BY original_source", (r'%ar88_sandbox%',))
    ar88_rows = cur.fetchall()

    expected_ar88_c = EXPECTED_AR88_COUNT if db_path == CHECKPOINT_DB_PATH else len(ar88_rows)
    if len(ar88_rows) != expected_ar88_c:
        print(f"[FAIL] Registros de AR88 en SQLite ({len(ar88_rows)}) != {expected_ar88_c}")
        conn.close()
        return False

    ar88_total_bytes = 0
    for r in ar88_rows:
        staged_p = r[2]
        exp_bytes = r[3]
        orig_s = r[1]

        # Validacion de aislamiento
        ok_iso, msg_iso = validate_staged_path_isolation(staged_p, allowed_stage_root=stage_root)
        if not ok_iso:
            print(f"[FAIL] Violacion de aislamiento en AR88: {msg_iso}")
            conn.close()
            return False

        # Validacion de arcname
        arcname = os.path.splitdrive(orig_s)[1].lstrip('\\/').replace('\\', '/')
        ok_arc, msg_arc = validate_zip_arcname(arcname)
        if not ok_arc:
            print(f"[FAIL] Arcname invalido en AR88: {msg_arc}")
            conn.close()
            return False

        if not os.path.exists(staged_p) or os.path.getsize(staged_p) != exp_bytes:
            print(f"[FAIL] Archivo AR88 falta o difiere en tamano: {staged_p}")
            conn.close()
            return False

        ar88_total_bytes += exp_bytes

    print(f"[PASS] 7. Aislamiento fisico verificado para los {len(ar88_rows)} archivos AR88 ({ar88_total_bytes / (1024**3):.2f} GiB).")

    # 7. Total Reconciliado (Exactamente 150,290)
    if db_path == CHECKPOINT_DB_PATH and core_zip_path == CORE_ZIP_PATH:
        cur.execute("SELECT count(*) FROM file_checkpoints WHERE norm_source != ?", (EXACT_FAKE_NORM_SOURCE,))
        total_legit = cur.fetchone()[0]
        if total_legit != EXPECTED_TOTAL_COUNT:
            print(f"[FAIL] Total de archivos legitimos reconciliados ({total_legit}) != {EXPECTED_TOTAL_COUNT}")
            conn.close()
            return False
        print(f"[PASS] 8. Reconciliacion exacta de inventario: exactamente {total_legit} archivos legitimos.")

    # 8. Calculo dinamico conservador de espacio (Correccion 6)
    core_bytes = os.path.getsize(core_zip_path)
    # En el peor caso AR88 se almacena con poca compresion + sobrecarga ZIP + 15 GiB margen
    required_space = core_bytes + ar88_total_bytes + (2 * 1024 * 1024 * 1024) + SAFETY_MARGIN_BYTES
    free_bytes = shutil.disk_usage(target_drive).free

    print(f"[INFO] Espacio libre en destino: {free_bytes / (1024**3):.2f} GiB")
    print(f"[INFO] Espacio conservador requerido: {required_space / (1024**3):.2f} GiB (incluye 15 GiB de margen)")

    if free_bytes < required_space:
        print(f"[FAIL] Espacio insuficiente en destino ({free_bytes / (1024**3):.2f} GiB < {required_space / (1024**3):.2f} GiB)")
        conn.close()
        return False
    print(f"[PASS] 9. Evaluacion dinamica de espacio libre aprobada.")

    conn.close()
    print("=" * 68)
    print("RESULTADO PREFLIGHT V5.2: APTO PARA FINALIZACION (--finalize)")
    print("=" * 68)
    return True

def run_finalize(db_path=None, core_zip_path=None, target_drive=None, stage_root=None, master_zip_path=None, final_report_path=None):
    """
    Modo Finalize completo de V5.2.
    Ejecuta el preflight estricto y procede con la construccion del maestro sin sobrescrituras.
    """
    if db_path is None: db_path = CHECKPOINT_DB_PATH
    if core_zip_path is None: core_zip_path = CORE_ZIP_PATH
    if target_drive is None: target_drive = TARGET_DRIVE
    if stage_root is None: stage_root = STAGE_ROOT
    if master_zip_path is None: master_zip_path = MASTER_ZIP_PATH
    if final_report_path is None: final_report_path = FINAL_REPORT_PATH

    print("=" * 68)
    print("  TEITAN FINALIZER V5.2 — MODO FINALIZE (CONSTRUCCION DEFINITIVA)")
    print("=" * 68)

    # 1. Preflight obligatorio estricto
    if not run_preflight(db_path, core_zip_path, target_drive, stage_root, master_zip_path, final_report_path):
        print("[BLOQUEO] Preflight fallo. Abortando finalizacion.")
        sys.exit(1)

    # 2. Doble guardia de no-sobrescritura (Correccion 5)
    if os.path.exists(master_zip_path):
        print(f"[BLOQUEO CRITICO] teitan.zip ya existe en {master_zip_path}. Prohibido sobrescribir.")
        sys.exit(1)
    if os.path.exists(final_report_path):
        print(f"[BLOQUEO CRITICO] {final_report_path} ya existe. Prohibido sobrescribir.")
        sys.exit(1)

    # 3. Obtener registros de AR88 desde SQLite en modo ro
    uri_db = f"file:{os.path.abspath(db_path).replace(os.sep, '/')}?mode=ro"
    conn = sqlite3.connect(uri_db, uri=True)
    cur = conn.cursor()
    cur.execute("SELECT norm_source, original_source, staged_path, bytes, sha256 FROM file_checkpoints WHERE norm_source LIKE ? AND status = 'VERIFIED' ORDER BY original_source", (r'%ar88_sandbox%',))
    ar88_entries = cur.fetchall()
    conn.close()

    # 4. Crear archivo temporal unico en destino
    temp_dir = os.path.dirname(master_zip_path)
    temp_master_zip = os.path.join(temp_dir, f"teitan.zip.finalizing_{os.getpid()}_{uuid.uuid4().hex}.zip")
    print(f"\nDestino Temporal: {temp_master_zip}")
    print(f"Destino Definitivo: {master_zip_path}")

    # Paso A: Clonar teitan_core.zip como base del maestro
    print("\n[FASE 1/3] Inicializando base del ZIP maestro clonando teitan_core.zip...")
    t0 = time.time()
    shutil.copyfile(core_zip_path, temp_master_zip)
    print(f"  -> Base transferida en {time.time() - t0:.1f}s.")

    # Paso B: Incorporar AR88 con calculo en vuelo de SHA-256 (Correccion 2) y fechas sanitizadas
    print("\n[FASE 2/3] Incorporando archivos de AR88 por streaming con verificacion de SHA-256 en vuelo...")
    date_substitutions = []
    t_start_ar88 = time.time()
    added_bytes = 0

    with zipfile.ZipFile(temp_master_zip, 'a', compression=zipfile.ZIP_DEFLATED, allowZip64=True, compresslevel=6) as zf:
        for idx, entry in enumerate(ar88_entries, start=1):
            orig_src = entry[1]
            staged_p = entry[2]
            exp_bytes = entry[3]
            expected_sha = entry[4]
            arcname = os.path.splitdrive(orig_src)[1].lstrip('\\/').replace('\\', '/')

            # Validar aislamiento de nuevo en vuelo
            ok_iso, msg_iso = validate_staged_path_isolation(staged_p, allowed_stage_root=stage_root)
            if not ok_iso:
                zf.close()
                if os.path.exists(temp_master_zip): os.remove(temp_master_zip)
                raise RuntimeError(f"Violacion de aislamiento en vuelo: {msg_iso}")

            st = os.stat(staged_p)
            dt_tuple, subst_reason = get_sanitized_zip_date_time(st.st_mtime)
            if subst_reason:
                date_substitutions.append({
                    "file": arcname,
                    "original_mtime": st.st_mtime,
                    "substituted_date": dt_tuple,
                    "reason": subst_reason
                })

            zinfo = zipfile.ZipInfo(filename=arcname, date_time=dt_tuple)
            zinfo.compress_type = zipfile.ZIP_DEFLATED
            zinfo.external_attr = (st.st_mode & 0xFFFF) << 16

            # Streaming y calculo de SHA-256 en vuelo
            file_sha = hashlib.sha256()
            read_bytes = 0

            with open(staged_p, "rb") as f_in, zf.open(zinfo, "w", force_zip64=(exp_bytes > 2*1024*1024*1024)) as f_out:
                while chunk := f_in.read(4 * 1024 * 1024):
                    file_sha.update(chunk)
                    read_bytes += len(chunk)
                    f_out.write(chunk)

            # Comprobaciones estrictas de identidad en vuelo (Correccion 2)
            actual_sha = file_sha.hexdigest().upper()
            if read_bytes != exp_bytes:
                zf.close()
                if os.path.exists(temp_master_zip): os.remove(temp_master_zip)
                raise RuntimeError(f"ABORTANDO: Tamano leido ({read_bytes}) != Esperado ({exp_bytes}) en {staged_p}")

            if actual_sha != expected_sha:
                zf.close()
                if os.path.exists(temp_master_zip): os.remove(temp_master_zip)
                raise RuntimeError(f"ABORTANDO: SHA-256 real ({actual_sha}) != Checkpoint ({expected_sha}) en {staged_p}")

            added_bytes += read_bytes
            if idx % 500 == 0 or exp_bytes > 100 * 1024 * 1024:
                el = time.time() - t_start_ar88
                mb_s = (added_bytes / (1024*1024)) / el if el > 0 else 0
                print(f"  AR88 Progreso: {idx}/{len(ar88_entries)} ({added_bytes / (1024**3):.2f} GiB procesados, {mb_s:.1f} MB/s)...")

    print(f"  -> Incorporacion y verificacion SHA-256 de AR88 completada en {time.time() - t_start_ar88:.1f}s.")

    # Paso C: Verificacion integral del temporal antes de publicar
    print("\n[FASE 3/3] Verificando integridad fisica final del ZIP temporal...")
    with zipfile.ZipFile(core_zip_path, 'r') as czf:
        core_c = len(czf.namelist())
    expected_total_c = EXPECTED_TOTAL_COUNT if master_zip_path == MASTER_ZIP_PATH else core_c + len(ar88_entries)

    with zipfile.ZipFile(temp_master_zip, 'r') as zf_check:
        final_names = zf_check.namelist()
        total_final = len(final_names)
        unique_final = len(set(final_names))
        if total_final != expected_total_c or unique_final != expected_total_c:
            zf_check.close()
            if os.path.exists(temp_master_zip): os.remove(temp_master_zip)
            print(f"[FAIL] Discrepancia en entradas del ZIP: Total {total_final}, Unicas {unique_final}, Esperadas {expected_total_c}")
            sys.exit(1)
        print(f"  -> Conteo verificado: exactamente {total_final} entradas unicas.")

        print("  -> Ejecutando test de integridad CRC completo (testzip)...")
        t_crc = time.time()
        bad_file = zf_check.testzip()
        if bad_file is not None:
            zf_check.close()
            if os.path.exists(temp_master_zip): os.remove(temp_master_zip)
            print(f"[FAIL] Error de CRC en archivo: {bad_file}")
            sys.exit(1)
        print(f"  -> Test CRC 100% superado en {time.time() - t_crc:.1f}s.")

    # Paso D: Calculo de SHA-256 definitivo
    print("  -> Calculando hash SHA-256 definitivo del archivo maestro...")
    t_sha = time.time()
    master_sha256 = calculate_sha256(temp_master_zip)
    print(f"  -> SHA-256: {master_sha256} (calculado en {time.time() - t_sha:.1f}s)")

    # Paso E: Publicacion sin sobrescritura (Correccion 5)
    if os.path.exists(master_zip_path):
        print(f"[BLOQUEO] teitan.zip aparecio concurrentemente en {master_zip_path}. Temporal conservado en: {temp_master_zip}")
        sys.exit(1)

    os.rename(temp_master_zip, master_zip_path)
    final_master_size = os.path.getsize(master_zip_path)
    print(f"\n[OK] teitan.zip publicado exitosamente en: {master_zip_path}")
    print(f"Tamano Real: {final_master_size} bytes ({final_master_size / (1024**3):.2f} GiB)")

    # Paso F: Guardar informe final consolidado sin sobrescritura
    summary_report = {
        "TIMESTAMP": time.strftime("%Y-%m-%d %H:%M:%S"),
        "STATUS": "BACKUP_COMPLETED_AND_VERIFIED",
        "MASTER_ZIP_PATH": master_zip_path,
        "MASTER_ZIP_BYTES": final_master_size,
        "MASTER_ZIP_GIB": round(final_master_size / (1024**3), 3),
        "MASTER_ZIP_SHA256": master_sha256,
        "TOTAL_FILES_IN_ZIP": total_final,
        "CORE_FILES_COUNT": EXPECTED_CORE_COUNT,
        "AR88_FILES_COUNT": EXPECTED_AR88_COUNT,
        "EXCLUSIONS_PRESENCE_LOCK": 4,
        "DATE_SUBSTITUTIONS_COUNT": len(date_substitutions),
        "DATE_SUBSTITUTIONS": date_substitutions,
        "CRC_INTEGRITY": "PASS",
        "BACKUP_COMPLETED": True
    }

    if os.path.exists(final_report_path):
        report_out = f"{final_report_path}.new_{int(time.time())}.json"
    else:
        report_out = final_report_path

    with open(report_out, "w", encoding="utf-8") as rf:
        json.dump(summary_report, rf, indent=2)

    print("\n" + "=" * 68)
    print(f"  INFORME CONSOLIDADO FINAL GENERADO EN: {report_out}")
    print("=" * 68)
    print(json.dumps(summary_report, indent=2))
    sys.exit(0)

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "--preflight":
        ok = run_preflight()
        sys.exit(0 if ok else 1)
    elif len(sys.argv) > 1 and sys.argv[1] == "--finalize":
        run_finalize()
    else:
        print("Uso:")
        print("  python finalize_master_v52.py --preflight")
        print("  python finalize_master_v52.py --finalize")
        sys.exit(1)
