"""
AgStreamConcatenator (Massive Binary Joiner)
============================================
Piping binario para unir inmensos mares de datos (Local o AWS S3)
con complejidad de memoria constante O(1).
Fuerza descargas y escrituras en Stream usando un "Buffer Fijo" de red (Ej. 16MB o 64MB)
para impedir problemas en la RAM. Soporta evasión inteligente de "Headers" en CSVs.
"""
import logging
import os
from urllib.parse import urlparse

logger = logging.getLogger(__name__)

class AgStreamConcatenator:
    """
    Concatenador masivo independiente.
    Útil cuando debes unir 500 archivos Parquet o CSV y Spark es demasiado costoso.
    """
    
    def __init__(self, s3_client=None):
        if s3_client is None:
            import boto3
            self.s3_client = boto3.client('s3')
        else:
            self.s3_client = s3_client
            
    def _parse_s3(self, s3_uri: str):
        u = urlparse(s3_uri)
        return u.netloc, u.path.lstrip('/')

    def concat_local(self, input_paths: list[str], output_path: str, has_header: bool = True, chunk_size_mb: int = 64):
        """
        Une archivos gigantes de disco a disco usando un límite rígido de memoria RAM.
        :param input_paths: Lista plana de ubicaciones locales.
        :param output_path: Archivo resultante monolítico.
        :param has_header: Verdadero asume que TODOS los archivos tienen un header (1ra línea). Se conserva el primer archivo, se salta en el resto.
        """
        if not input_paths:
            logger.warning("No hay archivos para procesar.")
            return

        chunk_size_bytes = chunk_size_mb * 1024 * 1024
        logger.info(f"💾 Iniciando Piping Binario Local -> RAM Máxima operativa: {chunk_size_mb} MB")

        # Modo Append Binario, si el archivo existe lo sobreescribe
        with open(output_path, 'wb') as f_out:
            for i, in_file in enumerate(input_paths):
                logger.info(f"   Volcando: {in_file}")
                
                if not os.path.exists(in_file):
                    logger.error(f"   [!] Archivo local inexistente, omitiendo: {in_file}")
                    continue

                with open(in_file, 'rb') as f_in:
                    # Lógica de evasión de Header para el archivo 2 en adelante
                    if has_header and i > 0:
                        _ = f_in.readline()  # Avanza el puntero ignorando la primera línea
                        
                    # Transferencia masiva mediante piping
                    while True:
                        chunk = f_in.read(chunk_size_bytes)
                        if not chunk:
                            break # Fin del archivo
                        f_out.write(chunk)
                        
        logger.info(f"✅ Fusión Local Completa. Destino: {output_path}")

    def concat_s3(self, input_s3_uris: list[str], output_s3_uri: str, has_header: bool = True, custom_part_size_mb: int = 16):
        """
        Lee como stream desde múltiples rutas de S3, salta headers redundantes, 
        y envía como Stream a otra ruta S3 usando Boto3 Multipart Uploads.
        Garantiza que la memoria O(1) fluya a través del servidor sin persistir archivos en disco local.
        """
        if not input_s3_uris:
            return

        dest_bucket, dest_key = self._parse_s3(output_s3_uri)
        chunk_size_bytes = custom_part_size_mb * 1024 * 1024
        
        logger.info(f"☁️ Iniciando Streaming Multipart S3 a S3 -> {output_s3_uri}")
        
        # 1. Iniciar Multipart Upload
        multipart = self.s3_client.create_multipart_upload(Bucket=dest_bucket, Key=dest_key)
        upload_id = multipart['UploadId']
        uploaded_parts = []
        part_number = 1
        
        current_buffer = bytearray()
        
        try:
            for i, in_uri in enumerate(input_s3_uris):
                src_bucket, src_key = self._parse_s3(in_uri)
                logger.info(f"   Stream-Reading: {in_uri}")
                
                # Obtener el stream del objeto
                response = self.s3_client.get_object(Bucket=src_bucket, Key=src_key)
                body_stream = response['Body']
                
                # Saltarse el header si es requerido
                if has_header and i > 0:
                    body_stream.readline() # Descartar primera línea
                
                # Leer en bloques y llenar Buffer Maestro
                for chunk in body_stream.iter_chunks(chunk_size=chunk_size_bytes):
                    current_buffer.extend(chunk)
                    
                    # Si el buffer supera los MB, empaquetar y subir Parte al Multipart API (minimo 5MB para S3)
                    if len(current_buffer) >= chunk_size_bytes:
                        part_resp = self.s3_client.upload_part(
                            Bucket=dest_bucket,
                            Key=dest_key,
                            PartNumber=part_number,
                            UploadId=upload_id,
                            Body=bytes(current_buffer)
                        )
                        uploaded_parts.append({'PartNumber': part_number, 'ETag': part_resp['ETag']})
                        part_number += 1
                        current_buffer.clear() # Liberar memoria
        
            # Escribir el residuo restante si existe
            if len(current_buffer) > 0:
                part_resp = self.s3_client.upload_part(
                    Bucket=dest_bucket,
                    Key=dest_key,
                    PartNumber=part_number,
                    UploadId=upload_id,
                    Body=bytes(current_buffer)
                )
                uploaded_parts.append({'PartNumber': part_number, 'ETag': part_resp['ETag']})
                current_buffer.clear()
            
            # Completar ensamblaje remoto en AWS
            self.s3_client.complete_multipart_upload(
                Bucket=dest_bucket,
                Key=dest_key,
                UploadId=upload_id,
                MultipartUpload={'Parts': uploaded_parts}
            )
            logger.info("✅ Fusión de Red S3 Completada.")
            
        except Exception as e:
            logger.error(f"Falla crítica en Piping de red S3. Abortando Multipart Upload. Error: {e}")
            self.s3_client.abort_multipart_upload(Bucket=dest_bucket, Key=dest_key, UploadId=upload_id)
            raise
