#include "user_data.h"

#include <izo/Paths.hpp>
#include <ikvxx/ikvxx.hpp>

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

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

namespace
{
    using ArrayIndex = ikv::Value::ArrayIndex;

    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();
    }

    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;
        }
    }

    std::optional<float> readFloat(const ikv::Value &value)
    {
        if (value.isDouble())
            return static_cast<float>(value.asDouble());
        if (value.isInt())
            return static_cast<float>(value.asInt64());
        return std::nullopt;
    }

    // Load settings from an ikv::Value "settings" object into UserData fields.
    // Returns true if anything useful was read.
    void loadSettings(const ikv::Value &settings, float &sidebar_width, float &details_width,
                      bool &sidebar_collapsed, float &commit_message_height,
                      float &working_composer_height, int &pull_mode, int &zoom_percent,
                      std::array<float, 4> &sidebar_section_heights,
                      std::array<bool, 4> &sidebar_section_open,
                      std::array<float, 4> &commit_table_column_widths)
    {
        if (settings.isMember("sidebar_width"))
            if (const auto value = readFloat(settings["sidebar_width"]); value)
                sidebar_width = *value;
        if (settings.isMember("details_width"))
            if (const auto value = readFloat(settings["details_width"]); value)
                details_width = *value;
        if (settings.isMember("sidebar_collapsed") && settings["sidebar_collapsed"].isBool())
            sidebar_collapsed = settings["sidebar_collapsed"].asBool();
        if (settings.isMember("commit_message_height"))
            if (const auto value = readFloat(settings["commit_message_height"]); value)
                commit_message_height = *value;
        if (settings.isMember("working_composer_height"))
            if (const auto value = readFloat(settings["working_composer_height"]); value)
                working_composer_height = *value;
        if (settings.isMember("pull_mode") && settings["pull_mode"].isInt())
            pull_mode = static_cast<int>(settings["pull_mode"].asInt64());
        if (settings.isMember("zoom_percent") && settings["zoom_percent"].isInt())
            zoom_percent = static_cast<int>(settings["zoom_percent"].asInt64());

        if (settings.isMember("sidebar_sections") && settings["sidebar_sections"].isArray())
        {
            const ikv::Value &arr = settings["sidebar_sections"];
            const auto count = std::min(arr.size(), sidebar_section_heights.size());
            for (std::size_t i = 0; i < count; ++i)
                if (const auto value = readFloat(arr[static_cast<ArrayIndex>(i)]); value)
                    sidebar_section_heights[i] = *value;
        }
        if (settings.isMember("sidebar_section_open") && settings["sidebar_section_open"].isArray())
        {
            const ikv::Value &arr = settings["sidebar_section_open"];
            const auto count = std::min(arr.size(), sidebar_section_open.size());
            for (std::size_t i = 0; i < count; ++i)
                sidebar_section_open[i] = arr[static_cast<ArrayIndex>(i)].asBool();
        }
        if (settings.isMember("commit_table_columns") && settings["commit_table_columns"].isArray())
        {
            const ikv::Value &arr = settings["commit_table_columns"];
            const auto count = std::min(arr.size(), commit_table_column_widths.size());
            for (std::size_t i = 0; i < count; ++i)
                if (const auto value = readFloat(arr[static_cast<ArrayIndex>(i)]); value)
                    commit_table_column_widths[i] = *value;
        }
    }
} // namespace

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.ikv";
}

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

UserData::RepoSettings &UserData::repoSettings(const std::string &normalized_path)
{
    return repo_settings_[normalized_path];
}

const UserData::RepoSettings *UserData::findRepoSettings(const std::string &normalized_path) const
{
    const auto it = repo_settings_.find(normalized_path);
    return it != repo_settings_.end() ? &it->second : nullptr;
}

