from __future__ import annotations

import dataclasses
import json
import typing

import cache

def main() -> None:
	recipes: list[Recipe] = cache.get('https://api.prunplanner.org/data/recipes/')
	buildings: dict[str, Building] = {m['building_ticker']: m for m in cache.get('https://api.prunplanner.org/data/buildings/')}
	materials: dict[str, Material] = {m['ticker']: m for m in cache.get('https://api.prunplanner.org/data/materials/')}
	raw_prices: list[RawPrice] = cache.get('https://refined-prun.github.io/refined-prices/all.json')
	for cx in ['AI1', 'CI1', 'IC1', 'NC1']:
		profits = calc_for_cx(cx, recipes, buildings, materials, raw_prices)
		with open(f'www/roi_{cx.lower()}.json', 'w') as f:
			json.dump([dataclasses.asdict(p) for p in profits], f, indent='\t')

def calc_for_cx(cx: str, recipes: typing.Collection[Recipe], buildings: typing.Mapping[str, Building],
		materials: typing.Mapping[str, Material], raw_prices: typing.Collection[RawPrice]) -> typing.Sequence[Profit]:
	# EXTREME DETAIL: We extract 'Bid' and 'Ask' directly from the raw_prices API payload.
	prices: dict[str, Price] = {
		p['MaterialTicker']: Price(p['VWAP7D'], p['AverageTraded7D'], p['VWAP30D'], p['Bid'], p['Ask']) for p in raw_prices # pyright: ignore[reportArgumentType]
		if p['ExchangeCode'] == cx
	}
	habitation: typing.Mapping[Worker, str] = {
		'pioneers': 'HB1',
		'settlers': 'HB2',
		'technicians': 'HB3',
		'engineers': 'HB4',
		'scientists': 'HB5',
	}
	hab_area_cost: dict[Worker, float] = {}
	hab_capex: dict[Worker, dict[str, float]] = {}
	for worker, hab in habitation.items():
		hab_area_cost[worker] = buildings[hab]['area_cost'] / 100
		base_capex = building_construction_cost(buildings[hab], prices)
		hab_capex[worker] = {k: v / 100 for k, v in base_capex.items()}

	profits: list[Profit] = []
	for recipe in recipes:
		if profit := calc_profit(recipe, buildings, hab_area_cost, hab_capex, materials, prices):
			profits.append(profit)
	profits.sort()
	return profits

def get_metrics(amount: float, price: Price) -> dict[str, float]:
	# EXTREME DETAIL: Helper function to generate a 3-part dictionary (VWAP, Bid, Ask) for any item quantity.
	# If Bid or Ask data is absent from the exchange, we gracefully fall back to the VWAP to prevent math crashes.
	v = price.vwap_7d or price.vwap_30d or 0.0
	b = price.bid if price.bid is not None else v
	a = price.ask if price.ask is not None else v
	return {'vwap': amount * v, 'bid': amount * b, 'ask': amount * a}

def calc_profit(recipe: Recipe, buildings: typing.Mapping[str, Building], hab_area_cost: typing.Mapping[Worker, float],
		hab_capex: typing.Mapping[Worker, dict[str, float]], materials: typing.Mapping[str, Material],
		prices: typing.Mapping[str, Price]) -> Profit | None:
	if len(recipe['outputs']) == 0:
		return

	building = buildings[recipe['building_ticker']]
	area = building['area_cost'] + sum(hab_area_cost[worker] * building[worker] for worker in hab_area_cost)
	runs_per_day = 24 * 60 * 60 * 1000 / recipe['time_ms'] * 1.25 # assume CoGC
	if building['building_ticker'] in ('FRM', 'ORC'):
		runs_per_day *= 1.1212 # promitor's fertility

	outputs: list[MatPrice] = []
	revenue = {'vwap': 0.0, 'bid': 0.0, 'ask': 0.0}
	output_prices: dict[str, PriceNonNull] = {}
	for output in recipe['outputs']:
		price = prices[output['material_ticker']]
		if price.vwap_7d is None or price.average_traded_7d is None:
			return # skip recipes with thinly traded outputs
		output_prices[output['material_ticker']] = typing.cast(PriceNonNull, price)
		
		# EXTREME DETAIL: Calculate total daily revenue outputs per metric type.
		m = get_metrics(output['material_amount'] * runs_per_day, price)
		for k in revenue: revenue[k] += m[k]
		outputs.append(MatPrice(output['material_ticker'], output['material_amount'], price.vwap_7d, price.bid, price.ask))

	input_costs: list[MatPrice] = []
	opex = {'vwap': 0.0, 'bid': 0.0, 'ask': 0.0}
	for input in recipe['inputs']:
		price = prices[input['material_ticker']]
		if price.vwap_7d is None:
			return # skip recipes with thinly traded inputs
		
		m = get_metrics(input['material_amount'] * runs_per_day, price)
		for k in opex: opex[k] += m[k]
		input_costs.append(MatPrice(input['material_ticker'], input['material_amount'], price.vwap_7d, price.bid, price.ask))

	worker_consumable = building_daily_cost(building, prices)
	for k in opex: opex[k] += worker_consumable[k]

	capex = building_construction_cost(building, prices)
	for worker, hab_cost in hab_capex.items():
		workers = building[worker]
		if workers > 0:
			for k in capex: capex[k] += hab_cost[k] * workers

	lowest_liquidity = min(recipe['outputs'],
			key=lambda output: output['material_amount'] / output_prices[output['material_ticker']].average_traded_7d)
	output_per_day = lowest_liquidity['material_amount'] * runs_per_day
	
	average_traded_7d = output_prices[lowest_liquidity['material_ticker']].average_traded_7d
	output_per_area = output_per_day / area
	market_capacity_area = average_traded_7d / output_per_area

	logistics_per_area = max(
		sum(materials[input['material_ticker']]['weight'] * input['material_amount'] for input in recipe['inputs']),
		sum(materials[input['material_ticker']]['volume'] * input['material_amount'] for input in recipe['inputs']),
		sum(materials[output['material_ticker']]['weight'] * output['material_amount'] for output in recipe['outputs']),
		sum(materials[output['material_ticker']]['volume'] * output['material_amount'] for output in recipe['outputs']),
	) * runs_per_day / area
	
	return Profit(outputs, recipe['recipe_name'],
			expertise=building['expertise'],
			building=building['building_ticker'],
			area=area,
			capex=capex,
			opex=opex,
			revenue=revenue,
			input_costs=input_costs,
			runs_per_day=runs_per_day,
			logistics_per_area=logistics_per_area,
			output_per_day=output_per_day,
			average_traded_7d=average_traded_7d,
			market_capacity_area=market_capacity_area)

