# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
import importlib
import inspect
import os
import pkgutil
import typing

from gcm.monitoring.features import feature_definitions

# This is the directory containing the feature classes
feature_dir = "../gcm/features/feature_definitions"
import_dir = "gcm.features.feature_definitions"
outfile_path = "../gcm/features/gen/"

indent = " " * 4

common_code = """#
# This file is autogenerated by the `bin/generate_features.py` script.
# To update:
# - Edit `gcm/features/feature_definitions/health_checks_features.py`
# - Run `python bin/generate_features.py`
# - Run `ufmt format gcm`

import logging
from pathlib import Path
from typing import Any, ClassVar, Dict, Optional

import tomli

logger = logging.getLogger(__name__)


class {class_name}:
    \"\"\"This class reads features from an (optional) `config_path` file
    such as the `health_checks/config_example/feature_example.toml` file.
    To support dynamic updates, we load the config file on every check
    that is performed, so that the values of the config can change during
    runtime and affect the execution environment.
    \"\"\"

    config_path: ClassVar[Optional[Path]]

    def load_config(self) -> Dict[str, Any]:
        if {class_name}.config_path is not None:
            try:
                with {class_name}.config_path.open("rb") as f:
                    features = tomli.load(f)
            except tomli.TOMLDecodeError as e:
                logger.exception(
                    f"Error reading toml file. {{{class_name}.config_path}} does not contain valid TOML. Error: {{e}}"
                )
                raise tomli.TOMLDecodeError(
                    f"{{{class_name}.config_path}} does not contain valid TOML.",
                ) from e
        else:
            raise ValueError(
                "The path for the config is not set. Please provide a config path argument."
            )

        return features

"""


def generate_file() -> None:
    try:
        path = feature_definitions.__path__
    except AttributeError as e:
        raise RuntimeError(f"{feature_definitions.__name__} is not a package") from e

    file_list: typing.List[str] = []
    for _, module_name, _ in pkgutil.iter_modules(path):
        if module_name != "__init__":
            module = importlib.import_module(f"{import_dir}.{module_name}")
            for name, cls in inspect.getmembers(module, inspect.isclass):
                body = ""
                # Generate a get function for each variable of the class
                for attr_name, attr_type in cls.__annotations__.items():
                    if typing.get_origin(attr_type) is typing.Union:
                        raise ValueError(
                            "Specified type of Union is not supported for feature values."
                        )

                    if attr_type is bool:
                        default_value = "False"
                    elif attr_type is int:
                        default_value = "-1"
                    elif attr_type is float:
                        default_value = "-1.0"
                    else:
                        raise ValueError(
                            f"Specified type: {attr_type} is not one of bool, int or float."
                        )

                    get_func_name = f"get_{name.lower()}_{attr_name}"
                    body += (
                        f"{indent}def {get_func_name}(self) -> {attr_type.__name__}:\n"
                    )
                    body += f"{indent*2}try:\n"
                    body += f"{indent*3}features = self.load_config()\n"
                    body += f"{indent*2}except Exception:\n"
                    body += f"{indent*3}return {default_value}\n"
                    body += f"{indent*2}value = features.get('{name}', {{}}).get('{attr_name}', {default_value})\n"
                    body += (
                        f"{indent*2}if not isinstance(value, {attr_type.__name__}):\n"
                    )
                    body += f"{indent*3}raise TypeError(f'Expected {attr_type.__name__} value for {name}.{attr_name}, got {{type(value).__name__}} instead.')\n"
                    body += f"{indent*2}return value\n"
                    body += "\n"

                outfile = os.path.join(
                    os.path.dirname(os.path.abspath(__file__)),
                    outfile_path + f"generated_features_{name.lower()}.py",
                )
                if outfile in file_list:
                    raise ValueError(
                        f"Each features class should have a unique name (case insensitive). Please modify {name}."
                    )
                with open(outfile, "w") as f:
                    class_name = "FeatureValue" + name
                    f.write(common_code.format(class_name=class_name))
                    f.write(body)


if __name__ == "__main__":
    generate_file()
