# -*- coding: utf-8 -*-
"""
TEITAN MASTER RESCUE ENGINE — CORRECCION FOCALIZADA V3 (ULTRA-SAFE)
Subsanacion estricta de los 3 fallos y requerimientos de auditoria:
1. Fallo 1 (Conservacion de archivos recuperados):
   - Nunca sobrescribe destructivamente un archivo ya existente en Kingston.
   - Si un archivo preexistente en Kingston no coincide con el nuevo, se preserva con version unica.
   - Uso de .atomic_tmp con flush y fsync.
2. Fallo 2 (Reanudacion real sin tocar SSD C:):
   - Se mantiene un archivo de estado persistente transaccional en Kingston: stage_checkpoint.json.
   - Si un archivo ya esta en el checkpoint con hash y tamano verificado, y el archivo en Kingston coincide,
     se declara RESUMED_VERIFIED y CERO bytes se leen del SSD TeamGroup.
3. Fallo 3 (Preservacion estricta de ZIPs anteriores):
   - Generacion de ZIPs en ruta temporal unica.
   - Si existe un ZIP definitivo anterior, se preserva con nombre unico con timestamp y contador anti-colision.
     NUNCA se sobreescribe ni se borra.
4. Cobertura obligatoria de raices y archivos externos:
   - Se valida la existencia fisica de todas las raices obligatorias; si alguna falta, el motor aborta y no declara cobertura.
   - Inclusion explicita del respaldo historico en Downloads\\Driver GT 710 Original\\Digital_Optimus_Complete_Project.zip.
"""

import os
import sys
import time
import shutil
import zipfile
import hashlib
import json
import string
import ctypes
import subprocess
import uuid

EXPECTED_KINGSTON_SERIAL_SUBSTR = "0026_B728_3F45_1265"
REQUIRED_MIN_FREE_SPACE_BYTES = 135 * 1024 * 1024 * 1024  # 135 GiB

# Raices y archivos obligatorios de presencia verificada
MANDATORY_ROOTS = [
    r"C:\xampp\htdocs\Digital Optimus",
    r"C:\Users\intel\AppData\Local\OptimusRuntime",
    r"C:\Users\intel\.gemini\antigravity",
    r"C:\xampp\mysql\data",
    r"C:\Users\intel\AR88_Sandbox"
]

MANDATORY_EXPLICIT_FILES = [
    r"C:\Users\intel\Downloads\Driver GT 710 Original\Digital_Optimus_Complete_Project.zip",
    r"C:\Users\intel\Downloads\DIGITAL_OPTIMUS_NORTE_MAESTRO.md.txt",
    r"C:\Users\intel\Downloads\LEEME_INSTRUCCIONES_MIGRACION.md",
    r"C:\Users\intel\Downloads\ORC_CIERRE_OPERATIVO_V1_1_0_2026-10-28.md",
    r"C:\Users\intel\Downloads\Digital_Optimus_Complete_Project.zip.sha256",
    r"C:\Users\intel\Desktop\AR72_CIRCUITO_AUTORREPARACION.txt",
    r"C:\Users\intel\Desktop\LICENSE"
]

def get_verified_target_volume():
    """Valida fisicamente que la letra pertenezca al SSD Kingston con serial y etiqueta esperados."""
    kernel32 = ctypes.windll.kernel32
    volumeNameBuffer = ctypes.create_unicode_buffer(1024)
    fileSystemNameBuffer = ctypes.create_unicode_buffer(1024)
    serial_number = ctypes.c_ulong()
    max_component_length = ctypes.c_ulong()
    file_system_flags = ctypes.c_ulong()

    candidate_drive = None
    for letter in string.ascii_uppercase:
        drive = f"{letter}:\\"
        try:
            res = kernel32.GetVolumeInformationW(
                ctypes.c_wchar_p(drive),
                volumeNameBuffer,
                ctypes.sizeof(volumeNameBuffer),
                ctypes.byref(serial_number),
                ctypes.byref(max_component_length),
                ctypes.byref(file_system_flags),
                fileSystemNameBuffer,
                ctypes.sizeof(fileSystemNameBuffer)
            )
            if res and volumeNameBuffer.value == "TEITAN_RESCATE":
                candidate_drive = drive
                break
        except Exception:
            pass

    if not candidate_drive:
        return None, "No se encontro volumen con etiqueta TEITAN_RESCATE"

    # Verificar mediante CIM que la letra candidata este en el Kingston
    drive_letter = candidate_drive[0]
    ps_cmd = f"Get-Partition -DriveLetter {drive_letter} | 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 None, f"El volumen {candidate_drive} reside en {friendly} (S/N: {serial}), NO en el Kingston esperado."
        else:
            return None, f"No fue posible consultar la procedencia fisica del volumen {candidate_drive}."
    except Exception as e:
        return None, f"Error validando disco fisico destino: {e}"

    # Validar espacio libre en el volumen
    free_bytes = shutil.disk_usage(candidate_drive).free
    if free_bytes < REQUIRED_MIN_FREE_SPACE_BYTES:
        return None, f"Espacio insuficiente en {candidate_drive}: {free_bytes / (1024**3):.2f} GiB libres (requeridos >= 135 GiB)."

    return candidate_drive, "OK"

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 load_checkpoint(checkpoint_file):
    if os.path.exists(checkpoint_file):
        try:
            with open(checkpoint_file, "r", encoding="utf-8") as f:
                return json.load(f)
        except Exception:
            return {}
    return {}

