#ifndef SENSORDEVICESPEC_H
#define SENSORDEVICESPEC_H

#include <array>
#include <cctype>
#include <optional>
#include <string_view>
#include <vector>
#include <string>
#include <iostream>
#include <sstream>
#include <algorithm>
#include <iterator>
#include <stdexcept>
#include <utility>

namespace smo {
namespace device {

/* Carrier used by the DAP spec parser to pass one parenthesized segment
 * (e.g. postrin(interest-pc=85) or pcloudLightAmbience(...)) up the reduction
 * stack; the spec_body reduction classifies segments and populates the
 * DeviceAttachmentSpec. Defined here so both the parser header and consumers
 * of the generated header see the type.
 */
struct DapSegment
{
    std::string name;
    std::vector<std::pair<std::string, std::string>> params;
};

class DeviceAttachmentSpec
{
public:
    friend std::ostream& operator<<(
        std::ostream& os, const DeviceAttachmentSpec& spec)
    {
        os << spec.stringify();
        return os;
    }

    bool operator==(const DeviceAttachmentSpec& other) const
    {
        return deviceIdentifier == other.deviceIdentifier &&
               sensorType == other.sensorType &&
               postrin == other.postrin &&
               negtrin == other.negtrin &&
               qualeIfaceApi == other.qualeIfaceApi &&
               stimBuffApi == other.stimBuffApi &&
               provider == other.provider &&
               deviceSelector == other.deviceSelector;
    }

public:
    std::string deviceIdentifier;
    char sensorType;
    /* postrin/negtrin hold the literal segment name ("postrin" /
     * "negtrin") when present, empty string when the DAP spec omits the
     * corresponding intrin specifier. Params vectors carry the params from
     * within the postrin(...)/negtrin(...) segment.
     */
    std::string postrin;
    std::vector<std::pair<std::string,std::string>> postrinParams;
    std::string negtrin;
    std::vector<std::pair<std::string,std::string>> negtrinParams;
    std::string qualeIfaceApi;
    std::vector<std::pair<std::string,std::string>> qualeIfaceApiParams;
    std::string stimBuffApi;
    std::vector<std::pair<std::string,std::string>> stimBuffApiParams;
    std::string provider;
    std::vector<std::pair<std::string,std::string>> providerParams;
    std::string deviceSelector;

    static void stringifyParams(
        std::ostream& os,
        const std::vector<std::pair<std::string,std::string>>& params)
    {
        for (const auto& param : params)
        {
            os << param.first;
            if (!param.second.empty()) {
                os << "=" << param.second;
            }
            os << " ";
        }
    }

    std::string stringify() const
    {
        std::ostringstream os;
        os << "Device Identifier: " << deviceIdentifier
            << ", Sensor Type: " << sensorType;

        if (!postrin.empty())
        {
            os << ", Postrin Params: (";
            stringifyParams(os, postrinParams);
            os << ")";
        }
        if (!negtrin.empty())
        {
            os << ", Negtrin Params: (";
            stringifyParams(os, negtrinParams);
            os << ")";
        }

        os << ", QualeIface API: " << qualeIfaceApi << ", QualeIface API Params: (";
        for (const auto& param : qualeIfaceApiParams)
        {
            os << param.first;
            if (!param.second.empty()) {
                os << "=" << param.second;
            }
            os << " ";
        }
        os << "), StimBuff API: " << stimBuffApi
            << ", StimBuff API Params: (";
        for (const auto& param : stimBuffApiParams)
        {
            os << param.first;
            if (!param.second.empty()) {
                os << "=" << param.second;
            }
            os << " ";
        }
        os << "), Provider: " << provider << ", Provider Params: (";
        for (const auto& param : providerParams)
        {
            os << param.first;
            if (!param.second.empty()) {
                os << "=" << param.second;
            }
            os << " ";
        }
        os << "), Device Selector: " << deviceSelector;

        return os.str();
    }

    /**
     * @brief Parse a required integer parameter from a parameter list
     * @param params The parameter vector to search in
     * @param paramName The name of the parameter to parse
     * @return The parsed integer value
     * @throws std::runtime_error if parameter is not found or cannot be parsed
     * @note The lattermost supplied matching param wins if multiple are present
     */
    static int parseRequiredParamAsInt(
		const std::vector<std::pair<std::string,std::string>>& params,
		const std::string& paramName
		)
    {
        return parseRequiredParamAsIntWithSynonyms(
            params,
            std::array<std::string_view, 1>{{paramName}},
            "No " + paramName + " specified in params");
    }

