#include "i18n.h"

#include <algorithm>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <unordered_map>
#include <unordered_set>

#include <Windows.h>

#include "../external/nlohmann/json.hpp"
#include "../module/item_pricing.h"
#include "../util/Config.h"
#include "../util/module_paths.h"
#include "logger.h"

namespace i18n
{
    namespace
    {
        std::mutex locale_mutex;
        std::string active_locale = "en";
        std::string loaded_locale;
        std::unordered_map<std::string, std::string> translations;
        bool initialized = false;

        const std::vector<language_entry> k_languages = {
            {"en", "lang_en_sel"},
            {"ru", "lang_ru_sel"},
            {"ge", "lang_ge_sel"},
            {"fr", "lang_fr_sel"},
            {"es", "lang_es_sel"},
            {"mx", "lang_mx_sel"},
            {"it", "lang_it_sel"},
            {"pl", "lang_pl_sel"},
            {"cs", "lang_cs_sel"},
            {"po", "lang_po_sel"},
            {"br", "lang_br_sel"},
            {"tu", "lang_tu_sel"},
            {"ch", "lang_ch_sel"},
            {"tw", "lang_tw_sel"},
            {"jp", "lang_jp_sel"},
            {"kr", "lang_kr_sel"},
            {"hr", "lang_hr_sel"},
            {"sr", "lang_sr_sel"},
            {"ro", "lang_ro_sel"},
            {"hu", "lang_hu_sel"},
            {"sk", "lang_sk_sel"},
        };

        std::vector<language_entry> available_languages_cache;
        bool languages_discovered = false;

        std::string normalize_locale(std::string code)
        {
            for (char& ch : code)
                ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));

            if (code == "de")
                return "ge";
            if (code == "ja")
                return "jp";
            if (code == "ko")
                return "kr";
            if (code == "pt")
                return "po";
            if (code == "pt-br" || code == "pt_br")
                return "br";
            if (code == "tr")
                return "tu";
            if (code == "zh" || code == "zh-cn" || code == "zh_cn" || code == "cn")
                return "ch";
            if (code == "zh-tw" || code == "zh_tw")
                return "tw";
            if (code == "es-mx" || code == "es_mx")
                return "mx";

