/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#include "app_fetch_endpoint.h"

#include <string.h>
#include <stdbool.h>

#include "applib/rockyjs/rocky_res.h"
#include "kernel/pbl_malloc.h"
#include "process_management/pebble_process_info.h"
#include "services/common/comm_session/session.h"
#include "services/common/put_bytes/put_bytes.h"
#include "services/common/system_task.h"
#include "services/normal/app_cache.h"
#include "services/normal/blob_db/app_db.h"
#include "services/normal/process_management/app_storage.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/math.h"
#include "util/uuid.h"

//! Used for keeping track of binaries that are loaded through put_bytes
typedef struct {
  AppInstallId app_id;
  uint32_t total_size;
  AppFetchResult prev_error;
  bool cancelling;
  bool in_progress;
  bool app;
  bool worker;
  bool resources;
} AppFetchState;

//! Command type
enum {
  APP_FETCH_INSTALL_COMMAND = 0x01,
} AppFetchCommand;

//! Possible results that come back from the INSTALL_COMMAND
enum {
  APP_FETCH_INSTALL_RESPONSE = 0x01,
} AppFetchResponse;

//! Possible results that come back from the INSTALL_COMMAND
enum {
  APP_FETCH_RESPONSE_STARTING = 0x01,
  APP_FETCH_RESPONSE_BUSY = 0x02,
  APP_FETCH_RESPONSE_UUID_INVALID = 0x03,
  APP_FETCH_RESPONSE_NO_DATA = 0x04,
} AppFetchInstallResult;

//! Data sent to mobile phone for an INSTALL_COMMAND
typedef struct PACKED {
  uint8_t command;
  Uuid uuid;
  AppInstallId app_id;
} AppFetchInstallRequest;

//! Timeout used to determine how long we should wait before the phone starts sending the app
//! we requested (by issuing a put_bytes request).
#define FETCH_TIMEOUT_MS 15000

//! State for the app fetch flow
static AppFetchState s_fetch_state;

//! Endpoint ID
static const uint16_t APP_FETCH_ENDPOINT_ID = 6001;

////////////////////////////
// Internal Helper Functions
////////////////////////////

//! Puts an error event with the given error code
static void prv_put_event_error(uint8_t error_code) {
  s_fetch_state.prev_error = error_code;
  PebbleEvent event = {
    .type = PEBBLE_APP_FETCH_EVENT,
    .app_fetch = {
      .type = AppFetchEventTypeError,
      .id = s_fetch_state.app_id,
      .error_code = error_code,
    }
  };
  event_put(&event);
}

//! Puts an event with the given progress
static void prv_put_event_progress(uint8_t percent) {
  PebbleEvent event = {
    .type = PEBBLE_APP_FETCH_EVENT,
    .app_fetch = {
      .type = AppFetchEventTypeProgress,
      .id = s_fetch_state.app_id,
      .progress_percent = percent,
    }
  };
  event_put(&event);
}

//! Simply posts the type of event given.
static void prv_put_event_simple(AppFetchEventType type) {
  PebbleEvent event = {
    .type = PEBBLE_APP_FETCH_EVENT,
    .app_fetch = {
      .type = type,
      .id = s_fetch_state.app_id,
    }
  };
  event_put(&event);
}


//! Recomputes and saves the progress percent for the current application fetch session
static uint8_t prv_compute_progress_percent(PutBytesObjectType type, unsigned int type_percent) {
  // Add 33(34) percent for each piece that has finished (or is unneeded)
  uint8_t percent = 0;
  if (s_fetch_state.app) {
    percent += 30;
  }
  if (s_fetch_state.worker) {
    percent += 10;
  }
  if (s_fetch_state.resources) {
    percent += 60;
  }

  // add in the progress for the currently transferring piece.
  percent += (type_percent / 3);

  // store value
  return MIN(100, percent);
}

