// MIT License
//
// Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#include "libpyrocpd.hpp"
#include "lib/output/format_path.hpp"
#include "lib/python/rocpd/source/common.hpp"
#include "lib/python/rocpd/source/functions.hpp"
#include "lib/python/rocpd/source/interop.hpp"
#include "lib/python/rocpd/source/otf2.hpp"
#include "lib/python/rocpd/source/perfetto.hpp"
#include "lib/python/rocpd/source/serialization/sql.hpp"
#include "lib/python/rocpd/source/sql_generator.hpp"
#include "lib/python/rocpd/source/types.hpp"

#include "lib/common/defines.hpp"
#include "lib/common/logging.hpp"
#include "lib/common/simple_timer.hpp"
#include "lib/common/utility.hpp"
#include "lib/output/agent_info.hpp"
#include "lib/output/kernel_symbol_info.hpp"
#include "lib/output/output_config.hpp"
#include "lib/output/output_stream.hpp"
#include "lib/output/sql/common.hpp"
#include "lib/output/timestamps.hpp"

#include <rocprofiler-sdk/agent.h>
#include <rocprofiler-sdk/fwd.h>
#include <rocprofiler-sdk/cxx/details/mpl.hpp>
#include <rocprofiler-sdk/cxx/details/tokenize.hpp>
#include <rocprofiler-sdk/cxx/hash.hpp>
#include <rocprofiler-sdk/cxx/operators.hpp>
#include <rocprofiler-sdk/cxx/perfetto.hpp>
#include <rocprofiler-sdk/cxx/utility.hpp>

#include <rocprofiler-sdk-rocpd/rocpd.h>
#include <rocprofiler-sdk-rocpd/sql.h>

#include <fmt/format.h>
#include <gotcha/gotcha.h>
#include <pybind11/detail/common.h>
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <sqlite3.h>

#include <atomic>
#include <future>
#include <mutex>
#include <utility>

namespace py = ::pybind11;

namespace rocpd
{
template <typename Tp>
auto
read_impl(sqlite3* conn, std::string_view conditions)
{
    auto query = std::string_view{};

    if constexpr(std::is_same<Tp, types::node>::value)
        query = "rocpd_info_node";
    else if constexpr(std::is_same<Tp, types::process>::value)
        query = "processes";
    else if constexpr(std::is_same<Tp, types::thread>::value)
        query = "threads";
    else if constexpr(std::is_same<Tp, types::region>::value)
        query = "regions";
    else if constexpr(std::is_same<Tp, types::kernel_dispatch>::value)
        query = "kernels";
    else if constexpr(std::is_same<Tp, types::agent>::value)
        query = "rocpd_info_agent";
    else
        static_assert(rocprofiler::sdk::mpl::assert_false<Tp>::value, "Unsupported read type");

    auto data = std::vector<Tp>{};
    if(conn)
    {
        auto ar = cereal::SQLite3InputArchive{
            conn, fmt::format("SELECT * FROM {} {}", query, conditions)};
        cereal::load(ar, data);
    }
    return data;
}

template <typename Tp>
auto
read_impl(py::object       obj,  // NOLINT(performance-unnecessary-value-param)
          std::string_view conditions)
{
    return read_impl<Tp>(rocpd::interop::get_connection(std::move(obj)), conditions);
}

template <typename Tp, typename ArgT = py::object>
auto
read(ArgT&& arg, std::string_view conditions = {})
{
    return read_impl<Tp>(std::forward<ArgT>(arg), conditions);
}

bool
is_sqlite3_connection(const py::object& obj)
{
    py::module_ sqlite3         = py::module_::import("sqlite3");
    py::object  connection_type = sqlite3.attr("Connection");
    return py::isinstance(obj, connection_type);
}

std::string
get_type_name(const py::object& obj)
{
    return obj.get_type().attr("__name__").cast<std::string>();
}

struct RocpdImportData
{
    RocpdImportData()  = default;
    ~RocpdImportData() = default;

    RocpdImportData(const RocpdImportData&)     = default;
    RocpdImportData(RocpdImportData&&) noexcept = default;
    RocpdImportData& operator=(const RocpdImportData&) = default;
    RocpdImportData& operator=(RocpdImportData&&) noexcept = default;

