r"""TEITAN_SFTP_REPARADO.py - EJECUTOR PRINCIPAL Y BIBLIOTECA DE TRANSFERENCIA SEGURA
Misión: TEITAN-SFTP-REPAIR-02
Cargador SFTP de alta seguridad, reanudación verificada y tasa optimizada.
Ubicación: E:\rescatado\LAB_SFTP_REPAIR_01\TEITAN_SFTP_REPARADO.py
No contiene contraseñas en código fuente ni ejecuta PHP ni conexiones no autorizadas.
"""
import base64
import getpass
import hashlib
import os
import posixpath
import shlex
import sys
import time
from pathlib import Path

try:
    import paramiko
except ImportError:
    paramiko = None

# Parámetros de Configuración por defecto
HOST = "laboptimus.com"
PORT = 22
USER = "laboptimus"
LOCAL_FILE = Path(r"E:\rescatado\proyecto_completo.zip")
PUBLIC_PATH = "/home/laboptimus/public_html/proyecto_completo.zip"
PRIVATE_PATH = "/home/laboptimus/teitan_respaldo_privado/proyecto_completo.zip"

DEFAULT_SEGMENT = 16 * 1024 * 1024   # 16 MiB por segmento de confirmación estricta
DEFAULT_BLOCK = 256 * 1024           # 256 KiB por operación pipelined (óptimo en benchmark)
SAMPLE_SIZE = 64 * 1024              # 64 KiB para muestras de prefijo
MAX_RETRIES = 10
KNOWN_HOSTS_PATH = Path.home() / ".ssh" / "known_hosts"

# Alias para compatibilidad con código legado interno
SEGMENT_SIZE = DEFAULT_SEGMENT
BLOCK_SIZE = DEFAULT_BLOCK
SAMPLE_LEN = SAMPLE_SIZE

class SafetyViolation(Exception):
    """Excepción emitida cuando una regla de seguridad o verificación crítica falla."""
    pass

class StrictHostKeyPolicy(paramiko.MissingHostKeyPolicy if paramiko else object):
    """Política de comprobación estricta de host key SSH con huella SHA-256."""
    def __init__(self, auto_prompt=True):
        self.auto_prompt = auto_prompt

    def missing_host_key(self, client, hostname, key):
        fingerprint = base64.b64encode(hashlib.sha256(key.asbytes()).digest()).decode().rstrip("=")
        key_type = key.get_name()
        print(f"\n=======================================================", flush=True)
        print(f"[ALERTA SSH] Servidor desconocido o huella no registrada:", flush=True)
        print(f"  SERVIDOR: {hostname}", flush=True)
        print(f"  TIPO CLAVE: {key_type}", flush=True)
        print(f"  HUELLA SHA256: {fingerprint}", flush=True)
        print(f"=======================================================", flush=True)
        
        if not self.auto_prompt:
            raise SafetyViolation(f"Host key no aprobada para {hostname}")
            
        resp = input("Escriba 'CONFIAR' para guardar la huella en known_hosts o Enter para abortar: ").strip()
        if resp != "CONFIAR":
            raise SafetyViolation(f"Rechazo de identidad de host: {hostname}")
            
        KNOWN_HOSTS_PATH.parent.mkdir(parents=True, exist_ok=True)
        if client is not None:
            client.get_host_keys().add(hostname, key_type, key)
            client.save_host_keys(str(KNOWN_HOSTS_PATH))
            print(f"[OK] Huella registrada en {KNOWN_HOSTS_PATH}")

def check_local_integrity(local_path=None, snapshot=None):
    """Garantiza que el ZIP original exista y permanezca estable sin modificaciones."""
    target = Path(local_path) if local_path is not None else LOCAL_FILE
    if not target.is_file():
        raise SafetyViolation(f"Archivo original no encontrado o no es archivo regular: {target}")
    st = target.stat()
    if snapshot is not None:
        if st.st_size != snapshot.st_size or st.st_mtime_ns != snapshot.st_mtime_ns:
            raise SafetyViolation("ALERTA CRITICA: El archivo ZIP local cambio de tamano o fecha.")
    return st

# Alias canónico
check_local_file = check_local_integrity

def get_remote_file_size(sftp, remote_path):
    """Obtiene el tamaño remoto manejando correctamente errores ENOENT (archivo inexistente)."""
    try:
        return sftp.stat(remote_path).st_size
    except FileNotFoundError:
        return None
    except Exception as e:
        if getattr(e, "errno", None) == 2:
            return None
        raise

