# -*- coding: utf-8 -*-
"""
TEITAN MASTER RESCUE FINALIZER — V5.1 (KINGSTON ISOLATED)
Ubicación: E:\\TEITAN_RESCUE_OUTPUT\\TOOLS\\finalize_master_v51.py

Propósito:
Cierre definitivo del rescate maestro de Digital Optimus / TEITAN leyendo
EXCLUSIVAMENTE desde el SSD KINGSTON (E:).
CERO lecturas, consultas o accesos al SSD en riesgo (C:).

Garantías:
1. Identidad de hardware del Kingston (Modelo y Serial).
2. Verificación de checkpoint SQLite en modo solo lectura (`mode=ro`).
3. Detección y filtrado documentado de la entrada de prueba ('c:\\disco_roto_no_existe_fake.dat').
4. Reconstrucción inequívoca del manifiesto final de 150,290 archivos (147,903 núcleo + 2,387 AR88).
5. Sanitización de fecha ZIP: time.localtime con captura de OSError y sustitución segura (2026-10-29).
6. Construcción del maestro reutilizando `teitan_core.zip` e incorporando los 2,387 de AR88 por streaming.
7. Verificación total CRC (`testzip()`) y SHA-256 final.
8. Salida estricta: exit code 0 en éxito, exit code 1 en cualquier error.
"""

import os
import sys
import time
import shutil
import zipfile
import hashlib
import json
import string
import ctypes
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")

EXPECTED_CORE_COUNT = 147903
EXPECTED_AR88_COUNT = 2387
EXPECTED_TOTAL_COUNT = 150290

SAFE_FALLBACK_DATE_TIME = (2026, 10, 29, 12, 0, 0)

