from typing import Dict, List, Any, Optional
from dataclasses import dataclass
import os

import yaml
import jinja2


class Unit:
    def __init__(self, name: str) -> None:
        self.name = name

class Units:
    def __init__(self) -> None:
        self.units: List[Unit] = []

    def load(self, lst: List[Any]) -> List[str]:
        assert_list(lst)
        for unit in lst:
            assert_type(unit, "", str)
            self.units.append(Unit(unit))
        return []

    def get(self, name: str) -> Optional[Unit]:
        for unit in self.units:
            if unit.name == name:
                return unit
        raise RuntimeError(f"unit {name} not found")


class Ingredient:
    def load(self, dct: Dict[str, Any]) -> List[str]:
        issues = []
        issues += assert_dict(dct, ["name"], ["wdid", "pricedb"])

        assert_type(dct, "name", str)
        self.name = dct["name"]

        self.wdid = -1
        if "wdid" in dct:
            assert_type(dct, "wdid", int)
            self.wdid = dct["wdid"]

        self.pricedb = None
        if "pricedb" in dct:
            assert_list(dct["pricedb"])
            self.pricedb = PriceDBs()
            issues += self.pricedb.load(dct["pricedb"])

        return issues


class Ingredients:
    def __init__(self) -> None:
        self.ingredients: List[Ingredient] = []

    def load(self, lst: List[Any]) -> List[str]:
        issues = []
        assert_list(lst)
        for ingdct in lst:
            ing = Ingredient()
            issues += ing.load(ingdct)
            self.ingredients.append(ing)
        return issues

    def get(self, name: str) -> Optional[Ingredient]:
        for ing in self.ingredients:
            if ing.name == name:
                return ing
        raise RuntimeError(f"ingredient {name} not found")


class PriceDBs:
    def __init__(self) -> None:
        self.pricedbs: List[PriceDB] = []

    def load(self, lst: List[Any]) -> List[str]:
        issues = []
        assert_list(lst)
        for elem in lst:
            pricedb = PriceDB()
            issues += pricedb.load(elem)
            self.pricedbs.append(pricedb)
        return issues


class PriceDB:
    def load(self, dct: Dict[str, Any]) -> List[str]:
        issues = []
        issues += assert_dict(dct, ["price"], ["amount", "unit"])

        if isinstance(dct["price"], float):
            self.price = dct["price"]
        elif isinstance(dct["price"], int):
            self.price = float(dct["price"])
        else:
            raise RuntimeError(f"{dct['price']} has to be int or float")

        self.amount = 1.0
        if "amount" in dct:
            if isinstance(dct["amount"], float):
                self.amount = dct["amount"]
            elif isinstance(dct["amount"], int):
                self.amount = float(dct["amount"])
            else:
                raise RuntimeError(f"{dct['amount']} has to be int or float")

        self.unit = None
        if "unit" in dct:
            assert_type(dct, "unit", str)
            try:
                self.unit = units.get(dct["unit"])
            except RuntimeError as e:
                issues.append(str(e))
        return issues


class IngredientInstance:
    def load(self, dct: Dict[str, Any]) -> List[str]:
        issues = []
        issues += assert_dict(dct, ["name"], ["amount", "unit", "note"])

        assert_type(dct, "name", str)
        self.name = dct["name"]

        try:
            self.ingredient = ingredients.get(self.name)
        except RuntimeError as e:
            issues.append(str(e))

        self.amount = 1.0
        if "amount" in dct:
            if isinstance(dct["amount"], float):
                self.amount = dct["amount"]
            elif isinstance(dct["amount"], int):
                self.amount = float(dct["amount"])
            else:
                raise RuntimeError(f"{dct['amount']} has to be int or float")

        self.unit = None
        if "unit" in dct:
            assert_type(dct, "unit", str)
            try:
                self.unit = units.get(dct["unit"])
            except RuntimeError as e:
                issues.append(str(e))

        self.note = ""
        if "note" in dct:
            assert_type(dct, "note", str)
            self.note = dct["note"]

        return issues


