import hashlib
from . import mparser as macholibre
from .jelly import Jelly
import plistlib
import logging
import requests
import struct

logger = logging.getLogger("nac")

BINARY_HASH = "e1181ccad82e6629d52c6a006645ad87ee59bd13"
BINARY_PATH = "emulated/IMDAppleServices"
BINARY_URL = "https://github.com/JJTech0130/nacserver/raw/main/IMDAppleServices"

FAKE_DATA = plistlib.load(open("emulated/data.plist", "rb"))
CF_OBJECTS = []
ETH_ITERATOR_HACK = False

def load_binary() -> bytes:
    import os
    if not os.path.exists(BINARY_PATH):
        logger.info("Downloading IMDAppleServices")
        resp = requests.get(BINARY_URL)
        b = resp.content
        open(BINARY_PATH, "wb").write(b)
    else:
        logger.debug("Using already downloaded IMDAppleServices")
        b = open(BINARY_PATH, "rb").read()
    if hashlib.sha1(b).hexdigest() != BINARY_HASH:
        raise Exception("Hashes don't match")
    return b

def get_x64_slice(binary: bytes) -> bytes:
    p = macholibre.Parser(binary)
    off, size = p.u_get_offset(cpu_type="X86_64")
    return binary[off : off + size]

def nac_init(j: Jelly, cert: bytes):
    cert_addr = j.malloc(len(cert))
    j.uc.mem_write(cert_addr, cert)

    out_validation_ctx_addr = j.malloc(8)
    out_request_bytes_addr = j.malloc(8)
    out_request_len_addr = j.malloc(8)

    ret = j.instr.call(
        0xB1DB0,
        [
            cert_addr,
            len(cert),
            out_validation_ctx_addr,
            out_request_bytes_addr,
            out_request_len_addr,
        ],
    )

    if ret != 0:
        n = ret & 0xffffffff
        n = (n ^ 0x80000000) - 0x80000000
        raise Exception(f"Error calling nac_init: {n}")
    
    validation_ctx_addr = j.uc.mem_read(out_validation_ctx_addr, 8)
    request_bytes_addr = j.uc.mem_read(out_request_bytes_addr, 8)
    request_len = j.uc.mem_read(out_request_len_addr, 8)

    request_bytes_addr = int.from_bytes(request_bytes_addr, 'little')
    request_len = int.from_bytes(request_len, 'little')

    logger.debug(f"Request @ {hex(request_bytes_addr)} : {hex(request_len)}")

    request = j.uc.mem_read(request_bytes_addr, request_len)
    validation_ctx_addr = int.from_bytes(validation_ctx_addr, 'little')
    
    return validation_ctx_addr, request

def nac_key_establishment(j: Jelly, validation_ctx: int, response: bytes):
    response_addr = j.malloc(len(response))
    j.uc.mem_write(response_addr, response)

    ret = j.instr.call(
        0xB1DD0,
        [
            validation_ctx,
            response_addr,
            len(response),
        ],
    )

    if ret != 0:
        n = ret & 0xffffffff
        n = (n ^ 0x80000000) - 0x80000000
        raise Exception(f"Error calling nac_submit: {n}")
    
def nac_sign(j: Jelly, validation_ctx: int, payload: bytes):
    out_validation_data_addr = j.malloc(8)
    out_validation_data_len_addr = j.malloc(8)

    # 1. Asignar memoria para el payload dinámico (como lo hace el código de iOS/Obj-C)
    if payload and len(payload) > 0:
        payload_addr = j.malloc(len(payload))
        j.uc.mem_write(payload_addr, payload)
        payload_len = len(payload)
    else:
        payload_addr = 0
        payload_len = 0

    ret = j.instr.call(
        0xB1DF0,
        [
            validation_ctx,
            payload_addr,  # Equivalente a 'data' / 'unk_bytes'
            payload_len,   # Equivalente a len(data) / 'unk_len'
            out_validation_data_addr,
            out_validation_data_len_addr,
        ],
    )

    if ret != 0:
        n = ret & 0xffffffff
        n = (n ^ 0x80000000) - 0x80000000
        raise Exception(f"Error calling nac_generate: {n}")
    
    validation_data_addr = int.from_bytes(j.uc.mem_read(out_validation_data_addr, 8), 'little')
    validation_data_len = int.from_bytes(j.uc.mem_read(out_validation_data_len_addr, 8), 'little')

    validation_data = j.uc.mem_read(validation_data_addr, validation_data_len)
    return validation_data


# --- Hooks de Emulación de Memoria y IOKit ---
def hook_code(uc, address: int, size: int, user_data):
    logger.debug(">>> Tracing instruction at 0x%x, instruction size = 0x%x" % (address, size))

