// Copyright 2024 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.h"

#include <lib/fit/defer.h>

#include <utility>

#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.h"
#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h"
#include "pw_bluetooth_sapphire/internal/host/common/log.h"
#include "pw_bluetooth_sapphire/internal/host/gatt/gatt.h"

using fuchsia::bluetooth::ErrorCode;
using fuchsia::bluetooth::Status;

using fuchsia::bluetooth::gatt::Client;
using fuchsia::bluetooth::gatt::RemoteService;
using fuchsia::bluetooth::gatt::ServiceInfo;
using fuchsia::bluetooth::gatt::ServiceInfoPtr;

namespace bthost {

GattClientServer::GattClientServer(bt::gatt::PeerId peer_id,
                                   bt::gatt::GATT::WeakPtr gatt,
                                   fidl::InterfaceRequest<Client> request)
    : GattServerBase(std::move(gatt), this, std::move(request)),
      peer_id_(peer_id),
      weak_self_(this) {}

void GattClientServer::ListServices(::fidl::VectorPtr<::std::string> fidl_uuids,
                                    ListServicesCallback callback) {
  // Parse the UUID list.
  std::vector<bt::UUID> uuids;
  if (fidl_uuids.has_value()) {
    // Allocate all at once and convert in-place.
    uuids.resize(fidl_uuids->size());
    for (size_t i = 0; i < uuids.size(); ++i) {
      if (!StringToUuid(fidl_uuids.value()[i], &uuids[i])) {
        bt_log(WARN,
               "fidl",
               "%s: Invalid UUID: %s (peer: %s)",
               __FUNCTION__,
               fidl_uuids.value()[i].c_str(),
               bt_str(peer_id_));
        callback(fidl_helpers::NewFidlError(
                     ErrorCode::INVALID_ARGUMENTS,
                     "Invalid UUID: " + fidl_uuids.value()[i]),
                 std::vector<ServiceInfo>(static_cast<size_t>(0u)));
        return;
      }
    }
  }

  auto cb = [callback = std::move(callback),
             peer_id = peer_id_,
             func = __FUNCTION__](bt::att::Result<> status, auto services) {
    std::vector<ServiceInfo> out;
    if (status.is_error()) {
      bt_log(WARN,
             "fidl",
             "%s: Failed to discover services (peer: %s)",
             func,
             bt_str(peer_id));
      auto fidl_status = fidl_helpers::ResultToFidlDeprecated(
          status, "Failed to discover services");
      callback(std::move(fidl_status), std::move(out));
      return;
    }

    out.resize(services.size());

    size_t i = 0;
    for (const auto& svc : services) {
      ServiceInfo service_info;
      service_info.id = svc->handle();
      service_info.primary = svc->info().kind == bt::gatt::ServiceKind::PRIMARY;
      service_info.type = svc->uuid().ToString();
      out[i++] = std::move(service_info);
    }
    callback(Status(), std::move(out));
  };

  gatt()->ListServices(peer_id_, std::move(uuids), std::move(cb));
}

void GattClientServer::ConnectToService(
    uint64_t id, ::fidl::InterfaceRequest<RemoteService> request) {
  if (connected_services_.count(id)) {
    bt_log(WARN,
           "fidl",
           "%s: service already requested (service: %lu, peer: %s)",
           __FUNCTION__,
           id,
           bt_str(peer_id_));
    return;
  }

  // Initialize an entry so that we remember when this request is in progress.
  connected_services_[id] = nullptr;

  bt::gatt::RemoteService::WeakPtr service = gatt()->FindService(peer_id_, id);

  // Automatically called on failure.
  auto fail_cleanup = fit::defer([this, id] { connected_services_.erase(id); });

  if (!service.is_alive()) {
    bt_log(WARN,
           "fidl",
           "%s: failed (service: %lu, peer: %s)",
           __FUNCTION__,
           id,
           bt_str(peer_id_));
    return;
  }

  // Clean up the server if either the peer device or the FIDL client
  // disconnects.
  auto self = weak_self_.GetWeakPtr();
  const char* func = __FUNCTION__;
  auto error_cb = [self, id, peer_id = peer_id_, func] {
    bt_log(DEBUG,
           "fidl",
           "%s: service disconnected (service: %lu, peer: %s)",
           func,
           id,
           bt_str(peer_id));
    if (self.is_alive()) {
      self->connected_services_.erase(id);
    }
  };

  if (!service->AddRemovedHandler(error_cb)) {
    bt_log(WARN,
           "fidl",
           "%s: failed to assign closed handler (service: %lu, peer: %s)",
           func,
           id,
           bt_str(self->peer_id_));
    return;
  }

  fail_cleanup.cancel();

  auto server = std::make_unique<GattRemoteServiceServer>(
      service->GetWeakPtr(), gatt(), peer_id_, std::move(request));
  server->set_error_handler(
      [cb = std::move(error_cb)](zx_status_t status) { cb(); });

  self->connected_services_[id] = std::move(server);
}

}  // namespace bthost