//! Cleans up the state of the app fetch endpoint. Always called from the system task
static void prv_cleanup(AppFetchResult result) {
  if (result != AppFetchResultSuccess) {
    put_bytes_cancel();
    app_cache_remove_entry(s_fetch_state.app_id);
    prv_put_event_error(result);
  }

  s_fetch_state.in_progress = false;

  PBL_LOG(LOG_LEVEL_INFO, "App fetch cleanup with result %d", result);
}

//! System task callback triggered by app_fetch_put_bytes_event_handler() when we are receiving
//! put_bytes messages in reponse to a fetch request to the phone.
void prv_put_bytes_event_system_task_cb(void *data) {
  PebblePutBytesEvent *pb_event = (PebblePutBytesEvent *)data;

  if (!s_fetch_state.in_progress) {
    return;
  }

  // If put_bytes has failed, let's just say fail and stop everything.
  if (pb_event->failed == true) {
    AppFetchResult error;
    if (s_fetch_state.cancelling) {
      PBL_LOG(LOG_LEVEL_WARNING, "Put bytes cancelled by user");
      error = AppFetchResultUserCancelled;
    } else {
      PBL_LOG(LOG_LEVEL_ERROR, "Put bytes failure");
      error = AppFetchResultPutBytesFailure;
    }

    prv_cleanup(error);
    goto finally;
  }

  if (pb_event->type == PebblePutBytesEventTypeInitTimeout) {
    PBL_LOG(LOG_LEVEL_WARNING, "Timed out waiting for putbytes request from phone");
    prv_cleanup(AppFetchResultTimeoutError);
  }

  // If this is an object that doesn't have a cookie, then we won't care about it.
  if (pb_event->has_cookie == false) {
    PBL_LOG(LOG_LEVEL_DEBUG, "Ignoring non cookie put_bytes event");
    goto finally;
  }

  // Check for the different types of PutBytes events.
  if (pb_event->type == PebblePutBytesEventTypeProgress) {
    // compute and save the new progress, then show it on the progress bar
    uint8_t percent =
        prv_compute_progress_percent(pb_event->object_type, pb_event->progress_percent);
    prv_put_event_progress(percent);
  } else if (pb_event->type == PebblePutBytesEventTypeCleanup) {
    // Mark off each finishing put_bytes transaction in our progress struct

    switch (pb_event->object_type) {
      case ObjectWatchApp:
        s_fetch_state.app = true;
        break;
      case ObjectWatchWorker:
        s_fetch_state.worker = true;
        break;
      case ObjectAppResources:
        s_fetch_state.resources = true;
        break;
      default:
        PBL_LOG(LOG_LEVEL_ERROR, "Got a PutBytes Object that we shouldn't have gotten");
        prv_cleanup(AppFetchResultGeneralFailure);
        goto finally;
    }

    // add the size of the finished PutBytes transaction to the total size.
    s_fetch_state.total_size += pb_event->total_size;
  }

  if (s_fetch_state.app && s_fetch_state.worker && s_fetch_state.resources) {
    // if everything has finished being transferred
    PBL_LOG(LOG_LEVEL_DEBUG, "All pieces (%"PRIu32" bytes) have been sent over put_bytes",
        s_fetch_state.total_size);

    // signify in the app cache that the app binaries are now loaded
    status_t added = app_cache_add_entry(s_fetch_state.app_id, s_fetch_state.total_size);
    if (added == S_SUCCESS) {
      const PebbleProcessMd *md = app_install_get_md(s_fetch_state.app_id, false);
      if (rocky_app_validate_resources(md) == RockyResourceValidation_Invalid) {
        PBL_LOG(LOG_LEVEL_ERROR, "Received app contains invalid JS bytecode");
        prv_put_event_error(AppFetchResultIncompatibleJSFailure);
      } else {
        // Set prev_error as a Success.
        s_fetch_state.prev_error = AppFetchResultSuccess;
        prv_put_event_simple(AppFetchEventTypeFinish);
      }
      app_install_release_md(md);
    } else {
      PBL_LOG(LOG_LEVEL_ERROR, "Failed to insert into app cache: %"PRId32, added);
      prv_put_event_error(AppFetchResultGeneralFailure);
    }
    prv_cleanup(AppFetchResultSuccess);

  } else if (pb_event->type == PebblePutBytesEventTypeCleanup) {
    // Start the timeout watchdog again so we can tell if things get hung up
    // before the phone starts sending the next putbytes object.
    // This will only trigger if we've completed a piece and are still
    // waiting for another one.
    put_bytes_expect_init(FETCH_TIMEOUT_MS);
  }

finally:
  kernel_free(pb_event);
}

