#include "user_data.h"

#include <izo/Paths.hpp>

extern "C"
{
#include <ikv.h>
}

#include <algorithm>
#include <fstream>
#include <iomanip>
#include <optional>
#include <sstream>
#include <utility>

#ifdef _WIN32
#include <windows.h>
#include <wincrypt.h>
#endif

namespace
{
    std::filesystem::path roaming_directory()
    {
        if (const auto config = izo::GetKnownPath(izo::KnownPath::Config); !config.empty())
            return config;
        if (const auto temporary = izo::GetKnownPath(izo::KnownPath::Temporary); !temporary.empty())
            return temporary;
        return std::filesystem::temp_directory_path();
    }

    const ikv_node_t *object_value(const ikv_node_t *object, const char *key, ikv_type_t type)
    {
        const ikv_node_t *value = object ? ikv_object_get(object, key) : nullptr;
        return value && ikv_node_type(value) == type ? value : nullptr;
    }

    std::optional<std::pair<std::string, std::string>> splitCredentialPayload(const std::string &payload)
    {
        const size_t separator = payload.find('\n');
        if (separator == std::string::npos)
            return std::nullopt;
        return std::pair<std::string, std::string>{
            payload.substr(0, separator),
            payload.substr(separator + 1)};
    }

#ifdef _WIN32
    std::string hexEncode(const std::string &value)
    {
        static constexpr char digits[] = "0123456789abcdef";
        std::string encoded;
        encoded.reserve(value.size() * 2);
        for (const unsigned char byte : value)
        {
            encoded.push_back(digits[(byte >> 4) & 0x0F]);
            encoded.push_back(digits[byte & 0x0F]);
        }
        return encoded;
    }

    std::optional<std::string> hexDecode(const std::string &value)
    {
        if (value.size() % 2 != 0)
            return std::nullopt;
        auto decode_nibble = [](char character) -> int
        {
            if (character >= '0' && character <= '9')
                return character - '0';
            if (character >= 'a' && character <= 'f')
                return 10 + (character - 'a');
            if (character >= 'A' && character <= 'F')
                return 10 + (character - 'A');
            return -1;
        };
        std::string decoded;
        decoded.reserve(value.size() / 2);
        for (size_t index = 0; index < value.size(); index += 2)
        {
            const int high = decode_nibble(value[index]);
            const int low = decode_nibble(value[index + 1]);
            if (high < 0 || low < 0)
                return std::nullopt;
            decoded.push_back(static_cast<char>((high << 4) | low));
        }
        return decoded;
    }

    std::optional<std::string> protectCredentialString(const std::string &value)
    {
        DATA_BLOB input{};
        input.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(value.data()));
        input.cbData = static_cast<DWORD>(value.size());
        DATA_BLOB output{};
        if (!CryptProtectData(&input, L"Gitree Git Credential", nullptr, nullptr, nullptr,
                              CRYPTPROTECT_UI_FORBIDDEN, &output))
            return std::nullopt;
        std::string protected_value(reinterpret_cast<const char *>(output.pbData), output.cbData);
        LocalFree(output.pbData);
        return hexEncode(protected_value);
    }

    std::optional<std::string> unprotectCredentialString(const std::string &value)
    {
        const auto binary = hexDecode(value);
        if (!binary)
            return std::nullopt;
        DATA_BLOB input{};
        input.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(binary->data()));
        input.cbData = static_cast<DWORD>(binary->size());
        DATA_BLOB output{};
        if (!CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr,
                                CRYPTPROTECT_UI_FORBIDDEN, &output))
            return std::nullopt;
        std::string plain(reinterpret_cast<const char *>(output.pbData), output.cbData);
        LocalFree(output.pbData);
        return plain;
    }
#endif

    std::string normalizePath(const std::string &path)
    {
        if (path.empty())
            return {};
        try
        {
            std::filesystem::path p(path);
            std::string result = p.lexically_normal().generic_string();
            if (result.size() > 1 && result.back() == '/')
                result.pop_back();
            return result;
        }
        catch (...)
        {
            return path;
        }
    }
}