    /**
     * @brief Parse an optional integer parameter from a parameter list
     * @param params The parameter vector to search in
     * @param paramName The name of the parameter to parse
     * @param defaultValue The default value to return if no parameter is found
     * @return The parsed integer value, or defaultValue if none found
     * @note The lattermost supplied matching param wins if multiple are present
     */
    static int parseOptionalParamAsInt(
		const std::vector<std::pair<std::string,std::string>>& params,
		const std::string& paramName,
		int defaultValue
		)
    {
        return parseOptionalParamAsIntWithSynonyms(
            params, std::array<std::string_view, 1>{{paramName}}, defaultValue);
    }

    /**
     * @brief Test whether a parameter name appears in a synonym collection
     */
    template <typename SynonymCollectionT>
    static bool namesContain(
		const SynonymCollectionT& synonymNames,
		std::string_view paramName)
    {
        return std::find(
            std::begin(synonymNames),
            std::end(synonymNames),
            paramName) != std::end(synonymNames);
    }

    /**
     * @brief Test whether a parameter list contains an exact parameter name
     */
    static bool paramsContain(
		const std::vector<std::pair<std::string,std::string>>& params,
		std::string_view paramName)
    {
        for (const auto& param : params)
        {
            if (param.first == paramName) {
                return true;
            }
        }

        return false;
    }

    /**
     * @brief Test whether a parameter name matches any synonym collection
     */
    template <typename... SynonymCollectionTs>
    static bool paramNameMatchesAnySynonymGroup(
		std::string_view paramName,
		const SynonymCollectionTs&... synonymGroups)
    {
        return (namesContain(synonymGroups, paramName) || ...);
    }

    /**
     * @brief Trim surrounding whitespace and lowercase a DAP param token
     */
    static std::string normalizeParamToken(std::string token)
    {
        while (!token.empty()
            && std::isspace(static_cast<unsigned char>(token.back())))
        {
            token.pop_back();
        }

        while (!token.empty()
            && std::isspace(static_cast<unsigned char>(token.front())))
        {
            token.erase(token.begin());
        }

        for (char& character : token)
        {
            character = static_cast<char>(
                std::tolower(static_cast<unsigned char>(character)));
        }

        return token;
    }

    /**
     * @brief Find the lattermost param matching any synonym (name + value pair)
     */
    template <typename SynonymCollectionT>
    static std::optional<std::pair<std::string, std::string>>
    findOptionalParamWithSynonyms(
		const std::vector<std::pair<std::string,std::string>>& params,
		const SynonymCollectionT& synonymNames)
    {
        for (auto paramIt = params.rbegin(); paramIt != params.rend(); ++paramIt)
        {
            if (namesContain(synonymNames, paramIt->first)) {
                return *paramIt;
            }
        }

        return std::nullopt;
    }

    /**
     * @brief Find the lattermost param with an exact name (name + value pair)
     */
    static std::optional<std::pair<std::string, std::string>> findOptionalParam(
		const std::vector<std::pair<std::string,std::string>>& params,
		std::string_view paramName)
    {
        return findOptionalParamWithSynonyms(
            params, std::array<std::string_view, 1>{{paramName}});
    }

    /**
     * @brief Parse a DAP integer param value
     * @throws std::runtime_error if parsing fails
     */
    static int parseParamValueAsInt(
		const std::string& paramName,
		const std::string& paramValue)
    {
        try {
            return std::stoi(paramValue);
        }
        catch (const std::invalid_argument&)
        {
            throw std::runtime_error(
                "Failed to parse '" + paramName + "' param value '"
                + paramValue + "' as integer");
        }
        catch (const std::out_of_range&)
        {
            throw std::runtime_error(
                "'" + paramName + "' param value '" + paramValue
                + "' is out of range");
        }
    }

    /**
     * @brief Parse a DAP integer param value; result must be strictly positive
     * @throws std::runtime_error if parsing fails or value is not positive
     */
    static int parseParamValueAsPositiveInt(
		const std::string& paramName,
		const std::string& paramValue)
    {
        const int parsedValue = parseParamValueAsInt(paramName, paramValue);
        if (parsedValue <= 0)
        {
            throw std::runtime_error(
                "'" + paramName + "' must be positive");
        }

        return parsedValue;
    }

    /**
     * @brief Parse a DAP boolean param value
     * @param value Raw param value; empty string means @p emptyMeansTrue
     * @throws std::runtime_error if the value is not recognized
     */
    static bool parseParamValueAsBool(
		const std::string& value,
		bool emptyMeansTrue = true)
    {
        if (value.empty()) {
            return emptyMeansTrue;
        }

        const std::string lowered = normalizeParamToken(value);
        if (lowered == "true" || lowered == "1" || lowered == "yes") {
            return true;
        }

        if (lowered == "false" || lowered == "0" || lowered == "no") {
            return false;
        }

        throw std::runtime_error(
            "Boolean param value '" + value
            + "' is not recognized (use true/false, 1/0, yes/no, or omit value)");
    }