class RecipePart:
    def load(self, dct: Dict[str, Any]) -> List[str]:
        issues = []
        issues += assert_dict(dct, ["title", "ingredients", "steps"], [])

        assert_type(dct, "title", str)
        self.title = dct["title"]

        assert_type(dct, "ingredients", list)
        self.ingredients: List[IngredientInstance] = []
        for ing in dct["ingredients"]:
            ingredient = IngredientInstance()
            issues += ingredient.load(ing)
            self.ingredients.append(ingredient)

        assert_type(dct, "steps", list)
        for elem in dct["steps"]:
            assert_type(elem, "", str)
        self.steps: List[str] = dct["steps"]

        return issues


class Recipe:
    def __init__(self) -> None:
        self.parts: List[RecipePart] = []
        self.srcpath = ""
        self.outpath = ""
        self.title = ""

    def load(self, dct: Dict[str, Any]) -> List[str]:
        rp = RecipePart()
        issues = rp.load(dct)
        self.parts = [rp]
        self.title = rp.title
        return issues


def load_file(file: str) -> Any:
    print(f"loading {file}")
    with open(file, encoding="utf-8") as f:
        ymltxt = f.read()
    return yaml.safe_load(ymltxt)


def assert_dict(
    dct: Dict[str, Any], required_keys: List[str], optional_keys: List[str]
) -> List[str]:
    issues = []
    if not isinstance(dct, dict):
        raise RuntimeError(f"{dct} has to be a dict")
    for reqkey in required_keys:
        if reqkey not in dct:
            issues.append(f"{reqkey} is required")
    extraelems = [x for x in dct.keys() if x not in required_keys + optional_keys]
    if len(extraelems) != 0:
        issues.append(f"{extraelems} not allowed")
    return issues


def assert_type(dct: Dict[str, Any], key: str, type: type) -> None:
    if key == "":
        if not isinstance(dct, type):
            raise RuntimeError(f"{key} has to be a {type}")
    elif key in dct and not isinstance(dct[key], type):
        raise RuntimeError(f"{key} has to be a {type}")


def assert_list(lst: List[Any]) -> None:
    if not isinstance(lst, list):
        raise RuntimeError(f"{lst} has to be a {list}")


def rendertemplate(
    template: jinja2.Template, format: str, file: str, args: Any
) -> None:
    print(f"rendering {file}")
    outstr = template.render(args)

    try:
        os.mkdir("out")
    except FileExistsError:
        pass

    try:
        os.mkdir(f"out/{format}")
    except FileExistsError:
        pass

    with open(f"out/{format}/{file}", "w", encoding="utf-8") as f:
        f.write(outstr)


units = Units()
ingredients = Ingredients()


def main() -> None:
    issues: List[str] = []

    unitsdct = load_file("recipes/units.yaml")
    issues += units.load(unitsdct)

    ingredientsdct = load_file("recipes/ingredients.yaml")
    issues += ingredients.load(ingredientsdct)

    if len(issues) != 0:
        for issue in issues:
            print("ERROR", issue)
        return

    files = []
    for _, _, filesx in os.walk("recipes/recipes"):
        files = filesx
        files.sort()

    recipes: List[Recipe] = []
    for file in files:
        if not file.endswith(".yaml"):
            print(f"unknown extension of {file}")
            continue
        recipe = Recipe()
        recipedct = load_file("recipes/recipes/" + file)
        issues += recipe.load(recipedct)
        recipe.srcpath = file
        recipe.outpath = file[:-5] + ".html"
        recipes.append(recipe)

    if len(issues) != 0:
        for issue in issues:
            print("ERROR", issue)
        return

    env = jinja2.Environment(
        loader=jinja2.FileSystemLoader("templates"),
        autoescape=jinja2.select_autoescape(),
    )

    indextemplate = env.get_template("index.html")
    rendertemplate(indextemplate, "html", "index.html", {"recipes": recipes})

    recipetemplate = env.get_template("recipe.html")
    for recipe in recipes:
        rendertemplate(recipetemplate, "html", recipe.outpath, {"recipe": recipe})


if __name__ == "__main__":
    main()