/*
 * SPDX-License-Identifier: 0BSD
 */

#define _GNU_SOURCE

#include "nsigner_client.h"

#include <arpa/inet.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>

#include <cJSON.h>

#include "auth_envelope.h"

#define NSIGNER_CLIENT_MAX_FRAME (65536U)

static void client_set_error(nsigner_client_t *client, const char *fmt, ...) {
    va_list ap;

    if (client == NULL) {
        return;
    }

    va_start(ap, fmt);
    vsnprintf(client->last_error, sizeof(client->last_error), fmt, ap);
    va_end(ap);
}

static int write_full(int fd, const void *buf, size_t len) {
    const unsigned char *p = (const unsigned char *)buf;
    size_t off = 0;

    while (off < len) {
        ssize_t n = write(fd, p + off, len - off);
        if (n < 0) {
            if (errno == EINTR) {
                continue;
            }
            return -1;
        }
        off += (size_t)n;
    }

    return 0;
}

static int read_full(int fd, void *buf, size_t len) {
    unsigned char *p = (unsigned char *)buf;
    size_t off = 0;

    while (off < len) {
        ssize_t n = read(fd, p + off, len - off);
        if (n == 0) {
            return -1;
        }
        if (n < 0) {
            if (errno == EINTR) {
                continue;
            }
            return -1;
        }
        off += (size_t)n;
    }

    return 0;
}

static int send_framed(int fd, const char *payload) {
    uint32_t len;
    uint32_t be_len;

    if (payload == NULL) {
        return -1;
    }

    len = (uint32_t)strlen(payload);
    be_len = htonl(len);

    if (write_full(fd, &be_len, sizeof(be_len)) != 0) {
        return -1;
    }
    if (write_full(fd, payload, len) != 0) {
        return -1;
    }

    return 0;
}

static int recv_framed(int fd, char **out_payload) {
    uint32_t be_len;
    uint32_t len;
    char *payload;

    if (out_payload == NULL) {
        return -1;
    }

    *out_payload = NULL;

    if (read_full(fd, &be_len, sizeof(be_len)) != 0) {
        return -1;
    }

    len = ntohl(be_len);
    if (len == 0 || len > NSIGNER_CLIENT_MAX_FRAME) {
        return -1;
    }

    payload = (char *)malloc((size_t)len + 1U);
    if (payload == NULL) {
        return -1;
    }

    if (read_full(fd, payload, len) != 0) {
        free(payload);
        return -1;
    }

    payload[len] = '\0';
    *out_payload = payload;
    return 0;
}

static int sleep_ms(int ms) {
    struct timespec ts;
    ts.tv_sec = ms / 1000;
    ts.tv_nsec = (long)(ms % 1000) * 1000000L;
    return nanosleep(&ts, NULL);
}

void nsigner_client_init(nsigner_client_t *client) {
    if (client == NULL) {
        return;
    }

    memset(client, 0, sizeof(*client));
    client->fd = -1;
    client->timeout_ms = 5000;
}

void nsigner_client_close(nsigner_client_t *client) {
    if (client == NULL) {
        return;
    }

    if (client->fd >= 0) {
        close(client->fd);
        client->fd = -1;
    }
}

int nsigner_client_connect_unix(nsigner_client_t *client,
                                const char *socket_name,
                                int timeout_ms) {
    struct sockaddr_un addr;
    socklen_t addr_len;
    int elapsed = 0;

    if (client == NULL || socket_name == NULL || socket_name[0] == '\0') {
        return -1;
    }

    timeout_ms = (timeout_ms > 0) ? timeout_ms : 5000;
    nsigner_client_close(client);

    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    addr.sun_path[0] = '\0';
    strncpy(&addr.sun_path[1], socket_name, sizeof(addr.sun_path) - 2);
    addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
    addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + strlen(socket_name));

    while (elapsed < timeout_ms) {
        int fd = socket(AF_UNIX, SOCK_STREAM, 0);
        if (fd < 0) {
            client_set_error(client, "socket failed: %s", strerror(errno));
            return -1;
        }

        if (connect(fd, (struct sockaddr *)&addr, addr_len) == 0) {
            client->fd = fd;
            client->timeout_ms = timeout_ms;
            client_set_error(client, "ok");
            return 0;
        }

        close(fd);
        sleep_ms(100);
        elapsed += 100;
    }

    client_set_error(client, "connect timeout: %s", strerror(errno));
    return -1;
}

