from translator.utils import *
import ipaddress
import json
import re


def checkIn(data):
    # Retrieve UUID
    uuid = data[:36]
    data = data[36:]

    # Retrieve IPs
    numIPs = int.from_bytes(data[0:4])
    data = data[4:]
    i = 0
    IPs = []
    while i < numIPs:
        ip = data[:4]
        data = data[4:]
        addr = str(ipaddress.ip_address(ip))
        IPs.append(addr)
        i += 1

    # Retrieve OS
    targetOS, data = getBytesWithSize(data)

    # Retrieve Architecture
    archOS = data[0]
    if archOS == 0x64:
        archOS = "x64"
    elif archOS == 0x86:
        archOS = "x86"
    else:
        archOS = ""
    data = data[1:]

    # Retrieve HostName
    hostname, data = getBytesWithSize(data)

    # Retrieve Username
    username, data = getBytesWithSize(data)

    # Retrieve Domain
    domain, data = getBytesWithSize(data)

    # Retrieve PID
    pid = int.from_bytes(data[0:4])
    data = data[4:]

    # Retrieve Process Name
    processName, data = getBytesWithSize(data)

    # Retrieve External IP
    externalIP, data = getBytesWithSize(data)

    # Parse callback context from remaining data if present
    # Format: [0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]...
    print(f"[CHECKIN] Datos restantes después de parsear campos estándar: {len(data)} bytes")
    if len(data) > 0:
        print(f"[CHECKIN] Primeros bytes de datos restantes: {data[:min(20, len(data))].hex()}")
    
    callback_context = {}
    while len(data) >= 5:  # At least: marker (1) + name_len (4)
        if data[0] == 0xF0:
            # Callback context marker found
            offset = 1
            
            # Extract field name
            if len(data) < offset + 4:
                break
            name_len = int.from_bytes(data[offset:offset+4], 'big')
            offset += 4
            
            if name_len == 0 or len(data) < offset + name_len:
                break
            
            try:
                field_name = data[offset:offset+name_len].decode('cp850', errors='ignore')
                offset += name_len
            except:
                break
            
            # Extract field value
            if len(data) < offset + 4:
                break
            value_len = int.from_bytes(data[offset:offset+4], 'big')
            offset += 4
            
            field_value = ""
            if value_len > 0:
                if len(data) < offset + value_len:
                    break
                try:
                    field_value = data[offset:offset+value_len].decode('cp850', errors='ignore')
                    offset += value_len
                except:
                    field_value = ""
            
            callback_context[field_name] = field_value
            print(f"[CHECKIN] Callback context: {field_name} = '{field_value}'")
            data = data[offset:]
        else:
            # No more callback context markers
            break

    dataJson = {
        "action": "checkin",
        "ips": IPs,
        "os": targetOS.decode('cp850'),
        "user": username.decode('cp850'),
        "host": hostname.decode('cp850'),
        "domain": domain.decode('UTF-16LE'),
        "process_name": processName.decode('cp850'),
        "pid": pid,
        "uuid": uuid.decode('cp850'),
        "architecture": archOS,
        "externalIP": externalIP.decode('cp850'),
    }
    
    # Add callback context if present
    # For checkin, callback context is added directly to the root message (not in a "callback" key)
    # This matches Mythic's documentation: callback fields can be at root level for checkin
    if callback_context:
        for key, value in callback_context.items():
            dataJson[key] = value
            print(f"[CHECKIN] Añadido campo '{key}' = '{value}' al JSON de checkin")
        print(f"[CHECKIN] Total callback context añadido: {len(callback_context)} campos: {list(callback_context.keys())}")
    else:
        print(f"[CHECKIN] No se encontró callback context en el mensaje de checkin")
    
    # Debug: Print final JSON structure
    print(f"[CHECKIN] JSON final tiene {len(dataJson)} campos: {list(dataJson.keys())}")
    
    return dataJson


def getTasking(data):
    numTasks = int.from_bytes(data[0:4])
    dataJson = {"action": "get_tasking", "tasking_size": numTasks}
    return dataJson


def create_artifact(base_artifact, artifact_value, needs_cleanup=False, resolved=False):
    """
    Helper function to create an artifact entry according to Mythic documentation.
    
    Args:
        base_artifact: Type of artifact (e.g., "Process Create", "File Write")
        artifact_value: The actual artifact (e.g., "cmd.exe /c whoami", "/path/to/file")
        needs_cleanup: Whether this artifact needs cleanup later
        resolved: Whether this artifact has been cleaned up
    
    Returns:
        Dictionary representing an artifact entry
    """
    return {
        "base_artifact": base_artifact,
        "artifact": artifact_value,
        "needs_cleanup": needs_cleanup,
        "resolved": resolved
    }


def parse_process_list(output_text):
    """
    Parsea el texto tab-separated de procesos al formato JSON del Process Browser.
    Formato esperado del agente (como Xenon): nombre\\tPPID\\tPID\\tarch\\tuser\\tsession\\n
    Formato requerido por Mythic Process Browser según docs:
    https://docs.mythic-c2.net/customizing/hooking-features/process_list
    """
    processes = []
    lines = output_text.strip().split('\n')
    
    for line in lines:
        line = line.strip()
        if not line:
            continue
            
        parts = line.split('\t')
        if len(parts) < 3:
            continue  # Necesitamos al menos nombre, PPID, PID
            
        try:
            # Campos requeridos según documentación de Mythic
            process_id = int(parts[2]) if len(parts) > 2 and parts[2].strip().isdigit() else None
            if process_id is None:
                continue  # Saltar si no hay PID válido
                
            process = {
                "process_id": process_id,
                "name": parts[0] if len(parts) > 0 and parts[0].strip() else "",
            }
            
            # Si el nombre está vacío o es inválido, saltar este proceso
            if not process["name"]:
                continue
            
            # Campos opcionales según documentación
            if len(parts) > 1 and parts[1].strip().isdigit():
                process["parent_process_id"] = int(parts[1])
                
            if len(parts) > 3 and parts[3].strip():
                process["architecture"] = parts[3].strip()
            
            if len(parts) > 4 and parts[4].strip():
                process["user"] = parts[4].strip()
            
            # Session ID se guarda como metadata
            if len(parts) > 5 and parts[5].strip().isdigit():
                process["session_id"] = int(parts[5])
            
            # Nota: No incluimos command_line como Xenon para mantener simplicidad
            # Si se necesita en el futuro, se puede agregar lectura de PEB
                    
            processes.append(process)
        except Exception as e:
            print(f"[ERROR] Error parseando línea de proceso: {line}, error: {e}")
            continue
    
    return processes


