// 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>

// 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;
}

// Find the parent USB device for an interface and check if serial matches
static int interface_matches_serial(io_object_t interface, const char *target_serial) {
    io_object_t parent = 0;
    kern_return_t kr;
    char serial[256];

    // Walk up to find the USB device parent
    io_object_t current = interface;
    IOObjectRetain(current);

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

        if (kr != KERN_SUCCESS) break;

        current = parent;

        // Check if this is an IOUSBHostDevice
        if (IOObjectConformsTo(current, "IOUSBHostDevice")) {
            if (get_string_property(current, CFSTR("USB Serial Number"), serial, sizeof(serial))) {
                IOObjectRelease(current);
                return strcmp(serial, target_serial) == 0;
            }
        }
    }

    return 0;
}

// Find the parent USB device for an interface by location ID
static int interface_matches_location(io_object_t interface, uint32_t target_location) {
    io_object_t parent = 0;
    kern_return_t kr;
    int64_t location;

    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")) {
            if (get_int_property(current, CFSTR("locationID"), &location)) {
                IOObjectRelease(current);
                return (uint32_t)location == target_location;
            }
        }
    }

    return 0;
}

// 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;
}

// 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);
    }

    // Find the USB device first
    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);
    }

    // Get the current configuration
    int64_t configuration = 0;
    get_int_property(usb_device, CFSTR("kUSBCurrentConfiguration"), &configuration);

    IOObjectRelease(usb_device);

    // Now find all matching interfaces
    io_iterator_t iterator;
    io_object_t interface;
    kern_return_t kr;

    ERL_NIF_TERM interfaces_map = enif_make_new_map(env);

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

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

    while ((interface = IOIteratorNext(iterator)) != 0) {
        int64_t interface_class = 0;
        int64_t interface_number = 0;

        // Check if this is a CDC Data interface (class 0x0A = 10)
        if (!get_int_property(interface, CFSTR("bInterfaceClass"), &interface_class) ||
            interface_class != 10) {
            IOObjectRelease(interface);
            continue;
        }

        // Check if this interface belongs to our target device
        int matches = 0;
        if (use_location) {
            matches = interface_matches_location(interface, location_id);
        } else {
            matches = interface_matches_serial(interface, identifier);
        }

        if (!matches) {
            IOObjectRelease(interface);
            continue;
        }

        // Get interface number
        get_int_property(interface, CFSTR("bInterfaceNumber"), &interface_number);

        // Find the network interface child (IOEthernetInterface)
        char network_if_name[64] = "";
        io_iterator_t child_iterator;
        io_object_t child;

        kr = IORegistryEntryGetChildIterator(interface, kIOServicePlane, &child_iterator);
        if (kr == KERN_SUCCESS) {
            while ((child = IOIteratorNext(child_iterator)) != 0) {
                // Look for AppleUSBNCMData or similar, then its IOEthernetInterface child
                io_iterator_t grandchild_iterator;
                io_object_t grandchild;

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

        // Add to interfaces map: interface_number => network_if_name
        ERL_NIF_TERM if_num_term = enif_make_int(env, (int)interface_number);
        ERL_NIF_TERM if_name_term;
        if (network_if_name[0]) {
            if_name_term = make_binary_string(env, network_if_name);
        } else {
            if_name_term = ATOM_NIL;
        }

        enif_make_map_put(env, interfaces_map, if_num_term, if_name_term, &interfaces_map);

        IOObjectRelease(interface);
    }

    IOObjectRelease(iterator);

    // Build result map: %{configuration: N, interfaces: %{...}}
    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 enif_make_tuple2(env, ATOM_OK, result_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},
    {"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)