UserData::UserData()
{
    directory_ = roaming_directory() / "Identity" / "Gitree";
    std::filesystem::create_directories(directory_);
    load();
}

UserData::~UserData()
{
    save();
}

std::string UserData::credentialScope(const std::string &remote_url)
{
    const size_t scheme = remote_url.find("://");
    if (scheme == std::string::npos)
        return remote_url;
    const size_t authority_begin = scheme + 3;
    const size_t authority_end = remote_url.find_first_of("/?#", authority_begin);
    return remote_url.substr(0, authority_end == std::string::npos ? remote_url.size() : authority_end);
}

std::filesystem::path UserData::dataPath() const
{
    return directory_ / "user_data.ikv2b";
}

void UserData::removeLegacyFiles() const
{
    std::error_code error;
    for (const char *name : {"imgui.ini", "settings.ini", "history.txt", "session.txt", "user_data.ikv"})
        std::filesystem::remove(directory_ / name, error);
}

void UserData::load()
{
    const std::filesystem::path binary_path = dataPath();
    if (ikv_node_t *root = ikvb_parse_file(binary_path.string().c_str()))
    {
        if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
        {
            if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
                sidebar_width_ = static_cast<float>(ikv_as_float(value));
            if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
                details_width_ = static_cast<float>(ikv_as_float(value));
            if (const ikv_node_t *value = object_value(settings, "sidebar_collapsed", IKV_BOOL))
                sidebar_collapsed_ = ikv_as_bool(value) != 0;
            if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
                pull_mode_ = static_cast<int>(ikv_as_int(value));
            if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
                zoom_percent_ = static_cast<int>(ikv_as_int(value));
            if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
            }
            if (const ikv_node_t *open = object_value(settings, "sidebar_section_open", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(open), static_cast<uint32_t>(sidebar_section_open_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    sidebar_section_open_[index] = ikv_as_bool(ikv_array_get(open, index)) != 0;
            }
            if (const ikv_node_t *widths = object_value(settings, "commit_table_columns", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(widths), static_cast<uint32_t>(commit_table_column_widths_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    commit_table_column_widths_[index] = static_cast<float>(ikv_as_float(ikv_array_get(widths, index)));
            }
        }
        if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
        {
            const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
            for (uint32_t index = 0; index < count; ++index)
            {
                const char *path = ikv_as_string(ikv_array_get(history, index));
                if (path && *path)
                {
                    std::string normalized = normalizePath(path);
                    if (std::find(recently_closed_.begin(), recently_closed_.end(), normalized) == recently_closed_.end())
                        recently_closed_.emplace_back(std::move(normalized));
                }
            }
        }
        if (const ikv_node_t *recent = object_value(root, "recent_repositories", IKV_ARRAY))
        {
            const uint32_t count = std::min<uint32_t>(ikv_array_size(recent), 100);
            for (uint32_t index = 0; index < count; ++index)
            {
                const char *path = ikv_as_string(ikv_array_get(recent, index));
                if (path && *path)
                {
                    std::string normalized = normalizePath(path);
                    if (std::find(recent_repositories_.begin(), recent_repositories_.end(), normalized) == recent_repositories_.end())
                        recent_repositories_.emplace_back(std::move(normalized));
                }
            }
        }
        if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
        {
            if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
                active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
            if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
            {
                for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
                {
                    const char *path = ikv_as_string(ikv_array_get(tabs, index));
                    open_repositories_.emplace_back(path ? path : "");
                }
            }
        }
        if (const ikv_node_t *credentials = object_value(root, "credentials", IKV_ARRAY))
        {
            for (uint32_t index = 0; index < ikv_array_size(credentials); ++index)
            {
                const ikv_node_t *entry = ikv_array_get(credentials, index);
                if (ikv_node_type(entry) != IKV_OBJECT)
                    continue;
                const ikv_node_t *scope = object_value(entry, "scope", IKV_STRING);
                const ikv_node_t *secret = object_value(entry, "secret", IKV_STRING);
                if (!scope || !secret)
                    continue;
                const char *scope_value = ikv_as_string(scope);
                const char *secret_value = ikv_as_string(secret);
                if (!scope_value || !secret_value || !*scope_value || !*secret_value)
                    continue;
                encrypted_credentials_[scope_value] = secret_value;
            }
        }
        ikv_free(root);
        sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
        details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
        pull_mode_ = std::clamp(pull_mode_, 0, 3);
        zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
        for (float &height : sidebar_section_heights_)
            height = std::clamp(height, 42.0f, 500.0f);
        for (float &width : commit_table_column_widths_)
            width = std::clamp(width, 0.0f, 1200.0f);
        if (open_repositories_.empty())
            active_repository_ = 0;
        else
            active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
        return;
    }

    const std::filesystem::path text_data_path = directory_ / "user_data.ikv";
    if (ikv_node_t *root = ikv_parse_file(text_data_path.string().c_str()))
    {
        if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
        {
            if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
                sidebar_width_ = static_cast<float>(ikv_as_float(value));
            if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
                details_width_ = static_cast<float>(ikv_as_float(value));
            if (const ikv_node_t *value = object_value(settings, "sidebar_collapsed", IKV_BOOL))
                sidebar_collapsed_ = ikv_as_bool(value) != 0;
            if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
                pull_mode_ = static_cast<int>(ikv_as_int(value));
            if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
                zoom_percent_ = static_cast<int>(ikv_as_int(value));
            if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
            }
            if (const ikv_node_t *open = object_value(settings, "sidebar_section_open", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(open), static_cast<uint32_t>(sidebar_section_open_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    sidebar_section_open_[index] = ikv_as_bool(ikv_array_get(open, index)) != 0;
            }
            if (const ikv_node_t *widths = object_value(settings, "commit_table_columns", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(widths), static_cast<uint32_t>(commit_table_column_widths_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    commit_table_column_widths_[index] = static_cast<float>(ikv_as_float(ikv_array_get(widths, index)));
            }
        }
        if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
        {
            const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
            for (uint32_t index = 0; index < count; ++index)
            {
                const char *path = ikv_as_string(ikv_array_get(history, index));
                if (path && *path)
                {
                    std::string normalized = normalizePath(path);
                    if (std::find(recently_closed_.begin(), recently_closed_.end(), normalized) == recently_closed_.end())
                        recently_closed_.emplace_back(std::move(normalized));
                }
            }
        }
        if (const ikv_node_t *recent = object_value(root, "recent_repositories", IKV_ARRAY))
        {
            const uint32_t count = std::min<uint32_t>(ikv_array_size(recent), 100);
            for (uint32_t index = 0; index < count; ++index)
            {
                const char *path = ikv_as_string(ikv_array_get(recent, index));
                if (path && *path)
                {
                    std::string normalized = normalizePath(path);
                    if (std::find(recent_repositories_.begin(), recent_repositories_.end(), normalized) == recent_repositories_.end())
                        recent_repositories_.emplace_back(std::move(normalized));
                }
            }
        }
        if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
        {
            if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
                active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
            if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
            {
                for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
                {
                    const char *path = ikv_as_string(ikv_array_get(tabs, index));
                    open_repositories_.emplace_back(path ? path : "");
                }
            }
        }
        if (const ikv_node_t *credentials = object_value(root, "credentials", IKV_ARRAY))
        {
            for (uint32_t index = 0; index < ikv_array_size(credentials); ++index)
            {
                const ikv_node_t *entry = ikv_array_get(credentials, index);
                if (ikv_node_type(entry) != IKV_OBJECT)
                    continue;
                const ikv_node_t *scope = object_value(entry, "scope", IKV_STRING);
                const ikv_node_t *secret = object_value(entry, "secret", IKV_STRING);
                if (!scope || !secret)
                    continue;
                const char *scope_value = ikv_as_string(scope);
                const char *secret_value = ikv_as_string(secret);
                if (!scope_value || !secret_value || !*scope_value || !*secret_value)
                    continue;
                encrypted_credentials_[scope_value] = secret_value;
            }
        }
        ikv_free(root);
        sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
        details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
        pull_mode_ = std::clamp(pull_mode_, 0, 3);
        zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
        for (float &height : sidebar_section_heights_)
            height = std::clamp(height, 42.0f, 500.0f);
        for (float &width : commit_table_column_widths_)
            width = std::clamp(width, 0.0f, 1200.0f);
        if (open_repositories_.empty())
            active_repository_ = 0;
        else
            active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
        save();
        return;
    }

    // Import the original files once when upgrading an existing installation.
    std::ifstream settings(directory_ / "settings.ini");
    std::string key;
    while (settings >> key)
    {
        if (key == "sidebar_width")
            settings >> sidebar_width_;
        else if (key == "details_width")
            settings >> details_width_;
        else if (key == "pull_mode")
            settings >> pull_mode_;
        else if (key == "zoom_percent")
            settings >> zoom_percent_;
        else if (key.rfind("sidebar_section_", 0) == 0)
        {
            const size_t index = static_cast<size_t>(std::stoul(key.substr(16)));
            float height = 0.0f;
            settings >> height;
            if (index < sidebar_section_heights_.size())
                sidebar_section_heights_[index] = height;
        }
    }
    sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
    details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
    pull_mode_ = std::clamp(pull_mode_, 0, 3);
    zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
    for (float &height : sidebar_section_heights_)
        height = std::clamp(height, 42.0f, 500.0f);

    std::ifstream history(directory_ / "history.txt");
    std::string path;
    while (history >> std::quoted(path))
    {
        if (!path.empty())
        {
            std::string normalized = normalizePath(path);
            if (std::find(recently_closed_.begin(), recently_closed_.end(), normalized) == recently_closed_.end())
                recently_closed_.push_back(std::move(normalized));
        }
        if (recently_closed_.size() == 100)
            break;
    }
    recent_repositories_ = recently_closed_;

    std::ifstream session(directory_ / "session.txt");
    session >> active_repository_;
    while (session >> std::quoted(path))
    {
        if (!path.empty())
            open_repositories_.push_back(normalizePath(path));
    }
    if (open_repositories_.empty())
        active_repository_ = 0;
    else
        active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
    save();
}

std::optional<SavedGitCredential> UserData::remoteCredential(const std::string &remote_url) const
{
    const auto iterator = encrypted_credentials_.find(credentialScope(remote_url));
    if (iterator == encrypted_credentials_.end())
        return std::nullopt;
#ifdef _WIN32
    const auto plain = unprotectCredentialString(iterator->second);
    if (!plain)
        return std::nullopt;
    const auto split = splitCredentialPayload(*plain);
    if (!split)
        return std::nullopt;
    return SavedGitCredential{remote_url, split->first, split->second};
#else
    return std::nullopt;
#endif
}

void UserData::addRecentRepository(const std::string &path)
{
    if (path.empty())
        return;
    const std::string normalized = normalizePath(path);
    std::erase(recent_repositories_, normalized);
    recent_repositories_.insert(recent_repositories_.begin(), normalized);
    if (recent_repositories_.size() > 100)
        recent_repositories_.resize(100);
    save();
}

void UserData::addRecentlyClosed(const std::string &path)
{
    if (path.empty())
        return;
    const std::string normalized = normalizePath(path);
    std::erase(recent_repositories_, normalized);
    recent_repositories_.insert(recent_repositories_.begin(), normalized);
    if (recent_repositories_.size() > 100)
        recent_repositories_.resize(100);
    std::erase(recently_closed_, normalized);
    recently_closed_.insert(recently_closed_.begin(), normalized);
    if (recently_closed_.size() > 100)
        recently_closed_.resize(100);
    save();
}

std::string UserData::takeRecentlyClosed()
{
    if (recently_closed_.empty())
        return {};
    std::string path = std::move(recently_closed_.front());
    recently_closed_.erase(recently_closed_.begin());
    save();
    return path;
}

void UserData::setRepositorySession(std::vector<std::string> paths, size_t active_repository)
{
    for (auto &path : paths)
        path = normalizePath(path);
    open_repositories_ = std::move(paths);
    active_repository_ = open_repositories_.empty()
                             ? 0
                             : std::min(active_repository, open_repositories_.size() - 1);
    save();
}

void UserData::storeRemoteCredential(const std::string &remote_url, const std::string &username,
                                     const std::string &password)
{
    const std::string scope = credentialScope(remote_url);
    if (scope.empty() || username.empty())
        return;
#ifdef _WIN32
    const auto protected_value = protectCredentialString(username + "\n" + password);
    if (!protected_value)
        return;
    encrypted_credentials_[scope] = *protected_value;
    save();
#else
    (void)password;
#endif
}

void UserData::clearRemoteCredential(const std::string &remote_url)
{
    encrypted_credentials_.erase(credentialScope(remote_url));
    save();
}

void UserData::save() const
{
    std::filesystem::create_directories(directory_);
    ikv_node_t *root = ikv_create_object("gitree");
    if (!root)
        return;

    ikv_node_t *settings = ikv_object_add_object(root, "settings");
    ikv_object_set_float(settings, "sidebar_width", sidebar_width_);
    ikv_object_set_float(settings, "details_width", details_width_);
    ikv_object_set_bool(settings, "sidebar_collapsed", sidebar_collapsed_);
    ikv_object_set_int(settings, "pull_mode", pull_mode_);
    ikv_object_set_int(settings, "zoom_percent", zoom_percent_);
    ikv_node_t *heights = ikv_object_add_array(settings, "sidebar_sections", IKV_FLOAT);
    for (const float height : sidebar_section_heights_)
        ikv_array_add_float(heights, height);
    ikv_node_t *open = ikv_object_add_array(settings, "sidebar_section_open", IKV_BOOL);
    for (const bool state : sidebar_section_open_)
        ikv_array_add_bool(open, state);
    ikv_node_t *widths = ikv_object_add_array(settings, "commit_table_columns", IKV_FLOAT);
    for (const float width : commit_table_column_widths_)
        ikv_array_add_float(widths, width);

    ikv_node_t *history = ikv_object_add_array(root, "recently_closed", IKV_STRING);
    for (const auto &path : recently_closed_)
        ikv_array_add_string(history, path.c_str());

    ikv_node_t *recent = ikv_object_add_array(root, "recent_repositories", IKV_STRING);
    for (const auto &path : recent_repositories_)
        ikv_array_add_string(recent, path.c_str());

    ikv_node_t *session = ikv_object_add_object(root, "session");
    ikv_object_set_int(session, "active_tab", static_cast<int64_t>(active_repository_));
    ikv_node_t *tabs = ikv_object_add_array(session, "tabs", IKV_STRING);
    for (const auto &path : open_repositories_)
        ikv_array_add_string(tabs, path.c_str());

    ikv_node_t *credentials = ikv_object_add_array(root, "credentials", IKV_OBJECT);
    for (const auto &[scope, secret] : encrypted_credentials_)
    {
        ikv_node_t *entry = ikv_array_add_object(credentials);
        ikv_object_set_string(entry, "scope", scope.c_str());
        ikv_object_set_string(entry, "secret", secret.c_str());
    }

    ikvb_write_file_version(dataPath().string().c_str(), root, IKV_VERSION_2);
    ikv_free(root);
    removeLegacyFiles();
}
