/*
 * Copyright (c) 2014-2016 Cesanta Software Limited
 * All rights reserved
 */

#include "mgos_ota_http_client.h"

#include "common/cs_dbg.h"
#include "mgos_hal.h"
#include "mgos_mongoose.h"
#include "mgos_config.h"
#include "mgos_ro_vars.h"
#include "mgos_timers.h"
#include "mgos_utils.h"

#if MGOS_ENABLE_UPDATER

static void fw_download_handler(struct mg_connection *c, int ev, void *p,
                                void *user_data) {
  struct mbuf *io = &c->recv_mbuf;
  struct update_context *ctx = (struct update_context *) user_data;
  int res = 0;
  struct mg_str *loc;
  (void) p;

  switch (ev) {
    case MG_EV_CONNECT: {
      int result = *((int *) p);
      if (result != 0) LOG(LL_ERROR, ("connect error: %d", result));
      break;
    }
    case MG_EV_RECV: {
      if (ctx->file_size == 0) {
        LOG(LL_DEBUG, ("Looking for HTTP header"));
        struct http_message hm;
        int parsed = mg_parse_http(io->buf, io->len, &hm, 0);
        if (parsed <= 0) {
          return;
        }
        if (hm.resp_code != 200) {
          if (hm.resp_code == 304) {
            ctx->result = 1;
            ctx->need_reboot = false;
            ctx->status_msg = "Not Modified";
            updater_finish(ctx);
          } else if ((hm.resp_code == 301 || hm.resp_code == 302) &&
                     (loc = mg_get_http_header(&hm, "Location")) != NULL) {
            /* NUL-terminate the URL. Every header must be followed by \r\n,
             * so there is deifnitely space there. */
            ((char *) loc->p)[loc->len] = '\0';
            /* We were told to look elsewhere. Detach update context from this
             * connection so that it doesn't get finalized when it's closed. */
            mgos_ota_http_start(ctx, loc->p);
            c->user_data = NULL;
          } else {
            ctx->result = -hm.resp_code;
            ctx->need_reboot = false;
            ctx->status_msg = "Invalid HTTP response code";
            updater_finish(ctx);
          }
          c->flags |= MG_F_CLOSE_IMMEDIATELY;
          return;
        }
        if (hm.body.len != 0) {
          LOG(LL_DEBUG, ("HTTP header: file size: %d", (int) hm.body.len));
          if (hm.body.len == (size_t) ~0) {
            LOG(LL_ERROR, ("Invalid content-length, perhaps chunked-encoding"));
            ctx->status_msg =
                "Invalid content-length, perhaps chunked-encoding";
            c->flags |= MG_F_CLOSE_IMMEDIATELY;
            break;
          } else {
            ctx->file_size = hm.body.len;
          }

          mbuf_remove(io, parsed);
        }
      }

      if (io->len != 0) {
        res = updater_process(ctx, io->buf, io->len);
        mbuf_remove(io, io->len);

        if (res == 0) {
          if (is_write_finished(ctx)) res = updater_finalize(ctx);
          if (res == 0) {
            /* Need more data, everything is OK */
            break;
          }
        }

        if (res < 0) {
          /* Error */
          LOG(LL_ERROR, ("Update error: %d %s", ctx->result, ctx->status_msg));
        }
        c->flags |= MG_F_CLOSE_IMMEDIATELY;
      }
      break;
    }
    case MG_EV_CLOSE: {
      if (ctx == NULL) break;

      if (is_write_finished(ctx)) updater_finalize(ctx);

      if (!is_update_finished(ctx)) {
        /* Update failed or connection was terminated by server */
        if (ctx->status_msg == NULL) ctx->status_msg = "Update failed";
        ctx->result = -1;
      } else if (is_reboot_required(ctx)) {
        LOG(LL_INFO, ("Rebooting device"));
        mgos_system_restart_after(100);
      }
      updater_finish(ctx);
      updater_context_free(ctx);
      c->user_data = NULL;
      break;
    }
  }
}

void mgos_ota_http_start(struct update_context *ctx, const char *url) {
  LOG(LL_INFO, ("Update URL: %s, ct: %d, isv? %d", url,
                ctx->fctx.commit_timeout, ctx->ignore_same_version));

  struct mg_connect_opts opts;
  memset(&opts, 0, sizeof(opts));

#if MG_ENABLE_SSL
  if (strlen(url) > 8 && strncmp(url, "https://", 8) == 0) {
    opts.ssl_server_name = mgos_sys_config_get_update_ssl_server_name();
    opts.ssl_ca_cert = mgos_sys_config_get_update_ssl_ca_file();
    opts.ssl_cert = mgos_sys_config_get_update_ssl_client_cert_file();
  }
#endif

  char ehb[150];
  char *extra_headers = ehb;
  mg_asprintf(&extra_headers, sizeof(ehb),
              "X-MGOS-Device-ID: %s %s\r\n"
              "X-MGOS-FW-Version: %s %s %s\r\n",
              (mgos_sys_config_get_device_id() ? mgos_sys_config_get_device_id() : "-"),
              mgos_sys_ro_vars_get_mac_address(),
              mgos_sys_ro_vars_get_arch(),
              mgos_sys_ro_vars_get_fw_version(),
              mgos_sys_ro_vars_get_fw_id());

  struct mg_connection *c = mg_connect_http_opt(
      mgos_get_mgr(), fw_download_handler, ctx, opts, url, extra_headers, NULL);

  if (extra_headers != ehb) free(extra_headers);

  if (c == NULL) {
    LOG(LL_ERROR, ("Failed to connect to %s", url));
    ctx->result = -10;
    ctx->need_reboot = false;
    ctx->status_msg = "Failed to connect";
    updater_finish(ctx);
    return;
  }

  ctx->nc = c;
}

static void mgos_ota_timer_cb(void *arg) {
  const struct mgos_config_update *mcu = mgos_sys_config_get_update();
  if (mcu->url == NULL) return;
  struct update_context *ctx = updater_context_create();
  if (ctx == NULL) return;
  ctx->ignore_same_version = true;
  ctx->fctx.commit_timeout = mcu->commit_timeout;
  mgos_ota_http_start(ctx, mcu->url);

  (void) arg;
}

bool mgos_ota_http_client_init(void) {
  const struct mgos_config_update *mcu = mgos_sys_config_get_update();
  if (mcu->url != NULL && mcu->interval > 0) {
    LOG(LL_INFO,
        ("Updates from %s, every %d seconds", mcu->url, mcu->interval));
    mgos_set_timer(mcu->interval * 1000, true /* repeat */, mgos_ota_timer_cb,
                   mcu->url);
  }
  return true;
}

#endif /* MGOS_ENABLE_UPDATER */
