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

#include "gatt_client_discovery.h"
#include "gatt_service_changed.h"
#include "gap_le_connection.h"

#include "ble_log.h"

#include "comm/bt_lock.h"
#include "comm/bt_conn_mgr.h"

#include "kernel/core_dump.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "gatt_client_accessors.h"
#include "system/logging.h"

#include <bluetooth/gatt.h>
#include <bluetooth/gatt_discovery.h>
#include <btutil/bt_device.h>

// TODO: virtualize the gatt_client_discovery_discover_all() call

//! Defined in gatt_client_subscriptions.c. Should only be called when receiving
//! notification of a service change
extern void gatt_client_subscription_cleanup_by_att_handle_range(
    struct GAPLEConnection *connection, ATTHandleRange *range);

//! Defined in gatt_client_accessors.c. Should only be needed in this module
extern BLEService gatt_client_att_handle_get_service(
    GAPLEConnection *connection, uint16_t att_handle, GATTServiceNode **service_node_out);

// -------------------------------------------------------------------------------------------------
// Static function prototypes

static BTErrno prv_run_next_job(GAPLEConnection *connection);

// -------------------------------------------------------------------------------------------------
// Wrappers around Bluetopia's API

#define MIN_ATT_HANDLE 0x1
#define MAX_ATT_HANDLE 0xFFFF

typedef struct DiscoveryJobQueue {
  ListNode node;
  ATTHandleRange hdl;
} DiscoveryJobQueue;

// Assumes we are holding the BT lock
static void prv_add_discovery_job(
    GAPLEConnection *connection, ATTHandleRange *hdl_range) {
  DiscoveryJobQueue *node = kernel_zalloc_check(sizeof(DiscoveryJobQueue));
  if (hdl_range) {
    node->hdl = *hdl_range;
  } else { // discover everything
    node->hdl = (ATTHandleRange) {
      .start = MIN_ATT_HANDLE,
      .end = MAX_ATT_HANDLE
    };
  }

  if (!connection->discovery_jobs) {
    list_init(&node->node);
    connection->discovery_jobs = node;
  } else {
    list_append((ListNode *)connection->discovery_jobs, (ListNode *)node);
  }
}

void gatt_client_discovery_discover_range(GAPLEConnection *connection, ATTHandleRange *hdl_range) {
  bt_lock();
  {
    prv_add_discovery_job(connection, hdl_range);
    if (!connection->gatt_is_service_discovery_in_progress) {
      prv_run_next_job(connection);
    }
  }
  bt_unlock();
}

// assumes bt lock is held
static BTErrno prv_run_next_job(GAPLEConnection *connection) {
  DiscoveryJobQueue *node = connection->discovery_jobs;
  if (!node) {
    return BTErrnoOK; // no more jobs to run
  }

  // Note, that the job only gets removed from the list after discovery
  // has finished or error'ed out. That way the watchdog retry mechanism
  // can simply call this routine again to kick off another discovery attempt

  PBL_LOG(LOG_LEVEL_INFO, "Starting BLE Service Discovery: 0x%x to 0x%x",
          node->hdl.start, node->hdl.end);
  ATTHandleRange hdl = {
    .start = node->hdl.start,
    .end = node->hdl.end
  };

  BTErrno rv = bt_driver_gatt_start_discovery_range(connection, &hdl);

  if (rv == BTErrnoOK) {
    // if we are back here because a timeout occurred, let the
    // driver handle resetting the watchdog timer (cc2564x issue)
    connection->gatt_is_service_discovery_in_progress = true;
  }

  return rv;
}