def building_construction_cost(building: Building, prices: typing.Mapping[str, Price]) -> dict[str, float]:
	cost = {'vwap': 0.0, 'bid': 0.0, 'ask': 0.0}
	for bc in building['costs']:
		m = get_metrics(bc['material_amount'], prices[bc['material_ticker']])
		for k in cost: cost[k] += m[k]
	# https://handbook.apex.prosperousuniverse.com/wiki/building-costs/#rocky-planets
	mcg = get_metrics(building['area_cost'] * 4, prices['MCG'])
	for k in cost: cost[k] += mcg[k]
	return cost

def building_daily_cost(building: Building, prices: typing.Mapping[str, Price]) -> dict[str, float]:
	consumption = {
		'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)],
	}
	cost = {'vwap': 0.0, 'bid': 0.0, 'ask': 0.0}
	for worker, mats in consumption.items():
		workers = building[worker]
		for mat, per_100 in mats:
			m = get_metrics(workers * per_100 / 100, prices[mat])
			for k in cost: cost[k] += m[k]
	return cost

Worker = typing.Literal['pioneers', 'settlers', 'technicians', 'engineers', 'scientists']

class Recipe(typing.TypedDict):
	recipe_name: str
	building_ticker: str
	inputs: list[RecipeMat]
	outputs: list[RecipeMat]
	time_ms: int

class RecipeMat(typing.TypedDict):
	material_ticker: str
	material_amount: int

class Building(typing.TypedDict):
	building_ticker: str
	expertise: str
	area_cost: int
	costs: list[BuildingMat]
	pioneers: int
	settlers: int
	technicians: int
	engineers: int
	scientists: int

class BuildingMat(typing.TypedDict):
	material_ticker: str
	material_amount: int

class Material(typing.TypedDict):
	ticker: str
	weight: float
	volume: float

class RawPrice(typing.TypedDict):
	MaterialTicker: str
	ExchangeCode: str
	VWAP7D: float | None
	AverageTraded7D: float | None
	VWAP30D: float | None
	Bid: float | None  # Added Bid extraction
	Ask: float | None  # Added Ask extraction

@dataclasses.dataclass(eq=False, frozen=True, slots=True)
class Price:
	vwap_7d: float | None
	average_traded_7d: float | None
	vwap_30d: float | None
	bid: float | None
	ask: float | None

@dataclasses.dataclass(eq=False, frozen=True, slots=True)
class PriceNonNull:
	vwap_7d: float
	average_traded_7d: float

@dataclasses.dataclass(eq=False, frozen=True, slots=True)
class Profit:
	outputs: typing.Collection[MatPrice]
	recipe: str
	expertise: str
	building: str
	area: float
	capex: dict[str, float]      # Transformed from float to Dict
	opex: dict[str, float]       # Transformed from float to Dict
	revenue: dict[str, float]    # Transformed from float to Dict
	input_costs: typing.Collection[MatPrice]
	runs_per_day: float
	logistics_per_area: float
	output_per_day: float
	average_traded_7d: float
	market_capacity_area: float 

	def __lt__(self, other: Profit) -> bool:
		# EXTREME DETAIL: We establish a baseline VWAP sort for the raw JSON payload.
		# Even though the frontend now dynamically resorts based on UI dropdown permutations,
		# sorting the initial JSON correctly saves the client from experiencing a 'pop-in' rearrangement
		# on their very first page load.
		p_a = self.revenue['vwap'] - self.opex['vwap']
		be_a = (self.capex['vwap'] + 3 * self.opex['vwap']) / p_a if p_a > 0 else 10000 - p_a
		
		p_b = other.revenue['vwap'] - other.opex['vwap']
		be_b = (other.capex['vwap'] + 3 * other.opex['vwap']) / p_b if p_b > 0 else 10000 - p_b
		return be_a < be_b

@dataclasses.dataclass(eq=False, frozen=True, slots=True)
class MatPrice:
	ticker: str
	amount: int
	vwap_7d: float
	bid: float | None
	ask: float | None

if __name__ == '__main__':
	main()