import re
import math
from collections import defaultdict
from fractions import Fraction

reaction_re = re.compile(
    r"((?:(?:\d+) (?:\w+))(?:, (?:\d+) (?:\w+))*) => (\d+) (\w+)")
_ex_line1 = "1 A, 2 B, 3 C => 2 D"
_ex_line2 = "1 E => 2 F"
assert reaction_re.findall(_ex_line1) == [('1 A, 2 B, 3 C', '2', 'D')]
assert reaction_re.findall(_ex_line2) == [('1 E', '2', 'F')]


class substrate:
    def __init__(self, scale, name):
        self.scale = int(scale)
        self.name = name

    def __repr__(self):
        return repr((self.scale, self.name))

    def __str__(self):
        return str((self.scale, self.name))


def parse_reaction_match(match):
    substrates = [substrate(*p.split(" ")) for p in match[0].split(", ")]
    return substrates, substrate(match[-2], match[-1])


def parse_data(data: str):
    matches = reaction_re.findall(data)
    rules = [parse_reaction_match(match) for match in matches]
    return rules


def construct_graph(rules):
    d = defaultdict(list)
    for rule in rules:
        d[rule[1].name] = (rule[1].scale, rule[0])
    return d


def rec_calc_ore_lossless(products, target, goal):
    if target == goal:
        return (Fraction(1), 1)
    # 1 BC <= 5 B, 7 C
    own_received, subs = products[target]
    # [(5 (3 8)), (7 (5 7)))]
    ore_needed_data = [(substr.scale, rec_calc_ore_lossless(
        products, substr.name, goal)) for substr in subs]

    # req: 5, rec per 1: 8/3
    ore_needed = Fraction(0)
    for req_substr, (rec_substr, substr_ore_needed) in ore_needed_data:
        per_1 = substr_ore_needed / rec_substr
        ore_needed += req_substr * per_1

    # in order to get 1 BC we need {ore_needed}
    return (Fraction(own_received), ore_needed)


ORE = "ORE"
FUEL = "FUEL"

def rec_calc_ore(products, demand, target):
    if target == ORE:
        return

    q_produced, subs = products[target]

    if demand[target] <= 0:
        return

    # as I can make only in multiples of {q_produced}
    reactions = int(math.ceil(demand[target] / q_produced))
    # I shall satisfy demand for nearest bigger number
    demand[target] -= reactions * q_produced

    # but I have some demands of my own
    for sub in subs:
        demand[sub.name] += sub.scale * reactions
        rec_calc_ore(
            products,
            demand,
            sub.name
        )

    return


def calculate_ore_needed(data: str):
    rules = parse_data(data)
    products = construct_graph(rules)
    demand = defaultdict(int)
    demand[FUEL] = 1
    rec_calc_ore(products, demand, FUEL)

    return demand[ORE]


def part1():
    return calculate_ore_needed(actual_data)


def calculate_yolo(data: str, yolo: int):
    rules = parse_data(data)
    products = construct_graph(rules)
    demand = defaultdict(int)
    demand[FUEL] = 1572300

    _step_by = 1
    fuel_total = demand[FUEL]
    while demand[ORE] < yolo:
        demand[FUEL] += _step_by
        fuel_total += _step_by
        rec_calc_ore(products, demand, FUEL)
        print(f"Can produce at least {fuel_total} fuel with 10^{math.log10(demand[ORE])} ore. ")

    # subtract one
    return demand



def part2():
    return calculate_yolo(actual_data, 1000000000000)

with open("input14") as fr:
    actual_data = fr.read().strip()

test_cases = [('''9 ORE => 2 A
8 ORE => 3 B
7 ORE => 5 C
3 A, 4 B => 1 AB
5 B, 7 C => 1 BC
4 C, 1 A => 1 CA
2 AB, 3 BC, 4 CA => 1 FUEL''',
               165),
              ('''157 ORE => 5 NZVS
165 ORE => 6 DCFZ
44 XJWVT, 5 KHKGT, 1 QDVJ, 29 NZVS, 9 GPVTF, 48 HKGWZ => 1 FUEL
12 HKGWZ, 1 GPVTF, 8 PSHF => 9 QDVJ
179 ORE => 7 PSHF
177 ORE => 5 HKGWZ
7 DCFZ, 7 PSHF => 2 XJWVT
165 ORE => 2 GPVTF
3 DCFZ, 7 NZVS, 5 HKGWZ, 10 PSHF => 8 KHKGT''',
               13312),
              ('''2 VPVL, 7 FWMGM, 2 CXFTF, 11 MNCFX => 1 STKFG
17 NVRVD, 3 JNWZP => 8 VPVL
53 STKFG, 6 MNCFX, 46 VJHF, 81 HVMC, 68 CXFTF, 25 GNMV => 1 FUEL
22 VJHF, 37 MNCFX => 5 FWMGM
139 ORE => 4 NVRVD
144 ORE => 7 JNWZP
5 MNCFX, 7 RFSQX, 2 FWMGM, 2 VPVL, 19 CXFTF => 3 HVMC
5 VJHF, 7 MNCFX, 9 VPVL, 37 CXFTF => 6 GNMV
145 ORE => 6 MNCFX
1 NVRVD => 8 CXFTF
1 VJHF, 6 MNCFX => 4 RFSQX
176 ORE => 6 VJHF''',
               180697),
              ('''171 ORE => 8 CNZTR
7 ZLQW, 3 BMBT, 9 XCVML, 26 XMNCP, 1 WPTQ, 2 MZWV, 1 RJRHP => 4 PLWSL
114 ORE => 4 BHXH
14 VRPVC => 6 BMBT
6 BHXH, 18 KTJDG, 12 WPTQ, 7 PLWSL, 31 FHTLT, 37 ZDVW => 1 FUEL
6 WPTQ, 2 BMBT, 8 ZLQW, 18 KTJDG, 1 XMNCP, 6 MZWV, 1 RJRHP => 6 FHTLT
15 XDBXC, 2 LTCX, 1 VRPVC => 6 ZLQW
13 WPTQ, 10 LTCX, 3 RJRHP, 14 XMNCP, 2 MZWV, 1 ZLQW => 1 ZDVW
5 BMBT => 4 WPTQ
189 ORE => 9 KTJDG
1 MZWV, 17 XDBXC, 3 XCVML => 2 XMNCP
12 VRPVC, 27 CNZTR => 2 XDBXC
15 KTJDG, 12 BHXH => 5 XCVML
3 BHXH, 2 VRPVC => 7 MZWV
121 ORE => 7 VRPVC
7 XCVML => 6 RJRHP
5 BHXH, 4 VRPVC => 5 LTCX''',
               2210736)]

for t_d, t_e in test_cases:
    res = calculate_ore_needed(t_d)
    assert res == t_e, f"{res} != {t_e}"
