#include <debian-installer.h>
#include <cdebconf/debconfclient.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include "mirrors.h"
#ifdef WITH_HTTP
#include "mirrors_http.h"
#endif
#ifdef WITH_FTP
#include "mirrors_ftp.h"
#endif

#if ! defined (WITH_HTTP) && ! defined (WITH_FTP)
#error Must compile with at least one of FTP or HTTP
#endif
#if defined (WITH_FTP) && defined (WITH_FTP_MANUAL)
#error Only one of WITH_FTP and WITH_FTP_MANUAL can be defined
#endif

static struct debconfclient *debconf;
static char *protocol = NULL;
static char *country  = NULL;
int show_progress = 1;

/* Are we installing from a CD that includes base system packages? */
static int base_on_cd = 0;

/* Available releases (suite/codename) on the mirror. */
static struct release_t releases[MAXRELEASES];

/*
 * Returns a string on the form "DEBCONF_BASE/protocol/supplied". The
 * calling function is responsible for freeing the string afterwards.
 */
static char *add_protocol(char *string) {
	char *ret;

	assert(protocol != NULL); /* Fetched by choose_protocol */
	asprintf(&ret, DEBCONF_BASE "%s/%s", protocol, string);
	return ret;
}

/*
 * Generates a list, suitable to be passed into debconf, from a
 * NULL-terminated string array.
 */
static char *debconf_list(char *list[]) {
	int len, i, size = 1;
	char *ret = 0;

	for (i = 0; list[i] != NULL; i++) {
		len = strlen(list[i]);
		size += len;
		ret = realloc(ret, size + 2);
		memcpy(ret + size - len - 1, list[i], len);
		if (list[i+1] != NULL) {
			ret[size++ - 1] = ',';
			ret[size++ - 1] = ' ';
		}
		ret[size -1] = '\0';
	}
	return ret;
}

/*
 * Returns the correct mirror list, depending on whether protocol is
 * set to http or ftp. Do NOT free the structure - it is a pointer to
 * the static list in mirrors_protocol.h
 */
static struct mirror_t *mirror_list(void) {
	assert(protocol != NULL);

#ifdef WITH_HTTP
	if (strcasecmp(protocol, "http") == 0)
		return mirrors_http;
#endif
#ifdef WITH_FTP
	if (strcasecmp(protocol, "ftp") == 0)
		return mirrors_ftp;
#endif
	return 0; // should never happen
}

/* Returns an array of hostnames of mirrors in the specified country
 * or using GeoDNS. */
static char **mirrors_in(char *country) {
	static char **ret;
	int i, j, num = 1;
	struct mirror_t *mirrors = mirror_list();

	ret = malloc(num * sizeof(char *));
	for (i = j = 0; mirrors[i].site != NULL; i++) {
		if (j == num - 1) {
			num *= 2;
			ret = realloc(ret, num * sizeof(char*));
		}
		if (! mirrors[i].country ||
		    strcmp(mirrors[i].country, country) == 0)
			ret[j++] = mirrors[i].site;
	}
	ret[j] = NULL;
	return ret;
}

/* returns true if there is a mirror in the specified country */
static inline int has_mirror(char *country) {
	char **mirrors;
	if (strcmp(country, MANUAL_ENTRY) == 0)
		return 1;
	mirrors = mirrors_in(country);
	return (mirrors[0] == NULL) ? 0 : 1;
}

/* Returns the root of the mirror, given the hostname. */
static char *mirror_root(char *mirror) {
	int i;

	struct mirror_t *mirrors = mirror_list();

	for (i = 0; mirrors[i].site != NULL; i++)
		if (strcmp(mirrors[i].site, mirror) == 0)
			return mirrors[i].root;
	return NULL;
}

/*
 * Get the default suite (can be a codename) to use; this is either a
 * preseeded value or a value set at build time.
 */
