#include "pcsc.h"

#include <cjson/cJSON_ex.h>
#include <euicc/interface.h>
#include <lpac/utils.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifdef _WIN32
#    include "pcsc_win32.h"
#    include <winscard.h>
#else
#    include <PCSC/winscard.h>
#    include <PCSC/wintypes.h>
#endif

#define ENV_DRV_IFID APDU_ENV_NAME(PCSC, DRV_IFID)
#define ENV_DRV_NAME APDU_ENV_NAME(PCSC, DRV_NAME)
#define ENV_DRV_IGNORE_NAME APDU_ENV_NAME(PCSC, DRV_IGNORE_NAME)

#define EUICC_INTERFACE_BUFSZ 264

// #define APDU_ST33_MAGIC "\x90\xBD\x36\xBB\x00"
#define APDU_TERMINAL_CAPABILITIES "\x80\xAA\x00\x00\x0A\xA9\x08\x81\x00\x82\x01\x01\x83\x01\x07"
#define APDU_OPENLOGICCHANNEL "\x00\x70\x00\x00\x01"
#define APDU_CLOSELOGICCHANNEL "\x00\x70\x80\xFF\x00"
#define APDU_SELECT_HEADER "\x00\xA4\x04\x00\xFF"

static SCARDCONTEXT pcsc_ctx;
static SCARDHANDLE pcsc_hCard;
static LPSTR pcsc_mszReaders;

static void pcsc_error(const char *method, const int32_t code) {
    fprintf(stderr, "%s failed: %08X (%s)\n", method, code, pcsc_stringify_error(code));
}

static bool is_ignored_reader_name(const char *reader) {
    char *value = getenv(ENV_DRV_IGNORE_NAME);
    if (value == NULL)
        return false;
    const char *token = NULL;
    for (token = strtok(value, ";"); token != NULL; token = strtok(NULL, ";")) {
        if (strstr(reader, token) == NULL)
            continue;
        return true; // reader name is in ignore list, skip
    }
    return false;
}

static int pcsc_ctx_open(void) {
    int ret;
    DWORD dwReaders;

    pcsc_ctx = 0;
    pcsc_hCard = 0;
    pcsc_mszReaders = NULL;

    ret = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &pcsc_ctx);
    if (ret != SCARD_S_SUCCESS) {
        pcsc_error("SCardEstablishContext()", ret);
        return -1;
    }

#ifdef SCARD_AUTOALLOCATE
    dwReaders = SCARD_AUTOALLOCATE;
    ret = SCardListReaders(pcsc_ctx, NULL, (LPSTR)&pcsc_mszReaders, &dwReaders);
#else
    // macOS does not support SCARD_AUTOALLOCATE, so we need to call SCardListReaders twice.
    // First call to get the size of the buffer, second call to get the actual data.
    ret = SCardListReaders(pcsc_ctx, NULL, NULL, &dwReaders);
    if (ret != SCARD_S_SUCCESS) {
        pcsc_error("SCardListReaders()", ret);
        return -1;
    }
    pcsc_mszReaders = malloc(sizeof(char) * dwReaders);
    if (pcsc_mszReaders == NULL) {
        fprintf(stderr, "malloc: not enough memory\n");
        return -1;
    }
    ret = SCardListReaders(pcsc_ctx, NULL, pcsc_mszReaders, &dwReaders);
#endif
    if (ret != SCARD_S_SUCCESS) {
        pcsc_error("SCardListReaders()", ret);
        return -1;
    }

    return 0;
}

static int pcsc_iter_reader(int (*callback)(int index, const char *reader, void *userdata), void *userdata) {
    int ret;
    LPSTR psReader;

    psReader = pcsc_mszReaders;
    for (int i = 0, n = 0;; i++) {
        char *p = pcsc_mszReaders + i;
        if (*p == '\0') {
            ret = callback(n, psReader, userdata);
            if (ret < 0)
                return -1;
            if (ret > 0)
                return 0;
            if (*(p + 1) == '\0') {
                break;
            }
            psReader = p + 1;
            n++;
        }
    }
    return -1;
}

