/* $Id: actions.c,v 1.36 2011/01/30 11:19:48 imil Exp $ */

/*
 * Copyright (c) 2009, 2010 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Emile "iMil" Heitor <imil@NetBSD.org> .
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 */

#include "pkgin.h"

#ifndef LOCALBASE
#define LOCALBASE "/usr/pkg" /* see DISCLAIMER below */
#endif

const char			*pkgin_cache = PKGIN_CACHE;
static int			upgrade_type = UPGRADE_NONE;

int
check_yesno(void)
{
	int c, r = 0;

	if (yesflag)
		return 1;
	else if (noflag)
		return 0;

	printf(MSG_PROCEED);

	if ((c = getchar()) == 'y')
	    	r = 1;
	else if (c == '\n' || c == EOF)
	    	return 0;

	/* avoid residual char */
	while((c = getchar()) != '\n' && c != EOF)
		continue;
	return r;
}

/* package removal */
static void
do_pkg_remove(Deptreehead *removehead)
{
	Pkgdeptree *premove;

	SLIST_FOREACH(premove, removehead, next) {
		/* file not available in the repository */
	    	if (premove->file_size == -1)
		    	continue;

		if (premove->depname == NULL)
			/* SLIST corruption, badly installed package */
			continue;
		printf(MSG_REMOVING, premove->depname);
#ifdef DEBUG
		printf("%s -f %s\n", PKG_DELETE, premove->depname);
#else
		fexec(PKG_DELETE, "-f", premove->depname, NULL);
#endif
	}
}

static void
pkg_download(Deptreehead *installhead)
{
	FILE		*fp;
	Pkgdeptree	*pinstall;
	struct stat	st;
	Dlfile		*dlpkg;
	char		pkg[BUFSIZ], query[BUFSIZ];

	printf(MSG_DOWNLOAD_PKGS);

	SLIST_FOREACH(pinstall, installhead, next) {
		snprintf(pkg, BUFSIZ,
		    "%s/%s%s", pkgin_cache, pinstall->depname, PKG_EXT);

		/* already fully downloaded */
		if (stat(pkg, &st) == 0 && st.st_size == pinstall->file_size)
		    	continue;

		umask(DEF_UMASK);
		if ((fp = fopen(pkg, "w")) == NULL)
			err(EXIT_FAILURE, MSG_ERR_OPEN, pkg);

		snprintf(query, BUFSIZ, PKG_URL, pinstall->depname);
		/* retrieve repository for package  */
		if (pkgindb_doquery(query, pdb_get_value, pkg) != 0)
			errx(EXIT_FAILURE, MSG_PKG_NO_REPO, pinstall->depname);

		strlcat(pkg, "/", sizeof(pkg));
		strlcat(pkg, pinstall->depname, sizeof(pkg));
		strlcat(pkg, PKG_EXT, sizeof(pkg));

		if ((dlpkg = download_file(pkg, NULL)) == NULL) {
			fprintf(stderr, MSG_PKG_NOT_AVAIL, pinstall->depname);
			if (!check_yesno())
				errx(EXIT_FAILURE, MSG_PKG_NOT_AVAIL,
				    pinstall->depname);
			pinstall->file_size = -1;
			fclose(fp);
			continue;
		}

		fwrite(dlpkg->buf, dlpkg->size, 1, fp);
		fclose(fp);

		XFREE(dlpkg->buf);
		XFREE(dlpkg);
	} /* download loop */
}

/* package installation. Don't rely on pkg_add's ability to fetch and
 * install as we want to keep control on packages installation order.
 * Besides, pkg_add cannot be used to install an "older" package remotely
 * i.e. apache 1.3
 */
static void
do_pkg_install(Deptreehead *installhead)
{
	Pkgdeptree	*pinstall;
	char		pkgpath[BUFSIZ];

	printf(MSG_INSTALL_PKG);

	SLIST_FOREACH(pinstall, installhead, next) {

		/* file not available in the repository */
	    	if (pinstall->file_size == -1)
		    	continue;

		printf(MSG_INSTALLING, pinstall->depname);
		snprintf(pkgpath, BUFSIZ,
			"%s/%s%s", pkgin_cache, pinstall->depname, PKG_EXT);

#ifdef DEBUG
		printf("%s -f %s\n", PKG_ADD, pkgpath);
#else
		fexec(PKG_ADD, "-f", pkgpath, NULL);
#endif
	} /* installation loop */
}

