import {cachedFetchJSON} from './cache';

const renderTarget = document.querySelector('#gov')!;
const planetInput = document.querySelector('#planet') as HTMLInputElement;
const popSelect = document.querySelector('#pop') as HTMLSelectElement;

function serializeToHash(planet: string, pop: Pop): void {
	const params = new URLSearchParams({'planet': planet, 'pop': pop});
	document.location.hash = params.toString();
}

function deserializeFromHash(): {planet: string, pop: Pop} | null {
	const params = new URLSearchParams(document.location.hash.substring(1));
	const planet = params.get('planet');
	const pop = params.get('pop');
	if (planet && pop && ['pio', 'set', 'tec', 'eng', 'sci'].includes(pop)) {
		planetInput.value = planet;
		popSelect.value = pop;
		return {planet, pop: pop as Pop};
	} else
		return null;
}

document.querySelector('form')!.addEventListener('submit', async (event) => {
	event.preventDefault();
	const planet = planetInput.value;
	const pop = popSelect.value as Pop;
	if (planet && pop) {
		await render(planet, pop);
		serializeToHash(planet, pop);
	}
});

{
	const deserialized = deserializeFromHash();
	if (deserialized)
		void render(deserialized.planet, deserialized.pop);
}

async function render(planetName: string, pop: Pop) {
	const loader = document.querySelector('#loader') as HTMLElement;
	loader.style.display = 'block';
	renderTarget.innerHTML = '';
	try {
		await _render(planetName, pop);
	} catch (e) {
		console.error(e);
		renderTarget.textContent = (e as Error).message;
	}
	loader.style.display = 'none';
}