static int pcsc_open_hCard_iter(int index, const char *reader, void *userdata) {
    DWORD dwActiveProtocol;

    const int id = getenv_or_default(ENV_DRV_IFID, (int)-1);
    if (id != -1 && id != index) {
        const char *part_name = getenv(ENV_DRV_NAME);
        if (strstr(reader, part_name) == NULL) {
            return 0;
        }
    }

    if (is_ignored_reader_name(reader)) {
        return 0; // skip ignored reader names
    }

    const int ret =
        SCardConnect(pcsc_ctx, reader, SCARD_SHARE_EXCLUSIVE, SCARD_PROTOCOL_T0, &pcsc_hCard, &dwActiveProtocol);
    if (ret != SCARD_S_SUCCESS) {
        pcsc_error("SCardConnect()", ret);
        // see <https://blog.apdu.fr/posts/2024/12/gnupg-and-pcsc-conflicts-episode-3/>
        if (ret == SCARD_E_SHARING_VIOLATION)
            return 0; // skip
        return -1;
    }

    return 1;
}

static int pcsc_open_hCard(void) { return pcsc_iter_reader(pcsc_open_hCard_iter, NULL); }

static void pcsc_close(void) {
    if (pcsc_mszReaders) {
// macOS does not support SCARD_AUTOALLOCATE, so we need to free the buffer manually.
#ifdef SCARD_AUTOALLOCATE
        SCardFreeMemory(pcsc_ctx, pcsc_mszReaders);
#else
        // on macOS, pcsc_mszReaders is allocated by malloc()
        free(pcsc_mszReaders);
#endif
    }

    if (pcsc_hCard) {
        SCardDisconnect(pcsc_hCard, SCARD_UNPOWER_CARD);
    }
    if (pcsc_ctx) {
        SCardReleaseContext(pcsc_ctx);
    }
    pcsc_ctx = 0;
    pcsc_hCard = 0;
    pcsc_mszReaders = NULL;
}

static int pcsc_transmit_lowlevel(uint8_t *rx, uint32_t *rx_len, const uint8_t *tx, const uint8_t tx_len) {
    int ret;
    DWORD rx_len_merged;

    rx_len_merged = *rx_len;
    ret = SCardTransmit(pcsc_hCard, SCARD_PCI_T0, tx, tx_len, NULL, rx, &rx_len_merged);
    if (ret != SCARD_S_SUCCESS) {
        pcsc_error("SCardTransmit()", ret);
        return -1;
    }

    *rx_len = rx_len_merged;
    return 0;
}

static void pcsc_logic_channel_close(uint8_t channel) {
    uint8_t tx[sizeof(APDU_CLOSELOGICCHANNEL) - 1];
    uint8_t rx[EUICC_INTERFACE_BUFSZ];
    uint32_t rx_len;

    memcpy(tx, APDU_CLOSELOGICCHANNEL, sizeof(tx));
    tx[3] = channel;

    rx_len = sizeof(rx);

    pcsc_transmit_lowlevel(rx, &rx_len, tx, sizeof(tx));
}

static int pcsc_logic_channel_open(const uint8_t *aid, uint8_t aid_len) {
    int channel = 0;
    uint8_t tx[EUICC_INTERFACE_BUFSZ];
    uint8_t *tx_wptr;
    uint8_t rx[EUICC_INTERFACE_BUFSZ];
    uint32_t rx_len;

    if (aid_len > 32) {
        goto err;
    }

    rx_len = sizeof(rx);
    if (pcsc_transmit_lowlevel(rx, &rx_len, (const uint8_t *)APDU_OPENLOGICCHANNEL, sizeof(APDU_OPENLOGICCHANNEL) - 1)
        < 0) {
        goto err;
    }

    if (rx_len != 3) {
        goto err;
    }

    if ((rx[1] & 0xF0) != 0x90) {
        goto err;
    }

    channel = rx[0];

    tx_wptr = tx;
    memcpy(tx_wptr, APDU_SELECT_HEADER, sizeof(APDU_SELECT_HEADER) - 1);
    tx_wptr += sizeof(APDU_SELECT_HEADER) - 1;
    memcpy(tx_wptr, aid, aid_len);
    tx_wptr += aid_len;

    tx[0] = (tx[0] & 0xF0) | channel;
    tx[4] = aid_len;

    rx_len = sizeof(rx);
    if (pcsc_transmit_lowlevel(rx, &rx_len, tx, tx_wptr - tx) < 0) {
        goto err;
    }

    if (rx_len < 2) {
        goto err;
    }

    switch (rx[rx_len - 2]) {
    case 0x90:
    case 0x61:
        return channel;
    default:
        goto err;
    }

err:
    if (channel) {
        pcsc_logic_channel_close(channel);
    }

    return -1;
}