// This function returns true if a retry started. If a retry did not start
// it sets e to BTErrnoOK if discovery completed or the actual error that happened
// which should be forwarded on
static bool prv_discovery_handle_timeout(GAPLEConnection *connection, BTErrno *e) {
  bool retry_started = false;
  BTErrno finalize_result = BTErrnoOK;
  // Executing on NewTimer task, so need to bt_lock():
  PBL_LOG(LOG_LEVEL_WARNING, "Service Discovery Watchdog Timeout");
  bt_lock();
  {
    if (!gap_le_connection_is_valid(connection)) {
      goto unlock;
    }

    if (bt_driver_gatt_stop_discovery(connection) != BTErrnoOK) {
      // Handle the race: Bluetopia service discovery has stopped in the mean time, for example
      // because of a disconnection, internal error or it completed right when the timer fired.
      goto unlock;
    }

    if (connection->gatt_service_discovery_retries == GATT_CLIENT_DISCOVERY_MAX_RETRY) {
#if !RELEASE && !UNITTEST
      core_dump_reset(true /* is_forced */);
#endif
      // Done retrying, just error out:
      finalize_result = BTErrnoServiceDiscoveryTimeout;
      goto unlock;
    }

    // Retry transparently (don't let the clients know):
    BTErrno ret_val = prv_run_next_job(connection);
    if (ret_val != BTErrnoOK) {
      // Start failed, just error out
      finalize_result = ret_val;
      goto unlock;
    }

    ++connection->gatt_service_discovery_retries;
    retry_started = true;
  }
unlock:
  *e = finalize_result;
  bt_unlock();

  return retry_started;
}

// -------------------------------------------------------------------------------------------------
extern uint8_t gatt_client_copy_service_refs_by_discovery_generation(
    const BTDeviceInternal *device, BLEService services_out[],
    uint8_t num_services, uint8_t discovery_gen);

static void prv_send_event(PebbleBLEGATTClientServiceEventInfo *info) {
  PebbleEvent e = (const PebbleEvent) {
    .type = PEBBLE_BLE_GATT_CLIENT_EVENT,
    .task_mask = 0,
    .bluetooth = {
      .le = {
        .gatt_client_service = {
          .info = info,
          .subtype = PebbleBLEGATTClientEventTypeServiceChange,
        },
      },
    },
  };
  // TODO: send only to tasks that are connected virtually
  event_put(&e);
}

static void prv_send_services_added_event(
    const GAPLEConnection *connection, BTErrno status) {

  uint8_t num_services_changed = (status == BTErrnoOK) ?
      list_count(&connection->gatt_remote_services->node) : 0;

  if (num_services_changed > BLE_GATT_MAX_SERVICES_CHANGED) {
    PBL_LOG(LOG_LEVEL_ERROR, "Remote has %u services, more than we can handle.",
            num_services_changed);
    num_services_changed = BLE_GATT_MAX_SERVICES_CHANGED;
  }

  size_t space_needed = num_services_changed * sizeof(PebbleBLEGATTClientServiceHandles)
      + sizeof(PebbleBLEGATTClientServiceEventInfo);

  PebbleBLEGATTClientServiceEventInfo *info = kernel_zalloc_check(space_needed);

  *info = (PebbleBLEGATTClientServiceEventInfo) {
    .type = PebbleServicesAdded,
    .device = connection->device,
    .status = status
  };

  info->services_added_data.num_services_added =
      gatt_client_copy_service_refs_by_discovery_generation(
          &connection->device, &info->services_added_data.services[0],
          BLE_GATT_MAX_SERVICES_CHANGED, connection->gatt_service_discovery_generation);

  prv_send_event(info);
}

static void prv_send_services_invalidate_all_event(
    const GAPLEConnection *connection, BTErrno status) {
  PebbleBLEGATTClientServiceEventInfo *info =
      kernel_zalloc_check(sizeof(PebbleBLEGATTClientServiceEventInfo));

  *info = (PebbleBLEGATTClientServiceEventInfo) {
    .type = PebbleServicesInvalidateAll,
    .device = connection->device,
    .status = status
  };

  prv_send_event(info);
}

extern void gatt_client_service_get_all_characteristics_and_descriptors(
    GAPLEConnection *connection, GATTService *service,
    BLECharacteristic *characteristics_hdls_out,
    BLEDescriptor *descriptor_hdls_out);