    /**
     * @brief Parse a required integer param using synonyms
     * @note The lattermost supplied matching param wins if multiple are present
     */
    template <typename SynonymCollectionT>
    static int parseRequiredParamAsIntWithSynonyms(
		const std::vector<std::pair<std::string,std::string>>& params,
		const SynonymCollectionT& synonymNames,
		const std::string& missingParamMessage)
    {
        const std::optional<std::pair<std::string, std::string>> matchedParam =
            findOptionalParamWithSynonyms(params, synonymNames);

        if (!matchedParam)
        {
            throw std::runtime_error(missingParamMessage);
        }

        return parseParamValueAsInt(matchedParam->first, matchedParam->second);
    }

    /**
     * @brief Parse an optional integer parameter from a parameter list using synonyms
     * @param params The parameter vector to search in
     * @param synonymNames The collection of synonymous parameter names to try
     * @param defaultValue The default value to return if no parameter is found
     * @return The parsed integer value, or defaultValue if none found
     * @note The lattermost supplied matching param wins if multiple are present
     */
    template <typename SynonymCollectionT>
    static int parseOptionalParamAsIntWithSynonyms(
		const std::vector<std::pair<std::string,std::string>>& params,
		const SynonymCollectionT& synonymNames,
		int defaultValue
		)
    {
        const std::optional<std::pair<std::string, std::string>> matchedParam =
            findOptionalParamWithSynonyms(params, synonymNames);

        if (!matchedParam) {
            return defaultValue;
        }

        return parseParamValueAsInt(matchedParam->first, matchedParam->second);
    }

    /**
     * @brief Parse a required non-empty param value using synonyms
     * @note The lattermost supplied matching param wins if multiple are present
     * @throws std::runtime_error if no matching param is found or value is empty
     */
    template <typename SynonymCollectionT>
    static std::string parseRequiredParamValueWithSynonyms(
		const std::vector<std::pair<std::string,std::string>>& params,
		const SynonymCollectionT& synonymNames,
		const std::string& missingOrEmptyValueMessage)
    {
        const std::optional<std::pair<std::string, std::string>> matchedParam =
            findOptionalParamWithSynonyms(params, synonymNames);

        if (!matchedParam || matchedParam->second.empty())
        {
            throw std::runtime_error(missingOrEmptyValueMessage);
        }

        return matchedParam->second;
    }

    /**
     * @brief Parse an optional non-empty param value using synonyms
     * @return Parsed value, or std::nullopt when the param is absent
     * @throws std::runtime_error if the param is present but its value is empty
     */
    template <typename SynonymCollectionT>
    static std::optional<std::string> parseOptionalParamValueWithSynonyms(
		const std::vector<std::pair<std::string,std::string>>& params,
		const SynonymCollectionT& synonymNames,
		const std::string& emptyValueMessage)
    {
        const std::optional<std::pair<std::string, std::string>> matchedParam =
            findOptionalParamWithSynonyms(params, synonymNames);

        if (!matchedParam) {
            return std::nullopt;
        }

        if (matchedParam->second.empty()) {
            throw std::runtime_error(emptyValueMessage);
        }

        return matchedParam->second;
    }

    /**
     * @brief Parse an optional boolean param using synonyms
     * @note The lattermost supplied matching param wins if multiple are present
     */
    template <typename SynonymCollectionT>
    static bool parseOptionalParamAsBoolWithSynonyms(
		const std::vector<std::pair<std::string,std::string>>& params,
		const SynonymCollectionT& synonymNames,
		bool defaultValue = false,
		bool emptyMeansTrue = true)
    {
        const std::optional<std::pair<std::string, std::string>> matchedParam =
            findOptionalParamWithSynonyms(params, synonymNames);

        if (!matchedParam) {
            return defaultValue;
        }

        return parseParamValueAsBool(matchedParam->second, emptyMeansTrue);
    }

    /**
     * @brief Reject params whose names are not covered by any synonym group
     */
    template <typename... SynonymCollectionTs>
    static void rejectUnknownParams(
		const std::vector<std::pair<std::string,std::string>>& params,
		const std::string& unknownParamMessagePrefix,
		const SynonymCollectionTs&... synonymGroups)
    {
        for (const auto& param : params)
        {
            if (!paramNameMatchesAnySynonymGroup(
                param.first, synonymGroups...))
            {
                throw std::runtime_error(
                    unknownParamMessagePrefix + param.first + "'");
            }
        }
    }
};

class InteroceptorDevAttachmentSpec : public DeviceAttachmentSpec
{
};

class ExtrospectorDevAttachmentSpec : public DeviceAttachmentSpec
{
};

} // namespace device
} // namespace smo

#endif // SENSORDEVICESPEC_H
