import {cachedFetchJSON} from './cache';
import {setupPopover} from './popover';

let blueprint: Record<string, number>;
let BUY: Set<string>;
let renderTarget: Element;
let cx: string;
let daysPerBundle: number;
const apiKey = document.querySelector('#api-key') as HTMLInputElement;

export function setupProduction(bp: Record<string, number>, buy: Set<string>, cxCode: string, days: number, target: Element) {
	blueprint = bp;
	BUY = buy;
	cx = cxCode;
	daysPerBundle = days;
	renderTarget = target;

	const storedApiKey = localStorage.getItem('punoted-api-key');
	if (storedApiKey)
		apiKey.value = storedApiKey;
	document.querySelector('#fetch')!.addEventListener('click', render);

	setupPopover();
	render();
}

async function render() {
	const loader = document.querySelector('#loader') as HTMLElement;
	loader.style.display = 'block';
	try {
		await _render();
		if (apiKey.value)
			localStorage.setItem('punoted-api-key', apiKey.value);
	} catch (e) {
		renderTarget.innerHTML = e instanceof Error ? e.message : String(e);
	}
	loader.style.display = 'none';
}

async function _render() {
	const [allPrices, {recipes, extractables}, buildingList, storage] = await Promise.all([
		cachedFetchJSON('https://refined-prun.github.io/refined-prices/all.json') as Promise<RawPrice[]>,
		recipeForMats(),
		cachedFetchJSON('https://api.prunplanner.org/data/buildings/') as Promise<Building[]>,
		fetchStorage(),
	]);
	const prices = Object.fromEntries(allPrices.filter((price) => price.ExchangeCode === cx)
			.map((price) => [price.MaterialTicker, price]));
	const buildings = Object.fromEntries(buildingList.map((b) => [b.building_ticker, b]));

	const production: Production = {};
	const extract: Record<string, number> = {};
	const buy: Record<string, number> = {};
	const analysisNodes: AnalysisNode[] = [];
	let cost = 0;
	for (const [mat, amount] of Object.entries(blueprint)) {
		const node = analyzeMat(mat, amount, production, extract, buy, prices, recipes, extractables, storage);
		cost += node.cost;
		analysisNodes.push(node);
	}

	// requiredMats = buy + production
	const requiredMats: Record<string, number> = {...buy};
	for (const buildingProduction of Object.values(production))
		for (const [mat, amount] of Object.entries(buildingProduction))
			requiredMats[mat] = (requiredMats[mat] ?? 0) + amount;

	const expertiseGroups: Record<string, string[]> = {};
	for (const building of buildingList) {
		if (!(building.building_ticker in production))
			continue;
		if (!expertiseGroups[building.expertise])
			expertiseGroups[building.expertise] = [];
		expertiseGroups[building.expertise].push(building.building_ticker);
	}

	renderTarget.innerHTML = '';
	renderTarget.append(
		renderAnalysis(analysisNodes),
		element('p', {textContent: `total cost: ${formatWhole(cost)}`}),
		renderProduction(expertiseGroups, production, storage, prices, recipes, buildings),
		renderMatList('extract', extract, storage),
		renderMatList('buy', buy, storage),
	);
}

function renderMatList(header: string, mats: Record<string, number>, storage: Record<string, number>): HTMLElement {
	const section = element('section');
	section.append(element('h2', {textContent: header}));
	const matsSorted = Object.entries(mats).sort(([a], [b]) => a.localeCompare(b));
	for (const [mat, amount] of matsSorted) {
		const div = element('div', {textContent: `${formatAmount(amount)}x${mat}`});
		const storageText = element('span', {textContent: ` (${formatAmount(storage[mat] ?? 0)})`});
		const percent = Math.min((storage[mat] ?? 0) / (amount * 2), 1);
		storageText.style.color = `color-mix(in xyz, #0aa ${percent * 100}%, #f80 )`;
		div.append(storageText);
		section.append(div);
	}
	return section;
}