    RocpdImportData(const py::object& _obj, const std::vector<std::string>& _dbs)
    : connection{_obj}
    , databases{_dbs}
    {
        if(py::isinstance<RocpdImportData>(_obj))
        {
            connection = _obj.cast<RocpdImportData>().connection;
            databases  = _obj.cast<RocpdImportData>().databases;
        }
        else
        {
            if(!is_sqlite3_connection(_obj))
            {
                auto _errmsg = fmt::format("libpyrocpd.RocpdImportData cannot be constructed "
                                           "from provided Python object of type {} (databases: {})",
                                           get_type_name(_obj),
                                           fmt::join(_dbs.begin(), _dbs.end(), ", "));
                ROCP_CI_LOG(WARNING) << _errmsg;
                throw py::type_error{_errmsg};
            }
        }
    }

    size_t size() const { return (connection) ? databases.size() : 0; }
    bool   empty() const { return databases.empty() || !connection; }

    py::object               connection = {};
    std::vector<std::string> databases  = {};
};

struct jinja_variables
{
    py::str uuid = py::none{};
    py::str guid = py::none{};
};
}  // namespace rocpd

PYBIND11_MODULE(libpyrocpd, pyrocpd)
{
    // namespace sdk  = ::rocprofiler::sdk;
    namespace tool   = ::rocprofiler::tool;
    namespace common = ::rocprofiler::common;

    py::doc("ROCm Profiling Data (RocPD) Python bindings");

    // initialize logging with control via ROCPD_LOG_LEVEL env variable
    common::init_logging("ROCPD");

    rocpd::interop::activate_gotcha_bindings();

    if(auto _thrd_safety = sqlite3_threadsafe(); _thrd_safety == 2)
    {
        ROCP_INFO_IF(sqlite3_config(SQLITE_CONFIG_MULTITHREAD) == SQLITE_MISUSE)
            << "sqlite3 cannot be configured to support multithreading";
    }

    pyrocpd.def(
        "format_path",
        [](std::string inp, const std::string& tag) {
            return tool::format_path(std::move(inp), tag);
        },
        "Resolve output keys in filepath");

    py::enum_<rocpd_sql_engine_t>(pyrocpd, "sql_engine", "Load schema engines")
        .value("sqlite3", ROCPD_SQL_ENGINE_SQLITE3);

    py::enum_<rocpd_sql_schema_kind_t>(pyrocpd, "sql_schema", "Load schema kinds")
        .value("rocpd_tables", ROCPD_SQL_SCHEMA_ROCPD_TABLES)
        .value("rocpd_indexes", ROCPD_SQL_SCHEMA_ROCPD_INDEXES)
        .value("rocpd_views", ROCPD_SQL_SCHEMA_ROCPD_VIEWS)
        .value("data_views", ROCPD_SQL_SCHEMA_ROCPD_DATA_VIEWS)
        .value("summary_views", ROCPD_SQL_SCHEMA_ROCPD_SUMMARY_VIEWS)
        .value("marker_views", ROCPD_SQL_SCHEMA_ROCPD_MARKER_VIEWS);

    py::enum_<rocpd_sql_options_t>(pyrocpd, "sql_option", "Load schema options")
        .value("none", ROCPD_SQL_OPTIONS_NONE)
        .value("sqlite3_pragma_foreign_keys", ROCPD_SQL_OPTIONS_SQLITE3_PRAGMA_FOREIGN_KEYS);

    py::enum_<tool::agent_indexing>(pyrocpd, "agent_indexing", "enum.Enum")
        .value("node", tool::agent_indexing::node)
        .value("logical_node", tool::agent_indexing::logical_node)
        .value("logical_node_type", tool::agent_indexing::logical_node_type);

    // demo for creating python bindings to a class
    py::class_<rocpd::types::agent>(pyrocpd, "agent")
        .def_readonly("node_id", &rocpd::types::agent::node_id)
        .def_readonly("logical_node_id", &rocpd::types::agent::logical_node_id)
        .def_readonly("gpu_index", &rocpd::types::agent::gpu_index)
        .def_readonly("name", &rocpd::types::agent::name)
        .def_readonly("user_name", &rocpd::types::agent::user_name)
        .def_readonly("product_name", &rocpd::types::agent::product_name);

    py::class_<rocpd::types::node>(pyrocpd, "node")
        .def(py::init<>())
        .def_readonly("nid", &rocpd::types::node::id)
        .def_readonly("hash", &rocpd::types::node::hash)
        .def_readonly("machine_id", &rocpd::types::node::machine_id)
        .def_readonly("hostname", &rocpd::types::node::hostname)
        .def_readonly("system_name", &rocpd::types::node::system_name)
        .def_readonly("system_release", &rocpd::types::node::release)
        .def_readonly("system_version", &rocpd::types::node::version);

    py::class_<rocpd::types::process>(pyrocpd, "process")
        .def(py::init<>())
        .def_readonly("nid", &rocpd::types::process::nid)
        .def_readonly("machine_id", &rocpd::types::process::machine_id)
        .def_readonly("hostname", &rocpd::types::process::hostname)
        .def_readonly("system_name", &rocpd::types::process::system_name)
        .def_readonly("system_release", &rocpd::types::process::system_release)
        .def_readonly("system_version", &rocpd::types::process::system_version)
        .def_readonly("ppid", &rocpd::types::process::ppid)
        .def_readonly("pid", &rocpd::types::process::pid)
        .def_readonly("init", &rocpd::types::process::init)
        .def_readonly("start", &rocpd::types::process::start)
        .def_readonly("end", &rocpd::types::process::end)
        .def_readonly("fini", &rocpd::types::process::fini)
        .def_readonly("command", &rocpd::types::process::command);

    py::class_<rocpd::types::thread>(pyrocpd, "thread")
        .def(py::init<>())
        .def_readonly("nid", &rocpd::types::thread::nid)
        .def_readonly("machine_id", &rocpd::types::thread::machine_id)
        .def_readonly("hostname", &rocpd::types::thread::hostname)
        .def_readonly("system_name", &rocpd::types::thread::system_name)
        .def_readonly("system_release", &rocpd::types::thread::system_release)
        .def_readonly("system_version", &rocpd::types::thread::system_version)
        .def_readonly("ppid", &rocpd::types::thread::ppid)
        .def_readonly("pid", &rocpd::types::thread::pid)
        .def_readonly("tid", &rocpd::types::thread::tid)
        .def_readonly("start", &rocpd::types::thread::start)
        .def_readonly("end", &rocpd::types::thread::end)
        .def_readonly("name", &rocpd::types::thread::name);

    py::class_<tool::output_config>(pyrocpd, "output_config", "Output configuration")
        .def(py::init<>())
        .def_readwrite("output_path", &tool::output_config::output_path)
        .def_readwrite("output_file", &tool::output_config::output_file)
        .def_readwrite("tmp_directory", &tool::output_config::tmp_directory)
        .def_readwrite("csv", &tool::output_config::csv_output)
        .def_readwrite("pftrace", &tool::output_config::pftrace_output)
        .def_readwrite("otf2", &tool::output_config::otf2_output)
        .def_readwrite("kernel_rename", &tool::output_config::kernel_rename)
        .def_readwrite("agent_index_value", &tool::output_config::agent_index_value)
        .def_readwrite("group_by_queue", &tool::output_config::group_by_queue)
        .def_readwrite("perfetto_shmem_size_hint", &tool::output_config::perfetto_shmem_size_hint)
        .def_readwrite("perfetto_buffer_size", &tool::output_config::perfetto_buffer_size)
        .def_readwrite("perfetto_backend", &tool::output_config::perfetto_backend)
        .def_readwrite("perfetto_buffer_fill_policy",
                       &tool::output_config::perfetto_buffer_fill_policy);

    py::class_<tool::metadata>(pyrocpd, "metadata")
        .def("set_process_id", &tool::metadata::set_process_id)
        .def("add_marker_message", &tool::metadata::add_marker_message)
        // .def("add_code_object", &tool::metadata::add_code_object)
        // .def("add_kernel_symbol", &tool::metadata::add_kernel_symbol)
        // .def("add_host_function", &tool::metadata::add_host_function)
        .def("add_string_entry", &tool::metadata::add_string_entry)
        .def("add_external_correlation_id", &tool::metadata::add_external_correlation_id)
        .def("add_agent",
             [](tool::metadata* md, const rocpd::types::agent& _agent) {
                 if(!md) return;
                 md->agents.emplace_back(_agent.base());
                 md->agents_map.emplace(_agent.id, _agent.base());
             })
        .def_readwrite("process_id", &tool::metadata::process_id)
        .def_readwrite("parent_process_id", &tool::metadata::parent_process_id)
        .def_readwrite("process_start_ns", &tool::metadata::process_start_ns)
        .def_readwrite("process_end_ns", &tool::metadata::process_end_ns)
        .def_readwrite("agents", &tool::metadata::agents_map)
        .def_readwrite("node_data", &tool::metadata::node_data)
        .def_readwrite("att_filenames", &tool::metadata::att_filenames)
        .def_readwrite("buffer_names", &tool::metadata::buffer_names)
        .def_readwrite("callback_names", &tool::metadata::callback_names)
        .def_readwrite("command_line", &tool::metadata::command_line);

    py::class_<rocpd::jinja_variables>(
        pyrocpd, "schema_jinja_variables", "Variables for jinja substitution")
        .def(py::init<>())
        .def_readwrite("uuid", &rocpd::jinja_variables::uuid)
        .def_readwrite("guid", &rocpd::jinja_variables::guid);

    py::class_<rocpd::RocpdImportData>(pyrocpd, "RocpdImportData", "RocPD database(s) instances")
        .def(py::init<>())
        .def(py::init<rocpd::RocpdImportData>())
        .def(py::init<py::object, std::vector<std::string>>())
        .def_readonly("connection", &rocpd::RocpdImportData::connection)
        .def_readonly("databases", &rocpd::RocpdImportData::databases);

    pyrocpd.def("load_schema",
                [](rocpd_sql_engine_t            engine,
                   rocpd_sql_schema_kind_t       kind,
                   rocpd_sql_options_t           options,
                   const rocpd::jinja_variables& variables) {
                    auto _callback = [](rocpd_sql_engine_t                        _engine,
                                        rocpd_sql_schema_kind_t                   _kind,
                                        rocpd_sql_options_t                       _options,
                                        const rocpd_sql_schema_jinja_variables_t* _variables,
                                        const char*                               _schema_path,
                                        const char*                               _schema_content,
                                        void* _user_data) -> void {
                        rocprofiler::common::consume_args(
                            _engine, _kind, _options, _variables, _schema_path);
                        auto* _data = static_cast<std::string*>(_user_data);
                        if(_data && _schema_content) *_data = std::string{_schema_content};
                    };

                    auto _uuid = std::optional<std::string>{};
                    if(!variables.uuid.is(py::none{}))
                        _uuid = py::cast<std::string>(variables.uuid);

                    auto _guid = std::optional<std::string>{};
                    if(!variables.guid.is(py::none{}))
                        _guid = py::cast<std::string>(variables.guid);

                    auto _rocpd_variables =
                        common::init_public_api_struct(rocpd_sql_schema_jinja_variables_t{});
                    if(_uuid) _rocpd_variables.uuid = _uuid->c_str();
                    if(_guid) _rocpd_variables.guid = _guid->c_str();

                    auto _hints = std::vector<const char*>{};
                    // for(const auto& itr : schema_path_hints)
                    //     _hints.emplace_back(itr.c_str());

                    auto _contents = std::string{};
                    ROCPD_CHECK(rocpd_sql_load_schema(engine,
                                                      kind,
                                                      options,
                                                      &_rocpd_variables,
                                                      _callback,
                                                      _hints.data(),
                                                      _hints.size(),
                                                      &_contents));
                    return _contents;
                });

    // NOLINTBEGIN(performance-unnecessary-value-param)

    // function which maps the python sqlite3.Connection object to the sqlite3*
    // pointer
    pyrocpd.def(
        "connect",
        [](std::string dbpath, py::args args, py::kwargs kwargs) {
            // import the sqlite3 module
            auto sqlite3_mod = py::module_::import("sqlite3");
            auto ret         = sqlite3_mod.attr("connect")(dbpath, *args, **kwargs);

            auto* db = rocpd::interop::map_connection(ret);
            // this is a no-op right now
            if(db) rocpd::functions::define_for_database(db);

            return ret;
        },
        "Open a database connection");

    pyrocpd.def(
        "write_perfetto",
        [](rocpd::RocpdImportData& data, const tool::output_config& output_cfg) -> bool {
            auto _create_agent_index =
                [&output_cfg](const rocpd::types::agent& _agent) -> tool::agent_index {
                auto ret_index = tool::create_agent_index(
                    output_cfg.agent_index_value,
                    _agent.node_id,                                      // absolute index
                    static_cast<uint32_t>(_agent.logical_node_id),       // relative index
                    static_cast<uint32_t>(_agent.logical_node_type_id),  // type-relative index
                    std::string_view(_agent.type));
                return ret_index;
            };
            // ORDER BY expression for kernel dispatches
            constexpr auto kernels_order_by =
                "agent_abs_index ASC, stream_id ASC, queue_id ASC, start ASC, end DESC";

            constexpr auto region_order_by = "start ASC, end DESC";
            constexpr auto sample_order_by = "timestamp ASC";

            auto perfetto_session = rocpd::output::PerfettoSession{output_cfg};
            auto sqlgen_perf      = common::simple_timer{
                fmt::format("Perfetto generation from {} SQL database(s)", data.size())};
            for(auto obj : {data.connection})
            {
                auto* conn  = rocpd::interop::get_connection(std::move(obj));
                auto  nodes = rocpd::read<rocpd::types::node>(conn);

                for(const auto& nitr : nodes)
                {
                    auto agents = rocpd::read<rocpd::types::agent>(
                        conn, fmt::format("WHERE guid = '{}' AND nid = {}", nitr.guid, nitr.id));
                    auto processes = rocpd::read<rocpd::types::process>(
                        conn, fmt::format("WHERE guid = '{}' AND nid = {}", nitr.guid, nitr.id));

                    for(const auto& pitr : processes)
                    {
                        ROCP_FATAL_IF(pitr.nid != nitr.id || pitr.guid != nitr.guid)
                            << fmt::format("Found process with a mismatched nid/guid. process: "
                                           "{}/{} vs. node: {}/{}",
                                           pitr.nid,
                                           pitr.guid,
                                           nitr.id,
                                           nitr.guid);
                        auto select_guid_nid_pid = [&nitr, &pitr](std::string_view tbl) {
                            return fmt::format("SELECT * FROM {} WHERE guid = '{}' AND nid "
                                               "= {} AND pid = {}",
                                               tbl,
                                               pitr.guid,
                                               nitr.id,
                                               pitr.pid);
                        };

                        auto _sqlgen_perft = common::simple_timer{fmt::format(
                            "Perfetto generation from SQL for process {} (total)", pitr.pid)};

                        auto kernels = rocpd::sql_generator<rocpd::types::kernel_dispatch>{
                            conn, select_guid_nid_pid("kernels"), kernels_order_by};

                        auto memory_allocations =
                            rocpd::sql_generator<rocpd::types::memory_allocation>{
                                conn, select_guid_nid_pid("memory_allocations")};

                        auto memory_copies = rocpd::sql_generator<rocpd::types::memory_copies>{
                            conn, select_guid_nid_pid("memory_copies")};

                        auto scratch_memory = rocpd::sql_generator<rocpd::types::scratch_memory>{
                            conn, select_guid_nid_pid("scratch_memory")};

                        auto counters = rocpd::sql_generator<rocpd::types::counter>{
                            conn, select_guid_nid_pid("counters_collection")};

                        auto regions = rocpd::sql_generator<rocpd::types::region>{
                            conn, select_guid_nid_pid("regions"), region_order_by};

                        auto samples = rocpd::sql_generator<rocpd::types::sample>{
                            conn, select_guid_nid_pid("samples"), sample_order_by};

                        auto threads = rocpd::sql_generator<rocpd::types::thread>{
                            conn, select_guid_nid_pid("threads")};

                        // absolute_index |-> (agent, agent_index)
                        auto agents_map =
                            std::unordered_map<uint64_t,
                                               std::pair<rocpd::types::agent, tool::agent_index>>{};

                        for(const auto& itr : agents)
                        {
                            auto new_index = _create_agent_index(itr);
                            agents_map.emplace(itr.absolute_index, std::make_pair(itr, new_index));
                        }

                        ROCP_TRACE << "Starting Perfetto generation from SQL for process "
                                   << pitr.pid;
                        auto _sqlgen_perfw = common::simple_timer{fmt::format(
                            "Perfetto generation from SQL for process {} (write)", pitr.pid)};
                        rocpd::output::write_perfetto(perfetto_session,
                                                      pitr,
                                                      agents_map,
                                                      threads,
                                                      regions,
                                                      samples,
                                                      kernels,
                                                      memory_copies,
                                                      scratch_memory,
                                                      memory_allocations,
                                                      counters);
                    }
                }
            }
            return true;
        },
        "Write pftrace output file from rocpd SQLite3 database");

    pyrocpd.def(
        "write_otf2",
        [](rocpd::RocpdImportData& data, const tool::output_config& output_cfg) {
            auto _create_agent_index =
                [&output_cfg](const rocpd::types::agent& _agent) -> tool::agent_index {
                auto ret_index = tool::create_agent_index(
                    output_cfg.agent_index_value,
                    _agent.node_id,                                      // absolute index
                    static_cast<uint32_t>(_agent.logical_node_id),       // relative index
                    static_cast<uint32_t>(_agent.logical_node_type_id),  // type-relative index
                    std::string_view(_agent.type));
                return ret_index;
            };

            constexpr auto kernels_order_by =
                "agent_abs_index ASC, stream_id ASC, queue_id ASC, start ASC, end DESC";

            // to initialise the OTF@ session properly we need to know:
            // (1) the process with the earliest start time
            // (2) find the process with the longest duration
            uint64_t min_start_time = std::numeric_limits<uint64_t>::max();
            uint64_t max_fini_time  = std::numeric_limits<uint64_t>::min();
            for(auto obj : {data.connection})
            {
                auto* conn = rocpd::interop::get_connection(std::move(obj));

                // min start
                sqlite3_stmt* _stmt_min_start_max_fini = nullptr;
                uint64_t      _min_start_time          = std::numeric_limits<uint64_t>::max();
                uint64_t      _max_fini_time           = std::numeric_limits<uint64_t>::min();

                sqlite3_prepare_v2(conn,
                                   "SELECT MIN(start), MAX(fini) FROM processes;",
                                   -1,
                                   &_stmt_min_start_max_fini,
                                   nullptr);
                if(sqlite3_step(_stmt_min_start_max_fini) == SQLITE_ROW)
                {
                    _min_start_time =
                        static_cast<uint64_t>(sqlite3_column_int64(_stmt_min_start_max_fini, 0));
                    _max_fini_time =
                        static_cast<uint64_t>(sqlite3_column_int64(_stmt_min_start_max_fini, 1));
                }

                sqlite3_finalize(_stmt_min_start_max_fini);
                min_start_time = std::min(min_start_time, _min_start_time);
                max_fini_time  = std::max(max_fini_time, _max_fini_time);
            }

            auto otf2_session =
                rocpd::output::OTF2Session(output_cfg, min_start_time, max_fini_time);

            auto sqlgen_otf2 = common::simple_timer{
                fmt::format("OTF2 generation from {} SQL database(s)", data.size())};

            uint16_t _process_counter = 0;
            for(auto obj : {data.connection})
            {
                auto* conn  = rocpd::interop::get_connection(std::move(obj));
                auto  nodes = rocpd::read<rocpd::types::node>(conn);
                for(const auto& nitr : nodes)
                {
                    auto agents = rocpd::read<rocpd::types::agent>(
                        conn, fmt::format("WHERE guid = '{}' AND nid = {}", nitr.guid, nitr.id));
                    auto processes = rocpd::read<rocpd::types::process>(
                        conn, fmt::format("WHERE guid = '{}' AND nid = {}", nitr.guid, nitr.id));

                    // absolute_index |-> (agent, agent_index)
                    auto agents_map = std::unordered_map<uint64_t, rocpd::output::extended_agent>{};

                    for(const auto& itr : agents)
                    {
                        const rocprofiler::tool::agent_index new_index = _create_agent_index(itr);
                        const std::string labeled_name = fmt::format("{}", itr.name);
                        agents_map.emplace(
                            itr.absolute_index,
                            rocpd::output::extended_agent{itr, new_index, labeled_name});
                    }

                    for(const auto& pitr : processes)
                    {
                        ROCP_FATAL_IF(pitr.nid != nitr.id || pitr.guid != nitr.guid)
                            << fmt::format("Found process with a mismatched nid/guid. process: "
                                           "{}/{} vs. node: {}/{}",
                                           pitr.nid,
                                           pitr.guid,
                                           nitr.id,
                                           nitr.guid);

                        auto select_guid_nid_pid =
                            [&nitr, &pitr](std::string_view tbl,
                                           std::string_view where_extra_condition = "") {
                                return fmt::format("SELECT * FROM {} WHERE guid = '{}' AND "
                                                   "nid = {} AND pid = {} {}",
                                                   tbl,
                                                   pitr.guid,
                                                   nitr.id,
                                                   pitr.pid,
                                                   where_extra_condition);
                            };

                        constexpr auto region_order_by = "start ASC, end DESC";

                        auto _sqlgen_otf2 = common::simple_timer{fmt::format(
                            "OTF2 generation from SQL for process {} (total)", pitr.pid)};

                        auto kernels = rocpd::sql_generator<rocpd::types::kernel_dispatch>{
                            conn, select_guid_nid_pid("kernels"), kernels_order_by};

                        auto memory_allocations =
                            rocpd::sql_generator<rocpd::types::memory_allocation>{
                                conn, select_guid_nid_pid("memory_allocations"), region_order_by};

                        auto memory_copies = rocpd::sql_generator<rocpd::types::memory_copies>{
                            conn, select_guid_nid_pid("memory_copies"), region_order_by};

                        auto regions = rocpd::sql_generator<rocpd::types::region>{
                            conn, select_guid_nid_pid("regions"), region_order_by};

                        auto threads = rocpd::sql_generator<rocpd::types::thread>{
                            conn, select_guid_nid_pid("threads")};

                        ROCP_TRACE << "Starting OTF2 generation from SQL for process " << pitr.pid;
                        auto _sqlgen_perfw = common::simple_timer{fmt::format(
                            "OTF2 generation from SQL for process {} (write)", pitr.pid)};
                        rocpd::output::write_otf2(otf2_session,
                                                  pitr,
                                                  _process_counter,
                                                  agents_map,
                                                  threads,
                                                  regions,
                                                  kernels,
                                                  memory_copies,
                                                  memory_allocations);
                        _process_counter++;
                    }
                }
            }
        },
        "Write OTF2 output file from rocpd SQLite3 database");

    // NOLINTEND(performance-unnecessary-value-param)

    // reads in all the agent info from database
    pyrocpd.def(
        "read_agents",
        [](const rocpd::RocpdImportData& data, const std::string& conditions) {
            return rocpd::read<rocpd::types::agent>(data.connection, conditions);
        },
        "Reads in the rocprofiler-sdk agents from the database");

    pyrocpd.def(
        "read_nodes",
        [](const rocpd::RocpdImportData& data, const std::string& conditions) {
            return rocpd::read<rocpd::types::node>(data.connection, conditions);
        },
        "Reads in the node information from the database");

    pyrocpd.def(
        "read_processes",
        [](const rocpd::RocpdImportData& data, const std::string& conditions) {
            return rocpd::read<rocpd::types::process>(data.connection, conditions);
        },
        "Reads in the process information from the database");

    pyrocpd.def(
        "read_threads",
        [](const rocpd::RocpdImportData& data, const std::string& conditions) {
            return rocpd::read<rocpd::types::thread>(data.connection, conditions);
        },
        "Reads in the thread information from the database");
}
