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

#include "secure_api.h"
#include "http_client.h"
#include "constants.h"
#include "crypto.h"
#include "cJSON.h"
#include "dev_name.h"
#include "storage.h"
#include "http_client.h"
#include "jwt.h"
#include "relays.h"

#include "esp_log.h"
#include "esp_app_desc.h"

static const char *TAG = "secure_api";

static char *process_secure_request(const char *uri, const char *body)
{
    char *url = calloc(1, 255);
    assert(url);
    size_t orig_len = storage_get_secure_origin(url);
    assert(orig_len > 0);
    strcat(url + strlen(url), uri);
    ESP_LOGI(TAG, "HTTP request on %s", url);

    char *root_cat = calloc(1, ROOT_CA_MAX_BYTES);
    assert(root_cat);
    assert(storage_get_root_ca(root_cat) > 0);

    http_request_opts opts = {
        .url = url,
        .root_ca = root_cat,
        .body = body,
        .method = body == NULL ? MethodGET : MethodPOST,
        .content_type = body == NULL ? NULL : "application/json"};
    char *res = http_client_exec(&opts);

    free(url);
    free(root_cat);

    return res;
}

static char *dev_escaped_name()
{
    unsigned char *name = (unsigned char *)dev_name();
    assert(name);
    size_t escaped_name_len = http_client_escape_uri(NULL, name, strlen((char *)name));
    unsigned char *escaped_name = calloc(1, escaped_name_len + 1);
    assert(escaped_name);
    http_client_escape_uri(escaped_name, name, strlen((char *)name));
    free(name);

    return (char *)escaped_name;
}

enum DevEnrollmentStatus secure_api_get_device_enrollment_status()
{
    ESP_LOGI(TAG, "Will check device enrollment status");

    // Prepare URI
    char *escaped_name = dev_escaped_name();
    char *uri = calloc(1, 255);
    assert(uri);
    sprintf(uri, "/devices_api/mgmt/enrollment_status?id=%s", escaped_name);
    free(escaped_name);

    char *res = process_secure_request(uri, NULL);

    free(uri);

    if (res == NULL)
    {
        ESP_LOGE(TAG, "Failed to query device enrollment status!");
        return DevEnrollError;
    }

    enum DevEnrollmentStatus s = DevEnrollError;
    cJSON *root = cJSON_Parse(res);
    if (root == NULL)
    {
        ESP_LOGE(TAG, "Failed to decode JSON response from server!");
        goto fail;
    }

    cJSON *status = cJSON_GetObjectItem(root, "status");
    if (status == NULL)
    {
        ESP_LOGE(TAG, "Status missing in response from server!");
        goto fail;
    }

    if (!strcmp(status->valuestring, "Unknown"))
        s = DevEnrollUnknown;
    else if (!strcmp(status->valuestring, "Pending"))
        s = DevEnrollPending;
    else if (!strcmp(status->valuestring, "Validated"))
        s = DevEnrollValidated;
    else
    {
        ESP_LOGE(TAG, "Unknown enrollment status: %s", status->valuestring);
        goto fail;
    }

fail:

    cJSON_Delete(root);
    free(res);

    return s;
}

/**
 * Generate device information. Pointer to be released by caller
 */
static cJSON *genDevInfo()
{
    const esp_app_desc_t *desc = esp_app_get_description();

    cJSON *json = cJSON_CreateObject();
    if (!json)
        return NULL;
    cJSON_AddStringToObject(json, "reference", DEV_REFERENCE);
    cJSON_AddStringToObject(json, "version", desc->version);
    cJSON_AddNumberToObject(json, "max_relays", relays_count());
    return json;
}

int secure_api_enroll_device()
{
    char *csr = crypto_get_csr();
    if (!csr)
    {
        ESP_LOGE(TAG, "Failed to get CSR!");
        return 1;
    }

    cJSON *obj = cJSON_CreateObject();
    if (!obj)
    {
        ESP_LOGE(TAG, "Failed allocate memory to store JSON object!");
        free(csr);
        return 1;
    }

    cJSON_AddItemToObject(obj, "info", genDevInfo());
    cJSON_AddStringToObject(obj, "csr", csr);
    free(csr);

    char *body = cJSON_PrintUnformatted(obj);
    cJSON_Delete(obj);

    if (!body)
    {
        ESP_LOGE(TAG, "Failed to generate JSON body!");
        return 1;
    }

    char *res = process_secure_request("/devices_api/mgmt/enroll", body);

    free(body);

    if (res == NULL)
    {
        ESP_LOGE(TAG, "Request failed!");
        return 1;
    }

    free(res);

    return 0;
}