def malloc(j: Jelly, length: int) -> int:
    return j.malloc(length)

def memset_chk(j: Jelly, dest: int, c: int, length: int, destlen: int):
    j.uc.mem_write(dest, bytes([c]) * length)
    return 0

def memcpy(j: Jelly, dest: int, src: int, length: int):
    orig = j.uc.mem_read(src, length)
    j.uc.mem_write(dest, bytes(orig))
    return 0

def bzero(j: Jelly, ptr: int, length: int):
    j.uc.mem_write(ptr, bytes([0]) * length)
    return 0

def arc4random(j: Jelly) -> int:
    import random
    return random.randint(0, 0xFFFFFFFF)

def _parse_cfstr_ptr(j: Jelly, ptr: int) -> str:
    size = struct.calcsize("<QQQQ")
    data = j.uc.mem_read(ptr, size)
    isa, flags, str_ptr, length = struct.unpack("<QQQQ", data)
    str_data = j.uc.mem_read(str_ptr, length)
    return str_data.decode("utf-8")

def _parse_cstr_ptr(j: Jelly, ptr: int) -> str:
    data = j.uc.mem_read(ptr, 256) 
    return data.split(b"\x00")[0].decode("utf-8")

def IORegistryEntryCreateCFProperty(j: Jelly, entry: int, key: int, allocator: int, options: int):
    key_str = _parse_cfstr_ptr(j, key)
    if key_str in FAKE_DATA["iokit"]:
        fake = FAKE_DATA["iokit"][key_str]
        CF_OBJECTS.append(fake)
        return len(CF_OBJECTS)
    else:
        return 0
        
def CFGetTypeID(j: Jelly, obj: int):
    obj = CF_OBJECTS[obj - 1]
    if isinstance(obj, bytes):
        return 1
    elif isinstance(obj, str):
        return 2
    else:
        raise Exception("Unknown CF object type")
                                                                                                        
def CFDataGetLength(j: Jelly, obj: int):
    obj = CF_OBJECTS[obj - 1]
    if isinstance(obj, bytes):
        return len(obj)
    else:
        raise Exception("Unknown CF object type")
    
def CFDataGetBytes(j: Jelly, obj: int, range_start: int, range_len: int, buf: int):
    obj = CF_OBJECTS[obj - 1]
    if isinstance(obj, bytes):
        # ¡CRÍTICO! range_len es la longitud (length), no el límite final.
        data = obj[range_start : range_start + range_len]
        j.uc.mem_write(buf, data)
        return 0
    else:
        raise Exception("Unknown CF object type")
    
def CFDictionaryCreateMutable(j: Jelly) -> int:
    CF_OBJECTS.append({})
    return len(CF_OBJECTS)

def maybe_object_maybe_string(j: Jelly, obj: int):
    if isinstance(obj, str):
        return obj
    elif obj > len(CF_OBJECTS):
        # ¡CRÍTICO! El emulador pasa punteros a memoria. 
        # Intentamos extraer el string real, de lo contrario la llave del dict no coincidirá.
        try:
            return _parse_cfstr_ptr(j, obj)
        except Exception:
            return obj
    else:
        return CF_OBJECTS[obj - 1]

def CFDictionaryGetValue(j: Jelly, d: int, key: int) -> int:
    d = CF_OBJECTS[d - 1]
    if key == 0xc3c3c3c3c3c3c3c3:
        key = "DADiskDescriptionVolumeUUIDKey" 
        
    key = maybe_object_maybe_string(j, key)
    if isinstance(d, dict):
        if key in d:
            val = d[key]
            CF_OBJECTS.append(val)
            return len(CF_OBJECTS)
        else:
            # Restauramos la excepción para saber EXACTAMENTE qué clave está faltando
            # si tu data.plist está incompleto.
            raise Exception(f"Key not found in FAKE_DATA: {key}")
    else:
        raise Exception("Unknown CF object type")
    
def CFDictionarySetValue(j: Jelly, d: int, key: int, val: int):
    d = CF_OBJECTS[d - 1]
    key = maybe_object_maybe_string(j, key)
    val = maybe_object_maybe_string(j, val)
    if isinstance(d, dict):
        d[key] = val
    else:
        raise Exception("Unknown CF object type")

def DADiskCopyDescription(j: Jelly) -> int:
    description = CFDictionaryCreateMutable(j)
    CFDictionarySetValue(j, description, "DADiskDescriptionVolumeUUIDKey", FAKE_DATA["root_disk_uuid"])
    return description    

