// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3

#include "address_book.h"

#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>

#include "../util/logger.h"
#include "../util/platform.h"

namespace fs = std::filesystem;
using json = nlohmann::json;

namespace dragonx {
namespace data {

AddressBook::AddressBook() = default;
AddressBook::~AddressBook() = default;

std::string AddressBook::getDefaultPath()
{
    // Co-locate with settings.json in the per-variant config dir (Lite -> ObsidianDragonLite/).
    // util::Platform::getConfigDir() owns the per-platform + per-variant path in one place.
    const std::string dir = util::Platform::getConfigDir();
    fs::create_directories(dir);
    return (fs::path(dir) / "addressbook.json").string();
}

bool AddressBook::load()
{
    file_path_ = getDefaultPath();
    
    std::ifstream file(file_path_);
    if (!file.is_open()) {
        // No file yet - that's OK
        return true;
    }
    
    try {
        json j;
        file >> j;
        
        entries_.clear();
        
        if (j.contains("entries") && j["entries"].is_array()) {
            size_t skipped = 0;
            for (const auto& entry : j["entries"]) {
                // W6-3: skip (and count) a malformed element rather than letting one bad entry throw and
                // abort the whole load — which would discard EVERY contact (entries_ was already cleared).
                if (!entry.is_object()) { ++skipped; continue; }
                try {
                    AddressBookEntry e;
                    e.label = entry.value("label", "");
                    e.address = entry.value("address", "");
                    e.notes = entry.value("notes", "");
                    // Legacy entries (no "scope") migrate to "global" so nothing disappears when
                    // multi-wallet scoping lands — a contact you already had stays visible everywhere.
                    e.scope = entry.value("scope", "global");
                    e.avatar = entry.value("avatar", "");
                    if (!e.address.empty()) entries_.push_back(e);
                } catch (const std::exception&) { ++skipped; }
            }
            if (skipped > 0)
                DEBUG_LOGF("Address book: skipped %zu malformed entr%s\n", skipped, skipped == 1 ? "y" : "ies");
        }
        
        DEBUG_LOGF("Address book loaded: %zu entries\n", entries_.size());
        ++revision_;
        return true;

    } catch (const std::exception& e) {
        DEBUG_LOGF("Error loading address book: %s\n", e.what());
        return false;
    }
}

bool AddressBook::save()
{
    if (file_path_.empty()) {
        file_path_ = getDefaultPath();
    }
    
    try {
        json j;
        j["entries"] = json::array();
        
        for (const auto& entry : entries_) {
            json e;
            e["label"] = entry.label;
            e["address"] = entry.address;
            e["notes"] = entry.notes;
            e["scope"] = entry.scope.empty() ? std::string("global") : entry.scope;
            if (!entry.avatar.empty()) e["avatar"] = entry.avatar;
            j["entries"].push_back(e);
        }
        
        // Atomic + durable: temp file + fsync + rename, so a crash mid-write can't
        // truncate addressbook.json (which is fully rewritten on every entry change).
        // Owner-only (0600) — it holds the user's saved contacts.
        if (!util::Platform::writeFileAtomically(file_path_, j.dump(2), /*restrictPermissions=*/true)) {
            DEBUG_LOGF("Could not write address book: %s\n", file_path_.c_str());
            return false;
        }
        DEBUG_LOGF("Address book saved: %zu entries\n", entries_.size());
        return true;

    } catch (const std::exception& e) {
        DEBUG_LOGF("Error saving address book: %s\n", e.what());
        return false;
    }
}

bool AddressBook::addEntry(const AddressBookEntry& entry)
{
    // Reject a duplicate only within the same visible set (same wallet or global) — the same
    // address may legitimately be a contact in two different wallets.
    if (hasVisibleDuplicate(entry.address, entry.scope)) {
        return false;
    }

    entries_.push_back(entry);
    ++revision_;
    return save();
}

bool AddressBook::updateEntry(size_t index, const AddressBookEntry& entry)
{
    if (index >= entries_.size()) {
        return false;
    }
    
    // Check for a duplicate visible alongside this entry's scope (excluding the entry being edited)
    if (hasVisibleDuplicate(entry.address, entry.scope, static_cast<int>(index))) {
        return false;
    }

    entries_[index] = entry;
    ++revision_;
    return save();
}

bool AddressBook::removeEntry(size_t index)
{
    if (index >= entries_.size()) {
        return false;
    }
    
    entries_.erase(entries_.begin() + index);
    ++revision_;
    return save();
}

int AddressBook::reattachLegacyScopes(const std::string& scopeId)
{
    if (scopeId.empty()) return 0;
    int rescoped = 0;
    for (auto& e : entries_) {
        if (e.isGlobal()) continue;                       // global stays global
        if (e.scope.rfind("w:", 0) == 0) continue;        // already a stable scope
        e.scope = scopeId;
        ++rescoped;
    }
    if (rescoped > 0) { ++revision_; save(); }
    return rescoped;
}

int AddressBook::findByAddress(const std::string& address) const
{
    for (size_t i = 0; i < entries_.size(); i++) {
        if (entries_[i].address == address) {
            return static_cast<int>(i);
        }
    }
    return -1;
}

bool AddressBook::hasVisibleDuplicate(const std::string& address, const std::string& scope,
                                      int excludeIndex) const
{
    AddressBookEntry probe; probe.scope = scope;  // reuse the isGlobal()/scope logic
    for (size_t i = 0; i < entries_.size(); i++) {
        if (static_cast<int>(i) == excludeIndex) continue;
        const auto& e = entries_[i];
        if (e.address != address) continue;
        // Collides if they'd ever be shown together: same wallet scope, or either is global.
        if (e.isGlobal() || probe.isGlobal() || e.scope == scope) return true;
    }
    return false;
}

} // namespace data
} // namespace dragonx