# Alias canónico
get_remote_size = get_remote_file_size

def verify_prefix_samples(sftp, local_path, remote_path, remote_size):
    """Verifica muestras representativas entre local y remoto antes de reanudar."""
    if not remote_size:
        return True
    local_st = check_local_integrity(local_path)
    if remote_size > local_st.st_size:
        raise SafetyViolation(
            f"ERROR: Archivo remoto ({remote_size} B) es MAYOR que el local ({local_st.st_size} B)."
        )
        
    offsets = {
        0,
        max(0, remote_size // 3 - SAMPLE_SIZE // 2),
        max(0, (2 * remote_size) // 3 - SAMPLE_SIZE // 2),
        max(0, remote_size - 1024 * 1024),
        max(0, remote_size - SAMPLE_SIZE)
    }
    
    with open(local_path, "rb") as local_f, sftp.open(remote_path, "rb") as remote_f:
        for offset in sorted(offsets):
            chunk_len = min(SAMPLE_SIZE, remote_size - offset)
            if chunk_len <= 0:
                continue
            local_f.seek(offset)
            remote_f.seek(offset)
            loc_data = local_f.read(chunk_len)
            rem_data = remote_f.read(chunk_len)
            if loc_data != rem_data:
                raise SafetyViolation(
                    f"DISCREPANCIA DETECTADA en offset {offset}. "
                    f"Los bytes remotos no coinciden con el ZIP local. Subida abortada."
                )
    print(f"[VERIFICACION] Muestras coinciden exactamente. Prefijo seguro: {remote_size/1024**3:.3f} GiB", flush=True)
    return True

def verify_samples(sftp, remote_path, r_size):
    """Envoltura de conveniencia orientada a la ruta local por defecto."""
    return verify_prefix_samples(sftp, str(LOCAL_FILE), remote_path, r_size)

def transfer_pipelined_segment(sftp, local_path, remote_path, start_pos, length, block_size=DEFAULT_BLOCK):
    """Transfiere un segmento con control de pipelining y sincronización segura al cerrar."""
    mode = "r+b" if start_pos > 0 else "wb"
    bytes_sent = 0
    with open(local_path, "rb") as src:
        src.seek(start_pos)
        with sftp.open(remote_path, mode) as dst:
            dst.set_pipelined(True)
            dst.seek(start_pos)
            remaining = length
            while remaining > 0:
                chunk = src.read(min(block_size, remaining))
                if not chunk:
                    raise SafetyViolation("Lectura incompleta del archivo local antes de terminar segmento.")
                dst.write(chunk)
                remaining -= len(chunk)
                bytes_sent += len(chunk)
        # dst.close() confirma la recepción de todos los writes pipelined

    actual_remote = get_remote_file_size(sftp, remote_path)
    expected = start_pos + length
    if actual_remote != expected:
        raise SafetyViolation(
            f"Fallo de confirmacion de segmento: esperado {expected} B, encontrado {actual_remote} B"
        )
    return bytes_sent

def transfer_segment(sftp, remote_path, start, length):
    """Envoltura de conveniencia orientada a la ruta local por defecto."""
    return transfer_pipelined_segment(sftp, str(LOCAL_FILE), remote_path, start, length, block_size=BLOCK_SIZE)

def protect_remote_environment(sftp, public_zip, private_zip, public_php, prompt_cb=None):
    """Comprueba el estado de las carpetas pública y privada respetando la integridad del hosting."""
    priv_dir = posixpath.dirname(private_zip)
    try:
        sftp.stat(priv_dir)
    except FileNotFoundError:
        try: sftp.mkdir(priv_dir)
        except Exception: pass
    except Exception as err:
        if getattr(err, "errno", None) == 2:
            try: sftp.mkdir(priv_dir)
            except Exception: pass
        else:
            raise

    pub_size = get_remote_file_size(sftp, public_zip)
    priv_size = get_remote_file_size(sftp, private_zip)
    
    if pub_size is not None and priv_size is not None:
        raise SafetyViolation(
            f"CONFLICTO: El ZIP existe tanto en {public_zip} ({pub_size} B) como en {private_zip} ({priv_size} B). "
            "Debe resolverse manualmente sin sobrescritura."
        )
        
    target_path = private_zip
    current_size = priv_size
    
    if pub_size is not None and priv_size is None:
        print(f"\n=======================================================")
        print(f"[PROTECCION HOSTING] El respaldo parcial ({pub_size/1024**3:.2f} GiB) esta en public_html.")
        print(f"Ruta actual: {public_zip}")
        print(f"Ruta privada recomendada: {private_zip}")
        print("Moverlo a la ruta privada es instantaneo y protege su privacidad.")
        print(f"=======================================================")
        allowed = False
        if prompt_cb is not None:
            allowed = prompt_cb("MOVER")
        else:
            resp = input("Escriba 'MOVER' para trasladar el archivo a la carpeta privada o Enter para dejarlo: ").strip()
            allowed = (resp == "MOVER")
            
        if allowed:
            sftp.rename(public_zip, private_zip)
            target_path = private_zip
            current_size = get_remote_file_size(sftp, private_zip)
            print("[OK] Archivo trasladado exitosamente a ruta privada.")
        else:
            target_path = public_zip
            current_size = pub_size
            print("[AVISO] Continuando en directorio public_html por decision del usuario.")

    return target_path, (current_size or 0)

# Alias canónico
check_hosting_directories = lambda sftp: protect_remote_environment(sftp, PUBLIC_PATH, PRIVATE_PATH, None)

def execute_remote_sha256_if_possible(ssh, remote_path, local_path):
    """Verifica sha256 remoto si el servidor tiene shell SSH disponible."""
    try:
        cmd = f"sha256sum -- {shlex.quote(remote_path)}"
        _, stdout, stderr = ssh.exec_command(cmd, timeout=30)
        stdout.channel.settimeout(600)
        line = stdout.readline().strip()
        status = stdout.channel.recv_exit_status()
        if status != 0 or not line:
            return None, "Servidor sin shell SSH o sin binario sha256sum"
        remote_hash = line.split()[0].lower()
        if len(remote_hash) != 64 or any(c not in "0123456789abcdef" for c in remote_hash):
            return None, "Respuesta invalida de sha256sum remoto"
            
        hasher = hashlib.sha256()
        with open(local_path, "rb") as f:
            for b in iter(lambda: f.read(8 * 1024 * 1024), b""):
                hasher.update(b)
        local_hash = hasher.hexdigest()
        
        if local_hash == remote_hash:
            return True, "SHA-256 Coincide perfectamente"
        else:
            return False, f"Discrepancia SHA-256: Local={local_hash} Remoto={remote_hash}"
    except Exception as e:
        return None, f"No se pudo ejecutar comando remoto: {type(e).__name__} ({e})"

def connect_ssh(password):
    """Establece conexión SSH/SFTP con política estricta y parámetros optimizados."""
    if paramiko is None:
        raise SafetyViolation("Libreria Paramiko no disponible.")
    ssh = paramiko.SSHClient()
    ssh.load_system_host_keys()
    if KNOWN_HOSTS_PATH.exists():
        ssh.load_host_keys(str(KNOWN_HOSTS_PATH))
    ssh.set_missing_host_key_policy(StrictHostKeyPolicy(auto_prompt=True))
    try:
        ssh.connect(
            HOST,
            PORT,
            username=USER,
            password=password,
            look_for_keys=False,
            allow_agent=False,
            timeout=30,
            auth_timeout=30,
            banner_timeout=30
        )
        transport = ssh.get_transport()
        if transport:
            transport.set_keepalive(15)
            transport.use_compression(False)
        sftp = paramiko.SFTPClient.from_transport(
            transport,
            window_size=16 * 1024 * 1024,
            max_packet_size=32768
        )
        sftp.get_channel().settimeout(120)
        return ssh, sftp
    except Exception:
        ssh.close()
        raise

def main():
    print("===============================================================================")
    print("      TEITAN SFTP REPARADO - REANUDACION SEGURA Y OPTIMIZADA                  ")
    print("===============================================================================")
    snapshot = check_local_integrity()
    total_bytes = snapshot.st_size
    total_gib = total_bytes / (1024**3)
    print(f"ARCHIVO LOCAL: {LOCAL_FILE}")
    print(f"TAMANO TOTAL : {total_bytes:,} bytes ({total_gib:.2f} GiB)")
    print("===============================================================================\n")

    password = getpass.getpass("Ingrese contrasena SFTP para laboptimus (no visible): ")
    if not password:
        sys.exit("[ABORTADO] No se proporciono contrasena.")

    retry_count = 0
    confirmed_pos = None
    target_remote_file = None
    session_start_time = time.monotonic()
    total_transferred_this_session = 0

    while True:
        ssh = sftp = None
        try:
            ssh, sftp = connect_ssh(password)
            retry_count = 0
            
            if target_remote_file is None:
                target_remote_file, confirmed_pos = protect_remote_environment(
                    sftp, PUBLIC_PATH, PRIVATE_PATH, None
                )
            else:
                confirmed_pos = get_remote_file_size(sftp, target_remote_file) or 0

            check_local_integrity(snapshot=snapshot)
            if confirmed_pos > total_bytes:
                raise SafetyViolation("Tamano remoto mayor al local. Proceso detenido.")

            verify_prefix_samples(sftp, str(LOCAL_FILE), target_remote_file, confirmed_pos)
            print(f"[ESTADO] Destino: {target_remote_file}")
            print(f"[ESTADO] Reanudando desde: {confirmed_pos/1024**3:.3f} GiB ({confirmed_pos:,} bytes)\n")

            while confirmed_pos < total_bytes:
                check_local_integrity(snapshot=snapshot)
                length = min(DEFAULT_SEGMENT, total_bytes - confirmed_pos)
                seg_start = time.monotonic()
                transfer_pipelined_segment(sftp, str(LOCAL_FILE), target_remote_file, confirmed_pos, length)
                
                confirmed_pos += length
                total_transferred_this_session += length
                seg_dur = max(0.01, time.monotonic() - seg_start)
                overall_dur = max(0.01, time.monotonic() - session_start_time)
                
                inst_speed = (length / (1024 * 1024)) / seg_dur
                avg_speed = (total_transferred_this_session / (1024 * 1024)) / overall_dur
                pct = (confirmed_pos / total_bytes) * 100
                
                print(
                    f"\r[SFTP] {pct:6.2f}% | "
                    f"GIB_TRANSFERIDOS={confirmed_pos/1024**3:6.2f}/{total_gib:6.2f} GiB | "
                    f"VEL_INST={inst_speed:5.2f} MiB/s | "
                    f"VEL_PROM={avg_speed:5.2f} MiB/s | "
                    f"BYTES_CONFIRMADOS={confirmed_pos:,}",
                    end="",
                    flush=True
                )

            print("\n\n[VALIDACION FINAL] Verificando tamano confirmado en el servidor...")
            final_size = get_remote_file_size(sftp, target_remote_file)
            if final_size != total_bytes:
                raise SafetyViolation(f"Tamano final no coincide: Remoto={final_size}, Esperado={total_bytes}")
            
            print(f"[EXITO] TAMANO_REMOTO_PASS={final_size:,} bytes")
            print("===============================================================================")
            print(" [FIN] LA SUBIDA Y CONFIRMACION HAN TERMINADO AL 100%.")
            print(" YA PUEDES COMENZAR A TRABAJAR EN TU HOSTING.")
            print("===============================================================================")
            return

        except (SafetyViolation, paramiko.AuthenticationException if paramiko else SafetyViolation) as e:
            print(f"\n\n[DETENCION DE SEGURIDAD] {type(e).__name__}: {e}", flush=True)
            sys.exit(2)
        except KeyboardInterrupt:
            print("\n\n[DETENIDO POR EL USUARIO] Progreso conservado intacto en el servidor.", flush=True)
            sys.exit(130)
        except Exception as e:
            retry_count += 1
            print(f"\n[INCIDENCIA DE RED] {type(e).__name__}: {e}", flush=True)
            if retry_count > MAX_RETRIES:
                print(f"[LIMITE DE REINTENTOS] Superados {MAX_RETRIES} intentos. Subida pausada.")
                sys.exit(1)
            wait_time = min(5 * retry_count, 30)
            print(f"[REINTENTO {retry_count}/{MAX_RETRIES}] Reconectando en {wait_time}s sin perder bytes...")
            time.sleep(wait_time)
        finally:
            if sftp:
                try: sftp.close()
                except Exception: pass
            if ssh:
                try: ssh.close()
                except Exception: pass

if __name__ == "__main__":
    main()
