/*
 * Copyright (C) 2013 Andreas Steffen
 * HSR Hochschule fuer Technik Rapperswil
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program 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 General Public License
 * for more details.
 */

#include "imv_policy_manager_usage.h"
#include "imv_workitem.h"

#include <library.h>
#include <utils/debug.h>

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

/* The default policy group #1 is assumed to always exist */
#define DEFAULT_GROUP_ID	1

/**
 * global debug output variables
 */
static int debug_level = 1;
static bool stderr_quiet = FALSE;

/**
 * attest dbg function
 */
static void stderr_dbg(debug_t group, level_t level, char *fmt, ...)
{
	va_list args;

	if (level <= debug_level)
	{
		if (!stderr_quiet)
		{
			va_start(args, fmt);
			vfprintf(stderr, fmt, args);
			fprintf(stderr, "\n");
			va_end(args);
		}
	}
}

/**
 * Collect all enforcements by iterating up through parent groups
 */
static bool iterate_enforcements(database_t *db, int device_id, int session_id,
								 int group_id)
{
	int id, type, file, dir, arg_int, parent, policy, max_age;
	int p_rec_fail, p_rec_noresult, e_rec_fail, e_rec_noresult, latest_rec;
	bool latest_success;
	char *argument;
	time_t now;
	enumerator_t *e, *e1, *e2;

	now = time(NULL);

	while (group_id)
	{
		e1 = db->query(db,
				"SELECT e.id, p.type, p.argument, p.file, p.dir, p.rec_fail, "
				"p.rec_noresult, e.policy, e.max_age, e.rec_fail, e.rec_noresult "
				"FROM enforcements AS e JOIN policies as p ON e.policy = p.id "
				"WHERE e.group_id = ?", DB_INT, group_id,
				 DB_INT, DB_INT, DB_TEXT, DB_INT, DB_INT, DB_INT, DB_INT,
				 DB_INT, DB_INT, DB_INT, DB_INT);
		if (!e1)
		{
			return FALSE;
		}
		while (e1->enumerate(e1, &id, &type, &argument, &file, &dir,
								 &p_rec_fail, &p_rec_noresult, &policy, &max_age,
								 &e_rec_fail, &e_rec_noresult))
		{
			/* check if the latest measurement of the device was successful */
			latest_success = FALSE;

			if (device_id)
			{
				e2 = db->query(db,
						"SELECT r.rec FROM results AS r "
						"JOIN sessions AS s ON s.id = r.session "
						"WHERE r.policy = ? AND s.device = ? AND s.time > ? "
						"ORDER BY s.time DESC",
						DB_INT, policy, DB_INT, device_id,
						DB_UINT, now - max_age,	DB_INT);
				if (!e2)
				{
					e1->destroy(e1);
					return FALSE;
				}
				if (e2->enumerate(e2, &latest_rec) &&
					latest_rec == TNC_IMV_ACTION_RECOMMENDATION_ALLOW)
				{
					latest_success = TRUE;
				}
				e2->destroy(e2);
			}

			if (latest_success)
			{
				/*skipping enforcement */
				printf("skipping enforcment %d\n", id);
				continue;
			}

			/* determine arg_int */
			switch ((imv_workitem_type_t)type)
			{
				case IMV_WORKITEM_FILE_REF_MEAS:
				case IMV_WORKITEM_FILE_MEAS:
				case IMV_WORKITEM_FILE_META:
					arg_int = file;
					break;
				case IMV_WORKITEM_DIR_REF_MEAS:
				case IMV_WORKITEM_DIR_MEAS:
				case IMV_WORKITEM_DIR_META:
					arg_int = dir;
					break;
				default:
					arg_int = 0;
			}

			/* insert a workitem */
			if (db->execute(db, NULL,
				"INSERT INTO workitems (session, enforcement, type, arg_str, "
				"arg_int, rec_fail, rec_noresult) VALUES (?, ?, ?, ?, ?, ?, ?)",
				DB_INT, session_id, DB_INT, id, DB_INT, type, DB_TEXT, argument,
				DB_INT, arg_int, DB_INT, e_rec_fail ? e_rec_fail : p_rec_fail,
				DB_INT, e_rec_noresult ? e_rec_noresult : p_rec_noresult) != 1)
			{
				e1->destroy(e1);
				fprintf(stderr, "could not insert workitem\n");
				return FALSE;
			}
		}
		e1->destroy(e1);

		e = db->query(db,
				"SELECT parent FROM groups WHERE id = ?",
				 DB_INT, group_id, DB_INT);
		if (!e)
		{
			return FALSE;
		}
		if (e->enumerate(e, &parent))
		{
			group_id = parent;
		}
		else
		{
			fprintf(stderr, "group information not found\n");
			group_id = 0;
		}
		e->destroy(e);
	}
	return TRUE;
}