static char *get_default_suite(void) {
	char *suite = NULL;
	FILE *f = NULL;
	char buf[SUITE_LENGTH];

	debconf_get(debconf, DEBCONF_BASE "suite");
	if (! base_on_cd && strlen(debconf->value) > 0) {
		/* Use preseeded or previously selected value. */
		suite = strdup(debconf->value);
	} else {
		/* Check for default suite/codename set at build time. */
		f = fopen("/etc/default-release", "r");
		if (f != NULL) {
			if (fgets(buf, SUITE_LENGTH - 1, f)) {
				if (buf[strlen(buf) - 1] == '\n')
					buf[strlen(buf) - 1] = '\0';
				suite = strdup(buf);
			}
			fclose(f);
		}
	}
	return suite;
}

/*
 * Unset most relevant seen flags to allow to correct preseeded values
 * when mirror is bad.
 */
void unset_seen_flags(void) {
	char *hostname, *directory;

	hostname = add_protocol("hostname");
	debconf_fset(debconf, hostname, "seen", "false");
	free(hostname);
	directory = add_protocol("directory");
	debconf_fset(debconf, directory, "seen", "false");
	free(directory);
	debconf_fset(debconf, DEBCONF_BASE "country", "seen", "false");
	debconf_fset(debconf, DEBCONF_BASE "suite", "seen", "false");
}

void log_invalid_release(const char *name, const char *field) {
	di_log(DI_LOG_LEVEL_WARNING,
		"broken mirror: invalid %s in Release file for %s", field, name);
}

static int get_release(struct release_t *release, const char *name);

/*
 * Cross-validate Release file by checking if it can also be accessed using
 * its codename if we got it using a suite and vice versa; if successful,
 * check that it really matches the earlier Release file.
 * Returns false only if an invalid Release file was found.
 */
static int cross_validate_release(struct release_t *release) {
	struct release_t t_release;
	int ret = 1;

	memset(&t_release, 0, sizeof(t_release));

	/* Preset status field to prevent endless recursion. */
	t_release.status = (release->status & (GET_SUITE | GET_CODENAME));

	/* Only one of the two following conditions can trigger. */
	if (release->suite != NULL && !(release->status & GET_SUITE)) {
		/* Cross-validate the suite */
		if (get_release(&t_release, release->suite)) {
			if ((t_release.status & IS_VALID) &&
			    strcmp(t_release.name, release->name) == 0) {
				release->status |= (t_release.status & GET_SUITE);
			} else {
				release->status &= ~IS_VALID;
				ret = 0;
			}
		}
	}
	if (release->name != NULL && !(release->status & GET_CODENAME)) {
		/* Cross-validate the codename (release->name) */
		if (get_release(&t_release, release->name)) {
			if ((t_release.status & IS_VALID) &&
			    strcmp(t_release.suite, release->suite) == 0) {
				release->status |= (t_release.status & GET_CODENAME);
			} else {
				release->status &= ~IS_VALID;
				ret = 0;
			}
		}
	}

	free(t_release.name);
	free(t_release.suite);

	return ret;
}

/*
 * Fetch a Release file, extract its Suite and Codename and check its valitity.
 */
static int get_release(struct release_t *release, const char *name) {
	char *command;
	FILE *f = NULL;
	char *hostname, *directory;
	char line[80];
	char buf[SUITE_LENGTH];

	hostname = add_protocol("hostname");
	debconf_get(debconf, hostname);
	free(hostname);
	hostname = strdup(debconf->value);
	directory = add_protocol("directory");
	debconf_get(debconf, directory);
	free(directory);
	directory = strdup(debconf->value);

	asprintf(&command, "wget -q %s://%s%s/dists/%s/Release -O - | grep -E '^(Suite|Codename):'",
		 protocol, hostname, directory, name);
	di_log(DI_LOG_LEVEL_DEBUG, "command: %s", command);
	f = popen(command, "r");
	free(command);
	free(hostname);
	free(directory);

	if (f != NULL) {
		while (fgets(line, sizeof(line), f) != NULL) {
			char *value;

			if (line[strlen(line) - 1] == '\n')
				line[strlen(line) - 1] = '\0';
			if ((value = strstr(line, ": ")) != NULL) {
				strncpy(buf, value + 2, SUITE_LENGTH - 1);
				buf[SUITE_LENGTH - 1] = '\0';
				if (strncmp(line, "Codename:", 9) == 0)
					release->name = strdup(buf);
				if (strncmp(line, "Suite:", 6) == 0)
					release->suite = strdup(buf);
			}
		}
		if (release->name != NULL && strcmp(release->name, name) == 0)
			release->status |= IS_VALID | GET_CODENAME;
		if (release->suite != NULL && strcmp(release->suite, name) == 0)
			release->status |= IS_VALID | GET_SUITE;

		if ((release->name != NULL || release->suite != NULL) &&
		    !(release->status & IS_VALID))
			log_invalid_release(name, "Suite or Codename");

		/* Cross-validate the Release file */
		if (release->status & IS_VALID)
			if (! cross_validate_release(release))
				log_invalid_release(name, (release->status & GET_SUITE) ? "Codename" : "Suite");

		/* In case there is no Codename field */
		if ((release->status & IS_VALID) && release->name == NULL)
			release->name = strdup(name);

		// di_log(DI_LOG_LEVEL_DEBUG, "get_release(): %s -> %s:%s (0x%x)",
		//	name, release->suite, release->name, release->status);
	}

	pclose(f);

	if (release->name != NULL) {
		return 1;
	} else {
		free(release->suite);
		return 0;
	}
}

