from typing import *
from aocpy import BaseChallenge, foldl
from dataclasses import dataclass
import re
from enum import Enum


class Material(Enum):
    ORE = 0
    CLAY = 1
    OBSIDIAN = 2
    GEODE = 3


MaterialTracker = Tuple[int, int, int, int]


@dataclass(init=False)
class Blueprint:
    number: int

    robots: Dict[Material, MaterialTracker]

    def __init__(
        self,
        number: int,
        ore_robot_cost: int,
        clay_robot_cost: int,
        obsidian_robot_cost_ore: int,
        obsidian_robot_cost_clay: int,
        geode_robot_cost_ore: int,
        geode_robot_cost_obsidian: int,
    ):
        self.number = number

        self.robots = {
            Material.ORE: (ore_robot_cost, 0, 0, 0),
            Material.CLAY: (clay_robot_cost, 0, 0, 0),
            Material.OBSIDIAN: (
                obsidian_robot_cost_ore,
                obsidian_robot_cost_clay,
                0,
                0,
            ),
            Material.GEODE: (geode_robot_cost_ore, 0, geode_robot_cost_obsidian, 0),
        }

    def iter_robots(self) -> Generator[Tuple[Material, MaterialTracker], None, None]:
        for key in self.robots:
            yield (key, self.robots[key])

    def __hash__(self) -> int:
        return hash(self.number)


parse_re = re.compile(
    r"Blueprint (\d+): Each ore robot costs (\d+) ore\. Each clay robot costs (\d+) ore\. Each obsidian robot costs (\d+) ore and (\d+) clay\. Each geode robot costs (\d+) ore and (\d+) obsidian\."
)


def parse(instr: str) -> List[Blueprint]:
    res: List[Blueprint] = []
    for line in instr.strip().splitlines():
        res.append(
            Blueprint(*map(int, parse_re.match(line).groups())),
        )
    return res


def calc_max_geodes(
    blueprint: Blueprint,
    max_time: int,
    materials: MaterialTracker,
    robots: MaterialTracker,
    robot_quota: MaterialTracker,
    minute: int,
    cannot_build: int,
) -> int:
    if minute == max_time + 1:
        return materials[Material.GEODE.value]

    try_build: int = 0
    for (robot_type, robot_materials) in blueprint.iter_robots():
        rtv = robot_type.value
        if cannot_build & (1 << rtv) != 0:
            continue

        if robot_type != Material.GEODE and robots[rtv] == robot_quota[rtv]:
            continue

        has_enough_materials = True
        for (required, available) in zip(robot_materials, materials):
            if required > available:
                has_enough_materials = False
                break

        if has_enough_materials:
            try_build = try_build | (1 << robot_type.value)

    materials = (
        materials[0] + robots[0],
        materials[1] + robots[1],
        materials[2] + robots[2],
        materials[3] + robots[3],
    )

    max_score = 0
    for i in range(5):
        if i == 4 and cannot_build | try_build != 0b1111:
            # always try not building anything
            sc = calc_max_geodes(
                blueprint,
                max_time,
                materials,
                robots,
                robot_quota,
                minute + 1,
                cannot_build | try_build,
            )
        else:
            if try_build & (1 << i) == 0:
                continue

            robot_type = Material(i)
            robot_materials = blueprint.robots[robot_type]

            # subtract materials required to build this robot
            mc = (
                materials[0] - robot_materials[0],
                materials[1] - robot_materials[1],
                materials[2] - robot_materials[2],
                materials[3],
            )

            # update robot counts
            rc = tuple((robots[j] + (1 if i == j else 0) for j in range(4)))

            # recurse
            sc = calc_max_geodes(
                blueprint, max_time, mc, rc, robot_quota, minute + 1, 0
            )

        if sc is not None and sc > max_score:
            max_score = sc

    return max_score


class Challenge(BaseChallenge):
    @staticmethod
    def one(instr: str) -> int:
        inp = parse(instr)

        quals: List[int] = []
        for bp in inp:
            robot_quota: Tuple[int] = tuple(max(x) for x in zip(*bp.robots.values()))
            quals.append(
                calc_max_geodes(bp, 24, (0, 0, 0, 0), (1, 0, 0, 0), robot_quota, 1, 0)
                * bp.number,
            )

        return sum(quals)

    @staticmethod
    def two(instr: str) -> int:
        inp = parse(instr)

        nums: List[int] = []
        for bp in inp[: min(3, len(inp))]:
            print(f"{bp.number=}", flush=True)
            robot_quota: Tuple[int] = tuple(max(x) for x in zip(*bp.robots.values()))
            nums.append(
                calc_max_geodes(bp, 32, (0, 0, 0, 0), (1, 0, 0, 0), robot_quota, 1, 0)
            )
            print(nums[-1], flush=True)

        return foldl(lambda x, y: x * y, nums, 1)