//! Put Bytes handler. Used for keeping track of progress and cleanup events. This is called
//! from KernelMain's event handler when it receives a PEBBLE_PUT_BYTES_EVENT event. put_bytes
//! posts these events to inform clients of progress.
void app_fetch_put_bytes_event_handler(PebblePutBytesEvent *pb_event) {
  // If an app fetch isn't in progress, ignore it.
  if (!s_fetch_state.in_progress) {
    return;
  }

  PebblePutBytesEvent *pb_event_copy = kernel_malloc_check(sizeof(PebblePutBytesEvent));
  memcpy(pb_event_copy, pb_event, sizeof(PebblePutBytesEvent));
  system_task_add_callback(prv_put_bytes_event_system_task_cb, pb_event_copy);
}

//! Callback for the system task to fire off the fetch request. Triggered by a call to
//! app_fetch_binaries().
static void prv_app_fetch_binaries_system_task_cb(void *data) {
  AppFetchInstallRequest *request = (AppFetchInstallRequest *)data;

  // check if Bluetooth is active. If so, this will send.
  bool successful = comm_session_send_data(comm_session_get_system_session(), APP_FETCH_ENDPOINT_ID,
      (uint8_t*)request, sizeof(AppFetchInstallRequest), COMM_SESSION_DEFAULT_TIMEOUT);

  // log it
  char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
  uuid_to_string(&request->uuid, uuid_buffer);
  PBL_LOG(LOG_LEVEL_INFO, "%s request for app with uuid: %s and app_id: %"PRIu32"",
      successful ? "Sent" : "Failed to send", uuid_buffer, request->app_id);

  // free before error checking
  kernel_free(request);

  // If Bluetooth wasn't active, then post the error and cleanup.
  if (!successful) {
    prv_cleanup(AppFetchResultNoBluetooth);
    return;
  }

  // We next expect app_fetch_put_bytes_event_handler() to be called when the phone
  // gets our fetch request and issues a putbytes request.
  // Start the timeout watchdog to catch us in case the phone never issues the putbytes request.
  put_bytes_expect_init(FETCH_TIMEOUT_MS);
}

//! Called from the system task. Translates an Endpoint error to an event error and sends
//! off the appropriate event.
void prv_handle_app_fetch_install_response(uint8_t result_code) {
  switch (result_code) {
    case APP_FETCH_RESPONSE_STARTING:
      PBL_LOG(LOG_LEVEL_INFO, "Phone confirmed it will start sending data");
      prv_put_event_simple(AppFetchEventTypeStart);
      put_bytes_expect_init(FETCH_TIMEOUT_MS);
      break;
    case APP_FETCH_RESPONSE_BUSY:
      PBL_LOG(LOG_LEVEL_WARNING, "Error: Phone is currently busy");
      prv_cleanup(AppFetchResultPhoneBusy);
      break;
    case APP_FETCH_RESPONSE_UUID_INVALID:
      PBL_LOG(LOG_LEVEL_WARNING, "Error: UUID Invalid");
      prv_cleanup(AppFetchResultUUIDInvalid);
      break;
    case APP_FETCH_RESPONSE_NO_DATA:
      PBL_LOG(LOG_LEVEL_WARNING, "Error: No data on phone");
      prv_cleanup(AppFetchResultNoData);
      break;
  }
}

/////////////////////////
// Exported App Fetch API
/////////////////////////