static bool policy_start(database_t *db, int session_id)
{
	enumerator_t *e;
	int device_id, product_id, gid, group_id = DEFAULT_GROUP_ID;
	u_int created;

	/* get session data */
	e = db->query(db,
			"SELECT s.device, s.product, d.created FROM sessions AS s "
			"LEFT JOIN devices AS d ON s.device = d.id WHERE s.id = ?",
			 DB_INT, session_id, DB_INT, DB_INT, DB_UINT);
	if (!e || !e->enumerate(e, &device_id, &product_id, &created))
	{
		DESTROY_IF(e);
		fprintf(stderr, "session %d not found\n", session_id);
		return FALSE;
	}
	e->destroy(e);

	/* if a device ID with a creation date exists, get all group memberships */
	if (device_id && created)
	{
		e = db->query(db,
				"SELECT group_id FROM groups_members WHERE device_id = ?",
				 DB_INT, device_id, DB_INT);
		if (!e)
		{
			return FALSE;
		}
		while (e->enumerate(e, &group_id))
		{
			if (!iterate_enforcements(db, device_id, session_id, group_id))
			{
				e->destroy(e);
				return FALSE;
			}
		}
		e->destroy(e);

		return TRUE;
	}

	/* determine if a default product group exists */
	e = db->query(db,
			"SELECT group_id FROM groups_product_defaults "
			"WHERE product_id = ?", DB_INT, product_id, DB_INT);
	if (!e)
	{
		return FALSE;
	}
	if (e->enumerate(e, &gid))
	{
		group_id = gid;
	}
	e->destroy(e);

	if (device_id && !created)
	{
		/* assign a newly created device to a default group */
		if (db->execute(db, NULL,
			"INSERT INTO groups_members (device_id, group_id) "
			"VALUES (?, ?)", DB_INT, device_id, DB_INT, group_id) != 1)
		{
			fprintf(stderr, "could not assign device to a default group\n");
			return FALSE;
		}

		/* set the creation date if it hasn't been set yet */
		if (db->execute(db, NULL,
				"UPDATE devices SET created = ? WHERE id = ?",
				DB_UINT, time(NULL), DB_INT, device_id) != 1)
		{
			fprintf(stderr, "creation date of device could not be set\n");
			return FALSE;
		}
	}

	return iterate_enforcements(db, device_id, session_id, group_id);
}

static bool policy_stop(database_t *db, int session_id)
{
	enumerator_t *e;
	int rec, policy;
	char *result;

	e = db->query(db,
			"SELECT w.rec_final, w.result, e.policy FROM workitems AS w "
			"JOIN enforcements AS e ON w.enforcement = e.id "
			"WHERE w.session = ? AND w.result IS NOT NULL",
			 DB_INT, session_id, DB_INT, DB_TEXT, DB_INT);
	if (e)
	{
		while (e->enumerate(e, &rec, &result, &policy))
		{
			db->execute(db, NULL,
				"INSERT INTO results (session, policy, rec, result) "
				"VALUES (?, ?, ?, ?)", DB_INT, session_id, DB_INT, policy,
				 DB_INT, rec, DB_TEXT, result);
		}
		e->destroy(e);
	}
	return db->execute(db, NULL,
				"DELETE FROM workitems WHERE session = ?",
				DB_UINT, session_id) >= 0;
}

int main(int argc, char *argv[])
{
	database_t *db;
	char *uri;
	int session_id;
	bool start, success;

	/* enable attest debugging hook */
	dbg = stderr_dbg;

	atexit(library_deinit);

	/* initialize library */
	if (!library_init(NULL, "imv_policy_manager"))
	{
		exit(SS_RC_LIBSTRONGSWAN_INTEGRITY);
	}
	if (!lib->plugins->load(lib->plugins,
			lib->settings->get_str(lib->settings, "imv_policy_manager.load",
				 "sqlite")))
	{
		exit(SS_RC_INITIALIZATION_FAILED);
	}

	if (argc < 3)
	{
		usage();
		exit(SS_RC_INITIALIZATION_FAILED);
	}
	if (streq(argv[1], "start"))
	{
		start = TRUE;
	}
	else if (streq(argv[1], "stop"))
	{
		start = FALSE;
	}
	else
	{
		usage();
		exit(SS_RC_INITIALIZATION_FAILED);
	}

	session_id = atoi(argv[2]);

	/* attach IMV database */
	uri = lib->settings->get_str(lib->settings,
			"imv_policy_manager.database",
			lib->settings->get_str(lib->settings,
				"charon.imcv.database",
				lib->settings->get_str(lib->settings,
					"libimcv.database", NULL)));
	if (!uri)
	{
		fprintf(stderr, "database uri not defined.\n");
		exit(SS_RC_INITIALIZATION_FAILED);
	}

	db = lib->db->create(lib->db, uri);
	if (!db)
	{
		fprintf(stderr, "opening database failed.\n");
		exit(SS_RC_INITIALIZATION_FAILED);
	}

	if (start)
	{
		success = policy_start(db, session_id);
	}
	else
	{
		success = policy_stop(db, session_id);
	}
	db->destroy(db);

	fprintf(stderr, "imv_policy_manager %s %s\n", start ? "start" : "stop",
			success ? "successful" : "failed");

	exit(EXIT_SUCCESS);
}