//! @note bt_lock is assumed to be taken by the caller
void gatt_client_discovery_handle_service_range_change(
    GAPLEConnection *connection, ATTHandleRange *range) {
  GATTServiceNode *service_node;
  BLEService service = gatt_client_att_handle_get_service(connection, range->start, &service_node);

  if (service == BLE_SERVICE_INVALID) {
    // Must be a new service
    return;
  }

  int memory_needed = service_node->service->num_characteristics * sizeof(BLECharacteristic) +
      service_node->service->num_descriptors * sizeof(BLEDescriptor);
  memory_needed +=
      sizeof(PebbleBLEGATTClientServiceEventInfo) + sizeof(PebbleBLEGATTClientServiceHandles);

  PebbleBLEGATTClientServiceEventInfo *info = kernel_zalloc_check(memory_needed);
  *info = (PebbleBLEGATTClientServiceEventInfo) {
    .type = PebbleServicesRemoved,
    .device = connection->device,
    .status = BTErrnoOK
  };

  info->services_removed_data.num_services_removed = 1;

  PebbleBLEGATTClientServiceHandles *remove_hdl = &info->services_removed_data.handles[0];

  remove_hdl->service = service;
  remove_hdl->uuid = service_node->service->uuid;
  remove_hdl->num_characteristics = service_node->service->num_characteristics;
  remove_hdl->num_descriptors = service_node->service->num_descriptors;
  gatt_client_service_get_all_characteristics_and_descriptors(
      connection, service_node->service,
      &remove_hdl->char_and_desc_handles[0],
      &remove_hdl->char_and_desc_handles[service_node->service->num_characteristics]);

  // a service has been removed/updated
  gatt_client_subscription_cleanup_by_att_handle_range(connection, range);
  ListNode **head = (ListNode **) &connection->gatt_remote_services;
  list_remove((ListNode *)service_node, head, NULL);
  kernel_free(service_node->service);
  service_node->service = NULL;
  kernel_free(service_node);

  prv_send_event(info);
}

static void prv_free_service_nodes(GAPLEConnection *connection) {
  GATTServiceNode *node = connection->gatt_remote_services;
  while (node) {
    GATTServiceNode *next = (GATTServiceNode *) node->node.next;
    kernel_free(node->service);
    node->service = NULL;
    kernel_free(node);
    node = next;
  }
  connection->gatt_remote_services = NULL;
}

static void prv_remove_current_discovery_job(GAPLEConnection *connection) {
  DiscoveryJobQueue *node = connection->discovery_jobs;
  if (!node) {
    return;
  }
  list_remove((ListNode *)connection->discovery_jobs,
              (ListNode **)&connection->discovery_jobs, NULL);
  kernel_free(node);

  // Handle the case where we are have received service change indication
  // messages for the same range in quick succession and have multiple jobs
  // scheduled as a result. This shouldn't be a frequent occurrence but see
  // PBL-24741 as an example

  DiscoveryJobQueue *new_job = connection->discovery_jobs;
  if (!new_job) {
    return; // nothing to do
  }

  if ((new_job->hdl.start == MIN_ATT_HANDLE) && (new_job->hdl.end == MAX_ATT_HANDLE)) {
    // we are rediscovering all services so flush everything
    prv_free_service_nodes(connection);
    prv_send_services_invalidate_all_event(
        connection, BTErrnoServiceDiscoveryDatabaseChanged);
  } else { // we are rediscovering one service
    gatt_client_discovery_handle_service_range_change(connection, &new_job->hdl);
  }
}

void gatt_client_cleanup_discovery_jobs(GAPLEConnection *connection) {
  bt_lock();
  {
    while (connection->discovery_jobs != NULL) {
      prv_remove_current_discovery_job(connection);
    }
  }
  bt_unlock();
}

static void prv_finalize_discovery(GAPLEConnection *connection, BTErrno errno) {
  if (errno != BTErrnoOK) {
    // Handle failure -- cleanup and dispatch event:
    prv_free_service_nodes(connection);
    gatt_client_subscriptions_cleanup_by_connection(connection, false /* should_unsubscribe */);
  }

  prv_remove_current_discovery_job(connection);
  connection->gatt_is_service_discovery_in_progress = false;
  connection->gatt_service_discovery_retries = 0;
  if (errno == BTErrnoServiceDiscoveryDatabaseChanged) {
    prv_send_services_invalidate_all_event(connection, errno);
  } else {
    prv_send_services_added_event(connection, errno);
  }
  ++connection->gatt_service_discovery_generation;
  prv_run_next_job(connection);
}

void bt_driver_cb_gatt_client_discovery_handle_indication(
    GAPLEConnection *connection, GATTService *service, BTErrno error) {

  // We experienced some kind of conversion error, pass it on
  if (error != BTErrnoOK) {
    prv_send_services_added_event(connection, error);
    return;
  }

  GATTServiceNode *node = kernel_zalloc_check(sizeof(GATTServiceNode));
  node->service = service;
  // tag the service with the generation it was discovered as a part of
  node->service->discovery_generation = connection->gatt_service_discovery_generation;

  bt_lock();
  {
    ListNode **head = (ListNode **) &connection->gatt_remote_services;
    if (*head) {
      list_append(*head, &node->node);
    } else {
      *head = &node->node;
    }
  }
  bt_unlock();
}