static int find_releases(void) {
	int nbr_suites = sizeof(suites)/SUITE_LENGTH;
	int i, r = 0;
	int bad_mirror = 0, have_default = 0;
	struct release_t release;
	char *default_suite;

	default_suite = get_default_suite();
	if (default_suite == NULL)
		di_log(DI_LOG_LEVEL_ERROR, "no default release specified");

	if (show_progress) {
		debconf_progress_start(debconf, 0, nbr_suites,
				       DEBCONF_BASE "checking_title");
		debconf_progress_info(debconf,
				      DEBCONF_BASE "checking_download");
	}

	/* Initialize releases; also ensures NULL termination of the array */
	memset(&releases, 0, sizeof(releases));

	/* Try to get Release files for all suites. */
	if (! base_on_cd) {
		for (i=0; i < nbr_suites && r < MAXRELEASES; i++) {
			memset(&release, 0, sizeof(release));
			if (get_release(&release, suites[i])) {
				if (release.status & IS_VALID) {
					if (strcmp(release.name, default_suite) == 0 ||
					    strcmp(release.suite, default_suite) == 0) {
						release.status |= IS_DEFAULT;
						have_default = 1;
					}
					/* Only list oldstable if it's the default */
					if (strcmp(suites[i], "oldstable") != 0 ||
					    (release.status & IS_DEFAULT))
						releases[r++] = release;
				} else {
					bad_mirror = 1;
					break;
				}
			}

			if (show_progress)
				debconf_progress_step(debconf, 1);
		}
		if (r == MAXRELEASES)
			di_log(DI_LOG_LEVEL_ERROR, "array overflow: more releases than allowed by MAXRELEASES");
		if (! bad_mirror && r == 0)
			di_log(DI_LOG_LEVEL_INFO, "mirror does not have any suite symlinks");
	}

	/* Try to get Release file using the default "suite". */
	if (! bad_mirror && (base_on_cd || ! have_default)) {
		memset(&release, 0, sizeof(release));
		if (get_release(&release, default_suite)) {
			if (release.status & IS_VALID) {
				release.status |= IS_DEFAULT;
				releases[r++] = release;
				have_default = 1;
			} else {
				bad_mirror = 1;
			}
		} else {
			di_log(DI_LOG_LEVEL_WARNING,
				"mirror does not support the specified release (%s)",
				default_suite);
		}
		if (r == MAXRELEASES)
			di_log(DI_LOG_LEVEL_ERROR, "array overflow: more releases than allowed by MAXRELEASES");
	}

	if (show_progress) {
		debconf_progress_set(debconf, nbr_suites);
		debconf_progress_stop(debconf);
	}

	if (r == 0 || bad_mirror) {
		unset_seen_flags();
		free(default_suite);
		free(release.name);
		free(release.suite);

		debconf_input(debconf, "critical", DEBCONF_BASE "bad");
		if (debconf_go(debconf) == 30)
			exit(10); /* back up to menu */
		else
			return 1; /* back to beginning of questions */
	}

	if (! base_on_cd && ! have_default) {
		unset_seen_flags();

		debconf_subst(debconf, DEBCONF_BASE "no-default",
			"RELEASE", default_suite);
		free(default_suite);

		debconf_input(debconf, "critical", DEBCONF_BASE "no-default");
		if (debconf_go(debconf) == 30) {
			exit(10); /* back up to menu */
		} else {
			debconf_get(debconf, DEBCONF_BASE "no-default");
			if (strcmp(debconf->value, "false"))
				return 1; /* back to beginning of questions */
		}
	} else {
		free(default_suite);
	}

	return 0;
}

