#include "GetbinClient.hpp"
#include <cpr/cpr.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include <iostream>
#include <filesystem>
#include <sstream>
#include <set>
#include <algorithm>

using json = nlohmann::json;

const std::string GetbinClient::DEFAULT_SERVER_HOST = "getpkg.xyz";

GetbinClient::GetbinClient(const std::vector<std::string>& servers) : servers_(servers) {
    // Initialize CPR (done automatically, but we could add global config here)
    if (servers_.empty()) {
        servers_.push_back(DEFAULT_SERVER_HOST);
    }
}

GetbinClient::GetbinClient() : servers_({DEFAULT_SERVER_HOST}) {
    // Backward compatibility constructor
}

std::string GetbinClient::getUserAgent() const {
    return "getpkg/1.0";
}

std::string GetbinClient::buildUrl(const std::string& serverUrl, const std::string& endpoint) const {
    std::string url = "https://" + serverUrl;
    if (!endpoint.empty() && endpoint[0] != '/') {
        url += "/";
    }
    url += endpoint;
    return url;
}

bool GetbinClient::downloadFromServer(const std::string& serverUrl, const std::string& toolName, 
                                     const std::string& arch, const std::string& outPath, 
                                     ProgressCallback progressCallback) {
    try {
        std::string url = buildUrl(serverUrl, "/object/" + toolName + ":" + arch);
        
        cpr::Session session;
        session.SetUrl(cpr::Url{url});
        session.SetHeader(cpr::Header{{"User-Agent", getUserAgent()}});
        session.SetTimeout(cpr::Timeout{30000}); // 30 seconds
        session.SetVerifySsl(cpr::VerifySsl{true});
        
        // Add progress callback if provided
        if (progressCallback) {
            session.SetProgressCallback(cpr::ProgressCallback{[progressCallback](cpr::cpr_off_t downloadTotal, cpr::cpr_off_t downloadNow,
                                                                                cpr::cpr_off_t uploadTotal, cpr::cpr_off_t uploadNow,
                                                                                intptr_t userdata) -> bool {
                return progressCallback(static_cast<size_t>(downloadNow), static_cast<size_t>(downloadTotal));
            }});
        }
        
        auto response = session.Get();
        
        if (response.status_code == 200) {
            std::ofstream ofs(outPath, std::ios::binary);
            if (ofs) {
                ofs.write(response.text.data(), response.text.size());
                return ofs.good();
            }
        } else if (response.status_code == 404) {
            // Not found - this is expected for arch fallback
            return false;
        } else {
            std::cerr << "[GetbinClient::downloadFromServer] HTTP " << response.status_code << " from " << serverUrl << ": " << response.error.message << std::endl;
        }
        
        return false;
    } catch (const std::exception& e) {
        std::cerr << "[GetbinClient::downloadFromServer] Exception with " << serverUrl << ": " << e.what() << std::endl;
        return false;
    }
}

bool GetbinClient::download(const std::string& toolName, const std::string& arch, const std::string& outPath, 
                           ProgressCallback progressCallback) {
    // Multi-server fallback logic: try each server in order
    for (const auto& server : servers_) {
        if (downloadFromServer(server, toolName, arch, outPath, progressCallback)) {
            return true;
        }
    }
    
    // If we get here, no server had the package
    return false;
}

