/*
 * SPDX-License-Identifier: BSD-2-Clause
 *
 *​ Copyright (c) 2025 The FreeBSD Foundation
 *​
 *​ Portions of this software were developed by
 * Tuukka Pasanen <tuukka.pasanen@ilmi.fi> under sponsorship from
 * the FreeBSD Foundation
 */

#include <stdlib.h>
#include <string.h>
#include <libpkgconf/libpkgconf.h>
#include "util.h"
#include "serialize.h"
#include "software.h"
#include "core.h"

/*
 * !doc
 *
 * .. c:function:: spdxtool_software_sbom_t *spdxtool_software_sbom_new(pkgconf_client_t *client, const char *spdx_id, const char *creation_id, const char *sbom_type)
 *
 *    Create new /Software/Sbom struct
 *
 *    :param pkgconf_client_t *client: The pkgconf client being accessed.
 *    :param const char *spdx_id: spdxId for this SBOM element
 *    :param const char *creation_id: id for CreationInfo
 *    :param const char *sbom_type: Sbom types can be found SPDX documention
 *    :return: NULL if some problem occurs and Sbom struct if not
 */
spdxtool_software_sbom_t *
spdxtool_software_sbom_new(pkgconf_client_t *client, const char *spdx_id, const char *creation_id, const char *sbom_type)
{
	if (!client || !spdx_id || !creation_id || !sbom_type)
		return NULL;

	spdxtool_software_sbom_t *sbom = calloc(1, sizeof(spdxtool_software_sbom_t));
	if (!sbom)
		goto err;

	sbom->type = "software_Sbom";
	sbom->spdx_id = strdup(spdx_id);
	sbom->creation_info = strdup(creation_id);
	sbom->sbom_type = strdup(sbom_type);

	if (!sbom->spdx_id || !sbom->creation_info || !sbom->sbom_type)
		goto err;

	return sbom;

err:
	pkgconf_error(client, "spdxtool_software_sbom_new: out of memory");
	spdxtool_software_sbom_free(sbom);
	return NULL;
}

/*
 * !doc
 *
 * .. c:function:: void spdxtool_software_sbom_free(spdxtool_software_sbom_t *sbom)
 *
 *    Free /Software/Sbom struct
 *
 *    :param spdxtool_software_sbom_t *sbom: Sbom struct to be freed.
 *    :return: nothing
 */
void
spdxtool_software_sbom_free(spdxtool_software_sbom_t *sbom)
{
	if(!sbom)
		return;

	free(sbom->spdx_id);
	free(sbom->creation_info);
	free(sbom->sbom_type);

	free(sbom);
}

/*
 * !doc
 *
 * .. c:function:: spdxtool_serialize_value_t *spdxtool_software_sbom_to_object(pkgconf_client_t *client, spdxtool_software_sbom_t *sbom)
 *
 *    Serialize /Software/Sbom struct to a JSON value tree. As a side effect,
 *    the package associated with the SBOM's rootElement is registered on the
 *    document via spdxtool_core_spdx_document_add_package, and relationship
 *    element IDs are registered via spdxtool_core_spdx_document_add_element.
 *
 *    :param pkgconf_client_t *client: The pkgconf client being accessed.
 *    :param spdxtool_software_sbom_t *sbom: Sbom struct to be serialized.
 *    :return: spdxtool_serialize_value_t * representing the Sbom object.
 */
