#!/usr/bin/env python3
"""Verify the recipe tree is closed and consistent.

Reads recipes.toml, ships.toml, modules.toml, and visuals.toml, then checks:

  1. Producers — every item consumed anywhere (recipe inputs, ship hull
     materials, module materials) is produced by some recipe. 'scrap' is
     exempt: it drops from destroyed ships.
  2. Visuals — every item that exists in the economy has an [items.*]
     entry in visuals.toml, and visuals.toml has no entries for items
     that no longer exist.
  3. Orphans — items that are produced but never consumed (warning only;
     'building_block' is exempt: the HQ consumes it).

It also prints which items are obtainable ONLY through reprocessing —
the combat-gated materials — so changes to that gate are visible.

Usage (from the repository root or anywhere else):

    python dota_factory/tools/verify_recipes.py
    python dota_factory/tools/verify_recipes.py --config-dir path/to/config

By default the config directory is resolved relative to this script
(../bin/app/data/config). Requires the 'toml' package on Python < 3.11
(pip install --user toml); on 3.11+ the standard tomllib is used.

Exits 1 if a producer or visuals check fails, 0 otherwise (warnings do
not affect the exit code).
"""

import argparse
import os
import sys


WORLD_SOURCED_ITEMS = {"scrap"}        # dropped by destroyed ships
IMPLICITLY_CONSUMED_ITEMS = {"building_block"}  # consumed by the HQ


def load_toml(path):
    try:
        import tomllib
        with open(path, "rb") as fh:
            return tomllib.load(fh)
    except ImportError:
        import toml
        return toml.load(path)


def recipe_outputs(recipe):
    """Every item the recipe can produce, across all of its output groups.

    `outputs = [...]` is the single-group shorthand; `[[recipe.output_group]]` is the
    several-group form (REQ-MAT-OUTPUT-GROUP).
    """
    if "output_group" in recipe:
        return [out for group in recipe["output_group"] for out in group["items"]]
    return recipe.get("outputs", [])


def main():
    default_dir = os.path.normpath(os.path.join(
        os.path.dirname(os.path.abspath(__file__)),
        "..", "bin", "app", "data", "config"))
    parser = argparse.ArgumentParser(
        description="Check recipe tree consistency across the config files.")
    parser.add_argument("--config-dir", default=default_dir,
                        help="directory containing the config toml files"
                             " (default: %(default)s)")
    args = parser.parse_args()

    recipes = load_toml(os.path.join(args.config_dir, "recipes.toml"))["recipe"]
    ships = load_toml(os.path.join(args.config_dir, "ships.toml"))["ship"]
    modules = load_toml(os.path.join(args.config_dir, "modules.toml"))["module"]
    visuals = load_toml(os.path.join(args.config_dir, "visuals.toml"))

    produced = {}   # item id -> [producer descriptions]
    consumed = {}   # item id -> [consumer descriptions]

    for recipe in recipes:
        for output in recipe_outputs(recipe):
            produced.setdefault(output["item"], []).append(
                "recipe '{}'".format(recipe["id"]))
        for inp in recipe.get("inputs", []):
            consumed.setdefault(inp["item"], []).append(
                "recipe '{}'".format(recipe["id"]))

    for ship in ships:
        for material in ship["schematic"]["materials"]:
            consumed.setdefault(material["item"], []).append(
                "ship '{}'".format(ship["id"]))

    for module in modules:
        for material in module["materials"]:
            consumed.setdefault(material["item"], []).append(
                "module '{}'".format(module["id"]))

    all_items = set(produced) | set(consumed) | WORLD_SOURCED_ITEMS
    visual_items = set(visuals.get("items", {}))

    errors = []
    warnings = []

    for item in sorted(consumed):
        if item not in produced and item not in WORLD_SOURCED_ITEMS:
            errors.append("no producer for '{}' (consumed by {})".format(
                item, ", ".join(sorted(set(consumed[item])))))

    for item in sorted(all_items - visual_items):
        errors.append("no [items.{}] entry in visuals.toml".format(item))
    for item in sorted(visual_items - all_items):
        warnings.append("visuals.toml entry [items.{}] matches no known item"
                        .format(item))

    for item in sorted(produced):
        if item not in consumed and item not in IMPLICITLY_CONSUMED_ITEMS:
            warnings.append("'{}' is produced but never consumed (by {})"
                            .format(item, ", ".join(sorted(set(produced[item])))))

    reprocessing_only = sorted(
        item for item, producers in produced.items()
        if all("reprocessing" in p for p in producers))

    print("{} items, {} recipes, {} ships, {} modules".format(
        len(all_items), len(recipes), len(ships), len(modules)))
    print("obtainable only via reprocessing: {}".format(
        ", ".join(reprocessing_only) if reprocessing_only else "(none)"))
    print()

    for warning in warnings:
        print("WARNING: {}".format(warning))
    for error in errors:
        print("ERROR: {}".format(error))
    if not errors and not warnings:
        print("all checks passed")
    elif not errors:
        print("no errors")

    return 1 if errors else 0


if __name__ == "__main__":
    sys.exit(main())
