// ifone_nif.c - IOKit NIF for querying USB NCM interfaces on macOS
#include <erl_nif.h>
#include <string.h>
#include <stdio.h>

// Pre-created atoms (initialized in nif_load)
static ERL_NIF_TERM ATOM_OK;
static ERL_NIF_TERM ATOM_ERROR;
static ERL_NIF_TERM ATOM_NIL;
static ERL_NIF_TERM ATOM_ENOMEM;
static ERL_NIF_TERM ATOM_CONFIGURATION;
static ERL_NIF_TERM ATOM_INTERFACES;
static ERL_NIF_TERM ATOM_MATCHING_FAILED;
static ERL_NIF_TERM ATOM_SERVICE_MATCHING_FAILED;
static ERL_NIF_TERM ATOM_NOT_FOUND;

static int nif_load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info) {
    (void)priv_data;
    (void)load_info;

    ATOM_OK = enif_make_atom(env, "ok");
    ATOM_ERROR = enif_make_atom(env, "error");
    ATOM_NIL = enif_make_atom(env, "nil");
    ATOM_ENOMEM = enif_make_atom(env, "enomem");
    ATOM_CONFIGURATION = enif_make_atom(env, "configuration");
    ATOM_INTERFACES = enif_make_atom(env, "interfaces");
    ATOM_MATCHING_FAILED = enif_make_atom(env, "matching_failed");
    ATOM_SERVICE_MATCHING_FAILED = enif_make_atom(env, "service_matching_failed");
    ATOM_NOT_FOUND = enif_make_atom(env, "not_found");

    return 0;
}

#ifdef __APPLE__
#include <IOKit/IOKitLib.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/network/IONetworkInterface.h>
#include <CoreFoundation/CoreFoundation.h>

// Apple's USB vendor ID.
#define APPLE_VENDOR_ID 0x05AC
// USB CDC-Data interface class (the NCM data interface on iOS devices).
#define CDC_DATA_CLASS 10

// Helper to convert CFString to C string
static int cf_string_to_c(CFStringRef cf_str, char *buffer, size_t buffer_size) {
    if (!cf_str) return 0;
    return CFStringGetCString(cf_str, buffer, buffer_size, kCFStringEncodingUTF8);
}

// Helper to create an Elixir binary from a C string
static ERL_NIF_TERM make_binary_string(ErlNifEnv *env, const char *str) {
    if (!str) {
        return enif_make_badarg(env);
    }
    size_t len = strlen(str);
    ERL_NIF_TERM bin;
    unsigned char *data = enif_make_new_binary(env, len, &bin);
    if (!data) {
        return enif_raise_exception(env, ATOM_ENOMEM);
    }
    memcpy(data, str, len);
    return bin;
}

// Helper to get a string property from an IOKit object
static int get_string_property(io_object_t obj, CFStringRef key, char *buffer, size_t buffer_size) {
    CFTypeRef value = IORegistryEntryCreateCFProperty(obj, key, kCFAllocatorDefault, 0);
    if (!value) return 0;

    int result = 0;
    if (CFGetTypeID(value) == CFStringGetTypeID()) {
        result = cf_string_to_c((CFStringRef)value, buffer, buffer_size);
    }
    CFRelease(value);
    return result;
}

// Helper to get an integer property from an IOKit object
static int get_int_property(io_object_t obj, CFStringRef key, int64_t *out_value) {
    CFTypeRef value = IORegistryEntryCreateCFProperty(obj, key, kCFAllocatorDefault, 0);
    if (!value) return 0;

    int result = 0;
    if (CFGetTypeID(value) == CFNumberGetTypeID()) {
        result = CFNumberGetValue((CFNumberRef)value, kCFNumberSInt64Type, out_value);
    }
    CFRelease(value);
    return result;
}

// Get the USB serial number for a network interface
static int get_serial_for_network_interface(const char *if_name, char *serial_out, size_t serial_size) {
    io_iterator_t iterator;
    io_object_t interface;
    kern_return_t kr;
    int found = 0;

    // Find the IOEthernetInterface with matching BSD Name
    CFMutableDictionaryRef matching = IOServiceMatching("IOEthernetInterface");
    if (!matching) return 0;

    kr = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator);
    if (kr != KERN_SUCCESS) return 0;

    while ((interface = IOIteratorNext(iterator)) != 0) {
        char bsd_name[64];
        if (get_string_property(interface, CFSTR("BSD Name"), bsd_name, sizeof(bsd_name))) {
            if (strcmp(bsd_name, if_name) == 0) {
                // Found the interface, now walk up to find USB device
                io_object_t parent = 0;
                io_object_t current = interface;
                IOObjectRetain(current);

                while (current) {
                    kr = IORegistryEntryGetParentEntry(current, kIOServicePlane, &parent);
                    IOObjectRelease(current);

                    if (kr != KERN_SUCCESS) break;

                    current = parent;

                    if (IOObjectConformsTo(current, "IOUSBHostDevice")) {
                        found = get_string_property(current, CFSTR("USB Serial Number"),
                                                    serial_out, serial_size);
                        IOObjectRelease(current);
                        break;
                    }
                }
            }
        }
        IOObjectRelease(interface);
        if (found) break;
    }

    IOObjectRelease(iterator);
    return found;
}