bool GetbinClient::upload(const std::string& serverUrl, const std::string& archivePath, 
                         std::string& outUrl, std::string& outHash, const std::string& token,
                         ProgressCallback progressCallback) {
    try {
        std::string url = buildUrl(serverUrl, "/upload");
        
        cpr::Session session;
        session.SetUrl(cpr::Url{url});
        session.SetHeader(cpr::Header{
            {"User-Agent", getUserAgent()},
            {"Authorization", "Bearer " + token}
        });
        session.SetTimeout(cpr::Timeout{300000}); // 5 minutes for uploads
        session.SetVerifySsl(cpr::VerifySsl{true});
        
        
        // Extract tool name and arch from archive path for labeltags
        // Archive path format: /path/to/tool-name:arch.tgz or similar
        std::string archiveName = std::filesystem::path(archivePath).filename().string();
        std::string toolNameArch = archiveName;
        if (toolNameArch.ends_with(".tgz")) {
            toolNameArch = toolNameArch.substr(0, toolNameArch.length() - 4);
        }
        
        // Create metadata JSON with labeltags
        json metadata;
        metadata["labeltags"] = json::array({toolNameArch});
        
        // Set up multipart form with file and metadata
        session.SetMultipart(cpr::Multipart{
            cpr::Part{"file", cpr::File{archivePath}},
            cpr::Part{"metadata", metadata.dump(), "application/json"}
        });
        
        // Add progress callback if provided
        if (progressCallback) {
            session.SetProgressCallback(cpr::ProgressCallback{[progressCallback](cpr::cpr_off_t downloadTotal, cpr::cpr_off_t downloadNow,
                                                                                cpr::cpr_off_t uploadTotal, cpr::cpr_off_t uploadNow,
                                                                                intptr_t userdata) -> bool {
                return progressCallback(static_cast<size_t>(uploadNow), static_cast<size_t>(uploadTotal));
            }});
        }
        
        auto response = session.Put();
        
        if (response.status_code == 200) {
            try {
                auto resp_json = json::parse(response.text);
                if (resp_json.contains("hash") && resp_json.contains("result") && resp_json["result"] == "success") {
                    outUrl = buildUrl(serverUrl, "/object/" + resp_json["hash"].get<std::string>());
                    outHash = resp_json["hash"].get<std::string>();
                    return true;
                }
            } catch (const json::exception& e) {
                // Try to extract from plain text response
                outUrl = "";
                outHash = response.text;
                // Remove trailing newline if present
                if (!outHash.empty() && outHash.back() == '\n') {
                    outHash.pop_back();
                }
                return !outHash.empty();
            }
        } else {
            std::cerr << "[GetbinClient::upload] HTTP " << response.status_code << " to " << serverUrl << ": " << response.error.message << std::endl;
            if (!response.text.empty()) {
                std::cerr << "[GetbinClient::upload] Response: " << response.text << std::endl;
            }
        }
        
        return false;
    } catch (const std::exception& e) {
        std::cerr << "[GetbinClient::upload] Exception with " << serverUrl << ": " << e.what() << std::endl;
        return false;
    }
}

bool GetbinClient::upload(const std::string& archivePath, std::string& outUrl, std::string& outHash, 
                         const std::string& token, ProgressCallback progressCallback) {
    // Backward compatibility: use first server
    if (servers_.empty()) {
        return false;
    }
    return upload(servers_[0], archivePath, outUrl, outHash, token, progressCallback);
}

bool GetbinClient::getHash(const std::string& serverUrl, const std::string& toolName, 
                         const std::string& arch, std::string& outHash) {
    try {
        std::string url = buildUrl(serverUrl, "/hash/" + toolName + ":" + arch);
        
        auto response = cpr::Get(cpr::Url{url},
                                cpr::Header{{"User-Agent", getUserAgent()}},
                                cpr::Timeout{10000}, // 10 seconds
                                cpr::VerifySsl{true});
        
        if (response.status_code == 200) {
            try {
                // Try JSON first
                auto resp_json = json::parse(response.text);
                if (resp_json.contains("hash")) {
                    outHash = resp_json["hash"].get<std::string>();
                    return true;
                }
            } catch (const json::exception&) {
                // Not JSON, treat as plain text
                outHash = response.text;
                // Remove trailing newline if present
                if (!outHash.empty() && outHash.back() == '\n') {
                    outHash.pop_back();
                }
                return !outHash.empty();
            }
        } else if (response.status_code == 404) {
            // Not found - this is expected for non-existent tools/archs
            return false;
        } else {
            std::cerr << "[GetbinClient::getHash] HTTP " << response.status_code << " from " << serverUrl << ": " << response.error.message << std::endl;
        }
        
        return false;
    } catch (const std::exception& e) {
        std::cerr << "[GetbinClient::getHash] Exception with " << serverUrl << ": " << e.what() << std::endl;
        return false;
    }
}

bool GetbinClient::getHash(const std::string& toolName, const std::string& arch, std::string& outHash) {
    // Multi-server fallback: try each server in order
    for (const auto& server : servers_) {
        if (getHash(server, toolName, arch, outHash)) {
            return true;
        }
    }
    
    // If we get here, no server had the package
    return false;
}

bool GetbinClient::findPackageServer(const std::string& toolName, const std::string& arch, 
                                   std::string& foundServer) const {
    // Check each server to see which one has the package
    for (const auto& server : servers_) {
        try {
            std::string url = buildUrl(server, "/hash/" + toolName + ":" + arch);
            
            auto response = cpr::Get(cpr::Url{url},
                                    cpr::Header{{"User-Agent", getUserAgent()}},
                                    cpr::Timeout{10000}, // 10 seconds
                                    cpr::VerifySsl{true});
            
            if (response.status_code == 200) {
                // Package found on this server
                foundServer = server;
                return true;
            }
            // Continue to next server if 404 or other error
        } catch (const std::exception& e) {
            // Continue to next server on exception
            std::cerr << "[GetbinClient::findPackageServer] Exception with " << server << ": " << e.what() << std::endl;
        }
    }
    
    // Package not found on any server
    return false;
}