/* Try to get translation for suite names */
static char *l10n_suite(const char *name) {
	char *template, *l10n_name;

	asprintf(&template, "%ssuites/%s", DEBCONF_BASE, name);
	if (! debconf_metaget(debconf, template, "description") &&
	    strlen(debconf->value))
		l10n_name = strdup(debconf->value);
	else
		l10n_name = strdup(name);

	free(template);
	return l10n_name;
}

static int check_base_on_cd(void) {
	FILE *fp;
	if ((fp = fopen("/cdrom/.disk/base_installable", "r"))) {
		base_on_cd = 1;
		fclose(fp);
	}
	return 0;
}

static int choose_protocol(void) {
#if defined (WITH_HTTP) && (defined (WITH_FTP) || defined (WITH_FTP_MANUAL))
	/* Both are supported, so ask. */
	debconf_subst(debconf, DEBCONF_BASE "protocol", "protocols", "http, ftp");
	debconf_input(debconf, "medium", DEBCONF_BASE "protocol");
#endif
	return 0;
}

static int get_protocol(void) {
#if defined (WITH_HTTP) && (defined (WITH_FTP) || defined (WITH_FTP_MANUAL))
	debconf_get(debconf, DEBCONF_BASE "protocol");
	protocol = strdup(debconf->value);
#else
#ifdef WITH_HTTP
	debconf_set(debconf, DEBCONF_BASE "protocol", "http");
	protocol = "http";
#endif
#ifdef WITH_FTP
	debconf_set(debconf, DEBCONF_BASE "protocol", "ftp");
	protocol = "ftp";
#endif
#endif /* WITH_HTTP && WITH_FTP */
	return 0;
}

static int choose_country(void) {
	if (country)
		free(country);
	country = NULL;

#if defined (WITH_FTP_MANUAL)
	assert(protocol != NULL);
	if (strcasecmp(protocol, "ftp") == 0)
		return 0;
#endif

	debconf_get(debconf, DEBCONF_BASE "country");
	if (! strlen(debconf->value)) {
		/* Not set yet. Seed with a default value. */
		if ((debconf_get(debconf, "debian-installer/country") == 0) &&
		    (debconf->value != NULL) ) {
			country = strdup (debconf->value);
			debconf_set(debconf, DEBCONF_BASE "country", country);
		}
	} else {
		country = debconf->value;
	}

	/* Ensure 'country' is set to something. */
	if (country == NULL || *country == 0)
		country = "US";

	char *countries;
	countries = add_protocol("countries");
	if (has_mirror(country)) {
		debconf_set(debconf, countries, country);
		debconf_fget(debconf, DEBCONF_BASE "country", "seen");
		debconf_fset(debconf, countries, "seen", debconf->value);
	}
	debconf_input(debconf, "high", countries);

	free (countries);
	return 0;
}

static int set_country(void) {
	char *countries;

#if defined (WITH_FTP_MANUAL)
	assert(protocol != NULL);
	if (strcasecmp(protocol, "ftp") == 0)
		return 0;
#endif

	countries = add_protocol("countries");
	debconf_get(debconf, countries);
	country = strdup(debconf->value);
	debconf_set(debconf, DEBCONF_BASE "country", country);

	free (countries);
	return 0;
}

static int manual_entry;

