#pragma once

#include <Windows.h>
#include <winhttp.h>
#include <algorithm>
#include <atomic>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>

#include <nlohmann/json.hpp>

#include "eft_api.h"
#include "../util/Config.h"
#include "../util/logger.h"
#include "../util/module_paths.h"

#pragma comment(lib, "winhttp.lib")

namespace item_pricing
{
    using json = nlohmann::json;

    inline std::mutex& prices_mutex()
    {
        static std::mutex m;
        return m;
    }

    inline std::atomic<bool>& prices_loaded()
    {
        static std::atomic<bool> loaded{ false };
        return loaded;
    }

    inline std::atomic<bool>& prices_loading()
    {
        static std::atomic<bool> loading{ false };
        return loading;
    }

    inline std::atomic<bool>& prices_updating()
    {
        static std::atomic<bool> updating{ false };
        return updating;
    }

    inline std::atomic<uint64_t>& prices_version()
    {
        static std::atomic<uint64_t> version{ 0 };
        return version;
    }

    inline std::mutex& status_mutex()
    {
        static std::mutex m;
        return m;
    }

    inline std::string& status_message()
    {
        static std::string message;
        return message;
    }

    inline std::mutex& path_mutex()
    {
        static std::mutex m;
        return m;
    }

    inline std::string& active_file_path()
    {
        static std::string path;
        return path;
    }

    inline std::unordered_map<std::string, int>& market_prices()
    {
        static std::unordered_map<std::string, int> prices;
        return prices;
    }

    inline std::string trim_config_value(std::string value)
    {
        if (value.empty()) return value;
        value.erase(0, value.find_first_not_of(" \t\r\n\""));
        value.erase(value.find_last_not_of(" \t\r\n\"") + 1);
        return value;
    }

    inline std::string get_module_directory(const char* module_name)
    {
        const std::string from_init = module_paths::dll_directory_utf8();
        if (!from_init.empty())
            return from_init;

        HMODULE module = GetModuleHandleA(module_name);
        if (!module) return {};

        wchar_t path[MAX_PATH]{};
        if (GetModuleFileNameW(module, path, MAX_PATH) == 0) return {};

        const std::filesystem::path module_path(path);
        if (!module_path.has_parent_path() || module_paths::is_system_directory(module_path.parent_path()))
            return {};

        return module_paths::wide_to_utf8(module_path.parent_path().wstring());
    }

    inline std::string get_process_directory()
    {
        wchar_t path[MAX_PATH]{};
        if (GetModuleFileNameW(nullptr, path, MAX_PATH) == 0) return {};

        std::wstring wpath(path);
        const size_t slash = wpath.find_last_of(L"\\/");
        if (slash == std::wstring::npos) return {};

        return module_paths::wide_to_utf8(wpath.substr(0, slash));
    }

    inline std::string join_path(const std::string& dir, const std::string& file)
    {
        if (dir.empty()) return file;
        if (dir.back() == '\\' || dir.back() == '/')
            return dir + file;
        return dir + "\\" + file;
    }

    inline std::string to_absolute_path(const std::string& path)
    {
        if (path.empty()) return path;

        char full_path[MAX_PATH]{};
        const DWORD length = GetFullPathNameA(path.c_str(), MAX_PATH, full_path, nullptr);
        if (length == 0 || length >= MAX_PATH)
            return path;

        return full_path;
    }

    inline bool file_exists(const std::string& path)
    {
        if (path.empty()) return false;

        const DWORD attributes = GetFileAttributesA(path.c_str());
        return attributes != INVALID_FILE_ATTRIBUTES
            && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
    }

    inline std::string get_appdata_config_directory()
    {
        char* appdata = nullptr;
        size_t length = 0;
        if (_dupenv_s(&appdata, &length, "APPDATA") != 0 || !appdata)
            return {};

        const std::string directory = std::string(appdata) + "\\TarkovAddons";
        free(appdata);

        CreateDirectoryW(
            std::wstring(directory.begin(), directory.end()).c_str(),
            nullptr);

        return directory;
    }

    inline std::string get_default_save_path()
    {
        const std::string directory = get_appdata_config_directory();
        if (directory.empty())
            return to_absolute_path("tarkov_prices.json");

        return to_absolute_path(join_path(directory, "tarkov_prices.json"));
    }

    inline void set_active_file_path(const std::string& path)
    {
        std::lock_guard<std::mutex> lock(path_mutex());
        active_file_path() = to_absolute_path(path);
    }