/* build the output line */
char *
action_list(char *flatlist, char *str)
{
	int		newsize;
	char	*newlist = NULL;

	if (flatlist == NULL)
		XSTRDUP(newlist, str);
	else {
		if (str == NULL)
			return flatlist;

		newsize = strlen(str) + strlen(flatlist) + 2;
		newlist = realloc(flatlist, newsize * sizeof(char));
		strlcat(newlist, " ", newsize);
		strlcat(newlist, str, newsize);
	}

	return newlist;
}

/* find required files (REQUIRES) from PROVIDES or filename */
static int
pkg_met_reqs(Impacthead *impacthead)
{
	int			met_reqs = 1, foundreq;
	Pkgimpact	*pimpact;
	Plisthead	*requireshead;
	Pkglist		*requires;
#ifdef CHECK_PROVIDES
	Pkgimpact	*impactprov;
	Plisthead	*provideshead;
	Pkglist		*provides;
#endif
	struct stat	sb;
	char		query[BUFSIZ];

	/* first, parse impact list */
	SLIST_FOREACH(pimpact, impacthead, next) {
		/* retreive requires list for package */
		snprintf(query, BUFSIZ, GET_REQUIRES_QUERY, pimpact->pkgname);
		requireshead = rec_pkglist(query);

		if (requireshead == NULL) /* empty requires list (very unlikely) */
			continue;

		/* parse requires list */
		SLIST_FOREACH(requires, requireshead, next) {

			foundreq = 0;

			/* for performance sake, first check basesys */
			if ((strncmp(requires->pkgname, LOCALBASE,
				    sizeof(LOCALBASE) - 1)) != 0) {
				if (stat(requires->pkgname, &sb) < 0) {
					printf(MSG_REQT_NOT_PRESENT,
						requires->pkgname, pimpact->pkgname);

					met_reqs = 0;
				}
				/* was a basysfile, no need to check PROVIDES */
				continue;
			}
			/* FIXME: the code below actually works, but there's no
			 * point losing performances when some REQUIRES do not match
			 * PROVIDES in pkg_summary(5). This is a known issue and will
			 * hopefuly be fixed.
			 */
#ifndef CHECK_PROVIDES
			continue;
#else
			/* search what local packages provide */
			provideshead = rec_pkglist(LOCAL_PROVIDES);
			SLIST_FOREACH(provides, provideshead, next) {
				if (strncmp(provides->pkgname,
						requires->pkgname,
						strlen(requires->pkgname)) == 0) {

					foundreq = 1;

					/* found, no need to go further*/
					break;
				} /* match */
			} /* SLIST_FOREACH LOCAL_PROVIDES */
			free_pkglist(provideshead);

			/* REQUIRES was not found on local packages, try impact list */
			if (!foundreq) {
				/* re-parse impact list to retreive PROVIDES */
				SLIST_FOREACH(impactprov, impacthead, next) {
					snprintf(query, BUFSIZ, GET_PROVIDES_QUERY,
						impactprov->pkgname);
					provideshead = rec_pkglist(query);

					if (provideshead == NULL)
						continue;

					/* then parse provides list for every package */
					SLIST_FOREACH(provides, provideshead, next) {
						if (strncmp(provides->pkgname,
								requires->pkgname,
								strlen(requires->pkgname)) == 0) {

							foundreq = 1;

							/* found, no need to go further
							   return to impactprov list */
							break;
						} /* match */
					}
					free_pkglist(provideshead);

					if (foundreq) /* exit impactprov list loop */
						break;

				} /* SLIST_NEXT impactprov */

			} /* if (!foundreq) LOCAL_PROVIDES -> impact list */

			/* FIXME: BIG FAT DISCLAIMER
			 * as of 04/2009, some packages described in pkg_summary
			 * have unmet REQUIRES. This is a known bug that makes the
			 * PROVIDES untrustable and some packages uninstallable.
			 * foundreq is forced to 1 for now for every REQUIRES
			 * matching LOCALBASE, which is hardcoded to "/usr/pkg"
			 */
			if (!foundreq) {
				printf(MSG_REQT_NOT_PRESENT_BUT, requires->pkgname);

				foundreq = 1;
			}
#endif
		} /* SLIST_FOREACH requires */
		free_pkglist(requireshead);
	} /* 1st impact SLIST_FOREACH */

	return met_reqs;
}