def parse_file_browser_list(output_text):
    """
    Parsea el texto tab-separated de lista de archivos al formato JSON del File Browser.
    Formato esperado del agente: path\nD/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME\n...
    Formato requerido por Mythic File Browser según docs:
    https://docs.mythic-c2.net/customizing/hooking-features/file-browser
    """
    import os
    from datetime import datetime
    import re
    
    lines = [l.strip() for l in output_text.strip().split('\n') if l.strip()]
    if len(lines) < 2:
        return None
    
    try:
        # Primera línea es el path del directorio
        dir_path = lines[0].strip()
        
        # Normalize path
        if dir_path.endswith('\\*'):
            dir_path = dir_path[:-2]
        if dir_path.endswith('*'):
            dir_path = dir_path[:-1]
        dir_path = dir_path.rstrip('\\/')
        
        # Determine parent path
        if dir_path.endswith(':\\') or dir_path == '':
            parent_path = ""
        else:
            parent_path = os.path.dirname(dir_path) if os.path.dirname(dir_path) else ""
        
        # Determine name
        name = os.path.basename(dir_path) if dir_path else ""
        if not name:
            if len(dir_path) == 2 and dir_path[1] == ':':
                name = dir_path[0] + ':'
                parent_path = ""
            else:
                name = dir_path
        
        # Parse file/directory entries
        # Format: D/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME
        files = []
        for line in lines[1:]:
            parts = line.split('\t')
            if len(parts) >= 4:
                file_type = parts[0].strip()  # D or F
                size_str = parts[1].strip()
                datetime_str = parts[2].strip()
                file_name = parts[3].strip() if len(parts) > 3 else ""
                
                # Skip empty or invalid entries
                if not file_type or not file_name:
                    continue
                
                # Skip . and ..
                if file_name in ['.', '..']:
                    continue
                
                try:
                    # Parse date: MM/DD/YYYY HH:MM:SS
                    date_obj = datetime.strptime(datetime_str, "%m/%d/%Y %H:%M:%S")
                    modify_time_ms = int(date_obj.timestamp() * 1000)
                    access_time_ms = modify_time_ms
                    
                    # Filter out invalid dates (before 1980 or after year 2100)
                    # This helps filter out files with corrupted timestamps (e.g., 1970 epoch issues)
                    if modify_time_ms < 315532800000 or modify_time_ms > 4102444800000:  # Before 1980-01-01 or after 2100-01-01
                        print(f"[FILE_BROWSER] Skipping file with invalid date: {file_name} (timestamp: {modify_time_ms})")
                        continue
                except:
                    modify_time_ms = 0
                    access_time_ms = 0
                    # Skip files with unparseable dates
                    print(f"[FILE_BROWSER] Skipping file with unparseable date: {file_name}")
                    continue
                
                try:
                    # Parse size - remove any non-digit characters
                    size_str_clean = re.sub(r'[^\d]', '', size_str)
                    if size_str_clean:
                        size = int(size_str_clean) if file_type == 'F' else 0
                    else:
                        size = 0
                except:
                    size = 0
                
                permissions = {
                    "attributes": "DIRECTORY" if file_type == 'D' else "FILE"
                }
                
                files.append({
                    "is_file": file_type == 'F',
                    "permissions": permissions,
                    "name": file_name,
                    "access_time": access_time_ms,
                    "modify_time": modify_time_ms,
                    "size": size
                })
        
        # Build file_browser response
        # Remove any duplicate files by name to ensure clean response
        seen_files = {}
        unique_files = []
        for f in files:
            file_name = f.get("name", "")
            # Only add if we haven't seen this file name before
            if file_name and file_name not in seen_files:
                seen_files[file_name] = True
                unique_files.append(f)
        
        # Use current timestamp for modify_time to help Mythic identify fresh responses
        import time
        current_timestamp_ms = int(time.time() * 1000)
        
        file_browser_data = {
            "host": "",  # Will be filled by Mythic based on callback
            "is_file": False,
            "permissions": {"attributes": "DIRECTORY"},
            "name": name,
            "parent_path": parent_path,
            "success": True,
            "access_time": current_timestamp_ms,  # Use current time to help with cache invalidation
            "modify_time": current_timestamp_ms,  # Use current time to help with cache invalidation
            "size": 0,
            "update_deleted": True,  # Tell Mythic this is the complete list - mark missing files as deleted
            "files": unique_files  # Use deduplicated files list - complete and fresh
        }
        
        print(f"[FILE_BROWSER] Sending fresh listing for '{name}' with {len(unique_files)} unique files (timestamp: {current_timestamp_ms}, update_deleted=True)")
        
        return file_browser_data
    except Exception as e:
        print(f"[ERROR] Error parseando lista de archivos: {e}")
        import traceback
        traceback.print_exc()
        return None