def verify_kingston_hardware():
    """Valida físicamente 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 física del volumen E:."
    except Exception as e:
        return False, f"Error validando hardware: {e}"

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 límites de 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():
    print("=" * 65)
    print("  TEITAN FINALIZER V5.1 — MODO PREFLIGHT (VALIDACION INDEPENDIENTE)")
    print("=" * 65)

    # 1. Hardware
    ok, msg = verify_kingston_hardware()
    if not ok:
        print(f"[FAIL] {msg}")
        return False
    print(f"[PASS] 1. {msg}")

    # 2. Checkpoint DB
    if not os.path.exists(CHECKPOINT_DB_PATH):
        print(f"[FAIL] No existe la base de checkpoint: {CHECKPOINT_DB_PATH}")
        return False

    uri_db = f"file:{CHECKPOINT_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] 2. Checkpoint SQLite íntegro (PRAGMA quick_check = ok)")

    # 3. Detección y análisis del registro duplicado
    cur.execute("SELECT staged_path, count(*) as c FROM file_checkpoints GROUP BY staged_path HAVING c > 1")
    dup_staged = cur.fetchall()
    print(f"[INFO] Grupos de staged_path duplicados en SQLite: {len(dup_staged)}")
    
    fake_entry = None
    for ds in dup_staged:
        cur.execute("SELECT norm_source, original_source, staged_path, bytes, sha256 FROM file_checkpoints WHERE staged_path = ?", (ds[0],))
        entries = cur.fetchall()
        for e in entries:
            if "disco_roto_no_existe_fake" in e[0]:
                fake_entry = e

    if fake_entry:
        print(f"[PASS] 3. Registro adicional identificado inequívocamente:")
        print(f"       norm_source: {fake_entry[0]}")
        print(f"       Origen: Artefacto inyectado en suite de pruebas aisladas.")
        print(f"       Resolución: Filtrado formalmente del manifiesto sin alterar la DB.")
    else:
        print(f"[FAIL] No se pudo identificar la causa del duplicado en staged_path.")
        conn.close()
        return False

    # 4. Validar teitan_core.zip
    if not os.path.exists(CORE_ZIP_PATH):
        print(f"[FAIL] No existe teitan_core.zip en: {CORE_ZIP_PATH}")
        conn.close()
        return False

    core_zf = zipfile.ZipFile(CORE_ZIP_PATH, 'r')
    core_namelist = core_zf.namelist()
    core_count = len(core_namelist)
    core_unique = len(set(core_namelist))
    if core_count != EXPECTED_CORE_COUNT or core_unique != EXPECTED_CORE_COUNT:
        print(f"[FAIL] teitan_core.zip tiene {core_count} entradas ({core_unique} únicas), esperadas {EXPECTED_CORE_COUNT}")
        core_zf.close()
        conn.close()
        return False
    core_zf.close()
    print(f"[PASS] 4. teitan_core.zip validado: exactamente {core_count} entradas únicas.")

    # 5. Validar los 2,387 archivos AR88 en SQLite y físicamente en Kingston
    cur.execute("SELECT norm_source, original_source, staged_path, bytes, sha256 FROM file_checkpoints WHERE norm_source LIKE ? AND status = 'VERIFIED'", (r'%ar88_sandbox%',))
    ar88_rows = cur.fetchall()
    if len(ar88_rows) != EXPECTED_AR88_COUNT:
        print(f"[FAIL] AR88 en SQLite tiene {len(ar88_rows)} registros, esperados {EXPECTED_AR88_COUNT}")
        conn.close()
        return False

    ar88_missing_or_mismatch = 0
    ar88_bytes = 0
    for r in ar88_rows:
        staged_p = r[2]
        exp_b = r[3]
        if not os.path.exists(staged_p) or os.path.getsize(staged_p) != exp_b:
            ar88_missing_or_mismatch += 1
        ar88_bytes += exp_b

    if ar88_missing_or_mismatch > 0:
        print(f"[FAIL] {ar88_missing_or_mismatch} archivos de AR88 no existen o difieren en tamaño en Kingston.")
        conn.close()
        return False
    print(f"[PASS] 5. {len(ar88_rows)} archivos AR88 verificados físicamente en Kingston ({ar88_bytes / (1024**3):.2f} GiB).")

    # 6. Comprobar solapamiento entre teitan_core.zip y AR88
    core_set = set(core_namelist)
    ar88_set = set(os.path.splitdrive(r[1])[1].lstrip('\\/').replace('\\', '/') for r in ar88_rows)
    overlap = core_set.intersection(ar88_set)
    if len(overlap) > 0:
        print(f"[FAIL] Solapamiento detectado entre teitan_core.zip y AR88: {len(overlap)} archivos.")
        conn.close()
        return False
    print(f"[PASS] 6. Cero solapamiento entre núcleo y AR88 (Conjuntos disyuntos perfectos).")

    # 7. Total Reconciliado
    total_reconstructed = len(core_set) + len(ar88_set)
    if total_reconstructed != EXPECTED_TOTAL_COUNT:
        print(f"[FAIL] Total reconstruido {total_reconstructed} != {EXPECTED_TOTAL_COUNT}")
        conn.close()
        return False
    print(f"[PASS] 7. Manifiesto maestro reconstruido inequívocamente: exactamente {total_reconstructed} archivos.")

    # 8. Espacio libre en Kingston
    free_bytes = shutil.disk_usage(TARGET_DRIVE).free
    # Espacio necesario para el nuevo ZIP maestro: ~45-55 GB + margen 15 GB = 70 GB
    est_needed = 70 * 1024 * 1024 * 1024
    if free_bytes < est_needed:
        print(f"[FAIL] Espacio libre en E: ({free_bytes / (1024**3):.2f} GiB) menor al estimado ({est_needed / (1024**3):.2f} GiB)")
        conn.close()
        return False
    print(f"[PASS] 8. Espacio libre en Kingston: {free_bytes / (1024**3):.2f} GiB (Margen de seguridad OK).")

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

def run_finalize():
    print("=" * 65)
    print("  TEITAN FINALIZER V5.1 — MODO FINALIZE (CONSTRUCCION DEL MAESTRO)")
    print("=" * 65)

    # Ejecutar preflight obligatorio previo
    if not run_preflight():
        print("[BLOQUEO] Preflight falló. Abortando finalización.")
        sys.exit(1)

    # Conectar en solo lectura a SQLite para obtener los 2,387 archivos AR88
    uri_db = f"file:{CHECKPOINT_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()

    # Preservar cualquier teitan.zip previo existente
    if os.path.exists(MASTER_ZIP_PATH):
        ts = time.strftime("%Y%m%d_%H%M%S")
        bak_name = f"{MASTER_ZIP_PATH}.bak_{ts}_{uuid.uuid4().hex[:6]}"
        print(f"[PRESERVACION] teitan.zip previo detectado. Preservando en:\n  -> {bak_name}")
        os.rename(MASTER_ZIP_PATH, bak_name)

    # Crear ZIP temporal con nombre único en Kingston
    temp_master_zip = f"{MASTER_ZIP_PATH}.finalizing_{os.getpid()}_{uuid.uuid4().hex}.zip"
    print(f"\nDestino Temporal: {temp_master_zip}")
    print(f"Destino Definitivo: {MASTER_ZIP_PATH}")

    # Paso A: Clonar / Copiar teitan_core.zip hacia el temporal como base estructural
    print("\n[FASE 1/3] Inicializando base del ZIP maestro a partir de teitan_core.zip...")
    t0 = time.time()
    shutil.copyfile(CORE_ZIP_PATH, temp_master_zip)
    t_copy = time.time() - t0
    print(f"  -> Base de 147,903 archivos del núcleo transferida en {t_copy:.1f}s.")

    # Paso B: Abrir en modo 'a' (append) y agregar por streaming los 2,387 archivos AR88 con ZipInfo sanitizado
    print("\n[FASE 2/3] Incorporando 2,387 archivos de AR88 por streaming con ZipInfo sanitizado...")
    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_path = entry[2]
            fsize = entry[3]
            arcname = os.path.splitdrive(orig_src)[1].lstrip('\\/').replace('\\', '/')

            st = os.stat(staged_path)
            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 directo desde Kingston hacia el ZIP
            with open(staged_path, "rb") as f_in, zf.open(zinfo, "w", force_zip64=(fsize > 2*1024*1024*1024)) as f_out:
                while chunk := f_in.read(4 * 1024 * 1024):
                    f_out.write(chunk)

            added_bytes += fsize
            if idx % 500 == 0 or fsize > 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)...")

    t_ar88 = time.time() - t_start_ar88
    print(f"  -> Incorporación de AR88 completada en {t_ar88:.1f}s.")

    # Paso C: Verificación integral física del archivo maestro temporal
    print("\n[FASE 3/3] Verificando integridad física del ZIP maestro...")
    print("  -> Comprobando número total de entradas...")
    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_COUNT or unique_final != EXPECTED_TOTAL_COUNT:
            print(f"[FAIL] Discrepancia en entradas del ZIP: Total {total_final}, Únicas {unique_final}, Esperadas {EXPECTED_TOTAL_COUNT}")
            sys.exit(1)
        print(f"  -> Entradas comprobadas: exactamente {total_final} entradas únicas.")

        print("  -> Ejecutando test de integridad CRC completo (testzip)...")
        t_crc = time.time()
        bad_file = zf_check.testzip()
        if bad_file is not None:
            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: Cálculo 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: Promoción atómica a teitan.zip definitivo
    os.replace(temp_master_zip, MASTER_ZIP_PATH)
    final_master_size = os.path.getsize(MASTER_ZIP_PATH)
    print(f"\n[OK] teitan.zip publicado definitivamente en: {MASTER_ZIP_PATH}")
    print(f"Tamaño Real: {final_master_size} bytes ({final_master_size / (1024**3):.2f} GiB)")

    # Paso F: Guardar informe final consolidado
    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
    }

    final_report_path = os.path.join(RESCUE_ROOT, "FINAL_MASTER_REPORT.json")
    with open(final_report_path, "w", encoding="utf-8") as rf:
        json.dump(summary_report, rf, indent=2)

    print("\n" + "=" * 65)
    print("  INFORME CONSOLIDADO FINAL GENERADO EN:")
    print(f"  {final_report_path}")
    print("=" * 65)
    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_v51.py --preflight")
        print("  python finalize_master_v51.py --finalize")
        sys.exit(1)