def save_checkpoint(checkpoint_file, checkpoint_data):
    temp_ckpt = f"{checkpoint_file}.tmp_{os.getpid()}"
    with open(temp_ckpt, "w", encoding="utf-8") as f:
        json.dump(checkpoint_data, f, indent=2)
        f.flush()
        os.fsync(f.fileno())
    os.replace(temp_ckpt, checkpoint_file)

def atomic_copy_and_verify(src, dst, checkpoint_data, checkpoint_file):
    """
    Copia atomica ultra-segura con checkpointing persistente:
    - Si ya existe en checkpoint con hash validado y el archivo fisico en Kingston coincide en tamano:
      Calcula hash UNICAMENTE sobre dst (Kingston). Si coincide con el checkpoint:
      RETORNA INMEDIATAMENTE SIN LEER CERO BYTES DE SRC (C:).
    - Si existe un archivo previo no validado, lo preserva antes de promover el nuevo.
    - Escritura en .atomic_tmp con fsync y doble verificacion hash.
    """
    norm_src = os.path.normcase(os.path.normpath(src))

    # 1. Comprobacion de checkpoint confiable (Reanudacion real sin tocar SSD C:)
    if norm_src in checkpoint_data:
        entry = checkpoint_data[norm_src]
        expected_bytes = entry.get("bytes")
        expected_sha = entry.get("sha256")
        if os.path.exists(dst) and os.path.getsize(dst) == expected_bytes and entry.get("status") == "VERIFIED":
            # Verificar integridad unicamente sobre el archivo ya presente en Kingston
            dst_sha = calculate_sha256(dst)
            if dst_sha == expected_sha:
                # NO LEER SRC. Retornar inmediatamente preservando el SSD deteriorado.
                return expected_bytes, dst_sha, "RESUMED_VERIFIED_NO_READ_SOURCE"

    # 2. Si se requiere transferir desde C: (primera copia o archivo no verificado)
    src_size = os.path.getsize(src)
    os.makedirs(os.path.dirname(dst), exist_ok=True)
    temp_dst = f"{dst}.atomic_tmp_{os.getpid()}_{uuid.uuid4().hex}"
    if os.path.exists(temp_dst):
        os.remove(temp_dst)

    sha_src = hashlib.sha256()
    total = 0
    with open(src, "rb") as fsrc, open(temp_dst, "wb") as fdst:
        while chunk := fsrc.read(4 * 1024 * 1024):
            sha_src.update(chunk)
            fdst.write(chunk)
            total += len(chunk)
        fdst.flush()
        os.fsync(fdst.fileno())

    source_hash = sha_src.hexdigest().upper()
    shutil.copystat(src, temp_dst)

    # 3. Comprobacion independiente del archivo escrito en Kingston
    dst_written_hash = calculate_sha256(temp_dst)
    if dst_written_hash != source_hash:
        if os.path.exists(temp_dst):
            os.remove(temp_dst)
        raise IOError(f"Discrepancia de integridad: Hash origen {source_hash} != Hash escrito {dst_written_hash}")

    # 4. Preservacion estricta de archivo previo en Kingston si existe y difiere
    if os.path.exists(dst):
        existing_sha = calculate_sha256(dst)
        if existing_sha != source_hash:
            # Preservar con sufijo unico
            unique_bak = f"{dst}.bak_{int(time.time())}_{uuid.uuid4().hex[:6]}"
            print(f"  [PRESERVACION] Archivo existente difiere. Preservando original en: {unique_bak}")
            os.rename(dst, unique_bak)
        else:
            # Ya era identico
            os.remove(temp_dst)
            checkpoint_data[norm_src] = {"sha256": source_hash, "bytes": total, "status": "VERIFIED", "dst": dst}
            save_checkpoint(checkpoint_file, checkpoint_data)
            return total, source_hash, "EXISTING_IDENTICAL_PRESERVED"

    # 5. Promocion segura
    os.replace(temp_dst, dst)

    # 6. Actualizar checkpoint transaccional
    checkpoint_data[norm_src] = {"sha256": source_hash, "bytes": total, "status": "VERIFIED", "dst": dst}
    save_checkpoint(checkpoint_file, checkpoint_data)

    return total, source_hash, "COPIED_AND_VERIFIED"