// Find a USB device by serial number or location ID
// Returns the device object (caller must release) or 0 if not found
static io_object_t find_usb_device(const char *serial, uint32_t location_id, int use_location) {
    io_iterator_t iterator;
    io_object_t device;
    kern_return_t kr;

    CFMutableDictionaryRef matching = IOServiceMatching("IOUSBHostDevice");
    if (!matching) return 0;

    kr = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator);
    if (kr != KERN_SUCCESS) return 0;

    while ((device = IOIteratorNext(iterator)) != 0) {
        if (use_location) {
            int64_t loc;
            if (get_int_property(device, CFSTR("locationID"), &loc)) {
                if ((uint32_t)loc == location_id) {
                    IOObjectRelease(iterator);
                    return device;
                }
            }
        } else {
            char dev_serial[256];
            if (get_string_property(device, CFSTR("USB Serial Number"), dev_serial, sizeof(dev_serial))) {
                if (strcmp(dev_serial, serial) == 0) {
                    IOObjectRelease(iterator);
                    return device;
                }
            }
        }
        IOObjectRelease(device);
    }

    IOObjectRelease(iterator);
    return 0;
}

// Find the BSD name of the IOEthernetInterface bound under a CDC-Data interface.
// The registry layout is: interface -> AppleUSBNCMData -> IOEthernetInterface.
// Writes "" into buffer if no network interface is bound yet.
static void find_ethernet_bsd_name(io_object_t interface, char *buffer, size_t buffer_size) {
    io_iterator_t child_iterator;
    io_object_t child;

    buffer[0] = '\0';

    if (IORegistryEntryGetChildIterator(interface, kIOServicePlane, &child_iterator) != KERN_SUCCESS) {
        return;
    }

    while ((child = IOIteratorNext(child_iterator)) != 0) {
        io_iterator_t grandchild_iterator;
        io_object_t grandchild;

        if (IORegistryEntryGetChildIterator(child, kIOServicePlane, &grandchild_iterator) == KERN_SUCCESS) {
            while ((grandchild = IOIteratorNext(grandchild_iterator)) != 0) {
                if (IOObjectConformsTo(grandchild, "IOEthernetInterface")) {
                    get_string_property(grandchild, CFSTR("BSD Name"), buffer, buffer_size);
                }
                IOObjectRelease(grandchild);
                if (buffer[0]) break;
            }
            IOObjectRelease(grandchild_iterator);
        }
        IOObjectRelease(child);
        if (buffer[0]) break;
    }

    IOObjectRelease(child_iterator);
}

// Build the %{interface_number => bsd_name | nil} map for a device by scanning
// its direct IOUSBHostInterface children for the CDC-Data class. When count_out
// is non-NULL, it receives the number of NCM data interfaces found.
static ERL_NIF_TERM build_ncm_interfaces_map(ErlNifEnv *env, io_object_t device, int *count_out) {
    ERL_NIF_TERM interfaces_map = enif_make_new_map(env);
    io_iterator_t child_iterator;
    io_object_t interface;
    int count = 0;

    if (IORegistryEntryGetChildIterator(device, kIOServicePlane, &child_iterator) == KERN_SUCCESS) {
        while ((interface = IOIteratorNext(child_iterator)) != 0) {
            int64_t interface_class = 0;

            if (IOObjectConformsTo(interface, "IOUSBHostInterface") &&
                get_int_property(interface, CFSTR("bInterfaceClass"), &interface_class) &&
                interface_class == CDC_DATA_CLASS) {
                int64_t interface_number = 0;
                get_int_property(interface, CFSTR("bInterfaceNumber"), &interface_number);

                char network_if_name[64];
                find_ethernet_bsd_name(interface, network_if_name, sizeof(network_if_name));

                ERL_NIF_TERM if_num_term = enif_make_int(env, (int)interface_number);
                ERL_NIF_TERM if_name_term = network_if_name[0]
                                                ? make_binary_string(env, network_if_name)
                                                : ATOM_NIL;

                enif_make_map_put(env, interfaces_map, if_num_term, if_name_term, &interfaces_map);
                count++;
            }

            IOObjectRelease(interface);
        }
        IOObjectRelease(child_iterator);
    }

    if (count_out) *count_out = count;
    return interfaces_map;
}

// Assemble the per-device result: %{configuration: N, interfaces: %{...}}
static ERL_NIF_TERM make_device_result(ErlNifEnv *env, io_object_t device, ERL_NIF_TERM interfaces_map) {
    int64_t configuration = 0;
    get_int_property(device, CFSTR("kUSBCurrentConfiguration"), &configuration);

    ERL_NIF_TERM keys[] = {ATOM_CONFIGURATION, ATOM_INTERFACES};
    ERL_NIF_TERM values[] = {enif_make_int(env, (int)configuration), interfaces_map};
    ERL_NIF_TERM result_map;
    enif_make_map_from_arrays(env, keys, values, 2, &result_map);
    return result_map;
}

