"""
TEITAN MASTER RESCUE ENGINE — TWO-STAGE STAGED COMPRESSION (NO ENCRYPTION)
Etapa 1: Copia física directa 1:1 desde SSD en riesgo (C:) a SSD Kingston (E:\TEITAN_STAGE).
         Prioridad: Núcleo crítico primero, luego VHDX/Laboratorios.
         Cálculo de SHA-256 en vuelo al copiar. Solo UNA lectura del SSD antiguo.
Etapa 2: Creación de teitan_core.zip y teitan.zip desde la copia ya segura en Kingston.
         Formato ZIP64 estándar, compresión ZIP_DEFLATED, SIN cifrado ni contraseñas.
Etapa 3: Verificación CRC de los ZIPs, comparación con el manifiesto y cálculo de SHA-256.
"""

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

def get_target_volume():
    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()

    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":
                return drive
        except Exception:
            pass
    return None

def copy_and_hash(src, dst):
    os.makedirs(os.path.dirname(dst), exist_ok=True)
    sha = hashlib.sha256()
    total = 0
    with open(src, "rb") as fsrc, open(dst, "wb") as fdst:
        while chunk := fsrc.read(4 * 1024 * 1024):
            sha.update(chunk)
            fdst.write(chunk)
            total += len(chunk)
    # Preserve metadata timestamps
    shutil.copystat(src, dst)
    return total, sha.hexdigest().upper()

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 get_core_file_list():
    core_dirs = [
        r"C:\xampp\htdocs\Digital Optimus",
        r"C:\Users\intel\AppData\Local\OptimusRuntime",
        r"C:\Users\intel\.gemini\antigravity",
        r"C:\xampp\mysql\data"
    ]
    external_files = [
        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"
    ]
    desktop_dir = r"C:\Users\intel\Desktop"
    downloads_dir = r"C:\Users\intel\Downloads"

    collected = []
    for cdir in core_dirs:
        if os.path.exists(cdir):
            for root, dirs, files in os.walk(cdir):
                for f in files:
                    collected.append(os.path.join(root, f))

    for ef in external_files:
        if os.path.exists(ef):
            collected.append(ef)

    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 collected:
                    collected.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 collected:
                    collected.append(full)

    return sorted(list(set(collected)))

def get_lab_file_list():
    ar88_dir = r"C:\Users\intel\AR88_Sandbox"
    collected = []
    if os.path.exists(ar88_dir):
        for root, dirs, files in os.walk(ar88_dir):
            for f in files:
                if not f.endswith(".tmp_zip"):
                    collected.append(os.path.join(root, f))
    return sorted(list(set(collected)))

def stage_copy_files(file_list, stage_root, label):
    print(f"\n[{label}] Copiando hacia SSD Kingston: {len(file_list)} archivos...")
    manifest = []
    missing_or_errors = []
    total_bytes = 0
    start_time = time.time()
    count = 0

    for fpath in file_list:
        if not os.path.exists(fpath):
            missing_or_errors.append({"path": fpath, "error": "Not Found"})
            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 = copy_and_hash(fpath, dst_path)
            total_bytes += bytes_copied
            count += 1
            manifest.append({
                "source": fpath,
                "arcname": arcname,
                "staged_path": dst_path,
                "bytes": bytes_copied,
                "sha256": file_sha
            })

            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 copiados, {mb_s:.1f} MB/s)...")

        except Exception as e:
            print(f"  [ERROR LECTURA EN SSD C:] {fpath} -> {e}")
            missing_or_errors.append({"path": fpath, "error": str(e)})

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