def build_master_inventory():
    """Compila el inventario maestro explicito verificando presencia de raices obligatorias."""
    # Validacion estricta de raices obligatorias
    missing_mandatory_roots = [r for r in MANDATORY_ROOTS if not os.path.exists(r)]
    if missing_mandatory_roots:
        raise RuntimeError(f"RAICES OBLIGATORIAS NO ENCONTRADAS: {missing_mandatory_roots}")

    missing_mandatory_files = [f for f in MANDATORY_EXPLICIT_FILES if not os.path.exists(f)]
    if missing_mandatory_files:
        raise RuntimeError(f"ARCHIVOS OBLIGATORIOS NO ENCONTRADOS: {missing_mandatory_files}")

    core_roots = [
        r"C:\xampp\htdocs\Digital Optimus",
        r"C:\Users\intel\AppData\Local\OptimusRuntime",
        r"C:\Users\intel\.gemini\antigravity",
        r"C:\xampp\mysql\data"
    ]
    lab_roots = [
        r"C:\Users\intel\AR88_Sandbox"
    ]

    core_inventory = []
    lab_inventory = []

    for croot in core_roots:
        for root, dirs, files in os.walk(croot):
            for f in files:
                core_inventory.append(os.path.join(root, f))

    for ef in MANDATORY_EXPLICIT_FILES:
        core_inventory.append(ef)

    # Evidencias externas de Desktop y Downloads
    desktop_dir = r"C:\Users\intel\Desktop"
    downloads_dir = r"C:\Users\intel\Downloads"
    if os.path.exists(desktop_dir):
        for f in os.listdir(desktop_dir):
            if f.endswith((".zip", ".txt", ".md", ".json")):
                full = os.path.join(desktop_dir, f)
                if full not in core_inventory:
                    core_inventory.append(full)

    if os.path.exists(downloads_dir):
        for f in os.listdir(downloads_dir):
            if f.startswith("ORC") or f.startswith("AR") or f.startswith("TEITAN_AR"):
                full = os.path.join(downloads_dir, f)
                if os.path.isfile(full) and full not in core_inventory:
                    core_inventory.append(full)

    for lroot in lab_roots:
        for root, dirs, files in os.walk(lroot):
            for f in files:
                # Excluir exclusivamente temporales del propio proceso
                if not f.endswith(".tmp_zip") and not (".atomic_tmp" in f):
                    lab_inventory.append(os.path.join(root, f))

    core_inventory = sorted(list(set(core_inventory)))
    lab_inventory = sorted(list(set(lab_inventory)))
    master_all = sorted(list(set(core_inventory + lab_inventory)))

    return {
        "core": core_inventory,
        "lab": lab_inventory,
        "master_all": master_all,
        "total_files": len(master_all)
    }