spdxtool_serialize_value_t *
spdxtool_software_sbom_to_object(pkgconf_client_t *client, spdxtool_software_sbom_t *sbom)
{
	spdxtool_serialize_value_t *ret = NULL;
	spdxtool_serialize_object_list_t *object_list = NULL;
	spdxtool_serialize_array_t *sbom_type_array = NULL;
	spdxtool_serialize_array_t *root_element_array = NULL;
	spdxtool_serialize_array_t *element_array = NULL;
	char *spdx_id = NULL;

	char sep = spdxtool_util_get_uri_separator(client);

	spdx_id = spdxtool_util_tuple_lookup(client, &sbom->rootElement->vars, "spdxId");
	if (!spdx_id)
		goto err;

	object_list = spdxtool_serialize_object_list_new();
	if (!object_list)
		goto err;

	sbom_type_array = spdxtool_serialize_array_new();
	if (!sbom_type_array)
		goto err;

	if (!spdxtool_serialize_array_add_string(sbom_type_array, sbom->sbom_type))
		goto err;

	root_element_array = spdxtool_serialize_array_new();
	if (!root_element_array)
		goto err;

	if (!spdxtool_serialize_array_add_string(root_element_array, spdx_id))
		goto err;

	element_array = spdxtool_serialize_array_new();
	if (!element_array)
		goto err;

	pkgconf_node_t *node = NULL;
	PKGCONF_FOREACH_LIST_ENTRY(sbom->rootElement->required.head, node)
	{
		pkgconf_dependency_t *dep = node->data;
		pkgconf_pkg_t *match = dep->match;
		pkgconf_buffer_t relationship_buf = PKGCONF_BUFFER_INITIALIZER;

		/* an unresolved (but tolerated) dependency has no match */
		if (match == NULL)
			continue;

		if (!pkgconf_buffer_append_fmt(&relationship_buf, "%s%cdependsOn%c%s", sbom->rootElement->id, sep, sep, match->id))
			goto err;

		char *relationship_str = pkgconf_buffer_freeze(&relationship_buf);
		if (!relationship_str)
			goto err;

		char *spdx_id_relation = spdxtool_util_get_spdx_id_string(client, "Relationship", relationship_str);
		free(relationship_str);
		if (!spdx_id_relation)
			goto err;

		if (!spdxtool_serialize_array_add_string(element_array, spdx_id_relation))
		{
			free(spdx_id_relation);
			goto err;
		}

		if (!spdxtool_core_spdx_document_add_element(client, sbom->spdx_document, spdx_id_relation))
		{
			free(spdx_id_relation);
			goto err;
		}

		free(spdx_id_relation);
	}

	PKGCONF_FOREACH_LIST_ENTRY(sbom->rootElement->requires_private.head, node)
	{
		pkgconf_dependency_t *dep = node->data;
		pkgconf_pkg_t *match = dep->match;
		pkgconf_buffer_t relationship_buf = PKGCONF_BUFFER_INITIALIZER;

		/* an unresolved (but tolerated) dependency has no match */
		if (match == NULL)
			continue;

		if (!pkgconf_buffer_append_fmt(&relationship_buf, "%s%cdependsOn%c%s", sbom->rootElement->id, sep, sep, match->id))
			goto err;

		char *relationship_str = pkgconf_buffer_freeze(&relationship_buf);
		if (!relationship_str)
			goto err;

		char *spdx_id_relation = spdxtool_util_get_spdx_id_string(client, "Relationship", relationship_str);
		free(relationship_str);
		if (!spdx_id_relation)
			goto err;

		if (!spdxtool_serialize_array_add_string(element_array, spdx_id_relation))
		{
			free(spdx_id_relation);
			goto err;
		}

		if (!spdxtool_core_spdx_document_add_element(client, sbom->spdx_document, spdx_id_relation))
		{
			free(spdx_id_relation);
			goto err;
		}

		free(spdx_id_relation);
	}

	char *value = spdxtool_util_tuple_lookup(client, &sbom->rootElement->vars, "hasDeclaredLicense");
	if (value)
	{
		if (!spdxtool_serialize_array_add_string(element_array, value))
		{
			free(value);
			goto err;
		}

		if (!spdxtool_core_spdx_document_add_element(client, sbom->spdx_document, value))
		{
			free(value);
			goto err;
		}

		free(value);
	}

	value = spdxtool_util_tuple_lookup(client, &sbom->rootElement->vars, "hasConcludedLicense");
	if (value)
	{
		if (!spdxtool_serialize_array_add_string(element_array, value))
		{
			free(value);
			goto err;
		}

		if (!spdxtool_core_spdx_document_add_element(client, sbom->spdx_document, value))
		{
			free(value);
			goto err;
		}

		free(value);
	}

	if (!(spdxtool_serialize_object_add_string(object_list, "type", sbom->type) &&
		spdxtool_serialize_object_add_string(object_list, "creationInfo", sbom->creation_info) &&
		spdxtool_serialize_object_add_string(object_list, "spdxId", sbom->spdx_id)))
	{
		goto err;
	}

	/* object_add_array always takes ownership of the array (it is freed even on
	 * failure), so clear our reference before checking the result to avoid a
	 * double free at the error label.
	 */
	bool ok = spdxtool_serialize_object_add_array(object_list, "software_sbomType", sbom_type_array);
	sbom_type_array = NULL;
	if (!ok)
		goto err;

	ok = spdxtool_serialize_object_add_array(object_list, "rootElement", root_element_array);
	root_element_array = NULL;
	if (!ok)
		goto err;

	ok = spdxtool_serialize_object_add_array(object_list, "element", element_array);
	element_array = NULL;
	if (!ok)
		goto err;

	if (!spdxtool_core_spdx_document_add_package(client, sbom->spdx_document, sbom->rootElement))
		goto err;

	ret = spdxtool_serialize_value_object(object_list);
	object_list = NULL;

err:
	if (!ret)
		pkgconf_error(client, "spdxtool_software_sbom_to_object: out of memory");

	free(spdx_id);
	spdxtool_serialize_object_list_free(object_list);
	spdxtool_serialize_array_free(sbom_type_array);
	spdxtool_serialize_array_free(root_element_array);
	spdxtool_serialize_array_free(element_array);
	return ret;
}