// Whether the device reports Apple's USB vendor ID.
static int is_apple_vendor(io_object_t device) {
    int64_t vendor_id = 0;
    return get_int_property(device, CFSTR("idVendor"), &vendor_id) && vendor_id == APPLE_VENDOR_ID;
}

// NIF: list_ncm_data_interfaces(serial_or_location)
// Returns: {:ok, %{configuration: N, interfaces: %{num => name, ...}}} | {:error, reason}
static ERL_NIF_TERM list_ncm_data_interfaces_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
    char identifier[256];
    uint32_t location_id = 0;
    int use_location = 0;

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    // Check if it's an integer (location ID) or binary (serial)
    ErlNifBinary bin;
    if (enif_get_uint(env, argv[0], &location_id)) {
        use_location = 1;
    } else if (enif_inspect_binary(env, argv[0], &bin)) {
        if (bin.size >= sizeof(identifier)) {
            return enif_make_badarg(env);
        }
        memcpy(identifier, bin.data, bin.size);
        identifier[bin.size] = '\0';
    } else {
        return enif_make_badarg(env);
    }

    io_object_t usb_device = find_usb_device(identifier, location_id, use_location);
    if (!usb_device) {
        return enif_make_tuple2(env, ATOM_ERROR, ATOM_NOT_FOUND);
    }

    ERL_NIF_TERM interfaces_map = build_ncm_interfaces_map(env, usb_device, NULL);
    ERL_NIF_TERM result_map = make_device_result(env, usb_device, interfaces_map);

    IOObjectRelease(usb_device);

    return enif_make_tuple2(env, ATOM_OK, result_map);
}

// NIF: list_devices()
// Enumerates all connected iOS devices and their NCM interfaces in one pass.
// Returns: {:ok, %{serial => %{configuration: N, interfaces: %{...}}}} | {:error, reason}
static ERL_NIF_TERM list_devices_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
    (void)argv;

    if (argc != 0) {
        return enif_make_badarg(env);
    }

    CFMutableDictionaryRef matching = IOServiceMatching("IOUSBHostDevice");
    if (!matching) {
        return enif_make_tuple2(env, ATOM_ERROR, ATOM_MATCHING_FAILED);
    }

    io_iterator_t iterator;
    if (IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator) != KERN_SUCCESS) {
        return enif_make_tuple2(env, ATOM_ERROR, ATOM_SERVICE_MATCHING_FAILED);
    }

    ERL_NIF_TERM devices_map = enif_make_new_map(env);
    io_object_t device;

    while ((device = IOIteratorNext(iterator)) != 0) {
        if (is_apple_vendor(device)) {
            char serial[256];
            if (get_string_property(device, CFSTR("USB Serial Number"), serial, sizeof(serial))) {
                int iface_count = 0;
                ERL_NIF_TERM interfaces_map = build_ncm_interfaces_map(env, device, &iface_count);

                // Only list Apple devices that expose at least one NCM interface, so
                // peripherals like keyboards — and iOS devices that have not brought
                // NCM up yet — are excluded. Mirrors the Linux behavior.
                if (iface_count > 0) {
                    ERL_NIF_TERM serial_term = make_binary_string(env, serial);
                    ERL_NIF_TERM result_map = make_device_result(env, device, interfaces_map);
                    enif_make_map_put(env, devices_map, serial_term, result_map, &devices_map);
                }
            }
        }

        IOObjectRelease(device);
    }

    IOObjectRelease(iterator);

    return enif_make_tuple2(env, ATOM_OK, devices_map);
}

// NIF: get_usb_serial_for_interface(interface_name)
// Returns: {:ok, serial} | {:error, :not_found}
static ERL_NIF_TERM get_usb_serial_for_interface_nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
    char if_name[64];
    char serial[256];

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    ErlNifBinary bin;
    if (!enif_inspect_binary(env, argv[0], &bin)) {
        return enif_make_badarg(env);
    }

    if (bin.size >= sizeof(if_name)) {
        return enif_make_badarg(env);
    }
    memcpy(if_name, bin.data, bin.size);
    if_name[bin.size] = '\0';

    if (get_serial_for_network_interface(if_name, serial, sizeof(serial))) {
        return enif_make_tuple2(env, ATOM_OK, make_binary_string(env, serial));
    }

    return enif_make_tuple2(env, ATOM_ERROR, ATOM_NOT_FOUND);
}

#endif

#ifndef __APPLE__
// Empty NIF table for non-macOS - the NIF won't be loaded anyway
static ErlNifFunc nif_funcs[] = {};
#else

static ErlNifFunc nif_funcs[] = {
    {"list_ncm_data_interfaces", 1, list_ncm_data_interfaces_nif, 0},
    {"list_devices", 0, list_devices_nif, 0},
    {"get_serial_for_interface", 1, get_usb_serial_for_interface_nif, 0}
};
#endif

ERL_NIF_INIT(Elixir.Ifone.USB.Darwin, nif_funcs, nif_load, NULL, NULL, NULL)
