/*
 * Sai server
 *
 * Copyright (C) 2019 - 2025 Andy Green <andy@warmcat.com>
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation:
 *  version 2.1 of the License.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *  MA  02110-1301  USA
 */

#include <libwebsockets.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <assert.h>

#include "s-private.h"

void
sais_get_task_metrics_estimates(struct vhd *vhd, sai_task_t *task)
{
	char query[256];
	sqlite3_stmt *stmt;

	task->est_peak_mem_kib = 256 * 1024; /* 256MiB default */
	task->est_cpu_load_pct = 10;
	task->est_disk_kib = 1024 * 1024; /* 1GiB default */

	if (!vhd->pdb_metrics)
		return;

	lws_snprintf(query, sizeof(query),
		     "SELECT AVG(peak_mem_rss), AVG(us_cpu_user), "
		     "AVG(stg_bytes), AVG(wallclock_us) "
		     "FROM build_metrics WHERE key = '%s'",
		     task->taskname);

	if (sqlite3_prepare_v2(vhd->pdb_metrics, query, -1, &stmt, NULL) != SQLITE_OK)
		return;

	if (sqlite3_step(stmt) == SQLITE_ROW) {
		uint64_t avg_us_cpu = (uint64_t)sqlite3_column_int64(stmt, 1);
		uint64_t avg_wallclock = (uint64_t)sqlite3_column_int64(stmt, 3);

		task->est_peak_mem_kib = (unsigned int)(sqlite3_column_int(stmt, 0) / 1024);
		if (avg_wallclock)
			task->est_cpu_load_pct = (unsigned int)((avg_us_cpu * 100) / avg_wallclock);
		task->est_disk_kib = (unsigned int)(sqlite3_column_int(stmt, 2) / 1024);
	}

	sqlite3_finalize(stmt);
}

int
sais_task_cancel(struct vhd *vhd, const char *task_uuid)
{
	sai_cancel_t *can;

	/*
	 * For every pss that we have from builders...
	 */
	lws_start_foreach_dll(struct lws_dll2 *, p, vhd->builders.head) {
		struct pss *pss = lws_container_of(p, struct pss, same);


		/*
		 * ... queue the task cancel message
		 */
		can = malloc(sizeof *can);
		if (!can)
			return -1;
		memset(can, 0, sizeof(*can));

		lws_strncpy(can->task_uuid, task_uuid, sizeof(can->task_uuid));

		lws_dll2_add_tail(&can->list, &pss->task_cancel_owner);

		lws_callback_on_writable(pss->wsi);

	} lws_end_foreach_dll(p);

	sais_taskchange(vhd->h_ss_websrv, task_uuid, SAIES_CANCELLED);

	/*
	 * Recompute startable task platforms and broadcast to all sai-power,
	 * after there has been a change in tasks
	 */
	sais_platforms_with_tasks_pending(vhd);

	return 0;
}

int
sais_task_stop_on_builders(struct vhd *vhd, const char *task_uuid)
{
	char event_uuid[33], builder_name[128], esc_uuid[129], q[128];
	struct pss *pss_match = NULL;
	sai_plat_t *cb;
	sqlite3 *pdb = NULL;
	sai_cancel_t *can;

	lwsl_notice("%s: builders count %d\n", __func__, vhd->builders.count);

	/*
	 * We will send the task cancel message only to the builder that was
	 * assigned the task, if any.
	 */

	sai_task_uuid_to_event_uuid(event_uuid, task_uuid);

	if (sais_event_db_ensure_open(vhd, event_uuid, 0, &pdb)) {
		lwsl_err("%s: unable to open event-specific database\n", __func__);
		return -1;
	}

	builder_name[0] = '\0';
	lws_sql_purify(esc_uuid, task_uuid, sizeof(esc_uuid));
	lws_snprintf(q, sizeof(q), "select builder_name from tasks where uuid='%s'",
		     esc_uuid);
	if (sqlite3_exec(pdb, q, sql3_get_string_cb, builder_name, NULL) !=
							SQLITE_OK ||
	    !builder_name[0]) {
		sais_event_db_close(vhd, &pdb);
		/*
		 * This is not an error... the task may not have had a builder
		 * assigned yet.  There's nothing to do.
		 */
		return 0;
	}
	sais_event_db_close(vhd, &pdb);

	cb = sais_builder_from_uuid(vhd, builder_name, __FILE__, __LINE__);
	if (!cb)
		/* Builder not connected, nothing to do */
		return 0;

	lws_start_foreach_dll(struct lws_dll2 *, p, vhd->builders.head) {
		struct pss *pss = lws_container_of(p, struct pss, same);
		if (pss->wsi == cb->wsi) {
			pss_match = pss;
			break;
		}
	} lws_end_foreach_dll(p);

	if (!pss_match)
		/* Builder is live but has no pss? */
		return 0;

	can = malloc(sizeof *can);
	if (!can)
		return -1;

	memset(can, 0, sizeof(*can));

	lws_strncpy(can->task_uuid, task_uuid, sizeof(can->task_uuid));

	lws_dll2_add_tail(&can->list, &pss_match->task_cancel_owner);
	lws_callback_on_writable(pss_match->wsi);

	return 0;
}

/*
 * Keep the task record itself, but remove all logs and artifacts related to
 * it and reset the task state back to WAITING.
 */