async function recipeForMats(): Promise<{recipes: Record<string, Recipe>, extractables: Set<string>}> {
	const [allRecipes, materials]: [Recipe[], Material[]] = await Promise.all([
		cachedFetchJSON('https://api.prunplanner.org/data/recipes/'),
		cachedFetchJSON('https://api.prunplanner.org/data/materials/'),
	]);

	const extractables = new Set(materials.filter(
		(m) => ['ores', 'minerals', 'liquids', 'gases'].includes(m.category_name)).map((m) => m.ticker));

	const chosenRecipes: Record<string, string> = {
		'AL': '6xALO 1xO 1xC 1xFLX=>4xAL',
		'C': '4xHCP=>4xC',
		'DRF': '50xNFI 1xDCS=>1xDRF',
		'FE': '6xFEO 1xC 1xO 1xFLX=>4xFE',
		'GL': '2xSIO 1xNA 1xSEN=>10xGL',
		'HCP': '14xH2O 1xNS=>8xHCP',
		'RE': '8xREO 1xC 1xO 1xFLX=>5xRE',
		'RG': '10xGL 15xPG 1xSEN=>10xRG',
		'SI': '3xSIO 1xC 1xO 1xFLX=>1xSI',
	}

	const matRecipes: Record<string, Recipe[]> = {}; // all ways to make a mat
	for (const recipe of allRecipes)
		for (const output of recipe.outputs) {
			if (extractables.has(output.material_ticker))
				continue;
			if (chosenRecipes[output.material_ticker] && recipe.recipe_name != chosenRecipes[output.material_ticker])
				continue;
			const recipes = matRecipes[output.material_ticker];
			if (recipes)
				recipes.push(recipe);
			else
				matRecipes[output.material_ticker] = [recipe];
		}

	const matRecipe: Record<string, Recipe> = {};
	for (const [mat, recipes] of Object.entries(matRecipes))
		if (recipes.length === 1)
			matRecipe[mat] = recipes[0];
	return {recipes: matRecipe, extractables};
}

function analyzeMat(mat: string, amount: number, production: Production, extract: Record<string, number>, buy: Record<string, number>,
		prices: Record<string, RawPrice>, recipes: Record<string, Recipe>, extractables: Set<string>, storage: Record<string, number>): AnalysisNode {
	const price = prices[mat];
	if (!price)
		throw new Error(`missing price for ${mat}`);
	const traded = price.AverageTraded30D ?? 0;

	const inStorage = storage[mat] ?? 0;

	if (BUY.has(mat)) {
		const matPrice = price.VWAP30D ?? price.Ask;
		if (matPrice == null) throw new Error(`missing ask price for ${mat}`);
		buy[mat] = (buy[mat] ?? 0) + amount;
		return {
			ticker: mat,
			amount,
			inStorage,
			acquisition: `buy: ${formatAmount(matPrice)}, daily traded ${formatFixed(traded, 1)}`,
			children: [],
			cost: matPrice * amount,
		};
	}

	if (extractables.has(mat)) {
		extract[mat] = (extract[mat] ?? 0) + amount;
		return {ticker: mat, amount, inStorage, acquisition: `extract`, children: [], cost: 0};
	}
	const recipe = recipes[mat];
	if (!recipe)
		throw new Error(`no recipe for ${mat}`);

	const building = recipe.building_ticker;
	if (!production[building])
		production[building] = {};
	production[building][mat] = (production[building][mat] ?? 0) + amount;

	const liquid = traded > amount * 2 ? 'liquid' : 'not liquid';

	let totalCost = 0;
	const children: AnalysisNode[] = [];
	for (const inputMat of recipe.inputs) {
		const inputAmount = inputMat.material_amount * amount / recipe.outputs[0].material_amount;
		const node = analyzeMat(inputMat.material_ticker, inputAmount, production, extract, buy, prices, recipes, extractables, storage);
		totalCost += node.cost;
		children.push(node);
	}
	return {
		ticker: mat,
		amount,
		inStorage,
		acquisition: `make (${building}, ${liquid})`,
		children,
		cost: totalCost,
	};
}

function renderAnalysis(nodes: AnalysisNode[]): HTMLElement {
	const section = element('section');
	for (const node of nodes)
		section.append(renderAnalysisNode(node));
	return section;
}

function renderAnalysisNode(node: AnalysisNode, level = 0): HTMLElement {
	const amountText = element('span', {textContent: `${formatAmount(node.amount)}x${node.ticker} `});
	const storageText = element('span', {textContent: `(${formatAmount(node.inStorage)})`});
	const percent = Math.min(node.inStorage / node.amount, 1);
	storageText.style.color = `color-mix(in xyz, #0aa ${percent * 100}%, #f80 )`;
	const acquisitionText = element('span', {textContent: ' ' + node.acquisition});

	let el;
	if (node.children.length === 0) {
		el = element('div', {className: 'analysis-node'});
		el.append(amountText, storageText, acquisitionText);
	} else {
		el = element('details', {className: 'analysis-node', open: level > 0 && percent < 1});
		const summary = element('summary');
		summary.append(amountText, storageText, acquisitionText);
		el.append(summary);
		for (const child of node.children)
			el.append(renderAnalysisNode(child, level + 1));
		el.append(document.createTextNode(`total cost: ${formatWhole(node.cost)}`));
	}
	if (level === 0)
		el.classList.add('root');
	return el;
}