static bool
serialize_copyright_lines_to_object(spdxtool_serialize_object_list_t *object_list, const pkgconf_list_t *copyright_lines)
{
	pkgconf_buffer_t copyright_buf = PKGCONF_BUFFER_INITIALIZER;
	const pkgconf_node_t *node;

	if (copyright_lines->head == NULL)
		return spdxtool_serialize_object_add_string(object_list, "software_copyrightText", "NOASSERTION") != NULL;

	PKGCONF_FOREACH_LIST_ENTRY(copyright_lines->head, node)
	{
		const pkgconf_bufferset_t *set = node->data;
		if (!pkgconf_buffer_join(&copyright_buf, '\n', pkgconf_buffer_str_or_empty(&set->buffer), NULL))
		{
			pkgconf_buffer_finalize(&copyright_buf);
			return false;
		}
	}

	bool ok = spdxtool_serialize_object_add_string(object_list, "software_copyrightText", pkgconf_buffer_str_or_empty(&copyright_buf)) != NULL;
	pkgconf_buffer_finalize(&copyright_buf);
	return ok;
}

/*
 * !doc
 *
 * .. c:function:: spdxtool_serialize_value_t *spdxtool_software_package_to_object(pkgconf_client_t *client, pkgconf_pkg_t *pkg, spdxtool_core_spdx_document_t *spdx)
 *
 *    Serialize /Software/Package struct to a JSON value tree. As a side effect,
 *    any license and dependency relationships generated during serialization are
 *    added to the document via spdxtool_core_spdx_document_add_relationship.
 *
 *    :param pkgconf_client_t *client: The pkgconf client being accessed.
 *    :param pkgconf_pkg_t *pkg: Package struct to be serialized.
 *    :param spdxtool_core_spdx_document_t *spdx: SpdxDocument to which generated relationships are added.
 *    :return: spdxtool_serialize_value_t * representing the Package object.
 */