sai_db_result_t
sais_task_clear_build_and_logs(struct vhd *vhd, const char *task_uuid, int from_rejection)
{
	char esc[96], cmd[256], event_uuid[33];
	sqlite3 *pdb = NULL;
	int ret;

	lwsl_notice("%s: task reset %s\n", __func__, task_uuid);

	if (!task_uuid[0])
		return SAI_DB_RESULT_OK;

	lwsl_notice("%s: received request to reset task %s\n", __func__, task_uuid);

	sai_task_uuid_to_event_uuid(event_uuid, task_uuid);

	if (sais_event_db_ensure_open(vhd, event_uuid, 0, &pdb)) {
		lwsl_err("%s: unable to open event-specific database\n",
				__func__);

		return SAI_DB_RESULT_ERROR;
	}

	lws_sql_purify(esc, task_uuid, sizeof(esc));
	lws_snprintf(cmd, sizeof(cmd), "delete from logs where task_uuid='%s'",
		     esc);

	ret = sqlite3_exec(pdb, cmd, NULL, NULL, NULL);
	if (ret != SQLITE_OK) {
		sais_event_db_close(vhd, &pdb);
		if (ret == SQLITE_BUSY)
			return SAI_DB_RESULT_BUSY;
		lwsl_err("%s: %s: %s: fail\n", __func__, cmd,
			 sqlite3_errmsg(pdb));
		return SAI_DB_RESULT_ERROR;
	}
	lws_snprintf(cmd, sizeof(cmd), "delete from artifacts where task_uuid='%s'",
		     esc);

	ret = sqlite3_exec(pdb, cmd, NULL, NULL, NULL);
	if (ret != SQLITE_OK) {
		sais_event_db_close(vhd, &pdb);
		if (ret == SQLITE_BUSY)
			return SAI_DB_RESULT_BUSY;
		lwsl_err("%s: %s: %s: fail\n", __func__, cmd,
			 sqlite3_errmsg(pdb));
		return SAI_DB_RESULT_ERROR;
	}

	sais_event_db_close(vhd, &pdb);

	sais_set_task_state(vhd, NULL, NULL, task_uuid, SAIES_WAITING, 1, 1);

	sais_task_stop_on_builders(vhd, task_uuid);

	/*
	 * Reassess now if there's a builder we can match to a pending task,
	 * but not if we are being reset due to a rejection... that would
	 * just cause us to spam the builder with the same task again
	 */

	if (!from_rejection) {
		lwsl_err("%s: scheduling sul_central to find a new task\n", __func__);
		lws_sul_schedule(vhd->context, 0, &vhd->sul_central, sais_central_cb, 1);
	}

	/*
	 * Recompute startable task platforms and broadcast to all sai-power,
	 * after there has been a change in tasks
	 */
	sais_platforms_with_tasks_pending(vhd);

	lwsl_notice("%s: exiting OK\n", __func__);

	return SAI_DB_RESULT_OK;
}

sai_db_result_t
sais_task_rebuild_last_step(struct vhd *vhd, const char *task_uuid)
{
	char esc[96], cmd[256], event_uuid[33];
	sqlite3 *pdb = NULL;
	lws_dll2_owner_t o;
	struct lwsac *ac = NULL;
	sai_task_t *task;
	int ret;

	if (!task_uuid[0])
		return SAI_DB_RESULT_OK;

	lwsl_notice("%s: received request to rebuild last step of task %s\n",
		    __func__, task_uuid);

	sai_task_uuid_to_event_uuid(event_uuid, task_uuid);

	if (sais_event_db_ensure_open(vhd, event_uuid, 0, &pdb)) {
		lwsl_err("%s: unable to open event-specific database\n",
				__func__);

		return SAI_DB_RESULT_ERROR;
	}

	lws_sql_purify(esc, task_uuid, sizeof(esc));
	lws_snprintf(cmd, sizeof(cmd), " and uuid='%s'", esc);
	ret = lws_struct_sq3_deserialize(pdb, cmd, NULL,
					 lsm_schema_sq3_map_task, &o, &ac, 0, 1);
	if (ret < 0 || !o.head) {
		sais_event_db_close(vhd, &pdb);
		lwsac_free(&ac);
		return SAI_DB_RESULT_ERROR;
	}

	task = lws_container_of(o.head, sai_task_t, list);

	if (task->build_step > 0) {
		lws_snprintf(cmd, sizeof(cmd),
			     "update tasks set build_step=%d where uuid='%s'",
			     task->build_step - 1, esc);

		ret = sqlite3_exec(pdb, cmd, NULL, NULL, NULL);
		if (ret != SQLITE_OK) {
			sais_event_db_close(vhd, &pdb);
			lwsac_free(&ac);
			if (ret == SQLITE_BUSY)
				return SAI_DB_RESULT_BUSY;

			lwsl_err("%s: %s: %s: fail\n", __func__, cmd,
				 sqlite3_errmsg(pdb));
			return SAI_DB_RESULT_ERROR;
		}
	}

	lwsac_free(&ac);
	sais_event_db_close(vhd, &pdb);

	sais_set_task_state(vhd, NULL, NULL, task_uuid, SAIES_WAITING, 0, 0);

	sais_task_stop_on_builders(vhd, task_uuid);

	lwsl_err("%s: scheduling sul_central to find a new task\n", __func__);
	lws_sul_schedule(vhd->context, 0, &vhd->sul_central, sais_central_cb, 1);

	sais_platforms_with_tasks_pending(vhd);

	lwsl_notice("%s: exiting OK\n", __func__);

	return SAI_DB_RESULT_OK;
}