async function _render(planetName: string, pop: Pop) {
	const encodedPlanetName = encodeURIComponent(planetName);
	const [planet, siteCounts, infras, allPrices]: [Planet, SiteCount[], Infrastructure[], Price[]] = await Promise.all([
		cachedFetchJSON(`https://api.fnar.net/planet/${encodedPlanetName}?include_population_reports=true`),
		cachedFetchJSON(`https://api.fnar.net/planet/sitecount?planet=${encodedPlanetName}`),
		cachedFetchJSON(`https://api.fnar.net/infrastructure?infrastructure=${encodedPlanetName}&include_upkeeps=true`),
		cachedFetchJSON('https://api.prunplanner.org/data/exchanges'),
	]);

	let lastPOPR = null;
	for (const report of planet.PopulationReports) {
		if (!lastPOPR || report.SimulationPeriod > lastPOPR.SimulationPeriod)
			lastPOPR = report;
	}
	if (lastPOPR === null) {
		renderTarget.textContent = `no POPR for ${planetName}`;
		return;
	}
	const lastPOPRts = Math.floor(new Date(lastPOPR.ReportTimestamp).getTime() / 1000);
	const nextPOPRts = lastPOPRts + 7 * 24 * 60 * 60;

	let currentPop = 0, lowerPop = 0;
	if (pop == 'pio')
		currentPop = lastPOPR.NextPopulationPioneer;
	else if (pop == 'set') {
		currentPop = lastPOPR.NextPopulationSettler;
		lowerPop = lastPOPR.NextPopulationPioneer;
	}
	else if (pop == 'tec') {
		currentPop = lastPOPR.NextPopulationTechnician;
		lowerPop = lastPOPR.NextPopulationSettler;
	}
	else if (pop == 'eng') {
		currentPop = lastPOPR.NextPopulationEngineer;
		lowerPop = lastPOPR.NextPopulationTechnician;
	}
	else if (pop == 'sci') {
		currentPop = lastPOPR.NextPopulationScientist;
		lowerPop = lastPOPR.NextPopulationEngineer;
	}

	const siteCount = siteCounts[0].Count;

	const totalNeeds: Record<Need, number> = {
		'safety': calcTotalNeeds(lastPOPR, 'safety'),
		'health': calcTotalNeeds(lastPOPR, 'health'),
		'comfort': calcTotalNeeds(lastPOPR, 'comfort'),
		'culture': calcTotalNeeds(lastPOPR, 'culture'),
		'education': calcTotalNeeds(lastPOPR, 'education'),
	};
	const totalPop = lastPOPR.NextPopulationPioneer + lastPOPR.NextPopulationSettler +
			lastPOPR.NextPopulationTechnician + lastPOPR.NextPopulationEngineer + lastPOPR.NextPopulationScientist;

	const currentPOPIFilled: POPIFill = new Map();
	for (const infra of infras) {
		const filled = calcPOPIFilled(infra);
		if (filled !== null)
			currentPOPIFilled.set(infra.Type, {tier: infra.CurrentLevel, numMats: filled});
	}

	const prices: Map<string, number> = new Map();
	for (const price of allPrices)
		if (price.exchange_code === 'IC1')
			prices.set(price.ticker, price.vwap_30d || price.ask);
		else if (price.exchange_code === 'UNIVERSE' && prices.get(price.ticker) === 0) // UNIVERSE always comes after IC1
			prices.set(price.ticker, price.vwap_30d);

	const results = paretoFront(pop, totalNeeds, siteCount, currentPOPIFilled, currentPop, lowerPop, totalPop, prices);

	renderTarget.innerHTML = `last POPR: ${lastPOPR.ReportTimestamp} (${lastPOPRts})
	<br>next POPR: ${new Date(nextPOPRts * 1000).toISOString()} (${nextPOPRts})

	<table>
		<tr>
			<th></th>
			<th>pio</th>
			<th>set</th>
			<th>tec</th>
			<th>eng</th>
			<th>sci</th>
		</tr>
		<tr>
			<th>population</th>
			<td>${lastPOPR.NextPopulationPioneer}<br>${formatDelta(lastPOPR.PopulationDifferencePioneer)}</td>
			<td>${lastPOPR.NextPopulationSettler}<br>${formatDelta(lastPOPR.PopulationDifferenceSettler)}</td>
			<td>${lastPOPR.NextPopulationTechnician}<br>${formatDelta(lastPOPR.PopulationDifferenceTechnician)}</td>
			<td>${lastPOPR.NextPopulationEngineer}<br>${formatDelta(lastPOPR.PopulationDifferenceEngineer)}</td>
			<td>${lastPOPR.NextPopulationScientist}<br>${formatDelta(lastPOPR.PopulationDifferenceScientist)}</td>
		</tr>
		<tr>
			<th>unemployed</th>
			<td>${unemployed(lastPOPR.NextPopulationPioneer, lastPOPR.PopulationDifferencePioneer, lastPOPR.OpenJobsPioneer, lastPOPR.UnemploymentRatePioneer)}</td>
			<td>${unemployed(lastPOPR.NextPopulationSettler, lastPOPR.PopulationDifferenceSettler, lastPOPR.OpenJobsSettler, lastPOPR.UnemploymentRateSettler)}</td>
			<td>${unemployed(lastPOPR.NextPopulationTechnician, lastPOPR.PopulationDifferenceTechnician, lastPOPR.OpenJobsTechnician, lastPOPR.UnemploymentRateTechnician)}</td>
			<td>${unemployed(lastPOPR.NextPopulationEngineer, lastPOPR.PopulationDifferenceEngineer, lastPOPR.OpenJobsEngineer, lastPOPR.UnemploymentRateEngineer)}</td>
			<td>${unemployed(lastPOPR.NextPopulationScientist, lastPOPR.PopulationDifferenceScientist, lastPOPR.OpenJobsScientist, lastPOPR.UnemploymentRateScientist)}</td>
		</tr>
	</table>

	<h2>needs</h2>
	<table>
		<tr>
			<th></th>
			<th>safety</th>
			<th>health</th>
			<th>comfort</th>
			<th>culture</th>
			<th>education</th>
		</tr>
		<tr>
			<td>last POPR</td>
			<td>${formatPct(lastPOPR.NeedFulfillmentSafety)}</td>
			<td>${formatPct(lastPOPR.NeedFulfillmentHealth)}</td>
			<td>${formatPct(lastPOPR.NeedFulfillmentComfort)}</td>
			<td>${formatPct(lastPOPR.NeedFulfillmentCulture)}</td>
			<td>${formatPct(lastPOPR.NeedFulfillmentEducation)}</td>
		</tr>
		<tr>
			<td>total needed</td>
			<td>${formatNum(totalNeeds['safety'])}</td>
			<td>${formatNum(totalNeeds['health'])}</td>
			<td>${formatNum(totalNeeds['comfort'])}</td>
			<td>${formatNum(totalNeeds['culture'])}</td>
			<td>${formatNum(totalNeeds['education'])}</td>
		</tr>
	</table>

	<h2>current POPI</h2>
	${siteCount} bases
	<table>
		<tr>
			${infras.map((infra) => {
				if (infra.CurrentLevel === 0)
					return '';
				const fill = currentPOPIFilled.get(infra.Type)!;
				return `<tr>
					<td>${infra.Type} T${infra.CurrentLevel}</td>
					<td>${fill.numMats}/${infra.Upkeeps.length}</td>
				</tr>`;
			}).join('')}
		</tr>
	</table>
	current projected ${pop.toUpperCase()} happiness:
	${formatPct(projectedHappiness(pop, totalNeeds, siteCount, currentPOPIFilled, null))}

	<h2>options</h2>
	<select id="gov_program">
		<option value="">(all)</option>
		${[...new Set(results.map((result) => result.govProgram))].map(
			(program) => `<option value="${program ?? 'no program'}">${program ?? 'no program'}</option>`).join('')}
	</select>
	${renderOptions(results, currentPOPIFilled, '')}
	`;

	const govProgramSelect = renderTarget.querySelector('#gov_program') as HTMLSelectElement;
	govProgramSelect.addEventListener('change', (event) => {
		renderTarget.querySelector('.options')!.outerHTML = renderOptions(results, currentPOPIFilled, govProgramSelect.value);
	});
}