def CFStringCreate(j: Jelly, string: str) -> int:
    CF_OBJECTS.append(string)
    return len(CF_OBJECTS)

def CFStringGetLength(j: Jelly, string: int) -> int:
    string = CF_OBJECTS[string - 1]
    if isinstance(string, str):
        return len(string)
    else:
        raise Exception("Unknown CF object type")

def CFStringGetCString(j: Jelly, string: int, buf: int, buf_len: int, encoding: int) -> int:
    string = CF_OBJECTS[string - 1]
    if isinstance(string, str):
        data = string.encode("utf-8")
        # ¡CRÍTICO! Prevenir desbordamiento y AGREGAR terminación nula obligatoria en C (\x00)
        if len(data) >= buf_len:
            data = data[:buf_len - 1]
        data += b"\x00"
        
        j.uc.mem_write(buf, data)
        return 1  # CFStringGetCString devuelve booleano (1 = Success)
    else:
        raise Exception("Unknown CF object type")
    
def IOServiceMatching(j: Jelly, name: int) -> int:
    name = _parse_cstr_ptr(j, name)
    name = CFStringCreate(j, name)
    d = CFDictionaryCreateMutable(j)
    CFDictionarySetValue(j, d, "IOProviderClass", name)
    return d
    
def IOServiceGetMatchingService(j: Jelly) -> int:
    return 92

def IOServiceGetMatchingServices(j: Jelly, port, match, existing) -> int:
    global ETH_ITERATOR_HACK
    ETH_ITERATOR_HACK = True
    j.uc.mem_write(existing, bytes([93]))
    return 0

def IOIteratorNext(j: Jelly, iterator: int) -> int:
    global ETH_ITERATOR_HACK
    if ETH_ITERATOR_HACK:
        ETH_ITERATOR_HACK = False
        return 94
    else:
        return 0
    
def IORegistryEntryGetParentEntry(j: Jelly, entry: int, _, parent: int) -> int:
    j.uc.mem_write(parent, bytes([entry + 100]))
    return 0
# --- Fin de Hooks ---


def get_cert() -> bytes:
    import requests, plistlib
    resp = requests.get("http://static.ess.apple.com/identity/validation/cert-1.0.plist")
    resp_plist = plistlib.loads(resp.content)
    cert = resp_plist["cert"]
    # Garantizar que sea formato de bytes puros compatible con Unicorn Engine
    return cert.data if hasattr(cert, "data") else bytes(cert)