bool bt_driver_cb_gatt_client_discovery_complete(GAPLEConnection *connection, BTErrno errno) {
  bool finalize_discovery = true;
  bt_lock();
  {
    if (errno == BTErrnoServiceDiscoveryTimeout) {
      if (prv_discovery_handle_timeout(connection, &errno)) {
        // if a retry started, don't generate any events yet
        finalize_discovery = false;
        goto unlock;
      }
      // it's possible the discovery completed before we handled the timeout, in which case
      // we get a BTErrnoOK which means we will get a completion event already
      finalize_discovery = (errno != BTErrnoOK);
    } else if (errno == BTErrnoServiceDiscoveryDisconnected) {
      finalize_discovery = false;
    }

    if (errno == BTErrnoOK) {
      // Completion of service discovery implies we are about to have more BLE
      // traffic (for example, ANCS notifications, PPoG communication). Keep the
      // channel at a high throughput speed for a little bit longer to handle these bursts.
      conn_mgr_set_ble_conn_response_time(connection, BtConsumerLeServiceDiscovery,
                                          ResponseTimeMin, 10);
    }

    if (finalize_discovery) {
      prv_finalize_discovery(connection, errno);
    }
  }
unlock:
  bt_unlock();
  return finalize_discovery;
}

BTErrno gatt_client_discovery_discover_all(const BTDeviceInternal *device) {
  BTErrno ret_val = BTErrnoOK;
  bt_lock();
  {
    GAPLEConnection *connection = gap_le_connection_by_device(device);
    if (!connection) {
      ret_val = BTErrnoInvalidParameter;
      goto unlock;
    }
    if (connection->gatt_is_service_discovery_in_progress) {
      ret_val = BTErrnoInvalidState;
      goto unlock;
    }
    if (connection->gatt_remote_services) {
      // Already discovered, no need to do it again!
      prv_send_services_added_event(connection, BTErrnoOK);
      goto unlock;
    }
    conn_mgr_set_ble_conn_response_time(connection, BtConsumerLeServiceDiscovery,
                                        ResponseTimeMin, 30);
    prv_add_discovery_job(connection, NULL);
    // if we get here there is no discovery in progress so dispatch the job
    ret_val = prv_run_next_job(connection);
  }
unlock:
  bt_unlock();
  return ret_val;
}

//! extern for gap_le_connnection.c
//! Cleans up any state and frees the associated memory of all the things this module might have
//! created for a given connection.
//! bt_lock() is assumed to be taken by the caller
void gatt_client_discovery_cleanup_by_connection(GAPLEConnection *connection, BTErrno reason) {
  if (connection->gatt_is_service_discovery_in_progress) {
    // Assuming "disconnection" reason is appropriate here:
    prv_finalize_discovery(connection, reason);
    bt_driver_gatt_handle_discovery_abandoned();
  } else {
    prv_free_service_nodes(connection);
  }
}

//! extern for gatt_service_changed.c
//! Same as gatt_client_discovery_discover_all, but cleans up existing service discovery
//! state and stops any existing service discovery process.
BTErrno gatt_client_discovery_rediscover_all(const BTDeviceInternal *device) {
  BTErrno ret_val = BTErrnoServiceDiscoveryDisconnected;
  bt_lock();
  {
    GAPLEConnection *connection = gap_le_connection_by_device(device);
    if (connection) {
      if (connection->gatt_is_service_discovery_in_progress) {
        // Remove any partial jobs which may be pending
        // since we are going to rediscover everything
        gatt_client_cleanup_discovery_jobs(connection);
        bt_driver_gatt_stop_discovery(connection);
      } else {
        // Queue up CCCD writes to unsubscribe all the subscriptions:
        gatt_client_subscriptions_cleanup_by_connection(connection, true /* should_unsubscribe */);
      }
      prv_finalize_discovery(connection, BTErrnoServiceDiscoveryDatabaseChanged);
      ret_val = gatt_client_discovery_discover_all(device);
    }
  }
  bt_unlock();
  return ret_val;
}