function renderOptions(results: ConfigResult[], currentPOPIFilled: POPIFill, govProgramFilter: string): string {
	const maxPop = Math.max(...results.map((result) => result.change));
	const bestUnitCost = Math.min(...results.filter((result) => result.change > 0).map((result) => result.cost / result.change));
	return `
	<table class="options">
		<tr>
			<th>config</th>
			<th>projected happiness</th>
			<th>projected change</th>
			<th>cost/day</th>
			<th>unit cost</th>
		</tr>
		${results.filter((result) => govProgramFilter === '' || govProgramFilter === (result.govProgram ?? 'no program')).map((result) => {
			let unitCost = '';
			if (result.change !== 0)
				unitCost = formatNum(result.cost / result.change);
			return `<tr>
				<td>
					${[...result.config.entries()].map(([building, fill]) => {
						let currentBuildingFill = currentPOPIFilled.get(building)!.numMats;
						let className = '';
						if (fill.numMats > currentBuildingFill) className = 'positive';
						else if (fill.numMats < currentBuildingFill) className = 'negative';
						return `${building}: <span class="${className}">${fill.numMats}</span>`;
					}).join('<br>')}
					${result.govProgram !== null ? `<br>${result.govProgram}` : ''}
				</td>
				<td>${formatPct(result.happiness)}</td>
				<td style="color: ${color(result.change, 0, maxPop)}">${formatNum(result.change)}</td>
				<td>${formatNum(result.cost)}</td>
				<td style="color: ${result.change > 0 ? color(bestUnitCost - result.cost / result.change, -bestUnitCost, 0) : '#777'}">
					${unitCost}
				</td>
			</tr>`;
		}).join('')}
	</table>
	`;
}

const formatNum = new Intl.NumberFormat(undefined, {maximumFractionDigits: 2}).format;
const formatPct = new Intl.NumberFormat(undefined, {style: 'percent', maximumFractionDigits: 2}).format;