/* check for conflicts and if needed files are present */
static int
pkg_has_conflicts(Plisthead *conflictshead, Pkgimpact *pimpact)
{
	int			has_conflicts = 0;
	Pkglist		*conflicts; /* SLIST conflicts pointer */
	char		*conflict_pkg, query[BUFSIZ];

	if (conflictshead == NULL)
		return 0;

	/* check conflicts */
	SLIST_FOREACH(conflicts, conflictshead, next) {
		if (pkg_match(conflicts->pkgname, pimpact->pkgname)) {

			/* got a conflict, retrieve conflicting local package */
			snprintf(query, BUFSIZ,
				GET_CONFLICT_QUERY, conflicts->pkgname);

			XMALLOC(conflict_pkg, BUFSIZ * sizeof(char));
			if (pkgindb_doquery(query,
					pdb_get_value, conflict_pkg) == 0)

				printf(MSG_CONFLICT_PKG,
					pimpact->pkgname, conflict_pkg);

			XFREE(conflict_pkg);

			has_conflicts = 1;
		} /* match conflict */
	} /* SLIST_FOREACH conflicts */

	return has_conflicts;
}

#define H_BUF 6

int
pkgin_install(char **pkgargs, uint8_t do_inst)
{
	int			installnum = 0, upgradenum = 0, removenum = 0;
	int			rc = EXIT_FAILURE;
	uint64_t   	file_size = 0, size_pkg = 0;
	Impacthead	*impacthead; /* impact head */
	Pkgimpact	*pimpact;
	Deptreehead	*removehead = NULL, *installhead = NULL;
	Pkgdeptree	*premove, *pinstall; /* not a Pkgdeptree, just for ease */
	Plisthead	*conflictshead; /* conflicts head */
	char		*toinstall = NULL, *toupgrade = NULL, *toremove = NULL;
	char		pkgpath[BUFSIZ], h_psize[H_BUF], h_fsize[H_BUF];
	struct stat	st;

	/* full impact list */
	if ((impacthead = pkg_impact(pkgargs)) == NULL) {
		printf(MSG_NOTHING_TO_DO);
		return rc;
	}

	/* check for required files */
	if (!pkg_met_reqs(impacthead)) {
		printf(MSG_REQT_MISSING);
		return rc;
	}

	/* conflicts list */
	conflictshead = rec_pkglist(LOCAL_CONFLICTS);

	/* browse impact tree */
	SLIST_FOREACH(pimpact, impacthead, next) {

		/* check for conflicts */
		if (pkg_has_conflicts(conflictshead, pimpact)) {
			if (!check_yesno()) {
				free_impact(impacthead);
				XFREE(impacthead);

				return rc;
			}
		}

		snprintf(pkgpath, BUFSIZ, "%s/%s%s",
			pkgin_cache, pimpact->pkgname, PKG_EXT);

		/* if package is not already downloaded or size mismatch, d/l it */
		if (stat(pkgpath, &st) < 0 || st.st_size != pimpact->file_size)
			file_size += pimpact->file_size;
		size_pkg += pimpact->size_pkg;

		switch (pimpact->action) {
		case TOUPGRADE:
			upgradenum++;
			installnum++;
			break;

		case TOINSTALL:
			installnum++;
			break;

		case TOREMOVE:
			removenum++;
			break;
		}
	}
	/* free conflicts list */
	free_pkglist(conflictshead);

	(void)humanize_number(h_fsize, H_BUF, (int64_t)file_size, "",
		HN_AUTOSCALE, HN_B | HN_NOSPACE | HN_DECIMAL);
	(void)humanize_number(h_psize, H_BUF, (int64_t)size_pkg, "",
		HN_AUTOSCALE, HN_B | HN_NOSPACE | HN_DECIMAL);

	/* check disk space */
	if (!fs_has_room(pkgin_cache, (int64_t)file_size))
		errx(EXIT_FAILURE, MSG_NO_CACHE_SPACE, pkgin_cache);
	if (!fs_has_room(LOCALBASE, (int64_t)size_pkg))
		errx(EXIT_FAILURE, MSG_NO_INSTALL_SPACE, LOCALBASE);

	printf("\n");

	if (upgradenum > 0) {
		/* record ordered remove list before upgrade */
		removehead = order_upgrade_remove(impacthead);

		SLIST_FOREACH(premove, removehead, next) {
			if (premove->computed == TOUPGRADE)
				toupgrade = action_list(toupgrade, premove->depname);
		}
		printf(MSG_PKGS_TO_UPGRADE, upgradenum, toupgrade);
		printf("\n");

		if (removenum > 0) {
			SLIST_FOREACH(premove, removehead, next) {
				if (premove->computed == TOREMOVE)
					toremove = action_list(toremove, premove->depname);
			}
			printf(MSG_PKGS_TO_REMOVE, removenum, toremove);
			printf("\n");
		}

	} else
		printf(MSG_NOTHING_TO_UPGRADE);

	if (installnum > 0) {
		/* record ordered install list */
		installhead = order_install(impacthead);

		SLIST_FOREACH(pinstall, installhead, next)
			toinstall = action_list(toinstall, pinstall->depname);

		printf(MSG_PKGS_TO_INSTALL, installnum, toinstall, h_fsize, h_psize);
		printf("\n");

		if (check_yesno()) {
			/* before erasing anything, download packages */
			pkg_download(installhead);

			if (do_inst) { /* real install, not a simple download */
				/* if there was upgrades, first remove old packages */
				if (upgradenum > 0) {
					printf(MSG_RM_UPGRADE_PKGS);
					do_pkg_remove(removehead);
				}
				/* then pass ordered install list */
				do_pkg_install(installhead);

				/* pure install, not called by pkgin_upgrade */
				if (upgrade_type == UPGRADE_NONE)
					update_db(LOCAL_SUMMARY, pkgargs);
				
				rc = EXIT_SUCCESS;
			}
		}
	} else
		printf(MSG_NOTHING_TO_INSTALL);


	XFREE(toinstall);
	XFREE(toupgrade);
	free_impact(impacthead);
	XFREE(impacthead);
	free_deptree(removehead);
	XFREE(removehead);
	free_deptree(installhead);
	XFREE(installhead);

	return rc;
}