spdxtool_serialize_value_t *
spdxtool_software_package_to_object(pkgconf_client_t *client, pkgconf_pkg_t *pkg, spdxtool_core_spdx_document_t *spdx)
{
	spdxtool_serialize_value_t *ret = NULL;
	spdxtool_serialize_object_list_t *object_list = NULL;
	spdxtool_serialize_array_t *originated_by = NULL;
	spdxtool_serialize_array_t *supplied_by = NULL;
	char *creation_info = NULL;
	char *spdx_id = NULL;
	char *agent = NULL;
	char *supplier = NULL;
	char *spdx_id_license = NULL;
	pkgconf_list_t relations = PKGCONF_LIST_INITIALIZER;
	pkgconf_list_t *cpy_relations = NULL;
	pkgconf_node_t *node = NULL;
	char sep = spdxtool_util_get_uri_separator(client);

	creation_info = spdxtool_util_tuple_lookup(client, &pkg->vars, "creationInfo");
	spdx_id = spdxtool_util_tuple_lookup(client, &pkg->vars, "spdxId");
	agent = spdxtool_util_tuple_lookup(client, &pkg->vars, "agent");

	if (!creation_info || !spdx_id || !agent)
		goto err;

	object_list = spdxtool_serialize_object_list_new();
	if (!object_list)
		goto err;

	originated_by = spdxtool_serialize_array_new();
	if (!originated_by)
		goto err;

	if (!spdxtool_serialize_array_add_string(originated_by, agent))
		goto err;

	if (!(spdxtool_serialize_object_add_string(object_list, "type", "software_Package") &&
		spdxtool_serialize_object_add_string(object_list, "creationInfo", creation_info) &&
		spdxtool_serialize_object_add_string(object_list, "spdxId", spdx_id) &&
		spdxtool_serialize_object_add_string(object_list, "name", pkg->realname)))
	{
		goto err;
	}

	/* object_add_array always takes ownership of the array (it is freed even on
	 * failure), so clear our reference before checking the result to avoid a
	 * double free at the error label.
	 */
	bool ok = spdxtool_serialize_object_add_array(object_list, "originatedBy", originated_by);
	originated_by = NULL;
	if (!ok)
		goto err;

	supplier = spdxtool_util_tuple_lookup(client, &pkg->vars, "suppliedBy");
	if (supplier)
	{
		supplied_by = spdxtool_serialize_array_new();
		if (!supplied_by)
			goto err;

		if (!spdxtool_serialize_array_add_string(supplied_by, supplier))
			goto err;

		ok = spdxtool_serialize_object_add_array(object_list, "suppliedBy", supplied_by);
		supplied_by = NULL;
		if (!ok)
			goto err;
	}

	if (!serialize_copyright_lines_to_object(object_list, &pkg->copyright))
		goto err;

	if (!spdxtool_serialize_object_add_string(object_list, "software_homePage",
		pkg->url ? pkg->url : ""))
	{
		goto err;
	}

	if (!spdxtool_serialize_object_add_string(object_list, "software_downloadLocation",
		pkg->source ? pkg->source : ""))
	{
		goto err;
	}

	if (!spdxtool_serialize_object_add_string(object_list, "software_packageVersion", pkg->version))
		goto err;

	PKGCONF_FOREACH_LIST_ENTRY(pkg->license.head, node)
	{
		const pkgconf_license_t *license = node->data;
		if (license->type == PKGCONF_LICENSE_EXPRESSION)
		{
			spdx_id_license = spdxtool_util_get_spdx_id_string(client, "simplelicensing_LicenseExpression", license->data);
			if (!spdx_id_license)
				goto err;

			pkgconf_license_insert(client, &relations, PKGCONF_LICENSE_UNKNOWN, spdx_id_license);
			free(spdx_id_license);
			spdx_id_license = NULL;
		}
	}

	char *tuple_license = spdxtool_util_tuple_lookup(client, &pkg->vars, "hasDeclaredLicense");
	if (tuple_license)
	{
		cpy_relations = calloc(1, sizeof(pkgconf_list_t));
		if (!cpy_relations)
		{
			free(tuple_license);
			goto err;
		}

		pkgconf_license_copy_list(client, cpy_relations, &relations);
		spdxtool_core_relationship_t *relationship = spdxtool_core_relationship_new(client, creation_info, tuple_license, spdx_id, cpy_relations, "hasDeclaredLicense");
		free(tuple_license);
		if (!relationship)
			goto err;
		if (!spdxtool_core_spdx_document_add_relationship(client, spdx, relationship))
			goto err;
		cpy_relations = NULL;
	}

	tuple_license = spdxtool_util_tuple_lookup(client, &pkg->vars, "hasConcludedLicense");
	if (tuple_license)
	{
		cpy_relations = calloc(1, sizeof(pkgconf_list_t));
		if (!cpy_relations)
		{
			free(tuple_license);
			goto err;
		}

		pkgconf_license_copy_list(client, cpy_relations, &relations);
		spdxtool_core_relationship_t *relationship = spdxtool_core_relationship_new(client, creation_info, tuple_license, spdx_id, cpy_relations, "hasConcludedLicense");
		free(tuple_license);
		if (!relationship)
			goto err;
		if (!spdxtool_core_spdx_document_add_relationship(client, spdx, relationship))
			goto err;
		cpy_relations = NULL;
	}
	pkgconf_license_free(&relations);

	PKGCONF_FOREACH_LIST_ENTRY(pkg->required.head, node)
	{
		pkgconf_dependency_t *dep = node->data;
		pkgconf_pkg_t *match = dep->match;
		pkgconf_buffer_t relationship_buf = PKGCONF_BUFFER_INITIALIZER;

		/* an unresolved (but tolerated) dependency has no match */
		if (match == NULL)
			continue;

		if (!pkgconf_buffer_append_fmt(&relationship_buf, "%s%cdependsOn%c%s", pkg->id, sep, sep, match->id))
			goto err;

		char *relationship_str = pkgconf_buffer_freeze(&relationship_buf);
		if (!relationship_str)
			goto err;

		char *spdx_id_relation = spdxtool_util_get_spdx_id_string(client, "Relationship", relationship_str);
		free(relationship_str);
		if (!spdx_id_relation)
			goto err;

		char *spdx_id_package = spdxtool_util_get_spdx_id_string(client, "Package", match->id);
		if (!spdx_id_package)
		{
			free(spdx_id_relation);
			goto err;
		}

		cpy_relations = calloc(1, sizeof(pkgconf_list_t));
		if (!cpy_relations)
		{
			free(spdx_id_relation);
			free(spdx_id_package);
			goto err;
		}

		pkgconf_license_insert(client, cpy_relations, PKGCONF_LICENSE_UNKNOWN, spdx_id_package);
		spdxtool_core_relationship_t *relationship = spdxtool_core_relationship_new(client, creation_info, spdx_id_relation, spdx_id, cpy_relations, "dependsOn");
		free(spdx_id_relation);
		free(spdx_id_package);
		if (!relationship)
			goto err;
		if (!spdxtool_core_spdx_document_add_relationship(client, spdx, relationship))
			goto err;
		cpy_relations = NULL;
	}

	PKGCONF_FOREACH_LIST_ENTRY(pkg->requires_private.head, node)
	{
		pkgconf_dependency_t *dep = node->data;
		pkgconf_pkg_t *match = dep->match;
		pkgconf_buffer_t relationship_buf = PKGCONF_BUFFER_INITIALIZER;

		/* an unresolved (but tolerated) dependency has no match */
		if (match == NULL)
			continue;

		if (!pkgconf_buffer_append_fmt(&relationship_buf, "%s%cdependsOn%c%s", pkg->id, sep, sep, match->id))
			goto err;

		char *relationship_str = pkgconf_buffer_freeze(&relationship_buf);
		if (!relationship_str)
			goto err;

		char *spdx_id_relation = spdxtool_util_get_spdx_id_string(client, "Relationship", relationship_str);
		free(relationship_str);
		if (!spdx_id_relation)
			goto err;

		char *spdx_id_package = spdxtool_util_get_spdx_id_string(client, "Package", match->id);
		if (!spdx_id_package)
		{
			free(spdx_id_relation);
			goto err;
		}

		cpy_relations = calloc(1, sizeof(pkgconf_list_t));
		if (!cpy_relations)
		{
			free(spdx_id_relation);
			free(spdx_id_package);
			goto err;
		}

		pkgconf_license_insert(client, cpy_relations, PKGCONF_LICENSE_UNKNOWN, spdx_id_package);
		spdxtool_core_relationship_t *relationship = spdxtool_core_relationship_new(client, creation_info, spdx_id_relation, spdx_id, cpy_relations, "dependsOn");
		free(spdx_id_relation);
		free(spdx_id_package);
		if (!relationship)
			goto err;
		cpy_relations = NULL;
		if (!spdxtool_core_relationship_set_scope(client, relationship, "development"))
		{
			spdxtool_core_relationship_free(relationship);
			goto err;
		}
		if (!spdxtool_core_spdx_document_add_relationship(client, spdx, relationship))
		{
			spdxtool_core_relationship_free(relationship);
			goto err;
		}
	}

	ret = spdxtool_serialize_value_object(object_list);
	object_list = NULL;

err:
	if (!ret)
		pkgconf_error(client, "spdxtool_software_package_to_object: out of memory");

	free(creation_info);
	free(spdx_id);
	free(agent);
	free(supplier);
	free(spdx_id_license);
	spdxtool_serialize_object_list_free(object_list);
	spdxtool_serialize_array_free(originated_by);
	spdxtool_serialize_array_free(supplied_by);
	return ret;
}