const WORKER_CONSUMPTION: Record<'pioneers' | 'settlers' | 'technicians' | 'engineers' | 'scientists', Record<string, number>> = {
	pioneers: {'COF': 0.5, 'DW': 4, 'RAT': 4, 'OVE': 0.5, 'PWO': 0.2},
	settlers: {'DW': 5, 'RAT': 6, 'KOM': 1, 'EXO': 0.5, 'REP': 0.2, 'PT': 0.5},
	technicians: {'DW': 7.5, 'RAT': 7, 'ALE': 1, 'MED': 0.5, 'SC': 0.1, 'HMS': 0.5, 'SCN': 0.1},
	engineers: {'DW': 10, 'MED': 0.5, 'GIN': 1, 'FIM': 7, 'VG': 0.2, 'HSS': 0.2, 'PDA': 0.1},
	scientists: {'DW': 10, 'MED': 0.5, 'WIN': 1, 'MEA': 7, 'NST': 0.1, 'LC': 0.2, 'WS': 0.05},
};

function buildingDailyCost(building: Building, prices: Record<string, RawPrice>): number {
	let cost = 0;
	for (const [workerType, mats] of Object.entries(WORKER_CONSUMPTION)) {
		const workers = building[workerType as keyof typeof WORKER_CONSUMPTION];
		for (const [mat, per100] of Object.entries(mats)) {
			const price = prices[mat].VWAP30D;
			if (price == null) throw new Error(`no price for ${mat}`);
			cost += price * workers * per100 / 100;
		}
	}
	return cost;
}

function renderProduction(expertiseGroups: Record<string, string[]>, production: Production, storage: Record<string, number>,
	prices: Record<string, RawPrice>, recipes: Record<string, Recipe>, buildings: Record<string, Building>): HTMLElement {
	const section = element('section');
	section.append(element('h2', {textContent: 'production'}));

	const matInputs: Record<string, {upstreamMat: string, amount: number}[]> = {};
	// mat → list of {outputMat, expertise, amount} that consume it as an input
	const matConsumers: Record<string, {downstreamMat: string, expertise: string, amount: number}[]> = {};
	for (const [expertise, productionBuildings] of Object.entries(expertiseGroups)) {
		for (const building of productionBuildings) {
			for (const [mat, totalAmount] of Object.entries(production[building])) {
				const recipe = recipes[mat];
				if (!matInputs[mat])
					matInputs[mat] = [];

				const outputPerRun = recipe.outputs.find((o) => o.material_ticker === mat)!.material_amount;
				for (const input of recipe.inputs) {
					const ticker = input.material_ticker;
					const amount = input.material_amount * totalAmount / outputPerRun;
					matInputs[mat].push({upstreamMat: ticker, amount});

					if (!matConsumers[ticker])
						matConsumers[ticker] = [];
					matConsumers[ticker].push({downstreamMat: mat, expertise, amount});
				}
			}
		}
	}

	let totalConsumablesCost = 0;
	for (const [expertise, productionBuildings] of Object.entries(expertiseGroups)) {
		section.append(element('h3', {textContent: expertise.toLocaleLowerCase()}));
		const imports: Record<string, number> = {};
		const exportTo: Record<string, Record<string, number>> = {};
		for (const building of productionBuildings) {
			const buildingRow = element('div', {className: 'building-row'});
			const mats = Object.entries(production[building]);
			let buildingMins = 0;
			for (const [mat, amount] of mats) {
				buildingRow.append(document.createTextNode(' '));
				buildingRow.append(renderProductionBuildingMat(expertise, mat, amount, storage,
						matInputs, matConsumers, imports, exportTo));

				const recipe = recipes[mat];
				const outputPerRun = recipe.outputs.find((o) => o.material_ticker === mat)!.material_amount;
				buildingMins += amount / outputPerRun * (recipe.time_ms / 1000 / 60);
			}
			const numBuildings = buildingMins / (24*60) / daysPerBundle / 1.605; // 160.5% efficiency
			const consumablesCost = buildingDailyCost(buildings[building], prices) * Math.round(Math.max(1, numBuildings));
			totalConsumablesCost += consumablesCost;
			buildingRow.prepend(document.createTextNode(
					`${formatFixed(numBuildings, 1)}x${building} (${formatWhole(consumablesCost)}/d)`));
			section.append(buildingRow);
		}

		const importDetails = element('details');
		importDetails.append(element('summary', {textContent: 'imports'}));
		for (const [mat, amount] of Object.entries(imports)) {
			if (recipes[mat] && buildings[recipes[mat].building_ticker].expertise == expertise)
				continue;
			importDetails.append(element('div', {textContent: `${formatAmount(amount)}x${mat}`}));
		}
		section.append(importDetails);

		const exportDetails = element('details');
		exportDetails.append(element('summary', {textContent: 'exports'}));
		for (const [expertise, mats] of Object.entries(exportTo)) {
			const exportToRow = element('div', {textContent: expertise.toLocaleLowerCase() + ': '});
			exportToRow.textContent += Object.entries(mats).map(([mat, amount]) => `${amount}x${mat}`).join(' ');
			exportDetails.append(exportToRow);
		}
		section.append(exportDetails);
	}

	section.append(element('h4', {textContent: `total consumables cost: ${formatWhole(totalConsumablesCost)}/day,
			${formatWhole(totalConsumablesCost * daysPerBundle)}/bundle`}));
	return section;
}