int
pkgin_remove(char **pkgargs)
{
	int			deletenum = 0, exists, rc;
	Deptreehead	pdphead, *removehead;
	Pkgdeptree	*pdp;
	Plisthead	*plisthead;
	char   		*todelete = NULL, **ppkgargs, *pkgname, *ppkg;

	SLIST_INIT(&pdphead);

	plisthead = rec_pkglist(LOCAL_PKGS_QUERY);

	if (plisthead == NULL)
		errx(EXIT_FAILURE, MSG_EMPTY_LOCAL_PKGLIST);

	/* act on every package passed to the command line */
	for (ppkgargs = pkgargs; *ppkgargs != NULL; ppkgargs++) {

		if ((pkgname = find_exact_pkg(plisthead, *ppkgargs)) == NULL) {
			printf(MSG_PKG_NOT_INSTALLED, *ppkgargs);
			continue;
		}
		XSTRDUP(ppkg, pkgname);
		trunc_str(ppkg, '-', STR_BACKWARD);

		/* record full reverse dependency list for package */
		full_dep_tree(ppkg, LOCAL_REVERSE_DEPS, &pdphead);

		XFREE(ppkg);

		exists = 0;
		/* check if package have already been recorded */
		SLIST_FOREACH(pdp, &pdphead, next) {
			if (strncmp(pdp->depname, pkgname,
					strlen(pdp->depname)) == 0) {
				exists = 1;
				break;
			}
		}

		if (exists) {
			XFREE(pkgname);
			continue; /* next pkgarg */
		}

		/* add package itself */
		XMALLOC(pdp, sizeof(Pkgdeptree));
		pdp->depname = pkgname;

		if (SLIST_EMPTY(&pdphead))
			/* identify unique package, don't cut it when ordering */
			pdp->level = -1;
		else
			pdp->level = 0;

		XSTRDUP(pdp->matchname, pdp->depname);
		trunc_str(pdp->matchname, '-', STR_BACKWARD);

		SLIST_INSERT_HEAD(&pdphead, pdp, next);
	} /* for pkgargs */

	free_pkglist(plisthead);

	/* order remove list */
	removehead = order_remove(&pdphead);

	SLIST_FOREACH(pdp, removehead, next) {
		deletenum++;
		todelete = action_list(todelete, pdp->depname);
	}

	if (todelete != NULL) {
		printf(MSG_PKGS_TO_DELETE, deletenum, todelete);
		if (check_yesno()) {
			do_pkg_remove(removehead);

			update_db(LOCAL_SUMMARY, NULL);

			rc = EXIT_SUCCESS;
		} else
			rc = EXIT_FAILURE;

	} else {
		printf(MSG_NO_PKGS_TO_DELETE);
		rc = EXIT_SUCCESS;
	}

	free_deptree(removehead);
	XFREE(removehead);
	XFREE(todelete);

	return rc;
}