function formatDelta(n: number, withPlus: boolean = true): string {
	if (n > 0)
		return `<span class="positive">${withPlus ? '+' : ''}${formatNum(n)}</span>`;
	else if (n < 0)
		return `<span class="negative">${formatNum(n)}</span>`;
	else
		return n.toString();
}

function color(n: number, low: number, high: number): string {
	// scale n from low..high to 0..1 clamped
	const scale = Math.min(Math.max((n - low) / (high - low), 0), 1);
	return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`;
}

function unemployed(people: number, change: number, openJobs: number, unemploymentRate: number): string {
	let nextUnemployed;
	if (openJobs <= people + change) {
		let prevUnemploymentRate = unemploymentRate;
		if (openJobs > 0)
			if (people - change > 0)
				prevUnemploymentRate = -openJobs / (people - change);
			else
				prevUnemploymentRate = -1
		const prevUnemployed = prevUnemploymentRate * (people - change);
		nextUnemployed = prevUnemployed + change;
	} else
		nextUnemployed = -openJobs + change;
	return formatDelta(Math.round(nextUnemployed), false);
}

function calcTotalNeeds(popReport: POPR, need: Need): number {
	return popReport.NextPopulationPioneer * NEEDS.pio[need]
		+ popReport.NextPopulationSettler * NEEDS.set[need]
		+ popReport.NextPopulationTechnician * NEEDS.tec[need]
		+ popReport.NextPopulationEngineer * NEEDS.eng[need]
		+ popReport.NextPopulationScientist * NEEDS.sci[need];
}

function calcPOPIFilled(infra: Infrastructure): number | null {
	if (infra.CurrentLevel === 0)
		return null;

	let filled = 0;
	for (const upkeep of infra.Upkeeps) {
		const nextConsumptionAmount = upkeep.StoreCapacity / 30 * upkeep.Duration; // # capacity is always 30 days
		if (upkeep.Stored >= nextConsumptionAmount)
			filled++;
	}
	return filled;
}

function paretoFront(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, currentPOPIFilled: POPIFill,
		currentPop: number, lowerPop: number, totalPop: number, prices: Map<string, number>): ConfigResult[] {
	const results: ConfigResult[] = [];
	let education = 0;
	if (pop !== 'pio') {
		education = EDUCATION[pop];
		education += (currentPOPIFilled.get('PLANETARY_BROADCASTING_HUB')?.tier ?? 0) * 0.001;
		education += (currentPOPIFilled.get('LIBRARY')?.tier ?? 0) * 0.002;
		education += (currentPOPIFilled.get('UNIVERSITY')?.tier ?? 0) * 0.004;
	}

	const govPrograms: (GovProgram | null)[] = [null];
	govPrograms.push(...(Object.keys(GOV_PROGRAMS) as GovProgram[]));

	for (const config of popiFillCombinations(currentPOPIFilled)) {
		for (const govProgram of govPrograms) {
			const happiness = projectedHappiness(pop, totalNeeds, siteCount, config, govProgram);
			let change = 0;
			if (happiness > 0.7 && ['pio', 'set', 'tec'].includes(pop))
				change = currentPop * (happiness - 0.7);
			else if (happiness < 0.5)
				change = 0.8 * currentPop * (happiness - 0.5);
			if (govProgram === 'pio immigration' && pop === 'pio') change += 500;
			else if (govProgram === 'set immigration' && pop === 'set') change += 200;
			else if (govProgram === 'tec immigration' && pop === 'tec') change += 100;
			else if (govProgram === 'eng immigration' && pop === 'eng') change += 50;
			else if (govProgram === 'sci immigration' && pop === 'sci') change += 25;

			if (pop !== 'pio') {
				let eduIn = lowerPop * education * happiness;
				if (govProgram === 'education I') eduIn *= 1.5;
				else if (govProgram === 'education II') eduIn *= 1.75;
				else if (govProgram === 'education III') eduIn *= 2;
				change += eduIn;
			}
			// TODO: education out

			let cost = calcCost(config, prices);
			if (govProgram !== null) {
				const program = GOV_PROGRAMS[govProgram];
				cost += (program.baseCost + program.popCost * totalPop) / 7;
			}

			// is any result better than this one?
			if (results.some((result) => (result.change >= change && result.cost < cost) ||
					(result.change > change && result.cost <= cost)))
				continue;
			// are any results worse than this one?
			for (let i = results.length - 1; i >= 0; i--)
				if ((results[i].change <= change && results[i].cost > cost) ||
						(results[i].change < change && results[i].cost >= cost))
					results.splice(i, 1);
			results.push({config, govProgram, happiness, change, cost});
		}
	}

	results.sort((a, b) => b.change - a.change);
	return results;
}

function* popiFillCombinations(currentPOPIFilled: POPIFill): Generator<POPIFill> {
	const entries = [...currentPOPIFilled.keys()];
	function* helper(index: number, current: POPIFill): Generator<POPIFill> {
		if (index === entries.length) {
			yield new Map(current);
			return;
		}
		const building = entries[index];
		const tier = currentPOPIFilled.get(building)!.tier;
		const maxBuildingMats = Object.keys(POPI[building].mats).length;
		for (let i = 0; i <= maxBuildingMats; i++) {
			current.set(building, {tier, numMats: i});
			yield* helper(index + 1, current);
		}
	}
	yield* helper(0, new Map());
}

function projectedHappiness(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, popiFilled: POPIFill,
		govProgram: GovProgram | null): number {
	let totalProvided: Record<Need, number> = {'safety': 50 * siteCount, 'health': 50 * siteCount,
		'comfort': 0, 'culture': 0, 'education': 0};
	for (const [building, fill] of popiFilled.entries()) {
		const {needs} = POPI[building];
		const maxBuildingMats = Object.keys(POPI[building].mats).length;
		for (const {need, supplied} of needs) {
			const provided = fill.numMats / maxBuildingMats * fill.tier * supplied;
			totalProvided[need] += provided;
		}
	}

	const satisfaction: Map<Need, number> = new Map(); // percentage of needs fulfilled
	for (const _need in totalProvided) {
		const need = _need as Need;
		satisfaction.set(need, Math.min(totalProvided[need] / totalNeeds[need], 1));
	}
	const safetyHealthCap = Math.min(satisfaction.get('safety')!, satisfaction.get('health')!);
	if (satisfaction.get('comfort')! > safetyHealthCap)
		satisfaction.set('comfort', safetyHealthCap);
	if (satisfaction.get('culture')! > safetyHealthCap)
		satisfaction.set('culture', safetyHealthCap);
	const comfortCultureCap = Math.min(satisfaction.get('comfort')!, satisfaction.get('culture')!);
	if (satisfaction.get('education')! > comfortCultureCap)
		satisfaction.set('education', comfortCultureCap);

	const weights = NEEDS[pop];
	let happiness = 1 - Object.values(weights).reduce((sum, weight) => sum + weight, 0); // assume 100% life support
	for (const [need, s] of satisfaction.entries())
		happiness += weights[need] * s;

	if (govProgram === 'festivities I')
		happiness += 0.05;
	else if (govProgram === 'festivities II')
		happiness += 0.1;
	else if (govProgram === 'festivities III')
		happiness += 0.2;
	return Math.min(happiness, 1);
}

function calcCost(config: POPIFill, prices: Map<string, number>): number {
	let cost = 0;
	for (const [building, fill] of config.entries()) {
		const {mats} = POPI[building];
		const matPrices: {ticker: string, costPerDay: number}[] = [];
		for (const [mat, amount] of Object.entries(mats)) {
			const matCost = prices.get(mat)!;
			if (!matCost)
				throw new Error('no price for ' + mat);
			matPrices.push({ticker: mat, costPerDay: matCost * amount});
		}
		matPrices.sort((a, b) => a.costPerDay - b.costPerDay);

		for (let i = 0; i < fill.numMats; i++)
			cost += matPrices[i].costPerDay * fill.tier;
	}
	return cost;
}

const NEEDS: Record<Pop, Record<Need, number>> = {
	'pio': {'safety': 0.25, 'health': 0.15, 'comfort': 0.03, 'culture': 0.02, 'education': 0.01},
	'set': {'safety': 0.30, 'health': 0.20, 'comfort': 0.03, 'culture': 0.03, 'education': 0.03},
	'tec': {'safety': 0.20, 'health': 0.30, 'comfort': 0.20, 'culture': 0.10, 'education': 0.05},
	'eng': {'safety': 0.10, 'health': 0.15, 'comfort': 0.35, 'culture': 0.20, 'education': 0.10},
	'sci': {'safety': 0.10, 'health': 0.10, 'comfort': 0.20, 'culture': 0.25, 'education': 0.30},
}
const POPI: Record<POPIBuilding, {needs: {need: Need, supplied: number}[], mats: Record<string, number>}> = {
	'SAFETY_STATION': {
		needs: [{need: 'safety', supplied: 2500}],
		mats: {'DW': 10, 'OFF': 10, 'SUN': 2},
	},
	'SECURITY_DRONE_POST': {
		needs: [{need: 'safety', supplied: 5000}],
		mats: {'POW': 1, 'RAD': 0.47, 'CCD': 0.07, 'SUD': 0.07},
	},
	'EMERGENCY_CENTER': {
		needs: [{need: 'safety', supplied: 1000}, {need: 'health', supplied: 1000}],
		mats: {'PK': 2, 'POW': 0.4, 'BND': 4, 'RED': 0.07, 'BSC': 0.07},
	},
	'INFIRMARY': {
		needs: [{need: 'health', supplied: 2500}],
		mats: {'OFF': 10, 'TUB': 6.67, 'STR': 0.67},
	},
	'HOSPITAL': {
		needs: [{need: 'health', supplied: 5000}],
		mats: {'PK': 2, 'SEQ': 0.4, 'BND': 4, 'SDR': 0.07, 'RED': 0.07, 'BSC': 0.13},
	},
	'WELLNESS_CENTER': {
		needs: [{need: 'health', supplied: 1000}, {need: 'comfort', supplied: 1000}],
		mats: {'KOM': 4, 'OLF': 2, 'DW': 6, 'DEC': 0.67, 'PFE': 2.67, 'SOI': 6.67},
	},
	'WILDLIFE_PARK': {
		needs: [{need: 'comfort', supplied: 2500}],
		mats: {'DW': 10, 'FOD': 6, 'PFE': 2, 'SOI': 3.33, 'DEC': 0.33},
	},
	'ARCADES': {
		needs: [{need: 'comfort', supplied: 5000}],
		mats: {'POW': 2, 'MHP': 2, 'OLF': 4, 'BID': 0.2, 'HOG': 0.2, 'EDC': 0.2},
	},
	'ART_CAFE': {
		needs: [{need: 'comfort', supplied: 1000}, {need: 'culture', supplied: 1000}],
		mats: {'MHP': 1, 'HOG': 1, 'UTS': 0.67, 'DEC': 0.67},
	},
	'ART_GALLERY': {
		needs: [{need: 'culture', supplied: 2500}],
		mats: {'MHP': 1, 'HOG': 1, 'UTS': 0.67, 'DEC': 0.67},
	},
	'THEATER': {
		needs: [{need: 'culture', supplied: 5000}],
		mats: {'POW': 1.4, 'MHP': 2, 'HOG': 1.4, 'OLF': 4, 'BID': 0.33, 'DEC': 0.67},
	},
	'PLANETARY_BROADCASTING_HUB': {
		needs: [{need: 'culture', supplied: 1000}, {need: 'education', supplied: 1000}],
		mats: {'OFF': 10, 'MHP': 1, 'SP': 1.33, 'AAR': 0.67, 'EDC': 0.27, 'IDC': 0.13},
	},
	'LIBRARY': {
		needs: [{need: 'education', supplied: 2500}],
		mats: {'MHP': 1, 'HOG': 1, 'CD': 0.33, 'DIS': 0.33, 'BID': 0.2},
	},
	'UNIVERSITY': {
		needs: [{need: 'education', supplied: 5000}],
		mats: {'COF': 10, 'REA': 10, 'TUB': 10, 'BID': 0.33, 'HD': 0.67, 'IDC': 0.2},
	},
};
const EDUCATION: Record<Exclude<Pop, 'pio'>, number> = {
	'set': 0.025,
	'tec': 0.02,
	'eng': 0.0125,
	'sci': 0.0075,
}
const GOV_PROGRAMS: Record<GovProgram, {baseCost: number, popCost: number}> = {
	'festivities I': {baseCost: 5000, popCost: 0.125},
	'festivities II': {baseCost: 10000, popCost: 0.25},
	'festivities III': {baseCost: 20000, popCost: 0.5},
	'education I': {baseCost: 5000, popCost: 0.15},
	'education II': {baseCost: 10000, popCost: 0.25},
	'education III': {baseCost: 20000, popCost: 0.4},
	'pio immigration': {baseCost: 10000, popCost: 0},
	'set immigration': {baseCost: 25000, popCost: 0},
	'tec immigration': {baseCost: 50000, popCost: 0},
	'eng immigration': {baseCost: 80000, popCost: 0},
	'sci immigration': {baseCost: 125000, popCost: 0},
}

type Pop = 'pio' | 'set' | 'tec' | 'eng' | 'sci';
type Need = 'safety' | 'health' | 'comfort' | 'culture' | 'education';
type POPIBuilding = 'SAFETY_STATION' | 'SECURITY_DRONE_POST' | 'EMERGENCY_CENTER' | 'INFIRMARY' | 'HOSPITAL' |
	'WELLNESS_CENTER' | 'WILDLIFE_PARK' | 'ARCADES' | 'ART_CAFE' | 'ART_GALLERY' | 'THEATER' |
	'PLANETARY_BROADCASTING_HUB' | 'LIBRARY' | 'UNIVERSITY';
type POPIFill = Map<POPIBuilding, {tier: number, numMats: number}>;
type GovProgram = 'festivities I' | 'festivities II' | 'festivities III' | 'education I' | 'education II' | 'education III' |
	'pio immigration' | 'set immigration' | 'tec immigration' | 'eng immigration' | 'sci immigration';
type ConfigResult = {config: POPIFill, govProgram: GovProgram | null, happiness: number, change: number, cost: number};

interface Planet {
	PopulationReports: POPR[]
}

interface POPR {
	SimulationPeriod: number;
	ReportTimestamp: string;
	NeedFulfillmentSafety: number;
	NeedFulfillmentHealth: number;
	NeedFulfillmentComfort: number;
	NeedFulfillmentCulture: number;
	NeedFulfillmentEducation: number;
	NextPopulationPioneer: number;
	NextPopulationSettler: number;
	NextPopulationTechnician: number;
	NextPopulationEngineer: number;
	NextPopulationScientist: number;
	PopulationDifferencePioneer: number;
	PopulationDifferenceSettler: number;
	PopulationDifferenceTechnician: number;
	PopulationDifferenceEngineer: number;
	PopulationDifferenceScientist: number;
	OpenJobsPioneer: number;
	OpenJobsSettler: number;
	OpenJobsTechnician: number;
	OpenJobsEngineer: number;
	OpenJobsScientist: number;
	UnemploymentRatePioneer: number;
	UnemploymentRateSettler: number;
	UnemploymentRateTechnician: number;
	UnemploymentRateEngineer: number;
	UnemploymentRateScientist: number;
}

interface SiteCount {
	Count: number;
}

interface Infrastructure {
	Type: POPIBuilding;
	CurrentLevel: number;
	Upkeeps: {Stored: number, StoreCapacity: number, 'Duration': number}[];
}

interface Price {
	ticker: string
	exchange_code: string
	vwap_30d: number
	ask: number
}
