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

#include <cache.h>
#include <cJSON.h>
#include <logger.h>
#include <database.h>
#include <status.h>
#include <models/relay.h>
#include <models/controller.h>
#include <models/schedule.h>
#include <models/junction_tag.h>
#include <models/junction_relay_schedule.h>
#include <models/tag.h>

static int
db_update_insert(relay_t *relay, sqlite3_stmt *stmt)
{
    LOGGER_DEBUG("saving relay '%s' into database (id: %d)\n", relay->name, relay->id);
    int rc;

    sqlite3_bind_int(stmt, 1, relay->id);
    sqlite3_bind_int(stmt, 2, relay->number);
    sqlite3_bind_text(stmt, 3, relay->name, -1, SQLITE_STATIC);
    sqlite3_bind_int(stmt, 4, relay->controller_id);

    rc = sqlite3_step(stmt);

    sqlite3_finalize(stmt);

    return rc != SQLITE_DONE;
}
static relay_t*
relay_db_select_mapper(sqlite3_stmt *stmt)
{
    relay_t *new_relay = malloc(sizeof(relay_t));
    new_relay->is_on = 0;

    for(int i = 0; i < sqlite3_column_count(stmt); i++)
    {
        const char *name = sqlite3_column_name(stmt, i);
        switch(name[0])
        {
            case 'c': // controller_id
                new_relay->controller_id = sqlite3_column_int(stmt, i);
                break;
            case 'i':
                new_relay->id = sqlite3_column_int(stmt, i);
                break;
            case 'n':
                switch(name[1])
                {
                    case 'a': // name
                        strncpy(new_relay->name, (const char*)sqlite3_column_text(stmt, i), 127);
                        break;
                    case 'u': // number
                        new_relay->number = sqlite3_column_int(stmt, i);
                        break;
                    default:
                        break;
                }
                break;
            default: // ignore columns not implemented
                break;
        }
    }

    schedule_t **schedules = schedule_get_relay_weekdays(new_relay->id);
    for(int i = 0; i < 7; ++i)
    {
        if(schedules[i] == NULL)
        {
            LOGGER_ERR("got only %d/7 schedules for relay_id %d\n", i, new_relay->id);
            relay_free(new_relay);
            free(schedules);
            return NULL;
        }
        new_relay->schedules[i] = schedules[i];
    }
    free(schedules); // don't free list, because contents are kept in relay->schedules

    relay_reload_active_schedule(new_relay);

    return new_relay;
}

static relay_t**
relay_db_select(sqlite3_stmt *stmt)
{
    relay_t **all_relays = malloc(sizeof(relay_t*));

    int row = 0;

    while(true)
    {
        int s;

        s = sqlite3_step(stmt);
        if (s == SQLITE_ROW)
        {
            relay_t *new_relay = relay_db_select_mapper(stmt);
            row++;

            all_relays = (relay_t**)realloc(all_relays, sizeof(relay_t*) * (row + 1));
            all_relays[row - 1] = new_relay;
        }
        else
        {
            if(s == SQLITE_DONE)
            {
                break;
            }
            else
            {
                LOGGER_ERR("error selecting relays from database: %s\n", sqlite3_errstr(s));
                break;
            }
        }
    }
    sqlite3_finalize(stmt);
    all_relays[row] = NULL;
    return all_relays;
}

int
relay_save(relay_t *relay)
{
    sqlite3_stmt *stmt;
    if(relay->id)
    {
        sqlite3_prepare_v2(global_database, "UPDATE relays set number = ?2, name = ?3, controller_id = ?4 WHERE id = ?1;", -1, &stmt, NULL);
    }
    else
    {
        sqlite3_prepare_v2(global_database, "INSERT INTO relays(number, name, controller_id) values (?2, ?3, ?4);", -1, &stmt, NULL);
    }

    int result = db_update_insert(relay, stmt);

    if(result)
    {
        if(relay->id)
        {
            LOGGER_ERR("error inserting data: %s\n", sqlite3_errmsg(global_database));
        }
        else
        {
            LOGGER_ERR("error updating data: %s\n", sqlite3_errmsg(global_database));
        }
    }
    else
    {
        if(relay->id == 0)
        {
            relay->id = sqlite3_last_insert_rowid(global_database);
        }
    }

    junction_relay_schedule_remove_for_relay(relay->id);
    for(int i = 0; i < 7; ++i)
    {
        junction_relay_schedule_insert(i, relay->id, relay->schedules[i]->id);
    }

    cache_invalidate_relay(relay->id);
    status_reload_entry(relay->id);

    return result;
}

void
relay_reload_active_schedule(relay_t *relay)
{
    time_t timestamp = time(NULL);
    struct tm *time_struct = localtime(&timestamp);
    relay->active_schedule = relay->schedules[helper_get_weekday(time_struct)];
}

void
relay_free(relay_t *relay)
{
    for(int i = 0; i < 7; ++i)
    {
        schedule_free(relay->schedules[i]);
    }
    free(relay);
}

void
relay_free_list(relay_t **relays)
{
    for(int i = 0; relays[i] != NULL; ++i)
    {
        relay_free(relays[i]);
    }
    free(relays);
}