static int choose_mirror(void) {
	char *list;

	debconf_get(debconf, DEBCONF_BASE "country");
#ifndef WITH_FTP_MANUAL
	manual_entry = ! strcmp(debconf->value, MANUAL_ENTRY);
#else
	if (! strcasecmp(protocol, "ftp") == 0)
		manual_entry = ! strcmp(debconf->value, MANUAL_ENTRY);
	else
		manual_entry = 1;
#endif

	if (! manual_entry) {
		char *mir = add_protocol("mirror");

		/* Prompt for mirror in selected country. */
		list = debconf_list(mirrors_in(country));
		debconf_subst(debconf, mir, "mirrors", list);
		free(list);

		debconf_input(debconf, "high", mir);
		free(mir);
	} else {
		char *host = add_protocol("hostname");
		char *dir = add_protocol("directory");

		/* Manual entry. */
		debconf_input(debconf, "critical", host);
		debconf_input(debconf, "critical", dir);

		free(host);
		free(dir);
	}

	return 0;
}

/* Check basic validity of the selected/entered mirror. */
static int validate_mirror(void) {
	char *mir;
	char *host;
	char *dir;
	int valid = 1;

	mir = add_protocol("mirror");
	host = add_protocol("hostname");
	dir = add_protocol("directory");

	if (! manual_entry) {
		char *mirror;
		char *root;

		/*
		 * Copy information about the selected
		 * mirror into mirror/{protocol}/{hostname,directory},
		 * which is the standard location other
		 * tools can look at.
		 */
		debconf_get(debconf, mir);
		mirror = strdup(debconf->value);
		debconf_set(debconf, host, mirror);
		root = mirror_root(mirror);
		free(mirror);
		if (root == NULL)
			valid = 0;
		else
			debconf_set(debconf, dir, root);
	} else {
		/* check if manually entered mirror is basically ok */
		debconf_get(debconf, host);
		if (debconf->value == NULL || strcmp(debconf->value, "") == 0 ||
		    strchr(debconf->value, '/') != NULL)
			valid = 0;
		debconf_get(debconf, dir);
		if (debconf->value == NULL || strcmp(debconf->value, "") == 0)
			valid = 0;
	}

	free(mir);
	free(host);
	free(dir);

	if (valid) {
		return 0;
	} else {
		unset_seen_flags();
		debconf_input(debconf, "critical", DEBCONF_BASE "bad");
		if (debconf_go(debconf) == 30)
			exit(10); /* back up to menu */
		else
			return 9; /* back up to choose_mirror */
	}
}

static int choose_proxy(void) {
	char *px = add_protocol("proxy");

	/* Always ask about a proxy. */
	debconf_input(debconf, "high", px);

	free(px);
	return 0;
}

static int set_proxy(void) {
	char *px = add_protocol("proxy");
	char *proxy_var;

	asprintf(&proxy_var, "%s_proxy", protocol);

	debconf_get(debconf, px);
	if (debconf->value != NULL && strlen(debconf->value)) {
		if (strchr(debconf->value, ':')) {
			setenv(proxy_var, debconf->value, 1);
		} else {
			char *proxy_value;
			asprintf(&proxy_value, "http://%s", debconf->value);
			setenv(proxy_var, proxy_value, 1);
			free(proxy_value);
		}
	} else {
		unsetenv(proxy_var);
	}

	free(proxy_var);
	free(px);

	return 0;
}

static int choose_suite(void) {
	char *choices_c[MAXRELEASES], *choices[MAXRELEASES], *list;
	int i, ret;
	int have_default = 0;

	ret = find_releases();
	if (ret)
		return ret;

	/* Also ensures NULL termination */
	memset(choices, 0, sizeof(choices));
	memset(choices_c, 0, sizeof(choices_c));

	/* Arrays can never overflow as we've already checked releases */
	for (i=0; releases[i].name != NULL; i++) {
		char *name;

		if (releases[i].status & GET_SUITE)
			name = releases[i].suite;
		else
			name = releases[i].name;

		choices_c[i] = name;
		if (strcmp(name, releases[i].name) != 0)
			asprintf(&choices[i], "%s${!TAB}-${!TAB}%s", releases[i].name,
				 l10n_suite(name));
		else
			choices[i] = l10n_suite(name);
		if (releases[i].status & IS_DEFAULT) {
			debconf_set(debconf, DEBCONF_BASE "suite", name);
			have_default = 1;
		}
	}

	list = debconf_list(choices_c);
	debconf_subst(debconf, DEBCONF_BASE "suite", "CHOICES-C", list);
	free(list);
	list = debconf_list(choices);
	debconf_subst(debconf, DEBCONF_BASE "suite", "CHOICES", list);
	free(list);

	/* If the base system can be installed from CD, don't allow to
	 * select a different suite
	 */
	if (! have_default)
		debconf_fset(debconf, DEBCONF_BASE "suite", "seen", "false");
	if (! base_on_cd)
		debconf_input(debconf, have_default ? "medium" : "critical",
			      DEBCONF_BASE "suite");

	return 0;
}