bool GetbinClient::deleteObject(const std::string& hash, const std::string& token) {
    try {
        // Use first server for backward compatibility
        if (servers_.empty()) {
            return false;
        }
        std::string url = buildUrl(servers_[0], "/deleteobject?hash=" + hash);
        
        auto response = cpr::Get(cpr::Url{url},
                                cpr::Header{
                                    {"User-Agent", getUserAgent()},
                                    {"Authorization", "Bearer " + token}
                                },
                                cpr::Timeout{30000}, // 30 seconds
                                cpr::VerifySsl{true});
        
        if (response.status_code == 200) {
            return true;
        } else {
            std::cerr << "[GetbinClient::deleteObject] HTTP " << response.status_code << ": " << response.error.message << std::endl;
            if (!response.text.empty()) {
                std::cerr << "[GetbinClient::deleteObject] Response: " << response.text << std::endl;
            }
        }
        
        return false;
    } catch (const std::exception& e) {
        std::cerr << "[GetbinClient::deleteObject] Exception: " << e.what() << std::endl;
        return false;
    }
}

bool GetbinClient::listPackages(std::vector<std::string>& outPackages) {
    try {
        // Use first server for backward compatibility
        if (servers_.empty()) {
            return false;
        }
        std::string url = buildUrl(servers_[0], "/dir");
        
        auto response = cpr::Get(cpr::Url{url},
                                cpr::Header{{"User-Agent", getUserAgent()}},
                                cpr::Timeout{30000}, // 30 seconds
                                cpr::VerifySsl{true});
        
        if (response.status_code == 200) {
            try {
                auto resp_json = json::parse(response.text);
                if (resp_json.contains("entries") && resp_json["entries"].is_array()) {
                    outPackages.clear();
                    std::set<std::string> uniqueTools;
                    
                    for (const auto& entry : resp_json["entries"]) {
                        if (entry.contains("labeltags") && entry["labeltags"].is_array()) {
                            for (const auto& labeltag : entry["labeltags"]) {
                                if (labeltag.is_string()) {
                                    std::string tag = labeltag.get<std::string>();
                                    // Extract tool name from "tool:arch" format
                                    size_t colonPos = tag.find(":");
                                    if (colonPos != std::string::npos) {
                                        std::string toolName = tag.substr(0, colonPos);
                                        if (!toolName.empty()) {
                                            uniqueTools.insert(toolName);
                                        }
                                    }
                                }
                            }
                        }
                    }
                    
                    // Convert set to vector
                    for (const auto& tool : uniqueTools) {
                        outPackages.push_back(tool);
                    }
                    return true;
                }
            } catch (const json::exception&) {
                // Try to parse as newline-separated list
                outPackages.clear();
                std::istringstream stream(response.text);
                std::string line;
                while (std::getline(stream, line)) {
                    if (!line.empty()) {
                        outPackages.push_back(line);
                    }
                }
                return !outPackages.empty();
            }
        } else {
            std::cerr << "[GetbinClient::listPackages] HTTP " << response.status_code << ": " << response.error.message << std::endl;
        }
        
        return false;
    } catch (const std::exception& e) {
        std::cerr << "[GetbinClient::listPackages] Exception: " << e.what() << std::endl;
        return false;
    }
}

bool GetbinClient::listAllEntries(std::vector<std::pair<std::string, std::vector<std::string>>>& outEntries) {
    try {
        // Use first server for backward compatibility
        if (servers_.empty()) {
            return false;
        }
        std::string url = buildUrl(servers_[0], "/dir");
        
        auto response = cpr::Get(cpr::Url{url},
                                cpr::Header{{"User-Agent", getUserAgent()}},
                                cpr::Timeout{30000}, // 30 seconds
                                cpr::VerifySsl{true});
        
        if (response.status_code == 200) {
            try {
                auto resp_json = json::parse(response.text);
                if (resp_json.contains("entries") && resp_json["entries"].is_array()) {
                    outEntries.clear();
                    
                    for (const auto& entry : resp_json["entries"]) {
                        if (entry.contains("hash") && entry.contains("labeltags") && 
                            entry["hash"].is_string() && entry["labeltags"].is_array()) {
                            
                            std::string hash = entry["hash"].get<std::string>();
                            std::vector<std::string> labeltags;
                            
                            for (const auto& tag : entry["labeltags"]) {
                                if (tag.is_string()) {
                                    labeltags.push_back(tag.get<std::string>());
                                }
                            }
                            
                            outEntries.push_back({hash, labeltags});
                        }
                    }
                    return true;
                }
            } catch (const json::exception& e) {
                std::cerr << "[GetbinClient::listAllEntries] JSON parse error: " << e.what() << std::endl;
            }
        } else {
            std::cerr << "[GetbinClient::listAllEntries] HTTP " << response.status_code << ": " << response.error.message << std::endl;
        }
        
        return false;
    } catch (const std::exception& e) {
        std::cerr << "[GetbinClient::listAllEntries] Exception: " << e.what() << std::endl;
        return false;
    }
}
