#include "TomlHelpers.h"

#include <sstream>
#include <utility>

namespace utility
{

// --- Error helpers --------------------------------------------------------

std::runtime_error makeError(const std::string& file,
                             const std::string& path,
                             const std::string& why)
{
    return std::runtime_error("Config: " + file + ": '" + path + "' " + why);
}

// --- Typed accessors (throw on missing or wrong type) ---------------------

int64_t requireInt(const toml::node_view<toml::node>& node,
                   const std::string& file,
                   const std::string& path)
{
    const std::optional<int64_t> value = node.value<int64_t>();
    if (!value)
    {
        throw makeError(file, path, "missing or not an integer");
    }
    return *value;
}

double requireDouble(const toml::node_view<toml::node>& node,
                     const std::string& file,
                     const std::string& path)
{
    if (const std::optional<double> v = node.value<double>())
    {
        return *v;
    }
    if (const std::optional<int64_t> v = node.value<int64_t>())
    {
        return static_cast<double>(*v);
    }
    throw makeError(file, path, "missing or not a number");
}

std::string requireString(const toml::node_view<toml::node>& node,
                          const std::string& file,
                          const std::string& path)
{
    const std::optional<std::string> value = node.value<std::string>();
    if (!value)
    {
        throw makeError(file, path, "missing or not a string");
    }
    return *value;
}

bool requireBool(const toml::node_view<toml::node>& node,
                 const std::string& file,
                 const std::string& path)
{
    const std::optional<bool> value = node.value<bool>();
    if (!value)
    {
        throw makeError(file, path, "missing or not a boolean");
    }
    return *value;
}

const toml::array& requireArray(const toml::node_view<toml::node>& node,
                                const std::string& file,
                                const std::string& path)
{
    const toml::array* arr = node.as_array();
    if (arr == nullptr)
    {
        throw makeError(file, path, "missing or not an array");
    }
    return *arr;
}

const toml::table& requireTable(const toml::node_view<toml::node>& node,
                                const std::string& file,
                                const std::string& path)
{
    const toml::table* tbl = node.as_table();
    if (tbl == nullptr)
    {
        throw makeError(file, path, "missing or not a table");
    }
    return *tbl;
}

Formula requireFormula(const toml::node_view<toml::node>& node,
                       const std::string& file,
                       const std::string& path)
{
    const std::string source = requireString(node, file, path);
    try
    {
        return Formula::compile(source);
    }
    catch (const std::exception& e)
    {
        throw makeError(file, path, std::string("formula error: ") + e.what());
    }
}

std::vector<std::string> requireStringArray(const toml::node_view<toml::node>& node,
                                            const std::string& file,
                                            const std::string& path)
{
    const toml::array& arr = requireArray(node, file, path);
    std::vector<std::string> result;
    result.reserve(arr.size());
    for (std::size_t i = 0; i < arr.size(); ++i)
    {
        const std::string elemPath = path + "[" + std::to_string(i) + "]";
        const std::optional<std::string> s = arr[i].value<std::string>();
        if (!s)
        {
            throw makeError(file, elemPath, "not a string");
        }
        result.push_back(*s);
    }
    return result;
}

std::vector<RecipeIngredient> parseIngredients(const toml::array& arr,
                                               const std::string& file,
                                               const std::string& path)
{
    std::vector<RecipeIngredient> result;
    result.reserve(arr.size());
    for (std::size_t i = 0; i < arr.size(); ++i)
    {
        const std::string elemPath = path + "[" + std::to_string(i) + "]";
        const toml::table* t = arr[i].as_table();
        if (t == nullptr)
        {
            throw makeError(file, elemPath, "not a table");
        }

        // We need a mutable node_view to reuse our helpers, which is fine
        // because the helpers never mutate.
        toml::table& mt = const_cast<toml::table&>(*t);

        RecipeIngredient ing;
        ing.item = requireString(mt["item"], file, elemPath + ".item");
        ing.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
        result.push_back(std::move(ing));
    }
    return result;
}

toml::table parseFile(const std::string& path, const std::string& file)
{
    try
    {
        return toml::parse_file(path);
    }
    catch (const toml::parse_error& e)
    {
        std::ostringstream oss;
        oss << "Config: " << file << ": TOML parse error: " << e.description()
            << " at " << e.source().begin;
        throw std::runtime_error(oss.str());
    }
}

}  // namespace utility