int nsigner_client_set_auth(nsigner_client_t *client,
                            const unsigned char privkey[32],
                            const char *label) {
    if (client == NULL || privkey == NULL) {
        return -1;
    }

    memcpy(client->auth_privkey, privkey, 32);
    client->auth_enabled = 1;

    if (label == NULL) {
        label = "";
    }
    strncpy(client->auth_label, label, sizeof(client->auth_label) - 1);
    client->auth_label[sizeof(client->auth_label) - 1] = '\0';

    return 0;
}

const char *nsigner_client_last_error(const nsigner_client_t *client) {
    if (client == NULL) {
        return "invalid_client";
    }
    return client->last_error;
}

int nsigner_client_request_raw(nsigner_client_t *client,
                               const char *request_json,
                               char **out_response_json) {
    char *response_json = NULL;

    if (client == NULL || request_json == NULL || out_response_json == NULL) {
        return -1;
    }
    if (client->fd < 0) {
        client_set_error(client, "not connected");
        return -1;
    }

    *out_response_json = NULL;

    if (send_framed(client->fd, request_json) != 0) {
        client_set_error(client, "send failed");
        return -1;
    }

    if (recv_framed(client->fd, &response_json) != 0) {
        client_set_error(client, "receive failed");
        return -1;
    }

    *out_response_json = response_json;
    client_set_error(client, "ok");
    return 0;
}

int nsigner_client_request(nsigner_client_t *client,
                           const char *id,
                           const char *method,
                           const cJSON *params,
                           char **out_response_json) {
    cJSON *root = NULL;
    cJSON *params_copy = NULL;
    cJSON *auth = NULL;
    char *request_json = NULL;
    int rc = -1;

    if (client == NULL || id == NULL || method == NULL || out_response_json == NULL) {
        return -1;
    }

    root = cJSON_CreateObject();
    if (root == NULL) {
        client_set_error(client, "out of memory");
        goto cleanup;
    }

    cJSON_AddStringToObject(root, "id", id);
    cJSON_AddStringToObject(root, "method", method);

    if (params != NULL) {
        params_copy = cJSON_Duplicate((cJSON *)params, 1);
    } else {
        params_copy = cJSON_CreateArray();
    }

    if (params_copy == NULL) {
        client_set_error(client, "failed to create params");
        goto cleanup;
    }
    cJSON_AddItemToObject(root, "params", params_copy);

    if (client->auth_enabled) {
        if (auth_envelope_build_for_request(id,
                                            method,
                                            params_copy,
                                            client->auth_privkey,
                                            client->auth_label,
                                            time(NULL),
                                            &auth) != 0) {
            client_set_error(client, "failed to build auth envelope");
            goto cleanup;
        }
        cJSON_AddItemToObject(root, "auth", auth);
        auth = NULL;
    }

    request_json = cJSON_PrintUnformatted(root);
    if (request_json == NULL) {
        client_set_error(client, "failed to serialize request");
        goto cleanup;
    }

    rc = nsigner_client_request_raw(client, request_json, out_response_json);

cleanup:
    cJSON_Delete(root);
    cJSON_Delete(auth);
    free(request_json);
    return rc;
}

int nsigner_client_get_public_key(nsigner_client_t *client,
                                  const char *id,
                                  const char *selector,
                                  char **out_response_json) {
    cJSON *params = cJSON_CreateArray();
    int rc;

    if (params == NULL) {
        return -1;
    }

    cJSON_AddItemToArray(params, cJSON_CreateString((selector != NULL) ? selector : ""));
    rc = nsigner_client_request(client, id, "get_public_key", params, out_response_json);
    cJSON_Delete(params);
    return rc;
}

int nsigner_client_sign_event(nsigner_client_t *client,
                              const char *id,
                              const char *event_json,
                              const char *role,
                              char **out_response_json) {
    cJSON *params = cJSON_CreateArray();
    cJSON *opts = cJSON_CreateObject();
    int rc;

    if (params == NULL || opts == NULL || event_json == NULL) {
        cJSON_Delete(params);
        cJSON_Delete(opts);
        return -1;
    }

    cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
    if (role != NULL && role[0] != '\0') {
        cJSON_AddStringToObject(opts, "role", role);
    }
    cJSON_AddItemToArray(params, opts);

    rc = nsigner_client_request(client, id, "sign_event", params, out_response_json);
    cJSON_Delete(params);
    return rc;
}