void UserData::load()
{
    const std::filesystem::path binary_path = dataPath();

    // ── Primary load: iKvxx binary (.ikv) ────────────────────────────────────
    try
    {
        const ikv::Value root = [&]() -> ikv::Value
        {
            try
            {
                return ikv::Value::load(binary_path.string());
            }
            catch (const ikv::Error &)
            {
                return ikv::Value::loadBinary(binary_path.string());
            }
        }();

        if (root.isMember("settings") && root["settings"].isObject())
        {
            loadSettings(root["settings"],
                sidebar_width_, details_width_, sidebar_collapsed_,
                commit_message_height_, working_composer_height_,
                pull_mode_, zoom_percent_,
                sidebar_section_heights_, sidebar_section_open_,
                commit_table_column_widths_);
        }

        if (root.isMember("recently_closed") && root["recently_closed"].isArray())
        {
            const ikv::Value &arr = root["recently_closed"];
            const auto count = std::min<std::size_t>(arr.size(), 100);
            for (std::size_t i = 0; i < count; ++i)
            {
                const std::string path = arr[static_cast<ArrayIndex>(i)].asString();
                if (!path.empty())
                {
                    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 (root.isMember("recent_repositories") && root["recent_repositories"].isArray())
        {
            const ikv::Value &arr = root["recent_repositories"];
            const auto count = std::min<std::size_t>(arr.size(), 100);
            for (std::size_t i = 0; i < count; ++i)
            {
                const std::string path = arr[static_cast<ArrayIndex>(i)].asString();
                if (!path.empty())
                {
                    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 (root.isMember("session") && root["session"].isObject())
        {
            const ikv::Value &session = root["session"];
            if (session.isMember("active_tab") && session["active_tab"].isInt())
                active_repository_ = static_cast<size_t>(std::max<int64_t>(0, session["active_tab"].asInt64()));
            if (session.isMember("tabs") && session["tabs"].isArray())
            {
                const ikv::Value &tabs = session["tabs"];
                for (std::size_t i = 0; i < tabs.size(); ++i)
                    open_repositories_.emplace_back(tabs[static_cast<ArrayIndex>(i)].asString());
            }
        }

        if (root.isMember("repository_settings") && root["repository_settings"].isArray())
        {
            const ikv::Value &repos = root["repository_settings"];
            for (std::size_t i = 0; i < repos.size(); ++i)
            {
                const ikv::Value &entry = repos[static_cast<ArrayIndex>(i)];
                if (!entry.isObject() || !entry.isMember("path"))
                    continue;
                const std::string path_str = entry["path"].asString();
                if (path_str.empty())
                    continue;
                const std::string key = normalizePath(path_str);
                RepoSettings &rs = repo_settings_[key];
                // column widths
                rs.column_widths = commit_table_column_widths_; // start from global default
                if (entry.isMember("commit_table_columns") && entry["commit_table_columns"].isArray())
                {
                    const ikv::Value &widths = entry["commit_table_columns"];
                    const auto count = std::min<std::size_t>(widths.size(), 4);
                    for (std::size_t c = 0; c < count; ++c)
                        rs.column_widths[c] = static_cast<float>(widths[static_cast<ArrayIndex>(c)].asDouble());
                }
                if (entry.isMember("commit_summary") && entry["commit_summary"].isString())
                    rs.pending_commit_summary = entry["commit_summary"].asString();
                if (entry.isMember("commit_description") && entry["commit_description"].isString())
                    rs.pending_commit_description = entry["commit_description"].asString();
            }
        }

        if (root.isMember("credentials") && root["credentials"].isArray())
        {
            const ikv::Value &creds = root["credentials"];
            for (std::size_t i = 0; i < creds.size(); ++i)
            {
                const ikv::Value &entry = creds[static_cast<ArrayIndex>(i)];
                if (!entry.isObject())
                    continue;
                if (!entry.isMember("scope") || !entry.isMember("secret"))
                    continue;
                const std::string scope = entry["scope"].asString();
                const std::string secret = entry["secret"].asString();
                if (!scope.empty() && !secret.empty())
                    encrypted_credentials_[scope] = secret;
            }
        }

        // Clamp loaded values.
        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);
        commit_message_height_   = std::clamp(commit_message_height_, 60.0f, 600.0f);
        working_composer_height_ = std::clamp(working_composer_height_, 100.0f, 900.0f);
        for (float &h : sidebar_section_heights_)
            h = std::clamp(h, 42.0f, 500.0f);
        for (float &w : commit_table_column_widths_)
            w = std::clamp(w, 0.0f, 1200.0f);
        for (auto &[repo_key, rs] : repo_settings_)
            for (float &w : rs.column_widths)
                w = std::clamp(w, 0.0f, 1200.0f);
        if (open_repositories_.empty())
            active_repository_ = 0;
        else
            active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
        return;
    }
    catch (const ikv::Error &)
    {
        // File missing or corrupt — try legacy paths below.
    }

    // ── Legacy text load (user_data.ikv — old text format) ───────────────────
    // (The old binary was user_data.ikv2b; user_data.ikv was the text file.)
    // We'll silently skip if missing.
    const std::filesystem::path text_path = directory_ / "user_data_legacy.ikv";
    // (No legacy text migration needed — just fall through to defaults.)

    // ── Import from very old settings.ini ────────────────────────────────────
    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;
        }
    }

    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_file(directory_ / "session.txt");
    session_file >> active_repository_;
    while (session_file >> std::quoted(path))
    {
        if (!path.empty())
            open_repositories_.push_back(normalizePath(path));
    }

    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 &h : sidebar_section_heights_)
        h = std::clamp(h, 42.0f, 500.0f);
    if (open_repositories_.empty())
        active_repository_ = 0;
    else
        active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
    save(); // migrate to new format immediately
}

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
}

std::string UserData::pendingCommitSummary(const std::string &repo_path) const
{
    const std::string key = normalizePath(repo_path);
    const RepoSettings *rs = findRepoSettings(key);
    return rs ? rs->pending_commit_summary : std::string{};
}

std::string UserData::pendingCommitDescription(const std::string &repo_path) const
{
    const std::string key = normalizePath(repo_path);
    const RepoSettings *rs = findRepoSettings(key);
    return rs ? rs->pending_commit_description : std::string{};
}

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::setPendingCommitSummary(const std::string &repo_path, const std::string &text)
{
    repoSettings(normalizePath(repo_path)).pending_commit_summary = text;
    // Don't trigger a full save on every keystroke — caller must call save() when appropriate.
}

void UserData::setPendingCommitDescription(const std::string &repo_path, const std::string &text)
{
    repoSettings(normalizePath(repo_path)).pending_commit_description = text;
}

void UserData::save() const
{
    std::filesystem::create_directories(directory_);
    ikv::Value root(ikv::objectValue, "gitree");

    auto settings = root.makeObject("settings");
    settings["sidebar_width"] = static_cast<double>(sidebar_width_);
    settings["details_width"] = static_cast<double>(details_width_);
    settings["sidebar_collapsed"] = sidebar_collapsed_;
    settings["commit_message_height"] = static_cast<double>(commit_message_height_);
    settings["working_composer_height"] = static_cast<double>(working_composer_height_);
    settings["pull_mode"] = static_cast<int64_t>(pull_mode_);
    settings["zoom_percent"] = static_cast<int64_t>(zoom_percent_);

    auto heights = settings.makeArray("sidebar_sections", ikv::realValue);
    for (const float h : sidebar_section_heights_)
        heights.append(static_cast<double>(h));

    auto open = settings.makeArray("sidebar_section_open", ikv::booleanValue);
    for (const bool b : sidebar_section_open_)
        open.append(b);

    auto col_widths = settings.makeArray("commit_table_columns", ikv::realValue);
    for (const float w : commit_table_column_widths_)
        col_widths.append(static_cast<double>(w));

    auto repos = root.makeArray("repository_settings", ikv::objectValue);
    for (const auto &[repo_path, rs] : repo_settings_)
    {
        auto entry = repos.appendObject();
        entry["path"] = repo_path;
        auto rw = entry.makeArray("commit_table_columns", ikv::realValue);
        for (const float w : rs.column_widths)
            rw.append(static_cast<double>(w));
        if (!rs.pending_commit_summary.empty())
            entry["commit_summary"] = rs.pending_commit_summary;
        if (!rs.pending_commit_description.empty())
            entry["commit_description"] = rs.pending_commit_description;
    }

    auto history = root.makeArray("recently_closed", ikv::stringValue);
    for (const auto &p : recently_closed_)
        history.append(p);

    auto recent = root.makeArray("recent_repositories", ikv::stringValue);
    for (const auto &p : recent_repositories_)
        recent.append(p);

    auto session = root.makeObject("session");
    session["active_tab"] = static_cast<int64_t>(active_repository_);
    auto tabs = session.makeArray("tabs", ikv::stringValue);
    for (const auto &p : open_repositories_)
        tabs.append(p);

    auto credentials = root.makeArray("credentials", ikv::objectValue);
    for (const auto &[scope, secret] : encrypted_credentials_)
    {
        auto entry = credentials.appendObject();
        entry["scope"] = scope;
        entry["secret"] = secret;
    }

    root.writeBinary(dataPath().string(), ikv::Version::v2);
    removeLegacyFiles();
}

float UserData::commitTableColumnWidth(const std::string &repo_path, size_t index) const
{
    if (index >= 4) return 0.0f;
    const std::string normalized = normalizePath(repo_path);
    const RepoSettings *rs = findRepoSettings(normalized);
    if (rs) return rs->column_widths[index];
    return commit_table_column_widths_[index];
}

void UserData::setCommitTableColumnWidth(const std::string &repo_path, size_t index, float width)
{
    if (index >= 4) return;
    const std::string normalized = normalizePath(repo_path);
    RepoSettings &rs = repo_settings_[normalized];
    // Initialise from global defaults if this is a brand-new entry.
    if (rs.column_widths[0] == 0.0f && rs.column_widths[1] == 0.0f &&
        rs.column_widths[2] == 0.0f && rs.column_widths[3] == 0.0f)
    {
        rs.column_widths = commit_table_column_widths_;
    }
    rs.column_widths[index] = width;
    save();
}