/* Set the codename for the selected suite. */
int set_codename (void) {
	char *suite;
	int i;

	/* As suite has been determined previously, this should not fail */
	debconf_get(debconf, DEBCONF_BASE "suite");
	if (strlen(debconf->value) > 0) {
		suite = strdup(debconf->value);

		for (i=0; releases[i].name != NULL; i++) {
			if (strcmp(releases[i].name, suite) == 0 ||
			    strcmp(releases[i].suite, suite) == 0) {
				char *codename;

				if (releases[i].status & GET_CODENAME)
					codename = releases[i].name;
				else
					codename = releases[i].suite;
				debconf_set(debconf, DEBCONF_BASE "codename", codename);
				di_log(DI_LOG_LEVEL_INFO,
					"suite/codename set to: %s/%s",
					suite, codename);
				break;
			}
		}

		free(suite);
	}

	return 0;
}

/* Check if the mirror carries the architecture that's being installed. */
int check_arch (void) {
	char *command;
	FILE *f = NULL;
	char *hostname, *directory, *codename = NULL;
	int valid = 0;

	hostname = add_protocol("hostname");
	debconf_get(debconf, hostname);
	free(hostname);
	hostname = strdup(debconf->value);
	directory = add_protocol("directory");
	debconf_get(debconf, directory);
	free(directory);
	directory = strdup(debconf->value);

	/* As codename has been determined previously, this should not fail */
	debconf_get(debconf, DEBCONF_BASE "codename");
	if (strlen(debconf->value) > 0) {
		codename = strdup(debconf->value);

		asprintf(&command, "wget -q %s://%s%s/dists/%s/main/binary-%s/Release -O - | grep ^Architecture:",
			 protocol, hostname, directory, codename, ARCH_TEXT);
		di_log(DI_LOG_LEVEL_DEBUG, "command: %s", command);
		f = popen(command, "r");
		free(command);

		if (f != NULL) {
			char buf[SUITE_LENGTH];
			if (fgets(buf, SUITE_LENGTH - 1, f))
				if (strlen(buf) > 1)
					valid = 1;
		}
		pclose(f);
	}

	free(hostname);
	free(directory);
	free(codename);

	if (valid) {
		return 0;
	} else {
		unset_seen_flags();
		di_log(DI_LOG_LEVEL_DEBUG, "architecture not supported by selected mirror");
		debconf_input(debconf, "critical", DEBCONF_BASE "noarch");
		if (debconf_go(debconf) == 30)
			exit(10); /* back up to menu */
		else
			return 1; /* back to beginning of questions */
	}
}

int main (int argc, char **argv) {
	int i;
	/* Use a state machine with a function to run in each state */
	int state = 0;
	int (*states[])() = {
		check_base_on_cd,
		choose_protocol,
		get_protocol,
		choose_country,
		set_country,
		choose_mirror,
		validate_mirror,
		choose_proxy,
		set_proxy,
		choose_suite,
		set_codename,
		check_arch,
		NULL,
	};

	if (argc > 1 && strcmp(argv[1], "-n") == 0)
		show_progress = 0;

	debconf = debconfclient_new();
	debconf_capb(debconf, "backup align");
	debconf_version(debconf, 2);

	di_system_init("choose-mirror");

	while (state >= 0 && states[state]) {
		int res;

		res = states[state]();
		if (res == 9) /* back up */
			state--;
		else if (res) /* back up to start */
			state = 0;
		else if (debconf_go(debconf)) /* back up */
			state--;
		else
			state++;
	}

	for (i=0; releases[i].name != NULL; i++) {
		free(releases[i].name);
		free(releases[i].suite);
	}

	return (state >= 0) ? 0 : 10; /* backed all the way out */
}