def build_zip_from_stage(staged_items, target_zip, description):
    temp_zip = target_zip + ".tmp_zip"
    if os.path.exists(temp_zip):
        os.remove(temp_zip)
    if os.path.exists(target_zip):
        os.remove(target_zip)

    print(f"\n--- GENERANDO {description} (ZIP64 SIN CIFRADO) ---")
    print(f"Destino Temporal: {temp_zip}")
    print(f"Destino Final:    {target_zip}")
    print(f"Total a empaquetar: {len(staged_items)} archivos")

    total_uncompressed = 0
    start_time = time.time()

    # Create standard ZIP64 with deflate, NO password / NO AES
    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 procesados, {mb_s:.1f} MB/s)...")

    elapsed = time.time() - start_time
    print(f"Empaquetado finalizado en {elapsed:.1f}s.")

    # Renaming to definitive
    os.rename(temp_zip, target_zip)
    final_size = os.path.getsize(target_zip)
    print(f"[OK] Archivo definitivo: {target_zip}")
    print(f"Tamano comprimido: {final_size} bytes ({final_size / (1024**3):.2f} GiB)")

    print("Verificando integridad CRC interna del ZIP...")
    with zipfile.ZipFile(target_zip, 'r') as zf:
        first_bad = zf.testzip()
        if first_bad is not None:
            raise RuntimeError(f"Fallo de integridad CRC en archivo: {first_bad}")
    print("[PASS] Integridad CRC 100% verificada sin errores.")

    print("Calculando SHA-256 definitivo...")
    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_pipeline():
    target_drive = get_target_volume()
    if not target_drive:
        print("[BLOQUEO] No se encontro la unidad 'TEITAN_RESCATE'.")
        print("Ejecute primero PREPARAR_SSD_KINGSTON_ADMIN.ps1 como Administrador.")
        sys.exit(1)

    print("=" * 65)
    print("  TEITAN — PIPELINE DE RESCATE INTEGRAL (STAGE + ZIP64)")
    print(f"  Unidad Kingston: {target_drive}")
    print("=" * 65)

    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)

    # 1. Copiar primero el núcleo crítico (lectura única del SSD C:)
    core_files = get_core_file_list()
    core_manifest, core_errors = stage_copy_files(core_files, stage_root, "NUCLEO_CRITICO")

    # Guardar manifiesto de núcleo
    with open(os.path.join(rescue_root, "core_manifest.json"), "w", encoding="utf-8") as f:
        json.dump({"manifest": core_manifest, "errors": core_errors}, f, indent=2)

    # Si hay errores críticos en el núcleo, detener
    if core_errors:
        print(f"[ALERTA] Errores encontrados en núcleo: {len(core_errors)}")

    # 2. Generar teitan_core.zip DESDE EL STAGE DE KINGSTON (cero lecturas a C:)
    core_zip_path = os.path.join(rescue_root, "teitan_core.zip")
    core_res = build_zip_from_stage(core_manifest, core_zip_path, "teitan_core.zip")

    # 3. Copiar Laboratorio AR88 (VHDXs, etc.) al stage
    lab_files = get_lab_file_list()
    lab_manifest, lab_errors = stage_copy_files(lab_files, stage_root, "LAB_AR88")

    all_manifest = core_manifest + lab_manifest
    all_errors = core_errors + lab_errors

    # Guardar manifiesto maestro
    with open(os.path.join(rescue_root, "master_manifest.json"), "w", encoding="utf-8") as f:
        json.dump({"manifest": all_manifest, "errors": all_errors}, f, indent=2)

    # 4. Generar teitan.zip maestro DESDE EL STAGE DE KINGSTON (cero lecturas a C:)
    master_zip_path = os.path.join(rescue_root, "teitan.zip")
    master_res = build_zip_from_stage(all_manifest, master_zip_path, "teitan.zip")

    # 5. Reporte Consolidado
    summary = {
        "TIMESTAMP": time.strftime("%Y-%m-%d %H:%M:%S"),
        "TEITAN_CORE_ZIP": core_res,
        "TEITAN_MASTER_ZIP": master_res,
        "TOTAL_ERRORS_OR_MISSING": len(all_errors),
        "ERRORS_LIST": all_errors,
        "BACKUP_COMPLETED": (len(all_errors) == 0 and core_res["integrity"] == "PASS" and master_res["integrity"] == "PASS")
    }

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

    print("\n" + "=" * 65)
    print("  INFORME CONSOLIDADO DEL RESCATE")
    print("=" * 65)
    print(json.dumps(summary, indent=2))

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "run":
        run_pipeline()
    else:
        print("Motor preparado. Uso: python teitan_backup_engine.py run")