# 2. Modificado para imitar el comportamiento de RelayConnectionManager de iOS
def get_session_info(req: bytes, identifiers: dict) -> bytes:
    # Empaquetamos identifiers como XML Plist tal cual lo hace NSPropertyListSerialization
    machine_info = plistlib.dumps(identifiers, fmt=plistlib.FMT_XML)

    body = {
        'session-info-request': req,
        'machine-info': machine_info,
    }
    
    body_bytes = plistlib.dumps(body, fmt=plistlib.FMT_XML)
    
    # Cabeceras exactas usadas en el script de Obj-C
    headers = {
        "x-protocol-version": "1850",
        "x-push-cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4ekNDQWx5Z0F3SUJBZ0lLQU5xU0E5R3V2UTFwWkRBTkJna3Foa2lHOXcwQkFRVUZBREJhTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNoTUtRWEJ3YkdVZ1NXNWpMakVWTUJNR0ExVUVDeE1NUVhCd2JHVWdhVkJvYjI1bE1SOHdIUVlEVlFRREV4WkJjSEJzWlNCcFVHaHZibVVnUkdWMmFXTmxJRU5CTUI0WERUSTJNREl5TURBNE1qa3hNRm9YRFRJNU1ESXlNREE0TWpreE1Gb3dnWU14TFRBckJnTlZCQU1XSkVJNVJqQTJRVFEyTFRCRlJFSXRORVl4T0MwNFJqUkJMVUk0UlRGRVJUazJRMFZFTnpFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ1RBa05CTVJJd0VBWURWUVFIRXdsRGRYQmxjblJwYm04eEV6QVJCZ05WQkFvVENrRndjR3hsSUVsdVl5NHhEekFOQmdOVkJBc1RCbWxRYUc5dVpUQ0JuekFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUUVBdWdFU1RGVTQ0Q29KWHIxdmRnMFllb0psa1pOQ1hJM1lmOU9uVUw1MmUyN0FZNUJBczJOSmtCMzBkaG55VVFFR0FWVE1ZTThPam9MQUFMTHdWY25CRTZrUUVLa2tjOGZqQmVPR0hYWkhraUlFVkxaUmdablRmOTFtWE9UeEF3WkRZRDBYM3hlUU1Sc3pqZ3g2NFJ4dE54RnpFU2kydU5oSGF4RWlndWRVTHNDQXdFQUFhT0JsVENCa2pBZkJnTlZIU01FR0RBV2dCU3kvaUVqUklhVmFublZnU2FPY3hEWXAweU9kREFkQmdOVkhRNEVGZ1FVWUxhZVorR2hqSnFBWnFKdjV2V2RWcWZGbzZrd0RBWURWUjBUQVFIL0JBSXdBREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdJQVlEVlIwbEFRSC9CQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQndNQ01CQUdDaXFHU0liM1kyUUdDZ0lFQWdVQU1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFFYTR6NE1VZkpHRU1vK1BwR0M3VkRhOXdNYzFYT01hOC9FVmNuSkhNdk5uMW56aDRzMXcxVDF1VlhBOTZ6V3lDYXF1OEQzNDVNKzRQL1ZwWEh1OVJoRytlYWk2YWlqMnFvTWl4cFdaQTZ3YXF5WWNQaWRYQTRZWGtvUFdQNENHc01TVEZOUUJocE4yeHFtZ0RoNDlaT3JETlBoOXRVUVo3UUYyNGNuc1cvSFAKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==",
        "x-apple-i-timezone": "GMT-6",
        "x-push-sig": "AQEs0vGsYIEvs5xoNPaURzTJoktv8Gwe2dV5i1VDQMx2J7XG8NoRRsaSiTFcCVaAn4UhxdGcK+DHe1g5PZs/MQN66UY77I8nQhmbePBpTPWJA2x0u7+C/bnmCw7ZIs8ofE1xFIq8vEGO8TRGMyRQmVb19NXVQTkS/K/SfZt7G2PSSw==",
        "x-apple-baa-cert": "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",
        "x-apple-i-device-type": "1",
        "accept": "*/*",
        "content-type": "application/x-apple-plist",
        "x-apple-i-locale": "es_MX",
        "x-apple-baa-sig": "MEQCIGxXfyJ1Z8gLOjUmlxyJ+riOPO2nNa/Ux12BGNvet1ZSAiAM2MOBy1+r3aRRHhtBsMVF/0FH17eJxfaZ77yVbdag9w==",
        "accept-encoding": "gzip"
    }

    resp = requests.post(
        "https://identity.ess.apple.com/WebObjects/TDIdentityService.woa/wa/initializeValidation", 
        data=body_bytes, 
        headers=headers,
        verify=False
    )
    resp_plist = plistlib.loads(resp.content)
    return resp_plist["session-info"]

def load_nac() -> Jelly:
    binary = load_binary()
    binary = get_x64_slice(binary)
    j = Jelly(binary)

    hooks = {
        "_malloc": malloc,
        "___stack_chk_guard": lambda: 0,
        "___memset_chk": memset_chk,
        "_sysctlbyname": lambda _: 0,
        "_memcpy": memcpy,
        "_kIOMasterPortDefault": lambda: 0,
        "_IORegistryEntryFromPath": lambda _: 1,
        "_kCFAllocatorDefault": lambda: 0,
        "_IORegistryEntryCreateCFProperty": IORegistryEntryCreateCFProperty,
        "_CFGetTypeID": CFGetTypeID,
        "_CFStringGetTypeID": lambda _: 2,
        "_CFDataGetTypeID": lambda _: 1,
        "_CFDataGetLength": CFDataGetLength,
        "_CFDataGetBytes": CFDataGetBytes,
        "_CFRelease": lambda _: 0,
        "_IOObjectRelease": lambda _: 0,
        "_statfs$INODE64": lambda _: 0,
        "_DASessionCreate": lambda _: 201,
        "_DADiskCreateFromBSDName": lambda _: 202,
        "_kDADiskDescriptionVolumeUUIDKey": lambda: 0,
        "_DADiskCopyDescription": DADiskCopyDescription,
        "_CFDictionaryGetValue": CFDictionaryGetValue,
        "_CFUUIDCreateString": lambda _, __, uuid: uuid,
        "_CFStringGetLength": CFStringGetLength,
        "_CFStringGetMaximumSizeForEncoding": lambda _, length, __: length,
        "_CFStringGetCString": CFStringGetCString,
        "_free": lambda _: 0,
        "_IOServiceMatching": IOServiceMatching,
        "_IOServiceGetMatchingService": IOServiceGetMatchingService,
        "_CFDictionaryCreateMutable": CFDictionaryCreateMutable,
        "_kCFBooleanTrue": lambda: 0,
        "_CFDictionarySetValue": CFDictionarySetValue,
        "_IOServiceGetMatchingServices": IOServiceGetMatchingServices,
        "_IOIteratorNext": IOIteratorNext,
        "___bzero": bzero,
        "_IORegistryEntryGetParentEntry": IORegistryEntryGetParentEntry,
        "_arc4random": arc4random
    }
    j.setup(hooks)
    return j