/* find closest match for packages to be upgraded */
static char *
narrow_match(Plisthead *remoteplisthead,
	char *pkgname, const char *fullpkgname)
{
	Pkglist	*pkglist;
	char	*rpkgname, *best_match = NULL;
	int		pkglen, fullpkglen, i, matchlen = 0;

	pkglen = strlen(pkgname);
	fullpkglen = strlen(fullpkgname);

	SLIST_FOREACH(pkglist, remoteplisthead, next) {
		XSTRDUP(rpkgname, pkglist->pkgname);
		trunc_str(rpkgname, '-', STR_BACKWARD);

		if (strlen(rpkgname) == pkglen &&
			strncmp(pkgname, rpkgname, pkglen) == 0) {

			for (i = 0;
				 i < fullpkglen && fullpkgname[i] == pkglist->pkgname[i];
				i++);

			if (i > matchlen) {
				matchlen = i;
				XSTRDUP(best_match, pkglist->pkgname);
			}

		}

		XFREE(rpkgname);
	} /* SLIST_FOREACH remoteplisthead */
	XFREE(pkgname);

	return best_match;
}

static char **
record_upgrades(Plisthead *plisthead, Plisthead *remoteplisthead)
{
	Pkglist	*pkglist;
	int		count = 0;
	char	**pkgargs;

	SLIST_FOREACH(pkglist, plisthead, next)
		count++;

	XMALLOC(pkgargs, (count + 2) * sizeof(char *));

	count = 0;
	SLIST_FOREACH(pkglist, plisthead, next) {
		XSTRDUP(pkgargs[count], pkglist->pkgname);
		trunc_str(pkgargs[count], '-', STR_BACKWARD);

		pkgargs[count] = narrow_match(remoteplisthead,
			pkgargs[count], pkglist->pkgname);

		if (pkgargs[count] == NULL)
			continue;

		count++;
	}
	pkgargs[count] = NULL;

	return pkgargs;
}

void
pkgin_upgrade(int uptype)
{
	Plisthead	*keeplisthead, *localplisthead, *remoteplisthead;
	char		**pkgargs;

	/* used for pkgin_install not to update database, this is done below */
	upgrade_type = uptype;

	/* record keepable packages */
	if ((keeplisthead = rec_pkglist(KEEP_LOCAL_PKGS)) == NULL)
		errx(EXIT_FAILURE, MSG_EMPTY_KEEP_LIST);

	/* upgrade all packages, not only keepables */
	if (uptype == UPGRADE_ALL) {
		if ((localplisthead = rec_pkglist(LOCAL_PKGS_QUERY)) == NULL)
			errx(EXIT_FAILURE, MSG_EMPTY_LOCAL_PKGLIST);
	} else
		/* upgrade only keepables packages */
		localplisthead = keeplisthead;

	if ((remoteplisthead = rec_pkglist(REMOTE_PKGS_QUERY)) == NULL)
		errx(EXIT_FAILURE, MSG_EMPTY_AVAIL_PKGLIST);

	pkgargs = record_upgrades(localplisthead, remoteplisthead);

	if (pkgin_install(pkgargs, DO_INST) == EXIT_SUCCESS) {
		/*
		 * full upgrade, we need to record keep-packages
		 * in order to restore them
		 */
		if (uptype == UPGRADE_ALL) {

			free_pkglist(localplisthead);
			free_list(pkgargs);
			/* record keep list */
			pkgargs = record_upgrades(keeplisthead, remoteplisthead);
		}

		update_db(LOCAL_SUMMARY, pkgargs);
	}

	free_list(pkgargs);

	free_pkglist(remoteplisthead);
	free_pkglist(keeplisthead);
}