            return code;
        }

        bool is_supported_locale(const std::string& code)
        {
            for (const language_entry& entry : k_languages)
            {
                if (entry.code == code)
                    return true;
            }
            return false;
        }

        std::filesystem::path path_from_utf8(const std::string& utf8)
        {
            if (utf8.empty())
                return {};

            const int wide_len = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
            if (wide_len <= 1)
                return std::filesystem::path(utf8);

            std::wstring wide(static_cast<size_t>(wide_len - 1), L'\0');
            MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, wide.data(), wide_len);
            return std::filesystem::path(wide);
        }

        std::wstring locale_code_wide(const std::string& locale_code)
        {
            return std::wstring(locale_code.begin(), locale_code.end());
        }

        std::vector<std::filesystem::path> collect_bsg_launcher_lang_dirs(
            const std::filesystem::path& start_dir)
        {
            std::vector<std::filesystem::path> found;
            if (start_dir.empty())
                return found;

            std::error_code ec;
            std::filesystem::path current = start_dir;
            current = std::filesystem::weakly_canonical(current, ec);

            for (int depth = 0; depth < 6 && !current.empty(); ++depth)
            {
                found.push_back(current / L"BsgLauncher" / L"lang");
                found.push_back(current / L"launcher" / L"lang");

                const std::filesystem::path parent = current.parent_path();
                if (!parent.empty() && parent != current)
                {
                    found.push_back(parent / L"BsgLauncher" / L"lang");
                    found.push_back(parent / L"launcher" / L"lang");
                }

                if (parent.empty() || parent == current)
                    break;
                current = parent;
            }

            return found;
        }

        std::vector<std::filesystem::path> lang_directories_wide()
        {
            std::vector<std::filesystem::path> dirs;
            auto add_unique = [&](const std::filesystem::path& dir)
            {
                if (dir.empty())
                    return;

                std::error_code ec;
                const std::filesystem::path normalized = std::filesystem::weakly_canonical(dir, ec);
                const std::filesystem::path& resolved = ec ? dir : normalized;
                if (std::find(dirs.begin(), dirs.end(), resolved) == dirs.end())
                    dirs.push_back(resolved);
            };

            Configuration cfg;
            if (cfg.load("launcher.cfg"))
            {
                const std::string configured = item_pricing::trim_config_value(cfg.get("langDir"));
                if (!configured.empty())
                {
                    add_unique(path_from_utf8(
                        item_pricing::to_absolute_path(item_pricing::resolve_configured_path(configured))));
                }
            }

            if (const std::filesystem::path dll_dir = module_paths::dll_directory_wide(); !dll_dir.empty())
            {
                add_unique(dll_dir / L"lang");
                for (const std::filesystem::path& dir : collect_bsg_launcher_lang_dirs(dll_dir))
                    add_unique(dir);
            }

            wchar_t process_path[MAX_PATH]{};
            if (GetModuleFileNameW(nullptr, process_path, MAX_PATH) != 0)
            {
                std::filesystem::path process_dir(process_path);
                if (process_dir.has_parent_path())
                {
                    process_dir = process_dir.parent_path();
                    for (const std::filesystem::path& dir : collect_bsg_launcher_lang_dirs(process_dir))
                        add_unique(dir);
                    add_unique(process_dir / L"lang");
                    add_unique(process_dir / L"TarkovAddons" / L"lang");
                    add_unique(process_dir / L"..\\BsgLauncher\\lang");
                    add_unique(process_dir / L"..\\launcher\\lang");
                }
            }

            add_unique(path_from_utf8(item_pricing::to_absolute_path("../BsgLauncher/lang")));
            add_unique(path_from_utf8(item_pricing::to_absolute_path("../launcher/lang")));
            add_unique(L"lang");
            return dirs;
        }

        std::vector<std::string> lang_directories()
        {
            std::vector<std::string> dirs;
            for (const std::filesystem::path& dir : lang_directories_wide())
                dirs.push_back(module_paths::wide_to_utf8(dir.wstring()));
            return dirs;
        }

        std::string lang_directory()
        {
            std::error_code ec;
            for (const std::filesystem::path& dir : lang_directories_wide())
            {
                if (std::filesystem::is_directory(dir, ec))
                    return module_paths::wide_to_utf8(dir.wstring());
            }
            return "lang";
        }

        bool locale_file_exists_wide(const std::wstring& locale_code)
        {
            const std::filesystem::path file_name = locale_code + L".json";
            for (const std::filesystem::path& dir : lang_directories_wide())
            {
                const std::filesystem::path file_path = dir / file_name;
                const DWORD attrs = GetFileAttributesW(file_path.c_str());
                if (attrs != INVALID_FILE_ATTRIBUTES && !(attrs & FILE_ATTRIBUTE_DIRECTORY))
                    return true;
            }
            return false;
        }

        std::string normalize_locale_file_stem(std::string stem)
        {
            for (char& ch : stem)
                ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));

            if (stem == "de")
                return "ge";
            if (stem == "ja")
                return "jp";
            if (stem == "ko")
                return "kr";
            if (stem == "pt")
                return "po";
            if (stem == "pt-br" || stem == "pt_br")
                return "br";
            if (stem == "tr")
                return "tu";
            if (stem == "zh" || stem == "zh-cn" || stem == "zh_cn" || stem == "cn")
                return "ch";
            if (stem == "zh-tw" || stem == "zh_tw")
                return "tw";
            if (stem == "es-mx" || stem == "es_mx")
                return "mx";

            return stem;
        }

        void collect_locale_codes_from_disk(std::unordered_set<std::string>& out)
        {
            for (const std::filesystem::path& dir : lang_directories_wide())
            {
                std::error_code ec;
                if (!std::filesystem::is_directory(dir, ec))
                    continue;

                for (const auto& entry : std::filesystem::directory_iterator(dir, ec))
                {
                    if (ec)
                        break;

                    const std::wstring ext = entry.path().extension().wstring();
                    if (_wcsicmp(ext.c_str(), L".json") != 0)
                        continue;

                    const DWORD attrs = GetFileAttributesW(entry.path().c_str());
                    if (attrs == INVALID_FILE_ATTRIBUTES || (attrs & FILE_ATTRIBUTE_DIRECTORY))
                        continue;

                    std::string stem = module_paths::wide_to_utf8(entry.path().stem().wstring());
                    stem = normalize_locale_file_stem(std::move(stem));
                    if (!stem.empty())
                        out.insert(stem);
                }
            }
        }

        bool locale_file_exists(const std::string& locale_code)
        {
            if (locale_file_exists_wide(locale_code_wide(locale_code)))
                return true;

            const std::string normalized = normalize_locale_file_stem(locale_code);
            if (normalized != locale_code)
                return locale_file_exists_wide(locale_code_wide(normalized));

            return false;
        }

        bool load_locale_file(const std::string& locale_code)
        {
            std::ifstream input;
            std::string loaded_path;
            const std::wstring locale_wide = locale_code_wide(locale_code);
            for (const std::filesystem::path& dir : lang_directories_wide())
            {
                const std::filesystem::path file_path = dir / (locale_wide + L".json");
                input.open(file_path, std::ios::binary);
                if (input.good())
                {
                    loaded_path = module_paths::wide_to_utf8(file_path.wstring());
                    break;
                }
                input.close();
            }
            if (!input.good())
                return false;

            try
            {
                nlohmann::json json;
                input >> json;
                if (!json.is_object())
                    return false;

                std::unordered_map<std::string, std::string> next;
                for (auto it = json.begin(); it != json.end(); ++it)
                {
                    if (it.value().is_string())
                        next[it.key()] = it.value().get<std::string>();
                }

                translations = std::move(next);
                loaded_locale = locale_code;
                return true;
            }
            catch (const std::exception& ex)
            {
                logger::warn(std::string("i18n: failed to parse ") + loaded_path + ": " + ex.what());
                return false;
            }
        }

        void ensure_locale_loaded(const std::string& locale_code)
        {
            if (loaded_locale == locale_code && !translations.empty())
                return;

            if (load_locale_file(locale_code))
                return;

            if (locale_code != "en" && load_locale_file("en"))
                return;

            translations.clear();
            loaded_locale = locale_code;
        }

        void apply_locale(const std::string& locale_code)
        {
            const std::string normalized = normalize_locale(locale_code);
            const std::string resolved = is_supported_locale(normalized) ? normalized : "en";
            ensure_locale_loaded(resolved);
            active_locale = resolved;
        }

        const language_entry* find_language_entry(const std::string& code)
        {
            for (const language_entry& entry : k_languages)
            {
                if (entry.code == code)
                    return &entry;
            }
            return nullptr;
        }

        const std::vector<language_entry>& english_fallback_languages()
        {
            static const std::vector<language_entry> fallback = {{"en", "lang_en_sel"}};
            return fallback;
        }

        void discover_available_languages()
        {
            std::unordered_set<std::string> codes_on_disk;
            collect_locale_codes_from_disk(codes_on_disk);

            std::vector<language_entry> next;
            next.reserve(codes_on_disk.size());
            for (const language_entry& entry : k_languages)
            {
                if (codes_on_disk.count(entry.code))
                    next.push_back(entry);
            }

            if (next.empty())
            {
                if (const language_entry* english = find_language_entry("en"))
                    next.push_back(*english);
            }

            available_languages_cache = std::move(next);

            std::string discovered;
            for (const language_entry& entry : available_languages_cache)
            {
                if (!discovered.empty())
                    discovered += ", ";
                discovered += entry.code;
            }
            logger::info("i18n: available languages: " + discovered + " (from " + lang_directory() + ")");
        }

        const std::vector<language_entry>& active_language_list()
        {
            if (!languages_discovered)
                return english_fallback_languages();
            return available_languages_cache;
        }
    }

    void init()
    {
        std::lock_guard<std::mutex> lock(locale_mutex);
        if (initialized)
            return;
        initialized = true;
        apply_locale("en");
    }

    void init_menu_languages()
    {
        std::lock_guard<std::mutex> lock(locale_mutex);
        if (languages_discovered)
            return;
        discover_available_languages();
        languages_discovered = true;
    }

    const std::string& current_locale()
    {
        return active_locale;
    }

    void set_locale(const std::string& locale_code)
    {
        std::lock_guard<std::mutex> lock(locale_mutex);
        apply_locale(locale_code);
    }

    const std::vector<language_entry>& supported_languages()
    {
        return k_languages;
    }

    const std::vector<language_entry>& available_languages()
    {
        return active_language_list();
    }

    int locale_to_index(const std::string& code)
    {
        const std::string normalized = normalize_locale(code);
        const std::vector<language_entry>& languages = active_language_list();
        for (size_t i = 0; i < languages.size(); ++i)
        {
            if (languages[i].code == normalized)
                return static_cast<int>(i);
        }
        return 0;
    }

    std::string index_to_locale(const int index)
    {
        const std::vector<language_entry>& languages = active_language_list();
        if (index < 0 || static_cast<size_t>(index) >= languages.size())
            return languages.empty() ? "en" : languages.front().code;
        return languages[static_cast<size_t>(index)].code;
    }

    std::string tr(const char* key)
    {
        if (!key || !*key)
            return {};
        return tr(std::string(key));
    }

    std::string tr(const std::string& key)
    {
        if (key.empty())
            return key;

        std::lock_guard<std::mutex> lock(locale_mutex);
        const auto iter = translations.find(key);
        if (iter != translations.end() && !iter->second.empty())
            return iter->second;
        return key;
    }

}