char *secure_api_get_dev_certificate()
{
    ESP_LOGI(TAG, "Will request device certificate");

    // Prepare URI
    char *escaped_name = dev_escaped_name();
    char *uri = calloc(1, 255);
    assert(uri);
    sprintf(uri, "/devices_api/mgmt/get_certificate?id=%s", escaped_name);
    free(escaped_name);

    char *res = process_secure_request(uri, NULL);

    free(uri);

    if (res == NULL)
    {
        ESP_LOGE(TAG, "Failed to query device certificate!");
        return NULL;
    }

    return res;
}

void secure_api_report_log_message(enum LogMessageSeverity severity, const char *msg)
{
    // Prepare signed payload
    cJSON *obj = cJSON_CreateObject();
    if (!obj)
    {
        ESP_LOGE(TAG, "Failed allocate memory to store JSON object!");
        return;
    }

    char *severity_s;
    switch (severity)
    {
    case Info:
        severity_s = "Info";
        break;
    case Warn:
        severity_s = "Warn";
        break;
    case Error:
        severity_s = "Error";
        break;
    default:
        severity_s = "Debug";
        break;
    }

    cJSON_AddStringToObject(obj, "severity", severity_s);
    cJSON_AddStringToObject(obj, "message", msg);
    char *payload = jwt_gen(obj);
    cJSON_Delete(obj);

    if (!payload)
    {
        ESP_LOGE(TAG, "Failed to build log report request!");
        return;
    }

    // Prepare request body
    cJSON *json_body = cJSON_CreateObject();
    if (!json_body)
    {
        ESP_LOGE(TAG, "Failed to allocated memory to store log report request body!");
        free(payload);
        return;
    }
    cJSON_AddStringToObject(json_body, "payload", payload);
    free(payload);

    char *body = cJSON_PrintUnformatted(json_body);
    cJSON_Delete(json_body);

    if (!body)
    {
        ESP_LOGE(TAG, "Failed to allocated memory to store encoded log report request body!");
        return;
    }

    // Send request
    char *res = process_secure_request("/devices_api/logging/record", body);

    free(body);

    if (!res)
    {
        ESP_LOGE(TAG, "Log reporting failed!");
    }

    free(res);
}

sync_response *secure_api_sync_device()
{
    cJSON *obj = cJSON_CreateObject();
    if (!obj)
    {
        ESP_LOGE(TAG, "Failed allocate memory to store JSON object!");
        return NULL;
    }

    cJSON_AddItemToObject(obj, "info", genDevInfo());

    char *encoded_req = jwt_gen(obj);
    cJSON_Delete(obj);

    if (!encoded_req)
    {
        ESP_LOGE(TAG, "Failed to encode JWT!");
        return NULL;
    }

    // Prepare request body
    cJSON *json_body = cJSON_CreateObject();
    if (!json_body)
    {
        ESP_LOGE(TAG, "Failed to allocated memory to store sync request body!");
        free(encoded_req);
        return NULL;
    }
    cJSON_AddStringToObject(json_body, "payload", encoded_req);
    free(encoded_req);

    char *body = cJSON_PrintUnformatted(json_body);
    cJSON_Delete(json_body);

    if (!body)
    {
        ESP_LOGE(TAG, "Failed to allocated memory to store encoded sync request body!");
        return NULL;
    }

    // Send request
    char *res = process_secure_request("/devices_api/mgmt/sync", body);
    free(body);
    if (res == NULL)
    {
        ESP_LOGE(TAG, "Sync request failed!");
        return NULL;
    }

    // Parse response
    cJSON *states = cJSON_Parse(res);
    free(res);

    if (!states)
    {
        ESP_LOGE(TAG, "Failed to decode sync response from server!");
        return NULL;
    }

    sync_response *sync_res = sync_response_parse(states);

    cJSON_Delete(states);

    if (!sync_res)
    {
        ESP_LOGE(TAG, "Failed to parse sync response from server!");
        return NULL;
    }

    return sync_res;
}