static int apdu_interface_connect(struct euicc_ctx *ctx) {
    uint8_t rx[EUICC_INTERFACE_BUFSZ];
    uint32_t rx_len;

    if (pcsc_open_hCard() < 0) {
        return -1;
    }

    rx_len = sizeof(rx);
    pcsc_transmit_lowlevel(rx, &rx_len, (const uint8_t *)APDU_TERMINAL_CAPABILITIES,
                           sizeof(APDU_TERMINAL_CAPABILITIES) - 1);

    return 0;
}

static void apdu_interface_disconnect(struct euicc_ctx *ctx) { pcsc_close(); }

static int apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx,
                                   uint32_t tx_len) {
    *rx = malloc(EUICC_INTERFACE_BUFSZ);
    if (!*rx) {
        fprintf(stderr, "SCardTransmit() RX buffer alloc failed\n");
        return -1;
    }
    *rx_len = EUICC_INTERFACE_BUFSZ;

    if (pcsc_transmit_lowlevel(*rx, rx_len, tx, tx_len) < 0) {
        free(*rx);
        *rx_len = 0;
        return -1;
    }

    return 0;
}

static int apdu_interface_logic_channel_open(struct euicc_ctx *ctx, const uint8_t *aid, uint8_t aid_len) {
    return pcsc_logic_channel_open(aid, aid_len);
}

static void apdu_interface_logic_channel_close(struct euicc_ctx *ctx, uint8_t channel) {
    pcsc_logic_channel_close(channel);
}

static int pcsc_list_iter(int index, const char *reader, void *userdata) {
    cJSON *json = userdata;
    cJSON *jreader;
    char index_str[16];

    snprintf(index_str, sizeof(index_str), "%d", index);

    jreader = cJSON_CreateObject();
    if (!jreader) {
        return -1;
    }

    if (!cJSON_AddStringOrNullToObject(jreader, "env", index_str)) {
        return -1;
    }

    if (!cJSON_AddStringOrNullToObject(jreader, "name", reader)) {
        return -1;
    }

    if (!cJSON_AddItemToArray(json, jreader)) {
        return -1;
    }

    return 0;
}

static int libapduinterface_init(struct euicc_apdu_interface *ifstruct) {
    set_deprecated_env_name(ENV_DRV_IFID, "DRIVER_IFID");
    set_deprecated_env_name(ENV_DRV_NAME, "DRIVER_NAME");

    memset(ifstruct, 0, sizeof(struct euicc_apdu_interface));

    if (pcsc_ctx_open() < 0) {
        return -1;
    }

    ifstruct->connect = apdu_interface_connect;
    ifstruct->disconnect = apdu_interface_disconnect;
    ifstruct->logic_channel_open = apdu_interface_logic_channel_open;
    ifstruct->logic_channel_close = apdu_interface_logic_channel_close;
    ifstruct->transmit = apdu_interface_transmit;

    return 0;
}

static int libapduinterface_main(int argc, char **argv) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <list>\n", argv[0]);
        return -1;
    }

    if (strcmp(argv[1], "list") == 0) {
        cJSON *payload;
        cJSON *data;

        payload = cJSON_CreateObject();
        if (!payload) {
            return -1;
        }

        if (!cJSON_AddStringOrNullToObject(payload, "env", ENV_DRV_IFID)) {
            return -1;
        }

        data = cJSON_CreateArray();
        if (!data) {
            return -1;
        }

        pcsc_iter_reader(pcsc_list_iter, data);

        if (!cJSON_AddItemToObject(payload, "data", data)) {
            return -1;
        }

        json_print("driver", payload);

        return 0;
    }

    return 0;
}

static void libapduinterface_fini(struct euicc_apdu_interface *ifstruct) {}

const struct euicc_driver driver_apdu_pcsc = {
    .type = DRIVER_APDU,
    .name = "pcsc",
    .init = (int (*)(void *))libapduinterface_init,
    .main = libapduinterface_main,
    .fini = (void (*)(void *))libapduinterface_fini,
};