# 3. Flujo modificado para aceptar identifiers y payloadToSign, como en Obj-C
def generate_validation_data(identifiers: dict, payload_to_sign: bytes) -> bytes:
    j = load_nac()
    logger.debug("Loaded NAC library")
    
    val_ctx, req = nac_init(j, get_cert())
    logger.debug("Initialized NAC")
    
    session_info = get_session_info(req, identifiers)
    logger.debug("Got session info")
    
    nac_key_establishment(j, val_ctx, session_info)
    logger.debug("Submitted session info")
    
    val_data = nac_sign(j, val_ctx, payload_to_sign)
    logger.info("Generated validation data")
    
    return bytes(val_data)

# Función equivalente al 'getIdentifiers' del script Obj-C
def get_identifiers() -> dict:
    """
    Extrae los identificadores del archivo FAKE_DATA (data.plist) simulando
    las llamadas a sysctl, MobileGestalt e IOKit de iOS.
    """
    # Extraemos los sub-diccionarios, usando diccionarios vacíos si no existen
    gestalt = FAKE_DATA.get("gestalt", {})
    sysctl = FAKE_DATA.get("sysctl", {})
    iokit = FAKE_DATA.get("iokit", {})

    # Helper para convertir los bytes de la MAC Address a un string XX:XX:XX:XX:XX:XX
    def format_mac(mac_bytes):
        if isinstance(mac_bytes, bytes) and len(mac_bytes) >= 6:
            return ":".join(f"{b:02x}" for b in mac_bytes[:6])
        return None

    # Helper para decodificar valores de IOKit que suelen venir como bytes (NSData)
    def decode_iokit_str(val):
        if isinstance(val, bytes):
            return val.decode("utf-8", errors="ignore").strip("\x00").strip()
        return val

    # 1. Model / Version (Prioridad: sysctl -> gestalt)
    real_model = sysctl.get("hw.machine") or gestalt.get("ProductType")
    real_build = sysctl.get("kern.osversion") or gestalt.get("BuildVersion")
    real_sys_version = gestalt.get("ProductVersion")

    # 2. Serial Number & UUID (Prioridad: gestalt -> iokit)
    real_serial = gestalt.get("SerialNumber") or decode_iokit_str(iokit.get("IOPlatformSerialNumber"))
    real_hw_uuid = gestalt.get("UniqueDeviceID") or decode_iokit_str(iokit.get("IOPlatformUUID"))

    # 3. Board ID & MLB (Extraídos de iokit o gestalt)
    real_board_id = decode_iokit_str(iokit.get("board-id"))
    real_mlb = decode_iokit_str(iokit.get("mlb-serial-number")) or gestalt.get("MLBSerialNumber")

    # 4. MAC Address (de iokit IOMACAddress)
    real_mac_string = format_mac(iokit.get("IOMACAddress"))

    # Construimos el diccionario final omitiendo los nulos (comportamiento idéntico a tu Obj-C)
    identifiers = {
        "software_name": "iPhone OS"
    }

    if real_build: identifiers["software_build_id"] = real_build
    if real_sys_version: identifiers["software_version"] = real_sys_version
    if real_serial: identifiers["serial_number"] = real_serial
    if real_model: identifiers["hardware_version"] = real_model
    if real_hw_uuid: identifiers["unique_device_id"] = real_hw_uuid
    if real_board_id: identifiers["board_id"] = real_board_id
    if real_mac_string: identifiers["macaddress"] = real_mac_string
    if real_mlb: identifiers["mlb_serial_number"] = real_mlb

    return identifiers

if __name__ == "__main__":
    from base64 import b64encode
    
    # Extraemos (o simulamos) los identificadores
    identifiers = get_identifiers()
    logger.info(f"Identifiers Reales: {identifiers}")
    
    # El payload simulado tal como lo tienes en el main() de Objective-C
    dummy_string = "<dict><key>test</key><string>payload</string></dict>"
    payload_de_tu_peticion_final = dummy_string.encode('utf-8')
    
    try:
        val_data = generate_validation_data(identifiers, payload_de_tu_peticion_final)
        b64 = b64encode(val_data).decode()
        logger.info(f"{b64}")
        
        # Opcional: Escribirlo a un archivo, similar a tu código
        # open("absinthe_response.b64", "w").write(b64)
    except Exception as e:
        logger.error(f"Failed to generate validation data: {e}")