#include "esp_err.h"
#include "esp_event_base.h"
#include "esp_log.h"
#include "esp_netif_types.h"
#include "esp_wifi.h"
#include "esp_wifi_default.h"

#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/task.h"

#include "nvs_flash.h"

#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>

#include "libssh2.h"

#include "wifi_private.h"

const char TAG[] = "main";

// assigned to an esp timer to run after a set time
static void reconnect_job(void* arg) {
  // TODO: if it's still disconnected try to reconnect
}
static void reconnect_in(int ticks_ms) {
}

EventGroupHandle_t s_wifi_event_group;
#define WIFI_FAIL_BIT (1)
#define WIFI_CONNECTED_BIT (2)

static int s_retry_num = 0;
static void event_handler(void* arg, esp_event_base_t event_base,
                          int32_t event_id, void* event_data) {
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        // TODO is this correct?
        xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
        if (s_retry_num < 5) {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGI(TAG, "retry to connect to the AP");
        } else {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        }
        ESP_LOGI(TAG,"connect to the AP fail");
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
        s_retry_num = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

void main_task(void* args) {

  // connect via TCP here
  struct addrinfo *results = 0;

  int rc = libssh2_init(0);
  if (rc) {
    ESP_LOGE(TAG, "libssh2 initialization failed: %d", rc);
    // TODO panic
  }

  while (true) {
    xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT, 0, pdTRUE, portMAX_DELAY); 
    if (!(xEventGroupGetBits(s_wifi_event_group) & WIFI_CONNECTED_BIT)) {
      continue;
    }

    struct addrinfo hints = {};
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype= SOCK_STREAM;

    ESP_LOGI(TAG, "looking up server...");
    rc = getaddrinfo("batmanbatman.local", "22", &hints, &results);
    if (rc) {
      ESP_LOGE(TAG, "getaddrinfo failed: %d", rc);
      break;
    }

    if (!results) {
      ESP_LOGE(TAG, "getaddrinfo returned no results");
      // TODO sleep and try again?
      break;
    }

    bool connect_success = false;
    libssh2_socket_t sock = LIBSSH2_INVALID_SOCKET;
    LIBSSH2_SESSION* session = 0;
    LIBSSH2_CHANNEL* channel = 0;

    for (struct addrinfo* rp = results; rp; rp = rp->ai_next) {
      ESP_LOGI(TAG, "creating socket fam %d type %d proto %d", rp->ai_family, rp->ai_socktype, rp->ai_protocol);
      sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);

      if (sock == LIBSSH2_INVALID_SOCKET) {
        ESP_LOGE(TAG, "failed to create socket");
        continue;
      }

      if (connect(sock, results->ai_addr, results->ai_addrlen) != -1) {
        ESP_LOGE(TAG, "connected successfully");
        connect_success = true;
        break;
      }

      close(sock);
      sock = LIBSSH2_INVALID_SOCKET;
    }

    freeaddrinfo(results);
    if (!connect_success) {
      // TODO I guess sleep for a bit and retry?
      break;
    }

    ESP_LOGI(TAG, "connection established, creating SSH session...");

    session = libssh2_session_init();
    if (!session) {
      ESP_LOGE(TAG, "could not create libssh2 session");
      break;
    }

    //libssh2_session_set_read_timeout(session, 20);

    libssh2_trace(session, -1);

    libssh2_session_handshake(session, sock);

    const char* fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);

    const char username[] = "shrooms";
    extern char _binary_shrooms_key_start[];
    extern char _binary_shrooms_key_end[];
    size_t shrooms_key_size = _binary_shrooms_key_end - _binary_shrooms_key_start;
    extern char _binary_shrooms_key_pub_start[];
    extern char _binary_shrooms_key_pub_end[];
    size_t shrooms_key_pub_size = _binary_shrooms_key_pub_end - _binary_shrooms_key_pub_start;

    ESP_LOGI(TAG, "pub key %p %p %d key %p %p %d",
             _binary_shrooms_key_pub_start,
             _binary_shrooms_key_pub_end,
             shrooms_key_pub_size,
             _binary_shrooms_key_start,
             _binary_shrooms_key_end,
             shrooms_key_size
             );
    rc = libssh2_userauth_publickey_frommemory(
        session,
        username,
        sizeof(username) - 1,
        _binary_shrooms_key_pub_start,
        shrooms_key_pub_size,
        _binary_shrooms_key_start,
        shrooms_key_size,
        NULL);

    if (rc) {
      ESP_LOGE(TAG, "could not authenticate, err %d", rc);
      break;
    }

    ESP_LOGE(TAG, "authenticated, opening channel...");
    channel = libssh2_channel_open_session(session);
    if (!channel) {
      ESP_LOGE(TAG, "unable to open channel");
      break;
    }

    rc = libssh2_channel_request_pty(channel, "vanilla");
    if (rc) {
      ESP_LOGE(TAG, "unable to request pty %d", rc);
      break;
    }

    ESP_LOGE(TAG, "executing command...");
    rc = libssh2_channel_exec(channel, "./runtest.sh");
    if (rc) {
      ESP_LOGE(TAG, "could not open channel");
    }
    //libssh2_session_set_timeout(session, 20);

    ESP_LOGE(TAG, "waiting for results...");
    while (!libssh2_channel_eof(channel)) {
      ESP_LOGE(TAG, "waiting for read loop");
      char tmp[256];
      ssize_t rlen = libssh2_channel_read(channel, tmp, sizeof(tmp) - 1);
      if (rlen < 0) {
        ESP_LOGE(TAG, "received err %d", rlen);
        continue;
      }

      tmp[rlen] = 0;

      ESP_LOGI(TAG, "received %s", tmp);
    }

    ESP_LOGI(TAG, "ssh exiting");
    //rc = libssh2_channel_get_get_exit_status(channel);

  //cleanup:
    if (libssh2_channel_close(channel)) {
      ESP_LOGE(TAG, "could not close channel");
    }
    if (channel) {
      libssh2_channel_free(channel);
    }
    channel = 0;

    if (session) {
      libssh2_session_disconnect(session, "normal shutdown");
      libssh2_session_free(session);
    }

    if (sock != LIBSSH2_INVALID_SOCKET) {
      shutdown(sock, 2);
      LIBSSH2_SOCKET_CLOSE(sock);
    }
    break;
  }

  while (true);
}


void app_main(void) {
  esp_err_t ret = nvs_flash_init();
  if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    ESP_ERROR_CHECK(nvs_flash_erase());
    ret = nvs_flash_init();
  }
  ESP_ERROR_CHECK(ret);

  wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();

  s_wifi_event_group = xEventGroupCreate();

  ESP_ERROR_CHECK(esp_netif_init());
  ESP_ERROR_CHECK(esp_event_loop_create_default());
  esp_netif_create_default_wifi_sta();

  esp_event_handler_instance_t instance_any_id;
  esp_event_handler_instance_t instance_got_ip;

  ESP_ERROR_CHECK(esp_event_handler_instance_register(
    WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id));
  ESP_ERROR_CHECK(esp_event_handler_instance_register(
    IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip));

  ESP_ERROR_CHECK(esp_wifi_init(&cfg));
  ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));

  wifi_config_t wifi_config = {
    .sta = {
      .ssid = WIFI_SSID,
      .password = WIFI_PASS,
      .threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK,
    },
  };

  ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
  ESP_ERROR_CHECK(esp_wifi_start());

  if (xTaskCreate(main_task, "ssh task", 4096, NULL, 17, NULL) != pdPASS) {
    ESP_LOGE(TAG, "unable to create ssh task");
    // TODO panic
  }
}