def stage_copy_group(file_list, stage_root, checkpoint_data, checkpoint_file, label):
    print(f"\n[{label}] Iniciando transferencia/verificacion hacia Kingston: {len(file_list)} archivos...")
    manifest = []
    errors = []
    total_bytes = 0
    start_time = time.time()
    count = 0

    for fpath in file_list:
        if not os.path.exists(fpath):
            errors.append({"path": fpath, "error": "Archivo no encontrado en origen"})
            continue
        try:
            drive, rel = os.path.splitdrive(os.path.normpath(fpath))
            arcname = rel.lstrip("\\/")
            dst_path = os.path.join(stage_root, arcname)

            bytes_copied, file_sha, status = atomic_copy_and_verify(fpath, dst_path, checkpoint_data, checkpoint_file)
            total_bytes += bytes_copied
            count += 1
            manifest.append({
                "source": fpath,
                "arcname": arcname,
                "staged_path": dst_path,
                "bytes": bytes_copied,
                "sha256": file_sha,
                "status": status
            })

            if count % 2500 == 0 or bytes_copied > 100 * 1024 * 1024:
                elapsed = time.time() - start_time
                mb_s = (total_bytes / (1024*1024)) / elapsed if elapsed > 0 else 0
                print(f"  [{label}] Progreso: {count}/{len(file_list)} ({total_bytes / (1024**3):.2f} GiB, {mb_s:.1f} MB/s) [{status}]...")

        except Exception as e:
            print(f"\n[BLOQUEO CRITICO LECTURA C:] {fpath} -> {e}")
            errors.append({"path": fpath, "error": str(e)})
            break

    elapsed = time.time() - start_time
    print(f"[{label}] Transferencia finalizada: {count} archivos procesados ({total_bytes / (1024**3):.2f} GiB) en {elapsed:.1f}s.")
    return manifest, errors

def preserve_and_create_zip_from_stage(staged_items, target_zip, description):
    """
    Crea ZIP64 leyendo exclusivamente de Kingston:
    - Si target_zip existe, se preserva con un nombre unico con timestamp y contador anti-colision.
    - NUNCA se sobreescribe ni se borra.
    - Se crea en un archivo temporal unico y luego se promueve.
    """
    if os.path.exists(target_zip):
        counter = 1
        ts = time.strftime("%Y%m%d_%H%M%S")
        backup_zip = f"{target_zip}.bak_{ts}_{counter:03d}"
        while os.path.exists(backup_zip):
            counter += 1
            backup_zip = f"{target_zip}.bak_{ts}_{counter:03d}"
        print(f"[PRESERVACION ESTRICTA] Archivo ZIP previo detectado. Preservando copia integra en:\n  -> {backup_zip}")
        os.rename(target_zip, backup_zip)

    temp_zip = f"{target_zip}.tmp_{os.getpid()}_{uuid.uuid4().hex}.zip"

    print(f"\n--- EMPAQUETANDO {description} (ZIP64 SIN CIFRADO) ---")
    print(f"Fuente: Kingston Stage")
    print(f"Destino Temporal: {temp_zip}")
    print(f"Destino Definitivo: {target_zip}")

    total_uncompressed = 0
    start_time = time.time()

    with zipfile.ZipFile(temp_zip, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=True, compresslevel=6) as zf:
        count = 0
        for item in staged_items:
            staged_file = item["staged_path"]
            arcname = item["arcname"]
            fsize = item["bytes"]
            zf.write(staged_file, arcname=arcname)
            total_uncompressed += fsize
            count += 1
            if count % 2500 == 0 or fsize > 100 * 1024 * 1024:
                elapsed = time.time() - start_time
                mb_s = (total_uncompressed / (1024*1024)) / elapsed if elapsed > 0 else 0
                print(f"  Progreso ZIP: {count}/{len(staged_items)} ({total_uncompressed / (1024**3):.2f} GiB, {mb_s:.1f} MB/s)...")

    # Verificacion de integridad CRC sobre el temporal antes de publicarlo
    print(f"Verificando integridad CRC interna en archivo temporal...")
    with zipfile.ZipFile(temp_zip, 'r') as zf:
        bad_file = zf.testzip()
        if bad_file is not None:
            if os.path.exists(temp_zip):
                os.remove(temp_zip)
            raise RuntimeError(f"ERROR INTEGRIDAD CRC: Archivo corrupto en ZIP: {bad_file}")
    print("[PASS] Verificacion CRC 100% integra.")

    # Promocion atomica
    os.replace(temp_zip, target_zip)
    final_size = os.path.getsize(target_zip)

    # SHA-256 definitivo
    print(f"Calculando SHA-256 de {target_zip}...")
    zip_sha = calculate_sha256(target_zip)
    print(f"SHA-256: {zip_sha}")

    return {
        "zip_path": target_zip,
        "files_count": count,
        "uncompressed_bytes": total_uncompressed,
        "compressed_bytes": final_size,
        "compressed_gb": round(final_size / (1024**3), 3),
        "sha256": zip_sha,
        "integrity": "PASS"
    }