//! Called by the system that triggers an app fetch install request
void app_fetch_binaries(const Uuid *uuid, AppInstallId app_id, bool has_worker) {
  if (s_fetch_state.in_progress) {
    PBL_LOG(LOG_LEVEL_WARNING, "Already an app fetch in progress. Ignoring request");
    return;
  }

  AppFetchInstallRequest *request = kernel_malloc_check(sizeof(AppFetchInstallRequest));

  // reset all state
  s_fetch_state = (AppFetchState){};

  // Mark whether the worker needs to be sent over.
  s_fetch_state.worker = !has_worker;
  s_fetch_state.app_id = app_id;
  s_fetch_state.in_progress = true;

  // populate fields
  request->command = APP_FETCH_INSTALL_COMMAND;
  request->uuid    = *uuid;
  request->app_id  = app_id;

  // Start "warming up" the connection, this will cause the low-latency period to start ~1s sooner.
  // Put bytes will extend the low-latency period after this:
  comm_session_set_responsiveness(comm_session_get_system_session(), BtConsumerPpAppFetch,
                                  ResponseTimeMin, MIN_LATENCY_MODE_TIMEOUT_APP_FETCH_SECS);

  system_task_add_callback(prv_app_fetch_binaries_system_task_cb, request);
}

AppFetchError app_fetch_get_previous_error(void) {
  AppFetchError error = {
    .error = s_fetch_state.prev_error,
    .id = s_fetch_state.app_id,
  };

  return error;
}

static void prv_cancel_fetch_from_system_task(void *data) {
  AppInstallId app_id = (AppInstallId)data;

  if ((!s_fetch_state.in_progress) ||
      ((s_fetch_state.app_id != app_id) && (app_id != INSTALL_ID_INVALID))) {
    PBL_LOG(LOG_LEVEL_DEBUG, "Attempted to cancel an app that is currently not being"
            " fetched: %"PRId32, app_id);
    return;
  }

  PBL_LOG(LOG_LEVEL_DEBUG, "Cancelling app fetch from system task");
  s_fetch_state.cancelling = true;
  put_bytes_cancel();
}

void app_fetch_cancel_from_system_task(AppInstallId app_id) {
  PBL_ASSERT_TASK(PebbleTask_KernelBackground);

  prv_cancel_fetch_from_system_task((void *)(uintptr_t)app_id);
}

void app_fetch_cancel(AppInstallId app_id) {
  // Everything within app fetch happens on the background task
  system_task_add_callback(prv_cancel_fetch_from_system_task, (void *)(uintptr_t)app_id);
}

bool app_fetch_in_progress(void) {
  return s_fetch_state.in_progress;
}

////////////////////////////
// Exported Callbacks
////////////////////////////

typedef struct __attribute__((__packed__)) {
  uint8_t command;
  uint8_t result_code;
} AppFetchResponseData;

//! System task callback triggered by app_fetch_protocol_msg_callback().
static void prv_app_fetch_protocol_handle_msg(AppFetchResponseData *response_data) {

  switch (response_data->command) {
    case APP_FETCH_INSTALL_RESPONSE:
      prv_handle_app_fetch_install_response(response_data->result_code);
      break;
    default:
      PBL_LOG(LOG_LEVEL_ERROR, "Invalid message received, command: %u result: %u",
          response_data->command, response_data->result_code);
      prv_cleanup(AppFetchResultGeneralFailure);
      break;
  }
}

//! Callback that is placed in the endpoints table. As of now, only responses will come through this
//! callback as all commands are originally sent to the phone.
void app_fetch_protocol_msg_callback(CommSession *session, const uint8_t *data, size_t length) {
  if (length < sizeof(AppFetchResponseData)) {
    PBL_LOG(LOG_LEVEL_ERROR, "Invalid message length %"PRIu32"", (uint32_t)length);
    prv_cleanup(AppFetchResultGeneralFailure);
    return;
  }

  if (!s_fetch_state.in_progress) {
    PBL_LOG(LOG_LEVEL_WARNING, "Got a message but app fetch not in progress. Ignoring");
    return;
  }

  prv_app_fetch_protocol_handle_msg((AppFetchResponseData *)data);
}