    inline std::string get_active_file_path()
    {
        std::lock_guard<std::mutex> lock(path_mutex());
        return active_file_path();
    }

    inline std::string get_save_path()
    {
        const std::string active = get_active_file_path();
        if (!active.empty())
            return active;

        return get_default_save_path();
    }

    inline std::string resolve_configured_path(const std::string& configured)
    {
        if (configured.empty()) return configured;
        if (configured.find(':') != std::string::npos || configured[0] == '\\')
            return configured;

        const std::string dll_dir = get_module_directory("TarkovAddons.dll");
        if (!dll_dir.empty())
            return join_path(dll_dir, configured);

        return configured;
    }

    inline void set_status_message(const std::string& message)
    {
        std::lock_guard<std::mutex> lock(status_mutex());
        status_message() = message;
    }

    inline std::string get_status_message()
    {
        std::lock_guard<std::mutex> lock(status_mutex());
        return status_message();
    }

    inline std::string get_active_file_mtime_string()
    {
        const std::string path = get_active_file_path();
        if (path.empty())
            return {};

        WIN32_FILE_ATTRIBUTE_DATA data{};
        if (!GetFileAttributesExA(path.c_str(), GetFileExInfoStandard, &data))
            return {};

        FILETIME local_ft{};
        if (!FileTimeToLocalFileTime(&data.ftLastWriteTime, &local_ft))
            return {};

        SYSTEMTIME st{};
        if (!FileTimeToSystemTime(&local_ft, &st))
            return {};

        char buf[64];
        snprintf(buf, sizeof(buf), "%04u-%02u-%02u %02u:%02u:%02u",
            st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
        return buf;
    }

    inline int extract_item_price(const json& item)
    {
        int price = 0;

        const auto low_price_it = item.find("lastLowPrice");
        if (low_price_it != item.end() && low_price_it->is_number())
            price = static_cast<int>(low_price_it->get<double>());

        if (price == 0)
        {
            const auto avg_price_it = item.find("avg24hPrice");
            if (avg_price_it != item.end() && avg_price_it->is_number())
                price = static_cast<int>(avg_price_it->get<double>());
        }

        if (price == 0)
        {
            const auto sell_for_it = item.find("sellFor");
            if (sell_for_it != item.end() && sell_for_it->is_array())
            {
                for (const auto& sell_option : *sell_for_it)
                {
                    const auto sell_price_it = sell_option.find("price");
                    if (sell_price_it != sell_option.end() && sell_price_it->is_number())
                    {
                        const int p = static_cast<int>(sell_price_it->get<double>());
                        if (p > price) price = p;
                    }
                }
            }
        }

        return price;
    }

    inline bool parse_price_root(const json& root)
    {
        try
        {
            const auto data_it = root.find("data");
            if (data_it == root.end()) return false;

            const auto items_it = data_it->find("items");
            if (items_it == data_it->end() || !items_it->is_array()) return false;

            std::unordered_map<std::string, int> parsed;
            parsed.reserve(items_it->size());

            for (const auto& item : *items_it)
            {
                const auto id_it = item.find("id");
                if (id_it == item.end() || !id_it->is_string()) continue;

                const int price = extract_item_price(item);
                if (price > 0)
                    parsed[id_it->get<std::string>()] = price;
            }

            if (parsed.empty()) return false;

            std::lock_guard<std::mutex> lock(prices_mutex());
            market_prices() = std::move(parsed);
            prices_version().fetch_add(1);
            return true;
        }
        catch (...)
        {
            return false;
        }
    }

    inline bool parse_price_file(std::ifstream& file)
    {
        try
        {
            json root;
            file >> root;
            return parse_price_root(root);
        }
        catch (...)
        {
            return false;
        }
    }

    inline bool parse_price_string(const std::string& body)
    {
        try
        {
            return parse_price_root(json::parse(body));
        }
        catch (...)
        {
            return false;
        }
    }

    inline bool ensure_parent_directory(const std::string& file_path)
    {
        const size_t slash = file_path.find_last_of("\\/");
        if (slash == std::string::npos)
            return true;

        const std::string directory = file_path.substr(0, slash);
        if (directory.empty())
            return true;

        CreateDirectoryW(
            std::wstring(directory.begin(), directory.end()).c_str(),
            nullptr);
        return true;
    }

    inline bool write_text_file(const std::string& path, const std::string& body)
    {
        ensure_parent_directory(path);

        std::ofstream out(path, std::ios::binary | std::ios::trunc);
        if (!out.is_open()) return false;
        out.write(body.data(), static_cast<std::streamsize>(body.size()));
        return out.good();
    }

    inline bool download_market_prices(std::string& out_body)
    {
        static constexpr const char* k_payload =
            R"({"query": "{ items { id name shortName avg24hPrice lastLowPrice sellFor { price source } } }"})";

        HINTERNET session = WinHttpOpen(
            L"TarkovAddons/1.0",
            WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
            WINHTTP_NO_PROXY_NAME,
            WINHTTP_NO_PROXY_BYPASS,
            0);
        if (!session) return false;

        HINTERNET connection = WinHttpConnect(
            session,
            L"api.tarkov.dev",
            INTERNET_DEFAULT_HTTPS_PORT,
            0);
        if (!connection)
        {
            WinHttpCloseHandle(session);
            return false;
        }

        HINTERNET request = WinHttpOpenRequest(
            connection,
            L"POST",
            L"/graphql",
            nullptr,
            WINHTTP_NO_REFERER,
            WINHTTP_DEFAULT_ACCEPT_TYPES,
            WINHTTP_FLAG_SECURE);
        if (!request)
        {
            WinHttpCloseHandle(connection);
            WinHttpCloseHandle(session);
            return false;
        }

        const std::wstring headers = L"Content-Type: application/json\r\nAccept: application/json\r\n";
        const DWORD payload_size = static_cast<DWORD>(strlen(k_payload));
        const bool sent = WinHttpSendRequest(
            request,
            headers.c_str(),
            static_cast<DWORD>(headers.size()),
            const_cast<char*>(k_payload),
            payload_size,
            payload_size,
            0) != FALSE;
        if (!sent || WinHttpReceiveResponse(request, nullptr) == FALSE)
        {
            WinHttpCloseHandle(request);
            WinHttpCloseHandle(connection);
            WinHttpCloseHandle(session);
            return false;
        }

        out_body.clear();
        for (;;)
        {
            DWORD available = 0;
            if (!WinHttpQueryDataAvailable(request, &available) || available == 0)
                break;

            std::vector<char> chunk(available);
            DWORD downloaded = 0;
            if (!WinHttpReadData(request, chunk.data(), available, &downloaded))
                break;

            out_body.append(chunk.data(), downloaded);
        }

        WinHttpCloseHandle(request);
        WinHttpCloseHandle(connection);
        WinHttpCloseHandle(session);

        return !out_body.empty() && out_body.find("\"data\"") != std::string::npos;
    }

    inline bool try_load_from_path(const std::string& path)
    {
        if (path.empty()) return false;

        const std::string absolute_path = to_absolute_path(path);
        if (!file_exists(absolute_path))
            return false;

        std::ifstream file(absolute_path, std::ios::binary);
        if (!file.is_open()) return false;

        if (!parse_price_file(file)) return false;

        set_active_file_path(absolute_path);
        logger::info("Loaded market prices from: " + absolute_path);
        return true;
    }

    inline std::vector<std::string> build_price_paths()
    {
        std::vector<std::string> paths;
        const std::string dll_dir = get_module_directory("TarkovAddons.dll");
        const std::string game_dir = get_process_directory();
        const std::string appdata_dir = get_appdata_config_directory();

        Configuration cfg;
        if (cfg.load("launcher.cfg"))
        {
            const std::string configured = trim_config_value(cfg.get("marketJson"));
            if (!configured.empty())
                paths.push_back(to_absolute_path(resolve_configured_path(configured)));
        }

        if (!dll_dir.empty())
            paths.push_back(to_absolute_path(join_path(dll_dir, "tarkov_prices.json")));

        if (!appdata_dir.empty())
            paths.push_back(to_absolute_path(join_path(appdata_dir, "tarkov_prices.json")));

        if (!game_dir.empty())
        {
            paths.push_back(to_absolute_path(join_path(game_dir, "..\\BsgLauncher\\tarkov_prices.json")));
            paths.push_back(to_absolute_path(join_path(game_dir, "..\\launcher\\tarkov_prices.json")));
            paths.push_back(to_absolute_path(join_path(game_dir, "tarkov_prices.json")));
        }

        paths.push_back(to_absolute_path("../BsgLauncher/tarkov_prices.json"));
        paths.push_back(to_absolute_path("../launcher/tarkov_prices.json"));
        paths.push_back(to_absolute_path("tarkov_prices.json"));

        std::vector<std::string> unique_paths;
        for (const std::string& path : paths)
        {
            if (path.empty()) continue;
            if (std::find(unique_paths.begin(), unique_paths.end(), path) == unique_paths.end())
                unique_paths.push_back(path);
        }

        return unique_paths;
    }

    inline bool load_market_prices(const bool force_reload = false)
    {
        if (!force_reload && prices_loaded().load())
        {
            std::lock_guard<std::mutex> lock(prices_mutex());
            return !market_prices().empty();
        }

        if (force_reload)
        {
            std::lock_guard<std::mutex> lock(prices_mutex());
            market_prices().clear();
            prices_loaded().store(false);
        }

        std::string searched_paths;
        for (const std::string& path : build_price_paths())
        {
            if (!searched_paths.empty())
                searched_paths += "\n";
            searched_paths += path;

            if (try_load_from_path(path))
            {
                prices_loaded().store(true);
                const std::string source = get_active_file_path();
                logger::info("Market price table size: " + std::to_string(market_prices().size()));
                (void)source;
                set_status_message("");
                return true;
            }
        }

        set_active_file_path(get_default_save_path());
        const std::string fallback_save_path = get_save_path();

        logger::warn("Market prices not loaded. Searched paths:\n" + searched_paths);
        logger::warn("New prices will be saved to: " + fallback_save_path);
        prices_loaded().store(true);
        (void)fallback_save_path;
        set_status_message("Market prices not loaded");
        return false;
    }

    inline bool refresh_market_prices()
    {
        std::string body;
        if (!download_market_prices(body))
        {
            set_status_message("Failed to download prices from tarkov.dev");
            logger::warn("Failed to download market prices from tarkov.dev");
            return false;
        }

        if (!parse_price_string(body))
        {
            set_status_message("Downloaded prices are invalid");
            logger::warn("Downloaded market prices failed validation");
            return false;
        }

        const std::string save_path = get_save_path();
        if (!write_text_file(save_path, body))
        {
            set_status_message("Prices updated in memory, but file save failed");
            logger::warn("Failed to save market prices to: " + save_path);
        }
        else
        {
            set_active_file_path(save_path);
            set_status_message("");
            logger::info("Saved market prices to: " + save_path);
        }

        prices_loaded().store(true);
        return true;
    }

    inline void refresh_async()
    {
        if (prices_updating().exchange(true))
            return;

        set_status_message("Updating prices from tarkov.dev...");
        std::thread([]()
        {
            refresh_market_prices();
            prices_updating().store(false);
        }).detach();
    }

    inline bool is_updating()
    {
        return prices_updating().load();
    }

    inline size_t get_price_count()
    {
        std::lock_guard<std::mutex> lock(prices_mutex());
        return market_prices().size();
    }

    inline uint64_t get_version()
    {
        return prices_version().load();
    }

    inline void load_async()
    {
        if (prices_loaded().load() || prices_loading().exchange(true))
            return;

        std::thread([]()
        {
            load_market_prices();
            prices_loading().store(false);
        }).detach();
    }

    inline void ensure_loaded()
    {
        if (!prices_loaded().load())
            load_market_prices();
    }

    inline bool is_loaded()
    {
        return prices_loaded().load();
    }

    inline bool has_prices()
    {
        if (!prices_loaded().load())
            return false;
        std::lock_guard<std::mutex> lock(prices_mutex());
        return !market_prices().empty();
    }

    inline size_t get_cache_bytes()
    {
        if (!prices_loaded().load())
            return 0;
        std::lock_guard<std::mutex> lock(prices_mutex());
        size_t bytes = 0;
        for (const auto& [key, value] : market_prices())
        {
            bytes += key.size() + sizeof(value) + 32;
        }
        return bytes;
    }

    inline void clear_cache()
    {
        {
            std::lock_guard<std::mutex> lock(prices_mutex());
            market_prices().clear();
        }
        prices_loaded().store(false);
        prices_version().fetch_add(1);
        set_status_message("");
    }

    inline int lookup_price(const std::string& template_id)
    {
        if (template_id.empty() || !prices_loaded().load()) return 0;

        std::lock_guard<std::mutex> lock(prices_mutex());
        const auto found = market_prices().find(template_id);
        if (found == market_prices().end()) return 0;
        return found->second;
    }

    inline int resolve_item_sell_price(Il2CppObject* item_obj, const std::string& template_id)
    {
        if (!has_prices()) return 0;

        std::string key = template_id;
        if (key.empty() && item_obj)
            key = eft_api::try_get_item_template_id(item_obj);

        return lookup_price(key);
    }
}