def postResponse(data):
    print(f"Tamaño del Base64 recibido: {len(data)} bytes")
    # Removed hex dump - use debug logging if needed
    print(f"[DEBUG] Primeros 100 bytes (repr): {repr(data[:100])}")

    resTaks = []
    
    # The format after removing POST_RESPONSE byte should be: [UUID tarea (36)][output_len (4)][output][...]
    # Try to detect UUID by looking for hyphen pattern at positions 8, 13, 18, 23
    uuidTask = None
    offset = 0
    
    # ALWAYS try to find UUID first (before checking for length)
    # UUID has format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 bytes)
    # UUID pattern is very distinctive: 8 hex chars, dash, 4 hex, dash, 4 hex, dash, 4 hex, dash, 12 hex
    # If we find a valid UUID pattern, it takes precedence over length interpretation
    # Try at offset 0 first
    print(f"[DEBUG] Buscando UUID en data de {len(data)} bytes")
    print(f"[DEBUG] Primeros 72 bytes (hex): {data[:72].hex() if len(data) >= 72 else data.hex()}")
    print(f"[DEBUG] Primeros 72 bytes (repr): {repr(data[:72]) if len(data) >= 72 else repr(data)}")
    
    if len(data) >= 36:
        potential_uuid = data[0:36]
        try:
            uuid_str = potential_uuid.decode('ascii', errors='replace')
            print(f"[DEBUG] Intentando UUID en offset 0: '{uuid_str}'")
            # Check if it looks like a UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
            # UUID pattern: 8 hex, dash, 4 hex, dash, 4 hex, dash, 4 hex, dash, 12 hex
            # Also check that the hyphens are at the correct positions
            if (len(uuid_str) == 36 and 
                uuid_str[8] == '-' and uuid_str[13] == '-' and 
                uuid_str[18] == '-' and uuid_str[23] == '-' and
                all(c in '0123456789abcdefABCDEF-' for c in uuid_str)):
                # Additional validation: ensure we have hex digits before each dash
                is_valid_uuid = (
                    all(c in '0123456789abcdefABCDEF' for c in uuid_str[0:8]) and
                    all(c in '0123456789abcdefABCDEF' for c in uuid_str[9:13]) and
                    all(c in '0123456789abcdefABCDEF' for c in uuid_str[14:18]) and
                    all(c in '0123456789abcdefABCDEF' for c in uuid_str[19:23]) and
                    all(c in '0123456789abcdefABCDEF' for c in uuid_str[24:36])
                )
                if is_valid_uuid:
                    # If it matches UUID pattern, trust it (UUID pattern is more reliable than length guess)
                    # UUIDs can have their first 4 bytes interpreted as any int value, so we can't use that to filter
                    uuidTask = potential_uuid
                    offset = 36
                    print(f"[DEBUG] UUID de tarea detectado en offset 0: {uuid_str}")
        except Exception as e:
            print(f"[DEBUG] Error al validar UUID en offset 0: {e}")
            pass
    
    # If not found at offset 0, try at offset 36 (might be UUID agente at offset 0)
    if not uuidTask and len(data) >= 72:
        potential_uuid = data[36:72]
        try:
            uuid_str = potential_uuid.decode('ascii', errors='replace')
            print(f"[DEBUG] Intentando UUID en offset 36: '{uuid_str}'")
            if (len(uuid_str) == 36 and uuid_str[8] == '-' and uuid_str[13] == '-' and 
                uuid_str[18] == '-' and uuid_str[23] == '-' and
                all(c in '0123456789abcdefABCDEF-' for c in uuid_str)):
                # Additional validation
                is_valid_uuid = (
                    all(c in '0123456789abcdefABCDEF' for c in uuid_str[0:8]) and
                    all(c in '0123456789abcdefABCDEF' for c in uuid_str[9:13]) and
                    all(c in '0123456789abcdefABCDEF' for c in uuid_str[14:18]) and
                    all(c in '0123456789abcdefABCDEF' for c in uuid_str[19:23]) and
                    all(c in '0123456789abcdefABCDEF' for c in uuid_str[24:36])
                )
                if is_valid_uuid:
                    uuidTask = potential_uuid
                    offset = 72
                    print(f"[DEBUG] UUID de tarea detectado en offset 36: {uuid_str}")
        except Exception as e:
            print(f"[DEBUG] Error al validar UUID en offset 36: {e}")
            pass
    
    # If still not found, check if data starts with a reasonable length
    # In this case, UUID might be missing (protocol issue)
    if not uuidTask and len(data) >= 4:
        potential_len = int.from_bytes(data[0:4], 'big')
        print(f"[DEBUG] Primeros 4 bytes como int: {potential_len}")
        
        # If potential_len looks reasonable (< 10MB), data might start directly with output_len
        if potential_len < 10 * 1024 * 1024 and potential_len > 0:
            print(f"[DEBUG] Data parece empezar con output_len ({potential_len}), UUID probablemente no está aquí")
            print("[WARN] UUID de tarea no está en el paquete - problema de protocolo")
            uuidTask = b"00000000-0000-0000-0000-000000000000"
            offset = 0
    
    if not uuidTask:
        print("[WARN] No se pudo encontrar UUID de tarea válido, usando placeholder")
        uuidTask = b"00000000-0000-0000-0000-000000000000"
        offset = 0
    
    data = data[offset:]

    # Extract output using getBytesWithSize
    # Format after UUID: [output_len(4)][output][marcadores...]
    # Even if output is empty, output_len will be present as 4 bytes (0x00000000)
    output = b""
    remaining_data = data
    
    if len(data) >= 4:
        # Always read output_len (4 bytes) and then output
        output, remaining_data = getBytesWithSize(data)
        print(f"[PARSER] Output extraído: {len(output)} bytes, remaining: {len(remaining_data)} bytes")
        
        # Verify that remaining_data starts with a marker if we expect markers
        # This helps detect protocol issues
        if len(remaining_data) > 0:
            KNOWN_MARKERS = {0xFF, 0xFE, 0xFD, 0xFC, 0xF9, 0xF8, 0xF7, 0xF6, 0xF4, 0xF3, 0xF2, 0xF0}
            if remaining_data[0] not in KNOWN_MARKERS and len(remaining_data) > 0:
                # This might be part of the output that wasn't properly extracted
                # This could happen if output_len was incorrectly interpreted
                print(f"[WARN] remaining_data no empieza con marcador conocido (0x{remaining_data[0]:02X})")
    else:
        # Not enough data for output_len
        print(f"[PARSER] Datos insuficientes después del UUID ({len(data)} bytes)")
        output = b""
        remaining_data = data
    
    print(f"Tamaño después de extraer UUID y output: {len(remaining_data)} bytes")
    print(f"Tamaño del output extraído: {len(output)} bytes")
    
    # Parse artifacts, credentials, and downloads
    # Artifacts format: [0xFF][type_len:4][type][value_len:4][value]
    # Credentials format: [0xFE][type_len:4][type][realm_len:4][realm][credential_len:4][credential][account_len:4][account]
    # Download registration: [0xFD][action:0][full_path_len:4][full_path][total_chunks:4][chunk_size:4]
    # Download chunk: [0xFC][action:1][file_id_len:4][file_id][chunk_num:4][chunk_data_len:4][chunk_data_base64]
    # Commands: [0xF7][action_len:4][action][cmd_len:4][cmd]
    artifacts_data = []
    credentials_data = []
    download_registration = None
    download_chunk = None
    upload_request = None
    commands_data = []
    tokens_data = []
    callback_tokens_data = []
    keylogs_data = []
    callback_context_data = {}  # Dictionary to store callback context fields (cwd, impersonation_context, etc.)
    
    print(f"[PARSER] Iniciando parsing de marcadores. remaining_data length: {len(remaining_data)} bytes")
    if len(remaining_data) > 0:
        print(f"[PARSER] Primer byte de remaining_data: 0x{remaining_data[0]:02X}")
    
    while len(remaining_data) >= 5:  # At least: marker (1) + type_len (4)
        if remaining_data[0] == 0xFF:
            # Artifact marker found
            offset = 1
            
            # Extract artifact type
            if len(remaining_data) < offset + 4:
                break
            type_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if type_len == 0 or len(remaining_data) < offset + type_len:
                break
            
            try:
                artifact_type = remaining_data[offset:offset+type_len].decode('cp850')
                offset += type_len
            except:
                break
            
            # Extract artifact value
            if len(remaining_data) < offset + 4:
                break
            value_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if value_len == 0 or len(remaining_data) < offset + value_len:
                break
            
            try:
                artifact_value = remaining_data[offset:offset+value_len].decode('cp850')
                offset += value_len
                
                artifacts_data.append({
                    "type": artifact_type,
                    "value": artifact_value
                })
                print(f"[ARTIFACTS] Detectado artifact: {artifact_type} = {artifact_value[:50]}")
                
                # Continue parsing for multiple artifacts
                remaining_data = remaining_data[offset:]
            except:
                break
        elif remaining_data[0] == 0xFE:
            # Credential marker found
            offset = 1
            
            # Extract credential type
            if len(remaining_data) < offset + 4:
                break
            type_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if type_len == 0 or len(remaining_data) < offset + type_len:
                break
            
            try:
                credential_type = remaining_data[offset:offset+type_len].decode('cp850')
                offset += type_len
            except:
                break
            
            # Extract realm
            if len(remaining_data) < offset + 4:
                break
            realm_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if realm_len > 0:
                if len(remaining_data) < offset + realm_len:
                    break
                try:
                    realm = remaining_data[offset:offset+realm_len].decode('cp850')
                    offset += realm_len
                except:
                    break
            else:
                realm = ""
            
            # Extract credential
            if len(remaining_data) < offset + 4:
                break
            credential_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if credential_len == 0 or len(remaining_data) < offset + credential_len:
                break
            
            try:
                credential = remaining_data[offset:offset+credential_len].decode('cp850')
                offset += credential_len
            except:
                break
            
            # Extract account
            if len(remaining_data) < offset + 4:
                break
            account_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if account_len == 0 or len(remaining_data) < offset + account_len:
                break
            
            try:
                account = remaining_data[offset:offset+account_len].decode('cp850')
                offset += account_len
                
                credentials_data.append({
                    "credential_type": credential_type,
                    "realm": realm,
                    "credential": credential,
                    "account": account
                })
                print(f"[CREDENTIALS] Detectado credential: {credential_type} para {account}@{realm if realm else '(local)'}")
                
                # Continue parsing for multiple credentials
                remaining_data = remaining_data[offset:]
            except:
                break
        elif remaining_data[0] == 0xFD:
            # Download registration marker found
            # Format: [0xFD][action:0][full_path_len:4][full_path][total_chunks:4][chunk_size:4]
            offset = 1
            
            # Skip action byte (should be 0 for registration)
            if len(remaining_data) < offset + 1:
                break
            action = remaining_data[offset]
            offset += 1
            
            if action != 0:
                break  # Not a registration message
            
            # Extract full_path
            if len(remaining_data) < offset + 4:
                break
            path_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if path_len == 0 or len(remaining_data) < offset + path_len:
                break
            
            try:
                full_path = remaining_data[offset:offset+path_len].decode('cp850')
                offset += path_len
            except:
                break
            
            # Extract total_chunks
            if len(remaining_data) < offset + 4:
                break
            total_chunks = int.from_bytes(remaining_data[offset:offset+4], 'big', signed=True)
            offset += 4
            
            # Extract chunk_size
            if len(remaining_data) < offset + 4:
                break
            chunk_size = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            # Store download registration info
            # If full_path is "SCREENSHOT", this is a screenshot, not a file download
            is_screenshot = (full_path == "SCREENSHOT")
            download_registration = {
                "full_path": full_path,
                "total_chunks": total_chunks,
                "chunk_size": chunk_size,
                "is_screenshot": is_screenshot
            }
            if is_screenshot:
                print(f"[SCREENSHOT] Registro detectado: chunks={total_chunks}, chunk_size={chunk_size}")
            else:
                print(f"[DOWNLOAD] Registro detectado: path={full_path}, chunks={total_chunks}, chunk_size={chunk_size}")
            
            remaining_data = remaining_data[offset:]
        elif remaining_data[0] == 0xFC:
            # Download chunk marker found
            # Format: [0xFC][action:1][file_id_len:4][file_id][chunk_num:4][chunk_data_len:4][chunk_data_base64]
            offset = 1
            
            # Skip action byte (should be 1 for chunk)
            if len(remaining_data) < offset + 1:
                break
            action = remaining_data[offset]
            offset += 1
            
            if action != 1:
                break  # Not a chunk message
            
            # Extract file_id
            if len(remaining_data) < offset + 4:
                break
            file_id_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if file_id_len == 0 or len(remaining_data) < offset + file_id_len:
                break
            
            try:
                file_id = remaining_data[offset:offset+file_id_len].decode('cp850')
                offset += file_id_len
            except:
                break
            
            # Extract chunk_num
            if len(remaining_data) < offset + 4:
                break
            chunk_num = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            # Extract chunk_data (base64)
            if len(remaining_data) < offset + 4:
                break
            chunk_data_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if chunk_data_len == 0 or len(remaining_data) < offset + chunk_data_len:
                break
            
            try:
                chunk_data_b64 = remaining_data[offset:offset+chunk_data_len].decode('cp850')
                offset += chunk_data_len
                
                # Store chunk info
                download_chunk = {
                    "file_id": file_id,
                    "chunk_num": chunk_num,
                    "chunk_data": chunk_data_b64
                }
                print(f"[DOWNLOAD] Chunk detectado: file_id={file_id}, chunk_num={chunk_num}, data_len={chunk_data_len}")
                
                remaining_data = remaining_data[offset:]
            except:
                break
        elif remaining_data[0] == 0xF8:
            # Upload request marker found
            # Format: [0xF8][file_id_len:4][file_id][chunk_size:4][chunk_num:4][full_path_len:4][full_path]
            offset = 1
            
            # Extract file_id
            if len(remaining_data) < offset + 4:
                break
            file_id_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if file_id_len == 0 or len(remaining_data) < offset + file_id_len:
                break
            
            try:
                file_id = remaining_data[offset:offset+file_id_len].decode('cp850')
                offset += file_id_len
            except:
                break
            
            # Extract chunk_size
            if len(remaining_data) < offset + 4:
                break
            chunk_size = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            # Extract chunk_num
            if len(remaining_data) < offset + 4:
                break
            chunk_num = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            # Extract full_path
            if len(remaining_data) < offset + 4:
                break
            path_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            full_path = ""
            if path_len > 0:
                if len(remaining_data) < offset + path_len:
                    break
                try:
                    full_path = remaining_data[offset:offset+path_len].decode('cp850')
                    offset += path_len
                except:
                    break
            
            # Store upload request info (will be used to construct Mythic JSON)
            upload_request = {
                "file_id": file_id,
                "chunk_size": chunk_size,
                "chunk_num": chunk_num,
                "full_path": full_path
            }
            print(f"[UPLOAD] Request detectado: file_id={file_id}, chunk_num={chunk_num}, chunk_size={chunk_size}, path={full_path}")
            
            remaining_data = remaining_data[offset:]
        elif remaining_data[0] == 0xF7:
            # Commands marker found
            # Format: [0xF7][action_len:4][action][cmd_len:4][cmd]
            offset = 1
            
            # Extract action ("add" or "remove")
            if len(remaining_data) < offset + 4:
                break
            action_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if action_len == 0 or len(remaining_data) < offset + action_len:
                break
            
            try:
                action = remaining_data[offset:offset+action_len].decode('utf-8')
                offset += action_len
            except:
                break
            
            # Extract cmd (command name)
            if len(remaining_data) < offset + 4:
                break
            cmd_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if cmd_len == 0 or len(remaining_data) < offset + cmd_len:
                break
            
            try:
                cmd = remaining_data[offset:offset+cmd_len].decode('utf-8')
                offset += cmd_len
                
                commands_data.append({
                    "action": action,
                    "cmd": cmd
                })
                print(f"[COMMANDS] Command {action} detectado: {cmd}")
                
                remaining_data = remaining_data[offset:]
            except:
                break
        elif remaining_data[0] == 0xF6:
            # Keylog marker found
            # Format: [0xF6][user_len:4][user][title_len:4][title][keys_len:4][keystrokes]
            offset = 1
            # user
            if len(remaining_data) < offset + 4:
                break
            user_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            if len(remaining_data) < offset + user_len:
                break
            user = remaining_data[offset:offset+user_len].decode('utf-8', errors='ignore')
            offset += user_len
            # title
            if len(remaining_data) < offset + 4:
                break
            title_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            if len(remaining_data) < offset + title_len:
                break
            window_title = remaining_data[offset:offset+title_len].decode('utf-8', errors='ignore')
            offset += title_len
            # keystrokes
            if len(remaining_data) < offset + 4:
                break
            keys_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            if len(remaining_data) < offset + keys_len:
                break
            keystrokes = remaining_data[offset:offset+keys_len].decode('utf-8', errors='ignore')
            offset += keys_len

            keylogs_data.append({
                "user": user,
                "window_title": window_title,
                "keystrokes": keystrokes,
            })
            print(f"[KEYLOG] user='{user}', title='{window_title}', len={len(keystrokes)}")
            remaining_data = remaining_data[offset:]
        elif remaining_data[0] == 0xF4:
            # Token marker found
            # Format: [0xF4 marker][token_id:4][host_len:4][host][description_len:4][description][user_len:4][user][groups_len:4][groups][thread_id:4][process_id:4][default_dacl_len:4][default_dacl][session_id:4][restricted:1][capabilities_len:4][capabilities][logon_sid_len:4][logon_sid][integrity_level_sid:4][app_container_number:4][app_container_sid_len:4][app_container_sid][privileges_len:4][privileges][handle:8]
            offset = 1
            
            if len(remaining_data) < offset + 4:
                break
            
            token_id = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            def read_string_field():
                nonlocal offset
                if len(remaining_data) < offset + 4:
                    return None
                str_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
                offset += 4
                if str_len == 0:
                    return ""
                if len(remaining_data) < offset + str_len:
                    return None
                try:
                    value = remaining_data[offset:offset+str_len].decode('utf-8', errors='ignore')
                    offset += str_len
                    return value
                except:
                    return ""
            
            def read_int32():
                nonlocal offset
                if len(remaining_data) < offset + 4:
                    return None
                value = int.from_bytes(remaining_data[offset:offset+4], 'big')
                offset += 4
                return value
            
            def read_int64():
                nonlocal offset
                if len(remaining_data) < offset + 8:
                    return None
                value = int.from_bytes(remaining_data[offset:offset+8], 'big')
                offset += 8
                return value
            
            def read_bool():
                nonlocal offset
                if len(remaining_data) < offset + 1:
                    return None
                value = remaining_data[offset] != 0
                offset += 1
                return value
            
            host = read_string_field()
            description = read_string_field()
            user = read_string_field()
            groups = read_string_field()
            thread_id = read_int32()
            process_id = read_int32()
            default_dacl = read_string_field()
            session_id = read_int32()
            restricted = read_bool()
            capabilities = read_string_field()
            logon_sid = read_string_field()
            integrity_level_sid = read_int32()
            app_container_number = read_int32()
            app_container_sid = read_string_field()
            privileges = read_string_field()
            handle = read_int64()
            
            if any(x is None for x in [thread_id, process_id, session_id, restricted, integrity_level_sid, app_container_number, handle]):
                break
            
            # Build token object according to Mythic documentation
            # Only include fields that have actual values (omit empty/zero optional fields)
            # This matches how other Mythic agents handle optional token fields
            token_obj = {
                "token_id": token_id  # required - always include
            }
            
            # Only add optional fields if they have meaningful values
            if host:
                token_obj["host"] = host
            if description:
                token_obj["description"] = description
            if user:
                token_obj["user"] = user
            # NOTE: The C agent currently sends domain_name as groups (incorrect, should be actual groups)
            # For now, we'll omit groups field entirely since it contains incorrect data
            # In the future, the C agent should extract and send actual group membership
            # if groups and groups.strip():
            #     token_obj["groups"] = groups
            if thread_id != 0:  # 0 is a valid thread_id, but often means "not provided"
                token_obj["thread_id"] = thread_id
            if process_id != 0:  # 0 is not a valid PID, so omit if 0
                token_obj["process_id"] = process_id
            if default_dacl:
                token_obj["default_dacl"] = default_dacl
            # session_id can be 0 (valid), so always include it
            token_obj["session_id"] = session_id
            # restricted can be False (valid), so always include it
            token_obj["restricted"] = restricted
            if capabilities:
                token_obj["capabilities"] = capabilities
            if logon_sid:
                token_obj["logon_sid"] = logon_sid
            if integrity_level_sid != 0:
                token_obj["integrity_level_sid"] = integrity_level_sid
            if app_container_number != 0:
                token_obj["app_container_number"] = app_container_number
            if app_container_sid:
                token_obj["app_container_sid"] = app_container_sid
            if privileges:
                token_obj["privileges"] = privileges
            if handle != 0:  # 0 is not a valid handle, so omit if 0
                token_obj["handle"] = handle
            
            tokens_data.append(token_obj)
            print(f"[TOKEN] token_id={token_id}, user='{user}', pid={process_id}, handle={handle}")
            remaining_data = remaining_data[offset:]
            print(f"[TOKEN] Tokens parseados hasta ahora: {len(tokens_data)}, remaining_data length: {len(remaining_data)} bytes")
            if len(remaining_data) > 0:
                print(f"[TOKEN] Siguiente byte en remaining_data: 0x{remaining_data[0]:02X}")
        elif remaining_data[0] == 0xF3:
            # Callback token marker found
            # Format: [0xF3 marker][action_len:4][action][host_len:4][host][token_id:4]
            offset = 1
            
            if len(remaining_data) < offset + 4:
                break
            
            action_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if action_len == 0 or len(remaining_data) < offset + action_len:
                break
            
            try:
                action = remaining_data[offset:offset+action_len].decode('utf-8', errors='ignore')
                offset += action_len
            except:
                break
            
            # Host
            if len(remaining_data) < offset + 4:
                break
            host_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            host = ""
            if host_len > 0:
                if len(remaining_data) < offset + host_len:
                    break
                try:
                    host = remaining_data[offset:offset+host_len].decode('utf-8', errors='ignore')
                    offset += host_len
                except:
                    pass
            
            # Token ID
            if len(remaining_data) < offset + 4:
                break
            token_id = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            callback_tokens_data.append({
                "action": action,
                "host": host,
                "token_id": token_id
            })
            print(f"[CALLBACK_TOKEN] action='{action}', host='{host}', token_id={token_id}")
            remaining_data = remaining_data[offset:]
        elif remaining_data[0] == 0xF0:
            # Callback context marker found
            # Format: [0xF0 marker][field_name_len:4][field_name][field_value_len:4][field_value]
            offset = 1
            
            # Extract field name
            if len(remaining_data) < offset + 4:
                break
            name_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            if name_len == 0 or len(remaining_data) < offset + name_len:
                break
            
            try:
                field_name = remaining_data[offset:offset+name_len].decode('cp850', errors='ignore')
                offset += name_len
            except:
                break
            
            # Extract field value
            if len(remaining_data) < offset + 4:
                break
            value_len = int.from_bytes(remaining_data[offset:offset+4], 'big')
            offset += 4
            
            field_value = ""
            if value_len > 0:
                if len(remaining_data) < offset + value_len:
                    break
                try:
                    field_value = remaining_data[offset:offset+value_len].decode('cp850', errors='ignore')
                    offset += value_len
                except:
                    field_value = ""
            
            # Store callback context field
            callback_context_data[field_name] = field_value
            print(f"[CALLBACK_CONTEXT] {field_name} = '{field_value}'")
            
            remaining_data = remaining_data[offset:]
        else:
            # No more markers
            break

    # Try to decode output - BOFs on Windows typically use Windows-1252 (cp1252) for Spanish characters
    # Try Windows-1252 first (for Windows BOF output with special chars like ñ, ó, etc.)
    # Then try UTF-8, then cp850 (legacy agent output), then latin-1 as last resort
    decoded_output = None
    for encoding in ['cp1252', 'utf-8', 'cp850', 'latin-1']:
        try:
            decoded_output = output.decode(encoding, errors='replace')
            # If we successfully decoded with Windows-1252 or UTF-8, use it
            if encoding in ['cp1252', 'utf-8']:
                # Check if there are replacement characters - if too many, try next encoding
                replacement_count = decoded_output.count('\ufffd')
                if replacement_count == 0 or (replacement_count < len(decoded_output) * 0.01):
                    # Less than 1% replacement chars, probably correct encoding
                    break
            else:
                # For cp850 and latin-1, use if no replacement chars or as fallback
                if '\ufffd' not in decoded_output or encoding == 'latin-1':
                    break
        except Exception as e:
            continue
    
    if decoded_output is None:
        print(f"Fallo al decodificar la salida con todas las codificaciones intentadas")
        decoded_output = "[ERROR DECODIFICANDO]"

    task_id_str = uuidTask.decode('cp850')
    
    # Intentar detectar si es una lista de procesos (formato tab-separated)
    # Formato esperado del comando ps: nombre\tPPID\tPID\tarch\tuser\tsession\n
    is_process_list = False
    processes = []
    
    # Detectar si es una lista de procesos: formato tab-separated con al menos 3 columnas
    # Formato: nombre\tPPID\tPID\t[arch]\t[user]\t[session]
    if decoded_output and '\t' in decoded_output:
        lines = [l.strip() for l in decoded_output.strip().split('\n') if l.strip()]
        if len(lines) > 0:
            # Verificar que la mayoría de líneas tengan formato de proceso
            # (al menos 3 columnas: nombre, PPID, PID)
            valid_lines = 0
            for line in lines[:10]:  # Revisar primeras 10 líneas
                parts = line.split('\t')
                if len(parts) >= 3:
                    # Verificar que PPID (índice 1) y PID (índice 2) sean números
                    # y que el nombre (índice 0) no esté vacío
                    if (parts[1].strip().isdigit() and 
                        parts[2].strip().isdigit() and 
                        parts[0].strip()):
                        valid_lines += 1
            
            # Si al menos 80% de las líneas revisadas tienen formato válido
            sample_size = len(lines[:10])
            if sample_size > 0 and valid_lines >= max(1, sample_size * 0.8):
                is_process_list = True
                print(f"[PROCESS_BROWSER] Detectado formato de lista de procesos ({valid_lines}/{sample_size} líneas válidas), parseando...")
                processes = parse_process_list(decoded_output)
                print(f"[PROCESS_BROWSER] Parseados {len(processes)} procesos")

    jsonTask = {
        "task_id": task_id_str,
        "completed": True,
    }
    
    # Add callback context if present
    if callback_context_data:
        jsonTask["callback"] = callback_context_data
        print(f"[CALLBACK_CONTEXT] Añadido callback context con {len(callback_context_data)} campos: {list(callback_context_data.keys())}")
    
    # Incluir user_output para que se vea en la consola, EXCEPTO cuando hay download chunks
    # (los chunks de download no necesitan user_output)
    # Always include user_output if there's output text (needed for tokens command to show in GUI)
    if not download_chunk and decoded_output.strip():
        jsonTask["user_output"] = decoded_output
    
    # Si detectamos una lista de procesos, también incluir el formato para Process Browser
    if is_process_list and len(processes) > 0:
        # Mark ALL processes with update_deleted=True to tell Mythic this is the complete list
        # According to Mythic docs: "setting this to true tells Mythic to mark any process not returned in this process array as deleted"
        # This effectively clears the cache by removing processes that no longer exist
        for process in processes:
            process["update_deleted"] = True
        jsonTask["processes"] = processes
        print(f"[PROCESS_BROWSER] Respuesta convertida al formato Process Browser con {len(processes)} procesos (update_deleted=True en TODOS los procesos para limpiar caché)")
    
    # Detectar y convertir formato File Browser (formato: path\nD/F\tSIZE\tDATE\tNAME)
    # El formato del agente es: primera línea es el path, luego líneas con D/F\tSIZE\tMM/DD/YYYY HH:MM:SS\tNAME
    # Solo procesar si NO es una lista de procesos (para evitar conflictos)
    file_browser_data = None
    if not is_process_list and decoded_output and '\t' in decoded_output and '\n' in decoded_output:
        lines = [l.strip() for l in decoded_output.strip().split('\n') if l.strip()]
        if len(lines) >= 2:
            # Verificar si el formato es de lista de archivos
            # Primera línea debería ser un path (contiene :\ o \\)
            first_line = lines[0]
            # Segunda línea debería empezar con D o F seguido de tab
            if len(lines) > 1:
                second_line = lines[1]
                parts = second_line.split('\t')
                # Verificar formato: D/F\tSIZE\tDATE\tNAME
                # DATE debería tener formato MM/DD/YYYY HH:MM:SS (contiene / y :)
                if (len(parts) >= 4 and 
                    parts[0].strip() in ['D', 'F'] and 
                    '/' in parts[2] and ':' in parts[2] and
                    (':' in first_line or '\\' in first_line)):
                    print(f"[FILE_BROWSER] Detectado formato de lista de archivos, parseando...")
                    file_browser_data = parse_file_browser_list(decoded_output)
                    if file_browser_data:
                        jsonTask["file_browser"] = file_browser_data
                        print(f"[FILE_BROWSER] Respuesta convertida al formato File Browser con {len(file_browser_data.get('files', []))} archivos/carpetas")
    
    # Support for artifacts: create artifacts from parsed data
    artifacts = []
    
    # Process all detected artifacts
    for artifact_info in artifacts_data:
        artifacts.append(create_artifact(
            artifact_info["type"],
            artifact_info["value"],
            needs_cleanup=False,  # Can be made configurable per artifact type
            resolved=False
        ))
    
    # Add artifacts array if we have any
    if len(artifacts) > 0:
        jsonTask["artifacts"] = artifacts
        print(f"[ARTIFACTS] Total de artifacts agregados: {len(artifacts)}")
    
    # Support for credentials: create credentials from parsed data
    credentials = []
    
    # Process all detected credentials
    for cred_info in credentials_data:
        credentials.append({
            "credential_type": cred_info["credential_type"],
            "realm": cred_info["realm"],
            "credential": cred_info["credential"],
            "account": cred_info["account"]
        })
    
    # Add credentials array if we have any
    if len(credentials) > 0:
        jsonTask["credentials"] = credentials
        print(f"[CREDENTIALS] Total de credentials agregados: {len(credentials)}")
    
    # Support for commands: create commands array from parsed data
    commands = []
    
    # Process all detected commands
    for cmd_info in commands_data:
        commands.append({
            "action": cmd_info["action"],
            "cmd": cmd_info["cmd"]
        })
    
    # Add commands array if we have any
    if len(commands) > 0:
        jsonTask["commands"] = commands
        print(f"[COMMANDS] Total de commands agregados: {len(commands)}")
    
    if len(keylogs_data) > 0:
        jsonTask["keylogs"] = keylogs_data
        print(f"[KEYLOG] Total de keylogs agregados: {len(keylogs_data)}")
    
    # Support for tokens: create tokens array from parsed data
    if len(tokens_data) > 0:
        jsonTask["tokens"] = tokens_data
        print(f"[TOKENS] Total de tokens agregados: {len(tokens_data)}")
    
    # Support for callback tokens: create callback_tokens array from parsed data
    if len(callback_tokens_data) > 0:
        jsonTask["callback_tokens"] = callback_tokens_data
        print(f"[CALLBACK_TOKENS] Total de callback_tokens agregados: {len(callback_tokens_data)}")
    
    # Add download information if present
    if download_registration:
        jsonTask["download"] = download_registration
        print(f"[DOWNLOAD] Download registration agregado al JSON: {json.dumps(download_registration)}")
    
    if download_chunk:
        # For download chunks, we ONLY send the download info, no user_output
        # Remove user_output if it was added
        if "user_output" in jsonTask:
            jsonTask.pop("user_output", None)
        
        jsonTask["download"] = {
            "chunk_num": download_chunk["chunk_num"],
            "file_id": download_chunk["file_id"],
            "chunk_data": download_chunk["chunk_data"]
        }
        print(f"[DOWNLOAD] Download chunk agregado al JSON: chunk_num={download_chunk['chunk_num']}, file_id={download_chunk['file_id']}, data_len={len(download_chunk['chunk_data'])}")
    
    # Add upload request information if present
    if upload_request:
        # For upload requests, we ONLY send the upload info, no user_output
        # Remove user_output if it was added
        if "user_output" in jsonTask:
            jsonTask.pop("user_output", None)
        
        jsonTask["upload"] = {
            "file_id": upload_request["file_id"],
            "chunk_size": upload_request["chunk_size"],
            "chunk_num": upload_request["chunk_num"],
            "full_path": upload_request["full_path"]
        }
        print(f"[UPLOAD] Upload request agregado al JSON: file_id={upload_request['file_id']}, chunk_num={upload_request['chunk_num']}, chunk_size={upload_request['chunk_size']}")

    resTaks.append(jsonTask)

    dataJson = {"action": "post_response", "responses": resTaks}
    
    # Log response size and token count for debugging
    json_str = json.dumps(dataJson)
    print(f"[RESPONSE] Tamaño del JSON: {len(json_str)} bytes")
    print(f"[RESPONSE] Número de tokens en respuesta: {len(tokens_data)}")
    print(f"[RESPONSE] Tiene user_output: {'user_output' in jsonTask}")
    if 'user_output' in jsonTask:
        print(f"[RESPONSE] Tamaño de user_output: {len(jsonTask['user_output'])} caracteres")
    
    # Validate token format - check first token if any
    if len(tokens_data) > 0:
        first_token = tokens_data[0]
        print(f"[RESPONSE] Primer token sample - token_id type: {type(first_token.get('token_id'))}, value: {first_token.get('token_id')}")
        print(f"[RESPONSE] Primer token sample - handle type: {type(first_token.get('handle'))}, value: {first_token.get('handle')}")
        print(f"[RESPONSE] Primer token sample - restricted type: {type(first_token.get('restricted'))}, value: {first_token.get('restricted')}")
        print(f"[RESPONSE] Primer token sample - session_id type: {type(first_token.get('session_id'))}, value: {first_token.get('session_id')}")
        # Check all required fields are present
        required_fields = ["token_id", "host", "user", "groups", "thread_id", "process_id", "session_id", "restricted", "integrity_level_sid", "app_container_number", "handle"]
        missing_fields = [field for field in required_fields if field not in first_token]
        if missing_fields:
            print(f"[ERROR] Campos faltantes en token: {missing_fields}")
        else:
            print(f"[RESPONSE] Todos los campos requeridos están presentes en el primer token")
    
    print(f"[RESPONSE] Enviando JSON a Mythic...")
    
    # Try to validate JSON serialization before sending
    try:
        json_test = json.dumps(dataJson)
        print(f"[RESPONSE] JSON serialización exitosa, tamaño: {len(json_test)} bytes")
        
        # Try to parse it back to ensure it's valid
        json.loads(json_test)
        print(f"[RESPONSE] JSON validación exitosa (parseable)")
    except Exception as e:
        print(f"[ERROR] Problema con JSON: {e}")
        import traceback
        traceback.print_exc()
    
    # Log first few tokens for debugging
    if len(tokens_data) > 0:
        print(f"[RESPONSE] Primer token completo: {json.dumps(tokens_data[0], indent=2)}")
        # Validate token_id is an integer (required)
        for i, tok in enumerate(tokens_data[:3]):  # Check first 3 tokens
            if "token_id" not in tok:
                print(f"[ERROR] Token {i} missing token_id!")
            if not isinstance(tok.get("token_id"), int):
                print(f"[ERROR] Token {i} token_id is not an integer: {type(tok.get('token_id'))}, value: {tok.get('token_id')}")
    
    # Log response structure
    if "responses" in dataJson:
        for resp in dataJson["responses"]:
            if "tokens" in resp:
                print(f"[RESPONSE] Response has {len(resp['tokens'])} tokens")
                # Check if all tokens have required token_id
                for i, tok in enumerate(resp["tokens"]):
                    if "token_id" not in tok:
                        print(f"[ERROR] Token {i} in response missing token_id!")
                    if not isinstance(tok.get("token_id"), int):
                        print(f"[ERROR] Token {i} token_id type error: {type(tok.get('token_id'))}")
    
    print(f"Respuesta generada: {json.dumps(dataJson, indent=4)[:2000]}...")  # Truncate huge JSON

    return dataJson


# ==========================================================
# NUEVAS FUNCIONES PARA SOPORTE SOCKS
# ==========================================================

def startSocks(data):
    """Parses incoming data for start_socks tasks."""
    if len(data) >= 4:
        port = int.from_bytes(data[:4], "big")
    else:
        port = 1080
    print(f"[DEBUG] startSocks(): puerto recibido -> {port}")
    return {"action": "start_socks", "port": port}


def stopSocks(data):
    """Parses incoming data for stop_socks tasks."""
    print("[DEBUG] stopSocks() recibido")
    return {"action": "stop_socks"}