cJSON*
relay_to_json(relay_t *relay)
{
    cJSON *json;

    char *cached = cache_get_json_relay(relay->id);
    if(cached)
    {
        json = cJSON_CreateRaw(cached);
        free(cached);
        return json;
    }

    controller_t *controller = controller_get_by_id(relay->controller_id);
    if(!controller)
    {
        LOGGER_WARNING("failed to get controller\n");
        return NULL;
    }

    char uuid_str[UUID_STR_LEN];
    uuid_unparse(controller->uid, uuid_str);
    LOGGER_DEBUG("JSONifying relay %s:%d\n", uuid_str, relay->number);

    relay_reload_active_schedule(relay);

    json = cJSON_CreateObject();

    cJSON *json_number = cJSON_CreateNumber(relay->number);
    if(json_number == NULL)
    {
        LOGGER_DEBUG("failed to make number\n");
        cJSON_Delete(json);
        return NULL;
    }
    cJSON_AddItemToObject(json, "number", json_number);

    cJSON *json_name = cJSON_CreateString(relay->name);
    if(json_name == NULL)
    {
        LOGGER_DEBUG("failed to make name\n");
        cJSON_Delete(json);
        return NULL;
    }
    cJSON_AddItemToObject(json, "name", json_name);

    cJSON *json_controller_id = cJSON_CreateString(uuid_str);
    if(json_controller_id == NULL)
    {
        LOGGER_DEBUG("failed to make controller id\n");
        cJSON_Delete(json);
        return NULL;
    }
    cJSON_AddItemToObject(json, "controller_id", json_controller_id);

    controller_free(controller);

    cJSON *json_is_on;
    switch(relay->is_on)
    {
        case 0:
            json_is_on = cJSON_CreateFalse();
            break;
        case 1:
            json_is_on = cJSON_CreateTrue();
            break;
        default:
            json_is_on = cJSON_CreateNull();
            break;
    }
    cJSON_AddItemToObject(json, "is_on", json_is_on);

    cJSON *json_active_schedule = schedule_to_json(relay->active_schedule);
    cJSON_AddItemToObject(json, "active_schedule", json_active_schedule);

    cJSON *json_schedules = cJSON_CreateArray();
    for(int i = 0; i < 7; ++i)
    {
        cJSON *json_schedule = schedule_to_json(relay->schedules[i]);
        cJSON_AddItemToArray(json_schedules, json_schedule);
    }
    cJSON_AddItemToObject(json, "schedules", json_schedules);

    cJSON *json_tags = cJSON_CreateArray();
    int *tags_ids = junction_tag_get_tags_for_relay_id(relay->id);
    if(tags_ids != NULL)
    {
        for(int i = 0; tags_ids[i] != 0; ++i)
        {
            char *tag = tag_get_tag(tags_ids[i]);
            if(tag == NULL)
            {
                continue;   
            }

            cJSON *json_tag = cJSON_CreateString(tag);
            if (json_tag == NULL)
            {
                LOGGER_DEBUG("failed to add tag from string '%s'\n", tag);
                free(tag);
                continue;
            }
            cJSON_AddItemToArray(json_tags, json_tag);
            free(tag);
        }
        free(tags_ids);
    }
    cJSON_AddItemToObject(json, "tags", json_tags);

    char *json_str = cJSON_Print(json);
    cache_put_json_relay(relay->id, json_str);
    cJSON_Delete(json);

    json = cJSON_CreateRaw(json_str);
    free(json_str);
    return json;
}

cJSON*
relay_list_to_json(relay_t **relays)
{
    cJSON *json = cJSON_CreateArray();

    for(int i = 0; relays[i] != NULL; ++i)
    {
        cJSON *json_relay = relay_to_json(relays[i]);
        cJSON_AddItemToArray(json, json_relay);
    }

    return json;
}

relay_t*
relay_get_by_id(int id)
{
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT * FROM relays WHERE id = ?1;", -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, id);

    relay_t **sql_result = relay_db_select(stmt);

    relay_t *result = sql_result[0];
    free(sql_result);

    return result;
}

relay_t*
relay_get_by_uid(uuid_t uid)
{
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT * FROM relays WHERE uid = ?1;", -1, &stmt, NULL);
    sqlite3_bind_blob(stmt, 1, uid, sizeof(uuid_t), SQLITE_STATIC);

    relay_t **sql_result = relay_db_select(stmt);

    relay_t *result = sql_result[0];
    free(sql_result);

    return result;
}

relay_t**
relay_get_all()
{
    LOGGER_DEBUG("getting all relays from database\n");
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT * FROM relays;", -1, &stmt, NULL);

    return relay_db_select(stmt);
}

relay_t**
relay_get_with_schedule(int schedule_id)
{
    LOGGER_DEBUG("getting relays [schedule_id=%d] from database\n", schedule_id);
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT DISTINCT relays.* FROM relays INNER JOIN junction_relay_schedule ON relays.id == junction_relay_schedule.relay_id WHERE junction_relay_schedule.schedule_id = ?1;", -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, schedule_id);

    return relay_db_select(stmt);
}

relay_t*
relay_get_for_controller(int controller_id, int relay_num)
{
    LOGGER_DEBUG("getting relay [controller_id=%d, relay_num=%d] from database\n", controller_id, relay_num);
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT * FROM relays WHERE controller_id = ?1 AND number = ?2;", -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, controller_id);
    sqlite3_bind_int(stmt, 2, relay_num);

    relay_t **sql_result = relay_db_select(stmt);

    relay_t *result = sql_result[0];
    free(sql_result);

    return result;

}

relay_t**
relay_get_by_controller_id(int controller_id)
{
    LOGGER_DEBUG("getting relays [controller_id=%d] from database\n", controller_id);
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT * FROM relays WHERE controller_id = ?1;", -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, controller_id);

    return relay_db_select(stmt);

}