function renderProductionBuildingMat(expertise: string, mat: string, amount: number, storage: Record<string, number>,
		matInputs: Record<string, {upstreamMat: string, amount: number}[]>,
		matConsumers: Record<string, {downstreamMat: string, expertise: string, amount: number}[]>,
		imports: Record<string, number>, exportTo: Record<string, Record<string, number>>): HTMLElement {
	const inStorage = storage[mat] ?? 0;
	const wrapper = element('span');

	const amountText = element('span', {textContent: `${formatAmount(amount)}x${mat}`});
	wrapper.append(amountText);

	const storageText = element('span', {textContent: ` (${formatAmount(inStorage)})`});
	wrapper.append(storageText);

	const percent = Math.min(inStorage / (amount * 2), 1);
	storageText.style.color = `color-mix(in xyz, #0aa ${percent * 100}%, #f80 )`;
	if (percent >= 1)
		amountText.style.color = '#777';
	else {
		const produceable = Object.values(matInputs[mat]).every((input) => storage[input.upstreamMat] ?? 0 >= input.amount);
		amountText.style.color = produceable ? '#0aa' : '#f80';
	}

	let tooltip = '';
	const inputs = matInputs[mat];
	if (inputs) {
		tooltip += inputs.map((i) => `${formatAmount(i.amount)}x${i.upstreamMat} (${storage[i.upstreamMat] ?? 0}) → ` +
				`${formatAmount(amount)}x${mat}`).join('\n') + '\n';
		for (const input of inputs)
			imports[input.upstreamMat] = (imports[input.upstreamMat] ?? 0) + input.amount;
	}
	const consumers = matConsumers[mat];
	if (consumers) {
		tooltip += consumers
			.map((c) => `${formatAmount(c.amount)}x${mat} → ${c.downstreamMat} (${c.expertise.toLocaleLowerCase()})`)
			.join('\n');
		for (const consumer of consumers) {
			if (consumer.expertise == expertise) // we aren't shipping it anywhere
				continue;
			if (!exportTo[consumer.expertise])
				exportTo[consumer.expertise] = {};
			exportTo[consumer.expertise][mat] = (exportTo[consumer.expertise][mat] ?? 0) + consumer.amount;
		}
	}
	amountText.dataset.tooltip = tooltip;
	return wrapper;
}

async function fetchStorage(): Promise<Record<string, number>> {
	if (!apiKey.value)
		return {};
	const users = await fetch('https://api.punoted.net/v1/storages/', {headers: {'X-Data-Token': apiKey.value}})
			.then((r) => r.json()) as PUNUserStore[];
	const items: Record<string, number> = {};
	for (const user of users)
		for (const storage of user.Storages)
			for (const item of storage.StorageItems)
				items[item.MaterialTicker] = (items[item.MaterialTicker] ?? 0) + item.MaterialAmount;
	return items;
}

function element<K extends keyof HTMLElementTagNameMap>(tagName: K,
		properties: Partial<HTMLElementTagNameMap[K]> = {}): HTMLElementTagNameMap[K] {
	const node = document.createElement(tagName);
	Object.assign(node, properties);
	return node;
}

function formatAmount(n: number): string {
	return n.toLocaleString(undefined, {maximumFractionDigits: 3});
}

function formatFixed(n: number, digits: number): string {
	return n.toLocaleString(undefined, {
		minimumFractionDigits: digits,
		maximumFractionDigits: digits,
	});
}

function formatWhole(n: number): string {
	return n.toLocaleString(undefined, {maximumFractionDigits: 0});
}

interface AnalysisNode {
	ticker: string
	amount: number
	inStorage: number
	acquisition: string
	children: AnalysisNode[]
	cost: number
}

interface Recipe {
	recipe_name: string
	building_ticker: string
	inputs: RecipeMat[]
	outputs: RecipeMat[]
	time_ms: number
}

interface RecipeMat {
	material_ticker: string
	material_amount: number
}

interface Material {
	ticker: string
	category_name: string
}

interface RawPrice {
	MaterialTicker: string
	ExchangeCode: string
	Ask: number | null
	AverageTraded30D: number | null
	VWAP30D: number | null
	Supply: number
}

interface Building {
	building_ticker: string
	expertise: string
	pioneers: number
	settlers: number
	technicians: number
	engineers: number
	scientists: number
}

interface PUNUserStore {
	Storages: Array<{
		StorageItems: Array<{
			MaterialTicker: string
			MaterialAmount: number
		}>
	}>
}

type Production = Record<string, Record<string, number>>;