def run_rescue_pipeline():
    target_drive, status = get_verified_target_volume()
    if not target_drive:
        print(f"[BLOQUEO DE DESTINO] {status}")
        sys.exit(1)

    print("=" * 65)
    print("  TEITAN — RESCATE INTEGRAL VERIFICADO (TWO-STAGE ZIP64 V3)")
    print(f"  Destino Kingston Validado: {target_drive} ({status})")
    print("=" * 65)

    # 1. Compilar inventario maestro y verificar raices obligatorias
    print("\n[PASO 1] Validando raices obligatorias y compilando Inventario Maestro...")
    inv = build_master_inventory()
    print(f"  Total Nucleo:     {len(inv['core'])} archivos")
    print(f"  Total Laboratorio: {len(inv['lab'])} archivos")
    print(f"  Total Maestro:    {inv['total_files']} archivos")

    rescue_root = os.path.join(target_drive, "TEITAN_RESCUE_OUTPUT")
    stage_root = os.path.join(rescue_root, "STAGED_COPIES")
    os.makedirs(stage_root, exist_ok=True)

    checkpoint_file = os.path.join(rescue_root, "stage_checkpoint.json")
    checkpoint_data = load_checkpoint(checkpoint_file)

    # 2. Transferencia segura del nucleo
    core_manifest, core_errors = stage_copy_group(inv["core"], stage_root, checkpoint_data, checkpoint_file, "NUCLEO_CRITICO")

    if core_errors:
        print("\n[ABORTANDO POR SEGURIDAD] Se detectaron errores de lectura en el nucleo critico.")
        print(f"Errores: {len(core_errors)}. Se preserva lo recuperado y se detienen las lecturas en C:.")
        return False

    # 3. Generar teitan_core.zip leyendo exclusivamente de Kingston
    core_zip_path = os.path.join(rescue_root, "teitan_core.zip")
    core_res = preserve_and_create_zip_from_stage(core_manifest, core_zip_path, "teitan_core.zip (NUCLEO)")

    # 4. Transferencia de Laboratorio AR88
    lab_manifest, lab_errors = stage_copy_group(inv["lab"], stage_root, checkpoint_data, checkpoint_file, "LABORATORIO_AR88")

    all_manifest = core_manifest + lab_manifest
    all_errors = core_errors + lab_errors

    # 5. Reconciliacion estricta de inventario
    expected_set = set(inv["master_all"])
    copied_set = set(item["source"] for item in all_manifest)
    missing_files = list(expected_set - copied_set)

    print(f"\n[RECONCILIACION]")
    print(f"  Esperados:  {len(expected_set)}")
    print(f"  Copiados:   {len(copied_set)}")
    print(f"  Faltantes:  {len(missing_files)}")

    if missing_files or all_errors:
        print("[ALERTA] Respaldo no completo al 100%. Preservando estado sin generar ZIP maestro incompleto.")
        return False

    # 6. Generar teitan.zip maestro leyendo exclusivamente de Kingston
    master_zip_path = os.path.join(rescue_root, "teitan.zip")
    master_res = preserve_and_create_zip_from_stage(all_manifest, master_zip_path, "teitan.zip (MAESTRO COMPLETO)")

    # 7. Resumen e informe consolidado
    summary = {
        "TIMESTAMP": time.strftime("%Y-%m-%d %H:%M:%S"),
        "TEITAN_CORE_ZIP": core_res,
        "TEITAN_MASTER_ZIP": master_res,
        "ARCHIVOS_ESPERADOS": len(expected_set),
        "ARCHIVOS_COPIADOS_Y_VERIFICADOS": len(copied_set),
        "ARCHIVOS_FALTANTES": len(missing_files),
        "TOTAL_ERRORES": len(all_errors),
        "BACKUP_COMPLETED": (len(missing_files) == 0 and len(all_errors) == 0 and master_res["integrity"] == "PASS")
    }

    with open(os.path.join(rescue_root, "RESCUE_AUDIT_REPORT.json"), "w", encoding="utf-8") as f:
        json.dump(summary, f, indent=2)

    print("\n" + "=" * 65)
    print("  INFORME CONSOLIDADO FINAL")
    print("=" * 65)
    print(json.dumps(summary, indent=2))
    return True

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "run":
        run_rescue_pipeline()
    elif len(sys.argv) > 1 and sys.argv[1] == "inventory":
        inv = build_master_inventory()
        print(f"ARCHIVOS_CORE: {len(inv['core'])}")
        print(f"ARCHIVOS_LAB:  {len(inv['lab'])}")
        print(f"ARCHIVOS_TOTAL: {inv['total_files']}")
    else:
        print("Motor preparado. Uso: python teitan_backup_engine.py [run | inventory]")
