#include "lemon_cli/lemonade_client.h"
#include "lemon/utils/url_utils.h"
#include <httplib.h>
#include <iostream>
#include <algorithm>
#include <iomanip>
#include <regex>
#include <sstream>
#include <nlohmann/json.hpp>
#include <sstream>

namespace lemonade {

using json = nlohmann::json;

static const int DEFAULT_CONNECTION_TIMEOUT_MS = 30000;
static const int DEFAULT_READ_TIMEOUT_MS = 30000;
static const int LONG_TIMEOUT_MS = 86400000;
static const double UNKNOWN_MODEL_SIZE = 0.0;

static std::regex build_name_filter_regex(const std::string& name_filter) {
    std::string regex_pattern;
    regex_pattern.reserve(name_filter.size() * 2);

    for (char ch : name_filter) {
        switch (ch) {
            case '*':
                regex_pattern += ".*";
                break;
            case '\\':
            case '^':
            case '$':
            case '.':
            case '|':
            case '?':
            case '+':
            case '(':
            case ')':
            case '[':
            case ']':
            case '{':
            case '}':
                regex_pattern += '\\';
                regex_pattern += ch;
                break;
            default:
                regex_pattern += ch;
                break;
        }
    }

    return std::regex(regex_pattern, std::regex_constants::ECMAScript | std::regex_constants::icase);
}

static bool starts_with(const std::string& value, const std::string& prefix) {
    return value.rfind(prefix, 0) == 0;
}

static std::string strip_canonical_prefix(const std::string& model_name) {
    static const std::vector<std::string> prefixes = {"user.", "extra.", "builtin."};
    for (const auto& prefix : prefixes) {
        if (starts_with(model_name, prefix)) {
            return model_name.substr(prefix.size());
        }
    }
    return model_name;
}

static int model_source_sort_rank(const std::string& model_name) {
    if (starts_with(model_name, "user.")) return 1;
    if (starts_with(model_name, "extra.")) return 2;
    if (starts_with(model_name, "builtin.")) return 3;
    return 0;
}

HttpError::HttpError(int status, std::string body, const std::string& message)
    : std::runtime_error(message), status_code_(status), response_body_(std::move(body)) {}

int HttpError::status_code() const {
    return status_code_;
}

const std::string& HttpError::response_body() const {
    return response_body_;
}

void LemonadeClient::parse_target_url(const std::string& input_host, std::string& out_clean_host, int& out_port, bool& out_is_ssl, bool override_default_port) {
    lemon::utils::parse_target_url(input_host, out_clean_host, out_port, out_is_ssl, override_default_port);
}

LemonadeClient::LemonadeClient(const std::string& host, int port, const std::string& api_key, bool is_ssl)
    : api_key_(api_key), port_(port), is_ssl_(is_ssl) {
    parse_target_url(host, host_, port_, is_ssl_);
}

LemonadeClient::~LemonadeClient() {}

std::string LemonadeClient::normalize_host(const std::string& host) const {
    if (host.empty() || host == "0.0.0.0" || host == "localhost") {
        return "127.0.0.1";
    }
    return host;
}

// Helper to create and configure httplib::Client (timeouts in milliseconds)
static httplib::Client make_client(const std::string& host, int port, const std::string& api_key, bool is_ssl,
                                    time_t connection_timeout_ms = DEFAULT_CONNECTION_TIMEOUT_MS, time_t read_timeout_ms = DEFAULT_READ_TIMEOUT_MS) {
#ifndef LEMONADE_HTTPLIB_HAS_TLS
    if (is_ssl) {
        throw std::runtime_error("HTTPS support is not compiled in this client.");
    }
#endif
    std::string format_host = lemon::utils::bracket_host_if_ipv6(host);
    std::string scheme = is_ssl ? "https" : "http";
    std::string url = scheme + "://" + format_host + ":" + std::to_string(port);
    httplib::Client cli(url);
    cli.set_connection_timeout(connection_timeout_ms / 1000, (connection_timeout_ms % 1000) * 1000);
    cli.set_read_timeout(read_timeout_ms / 1000, (read_timeout_ms % 1000) * 1000);

    if (api_key != "") {
        cli.set_bearer_token_auth(api_key);
    }
    return cli;
}

static void assert_http_ok(const httplib::Result& res) {
    if (!res) {
        throw std::runtime_error(
            "Could not connect to Lemonade server (" + httplib::to_string(res.error()) + ").\n"
            "Make sure the server is running and try again.");
    } else if (res->status == 401) {
        throw std::runtime_error("Forbidden by the server. Did you set the API key?");
    } else if (res->status != 200) {
        throw HttpError(res->status, res->body,
                        "Request failed: " + std::to_string(res->status));
    }

}

std::string extract_server_error_message(const HttpError& error) {
    if (!error.response_body().empty()) {
        try {
            auto parsed = json::parse(error.response_body());
            if (parsed.contains("error")) {
                const auto& err = parsed["error"];
                if (err.is_string()) {
                    return err.get<std::string>();
                }
                // OpenAI-style bodies nest the text under error.message.
                if (err.is_object() && err.contains("message") && err["message"].is_string()) {
                    return err["message"].get<std::string>();
                }
            }
        } catch (const json::exception&) {
        }
    }
    return error.what();
}

static void print_response_warnings(const json& value, const std::string& indent = "") {
    if (value.contains("warnings") && value["warnings"].is_array()) {
        for (const auto& warning : value["warnings"]) {
            if (warning.is_string()) {
                std::cout << indent << "Warning: " << warning.get<std::string>()
                          << std::endl;
            }
        }
        return;
    }
    if (value.contains("warning") && value["warning"].is_string()) {
        std::cout << indent << "Warning: " << value["warning"].get<std::string>()
                  << std::endl;
    }
}

// Overloaded make_request with configurable timeouts (in milliseconds)
std::string LemonadeClient::make_request(const std::string& path, const std::string& method,
                                          const std::string& body, const std::string& content_type,
                                          time_t connection_timeout_ms, time_t read_timeout_ms) const {
    std::string normalized_host = normalize_host(host_);
    httplib::Client cli = make_client(normalized_host, port_, api_key_, is_ssl_, connection_timeout_ms, read_timeout_ms);

    httplib::Result res;

    if (method == "GET") {
        res = cli.Get(path);
    } else if (method == "POST") {
        res = cli.Post(path, body, content_type);
    } else if (method == "DELETE") {
        res = cli.Delete(path);
    } else {
        throw std::runtime_error("Unsupported HTTP method: " + method);
    }

    assert_http_ok(res);
    return res->body;

}

// Helper function to handle SSE streaming response. If `should_abort` is
// non-null and returns true, the content receiver returns `false`, which makes
// httplib close the connection and return immediately — used to stop streaming
// responses on Ctrl-C without waiting for the next chunk.
static httplib::Result handle_sse_stream(httplib::Client& cli, const std::string& path, const std::string& body, const std::string& content_type,
                              std::function<void(const std::string& event_type, const std::string& event_data)> callback,
                              std::function<bool()> should_abort = nullptr) {
    std::string buffer;
    std::string raw_response_body;
    bool saw_sse_event = false;

    auto res = cli.Post(path, httplib::Headers(), body, content_type,
        [&](const char* data, size_t len) {
            if (should_abort && should_abort()) {
                return false;
            }
            raw_response_body.append(data, len);
            buffer.append(data, len);

            size_t pos;
            while ((pos = buffer.find("\n\n")) != std::string::npos) {
                std::string message = buffer.substr(0, pos);
                buffer.erase(0, pos + 2);

                std::string event_type;
                std::string event_data;

                std::istringstream stream(message);
                std::string line;
                while (std::getline(stream, line)) {
                    if (line.substr(0, 6) == "event:") {
                        event_type = line.substr(7);
                        while (!event_type.empty() && event_type[0] == ' ') {
                            event_type.erase(0, 1);
                        }
                    } else if (line.substr(0, 5) == "data:") {
                        event_data = line.substr(6);
                        while (!event_data.empty() && event_data[0] == ' ') {
                            event_data.erase(0, 1);
                        }
                    }
                }

                if (!event_data.empty()) {
                    saw_sse_event = true;
                    callback(event_type, event_data);
                    if (should_abort && should_abort()) {
                        return false;
                    }
                }
            }

            return true;
        });

    if (res && !saw_sse_event && !raw_response_body.empty()) {
        res->body = raw_response_body;
    }

    return res;
}

// Overloaded make_request for streaming SSE responses (timeouts in milliseconds)
bool LemonadeClient::make_request(const std::string& path, const std::string& method,
                                   const std::string& body, const std::string& content_type,
                                   std::function<void(const std::string& event_type, const std::string& event_data)> callback,
                                   time_t connection_timeout_ms, time_t read_timeout_ms,
                                   std::function<bool()> should_abort) const {
    std::string normalized_host = normalize_host(host_);
    httplib::Client cli = make_client(normalized_host, port_, api_key_, is_ssl_, connection_timeout_ms, read_timeout_ms);

    if (method == "POST") {
        auto res = handle_sse_stream(cli, path, body, content_type, callback, should_abort);
        // If we deliberately aborted, suppress the "connection closed"-style
        // error that httplib reports — the caller asked for this.
        if (should_abort && should_abort()) {
            return false;
        }
        assert_http_ok(res);

        return true;
    }

    throw std::runtime_error("Streaming only supports POST method");
}

int LemonadeClient::check_model_updates() const {
    try {
        std::string response = make_request(
            "/api/v1/models/check-updates",
            "POST",
            "{}",
            "application/json",
            DEFAULT_CONNECTION_TIMEOUT_MS,
            LONG_TIMEOUT_MS);
        auto result = json::parse(response);

        const auto& models = result.at("models");
        if (!models.is_array()) {
            throw std::runtime_error("Server returned an invalid model update response");
        }

        if (result.contains("failed_models") && result["failed_models"].is_object() && !result["failed_models"].empty()) {
            std::cerr << "Failed to check updates for:" << std::endl;
            for (const auto& [name, err] : result["failed_models"].items()) {
                std::cerr << "  - " << name << ": " << err.get<std::string>() << std::endl;
            }
            return 1;
        }

        if (models.empty()) {
            std::cout << "All downloaded models are up to date." << std::endl;
            return 0;
        }

        std::cout << "Updates available for " << models.size() << " model(s):" << std::endl;
        for (const auto& model : models) {
            if (model.is_string()) {
                std::cout << "  - " << model.get<std::string>() << std::endl;
            }
        }
        return 0;
    } catch (const HttpError& e) {
        std::cerr << "Error checking model updates: "
                  << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error checking model updates: " << e.what() << std::endl;
        return 1;
    }
}

json LemonadeClient::fetch_health() const {
    return json::parse(make_request("/api/v1/health", "GET", "", "", 500, 500));
}

int LemonadeClient::update_models(const std::vector<std::string>& models, bool check_only, bool json_output, bool wait_for_completion) const {

    try {
        if (check_only) {
            json body = {
                {"models", models},
                {"dry_run", true}
            };

            std::string response = make_request(
                "/internal/models/sync",
                "POST",
                body.dump(),
                "application/json",
                DEFAULT_CONNECTION_TIMEOUT_MS,
                LONG_TIMEOUT_MS);
            auto result = json::parse(response);

            if (json_output) {
                std::cout << result.dump(2) << std::endl;
                bool failed = (result.value("status", "") == "failed") ||
                              (result.contains("failed_models") && result["failed_models"].is_object() && !result["failed_models"].empty()) ||
                              (result.contains("terminal_error") && !result["terminal_error"].get<std::string>().empty());
                return failed ? 1 : 0;
            }

            int updated_count = result.value("updated_count", 0);
            int checked_count = result.value("checked_count", 0);
            std::cout << "Checked " << checked_count << " model(s). " << updated_count << " update(s) available." << std::endl;

            if (result.contains("models_updated") && result["models_updated"].is_array() && !result["models_updated"].empty()) {
                std::cout << "Updates available:" << std::endl;
                for (const auto& m : result["models_updated"]) {
                    std::cout << "  - " << m.get<std::string>() << std::endl;
                }
            }

            if (result.contains("models_up_to_date") && result["models_up_to_date"].is_array() && !result["models_up_to_date"].empty()) {
                std::cout << "Up to date:" << std::endl;
                for (const auto& m : result["models_up_to_date"]) {
                    std::cout << "  - " << m.get<std::string>() << std::endl;
                }
            }

            if ((result.contains("failed_models") && result["failed_models"].is_object() && !result["failed_models"].empty()) ||
                (result.contains("terminal_error") && !result["terminal_error"].get<std::string>().empty())) {
                if (result.contains("failed_models") && result["failed_models"].is_object() && !result["failed_models"].empty()) {
                    std::cerr << "Failed:" << std::endl;
                    for (const auto& [name, err] : result["failed_models"].items()) {
                        std::cerr << "  - " << name << ": " << err.get<std::string>() << std::endl;
                    }
                }
                if (result.contains("terminal_error") && !result["terminal_error"].get<std::string>().empty()) {
                    std::cerr << "Terminal error: " << result["terminal_error"].get<std::string>() << std::endl;
                }
                return 1;
            }
            return 0;
        }

        auto post_sync = [this](const json& body) -> json {
            try {
                std::string resp = make_request(
                    "/internal/models/sync",
                    "POST",
                    body.dump(),
                    "application/json",
                    DEFAULT_CONNECTION_TIMEOUT_MS,
                    LONG_TIMEOUT_MS);
                return json::parse(resp);
            } catch (const HttpError& e) {
                if (e.status_code() == 202 && !e.response_body().empty()) {
                    return json::parse(e.response_body());
                }
                throw;
            }
        };

        auto is_failed_result = [](const json& res) -> bool {
            std::string status = res.value("status", "");
            return (status == "failed") || (status == "not_found") ||
                   (res.contains("failed_models") && res["failed_models"].is_object() && !res["failed_models"].empty()) ||
                   (res.contains("terminal_error") && !res["terminal_error"].get<std::string>().empty());
        };

        if (!wait_for_completion) {
            json body = {
                {"models", models},
                {"async", true}
            };

            auto result = post_sync(body);

            if (json_output) {
                std::cout << result.dump(2) << std::endl;
                return is_failed_result(result) ? 1 : 0;
            }

            if (result.value("already_in_progress", false)) {
                std::cout << "Model synchronization is already in progress." << std::endl;
                return 0;
            }

            if (is_failed_result(result)) {
                if (result.contains("failed_models") && result["failed_models"].is_object() && !result["failed_models"].empty()) {
                    std::cerr << "Failed:" << std::endl;
                    for (const auto& [name, err] : result["failed_models"].items()) {
                        std::cerr << "  - " << name << ": " << err.get<std::string>() << std::endl;
                    }
                }
                if (result.contains("terminal_error") && !result["terminal_error"].get<std::string>().empty()) {
                    std::cerr << "Terminal error: " << result["terminal_error"].get<std::string>() << std::endl;
                }
                return 1;
            }

            std::cout << "Model synchronization dispatched in background." << std::endl;
            std::cout << "Use 'lemonade update-models --wait' to monitor progress." << std::endl;

            return 0;
        }

        json dispatch_body = {
            {"models", models},
            {"async", true},
            {"attach_if_running", models.empty()}
        };

        auto dispatch_result = post_sync(dispatch_body);

        if (is_failed_result(dispatch_result) && !dispatch_result.value("already_in_progress", false)) {
            if (json_output) {
                std::cout << dispatch_result.dump(2) << std::endl;
            } else {
                std::cerr << "Error starting model update: " << dispatch_result.value("terminal_error", "Operation failed") << std::endl;
            }
            return 1;
        }
        bool already_running = dispatch_result.value("already_in_progress", false);
        uint64_t target_sync_id = dispatch_result.value("sync_id", 0);

        if (!json_output) {
            if (already_running) {
                std::cout << "Model synchronization is currently running in background. Waiting for completion..." << std::endl;
            } else {
                std::cout << "Starting model synchronization on server... Waiting for completion..." << std::endl;
            }
        }

        json final_result;
        std::string last_reported_model;
        while (true) {
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
            std::string poll_url = target_sync_id > 0
                ? ("/internal/models/sync/status?sync_id=" + std::to_string(target_sync_id))
                : "/internal/models/sync/status";
            std::string check_resp = make_request(
                poll_url,
                "GET",
                "",
                "",
                DEFAULT_CONNECTION_TIMEOUT_MS,
                LONG_TIMEOUT_MS);
            final_result = json::parse(check_resp);
            if (target_sync_id > 0) {
                if (final_result.value("status", "") != "in_progress" &&
                    (final_result.value("completed_sync_id", 0) >= target_sync_id ||
                     final_result.value("sync_id", 0) == target_sync_id)) {
                    break;
                }
            } else {
                if (!final_result.value("already_in_progress", false) && final_result.value("status", "") != "in_progress") {
                    break;
                }
            }

            if (final_result.contains("progress") && final_result["progress"].is_object()) {
                const auto& prog = final_result["progress"];
                std::string cur_model = prog.value("current_model", "");
                int percent = prog.value("percent", 0);
                size_t bytes = prog.value("bytes_downloaded", (size_t)0);
                size_t total = prog.value("bytes_total", (size_t)0);
                if (!cur_model.empty() && !json_output) {
                    last_reported_model = cur_model;
                    std::cerr << "\r\033[K[sync] " << cur_model;
                    if (total > 0) {
                        double mb_dl = static_cast<double>(bytes) / (1024.0 * 1024.0);
                        double mb_total = static_cast<double>(total) / (1024.0 * 1024.0);
                        std::cerr << " (" << percent << "% - " << std::fixed << std::setprecision(1) << mb_dl << " MB / " << mb_total << " MB)";
                    }
                    std::cerr << std::flush;
                }
            }
        }

        if (!last_reported_model.empty()) {
            std::cerr << std::endl;
        }

        if (json_output) {
            std::cout << final_result.dump(2) << std::endl;
            return is_failed_result(final_result) ? 1 : 0;
        }

        int checked_count = final_result.value("checked_count", 0);
        int updated_count = final_result.value("updated_count", 0);
        std::cout << "Model synchronization completed." << std::endl;
        std::cout << "Checked " << checked_count << " model(s). " << updated_count << " update(s) applied." << std::endl;

        if (final_result.contains("terminal_error") && !final_result["terminal_error"].get<std::string>().empty()) {
            std::cerr << "Terminal error: " << final_result["terminal_error"].get<std::string>() << std::endl;
        }

        if (final_result.contains("models_updated") && final_result["models_updated"].is_array() && !final_result["models_updated"].empty()) {
            std::cout << "Updated models:" << std::endl;
            for (const auto& m : final_result["models_updated"]) {
                std::cout << "  - " << m.get<std::string>() << std::endl;
            }
        }

        if (is_failed_result(final_result)) {
            if (final_result.value("status", "") == "not_found") {
                std::cerr << "Sync job " << (target_sync_id > 0 ? std::to_string(target_sync_id) : "") << " was not found or has expired on the server." << std::endl;
            }
            if (final_result.contains("failed_models") && final_result["failed_models"].is_object() && !final_result["failed_models"].empty()) {
                std::cerr << "Failed:" << std::endl;
                for (const auto& [name, err] : final_result["failed_models"].items()) {
                    std::cerr << "  - " << name << ": " << err.get<std::string>() << std::endl;
                }
            }
            return 1;
        }

        return 0;
    } catch (const HttpError& e) {

        std::cerr << "Error syncing models: "
                  << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error syncing models: " << e.what() << std::endl;
        return 1;
    }
}

// Shared by status() and status_json() so an unreachable/erroring server
// prints the exact same message on both paths.
static int print_health_fetch_error(const json::exception& e) {
    std::cerr << "Error parsing health response JSON: " << e.what() << std::endl;
    return 1;
}

static int print_health_fetch_error(const HttpError& e) {
    std::cerr << "Error fetching health status: " << extract_server_error_message(e)
              << std::endl;
    return 1;
}

static int print_health_fetch_error(const std::exception& e) {
    const std::string error = e.what();
    if (error.find("Connection failed:") == 0) {
        std::cerr << "Server is not running" << std::endl;
    } else {
        std::cerr << "Error fetching health status: " << error << std::endl;
    }
    return 1;
}

int LemonadeClient::status(int display_port) const {
    try {
        auto json_response = fetch_health();

        int port = display_port > 0 ? display_port : port_;
        std::cout << "Server is running on port " << port << std::endl;
        std::cout << std::endl;

        // Server info table
        std::cout << std::left << std::setw(20) << "Property" << "Value" << std::endl;
        std::cout << std::string(50, '-') << std::endl;

        if (json_response.contains("version")) {
            std::cout << std::left << std::setw(20) << "Version"
                      << json_response["version"].get<std::string>() << std::endl;
        }
        if (json_response.contains("websocket_port")) {
            std::cout << std::left << std::setw(20) << "WebSocket Port"
                      << json_response["websocket_port"].get<int>() << std::endl;
        }
        if (json_response.contains("max_models") && json_response["max_models"].is_object()) {
            std::cout << std::left << std::setw(20) << "Max Models/Type"
                      << json_response["max_models"]["llm"].get<int>() << std::endl;
        }

        // Loaded models table
        if (json_response.contains("all_models_loaded") && json_response["all_models_loaded"].is_array() &&
            !json_response["all_models_loaded"].empty()) {
            std::cout << std::endl;
            std::cout << std::left
                      << std::setw(30) << "Model"
                      << std::setw(10) << "Type"
                      << std::setw(10) << "Device"
                      << std::setw(14) << "Recipe"
                      << std::setw(9) << "Ctx"
                      << std::setw(10) << "Status"
                      << "Checkpoint" << std::endl;
            std::cout << std::string(119, '-') << std::endl;

            for (const auto& model : json_response["all_models_loaded"]) {
                if (!model.is_object()) continue;

                std::string model_name = model.value("model_name", "-");
                if (model.value("pinned", false)) {
                    model_name += " (pinned)";
                }

                const json recipe_options = model.value("recipe_options", json::object());
                std::string ctx_size = "-";
                if (recipe_options.contains("ctx_size") && recipe_options["ctx_size"].is_number()) {
                    ctx_size = std::to_string(recipe_options["ctx_size"].get<int>());
                }

                std::cout << std::left
                          << std::setw(30) << model_name
                          << std::setw(10) << model.value("type", "-")
                          << std::setw(10) << model.value("device", "-")
                          << std::setw(14) << model.value("recipe", "-")
                          << std::setw(9) << ctx_size
                          << std::setw(10) << model.value("status", "-")
                          << model.value("checkpoint", "-") << std::endl;
            }
        } else {
            std::cout << std::endl;
            std::cout << "No models loaded." << std::endl;
        }

        return 0;

    } catch (const json::exception& e) {
        return print_health_fetch_error(e);
    } catch (const HttpError& e) {
        return print_health_fetch_error(e);
    } catch (const std::exception& e) {
        return print_health_fetch_error(e);
    }
}

int LemonadeClient::status_json(int display_port) const {
    try {
        auto health = fetch_health();

        json out;
        out["port"] = display_port > 0 ? display_port : port_;
        out["version"] = health.value("version", "");
        out["websocket_port"] = health.value("websocket_port", 0);
        out["models"] = json::array();

        for (const auto& model : health.value("all_models_loaded", json::array())) {
            if (!model.is_object()) continue;

            json entry;
            entry["model_name"] = model.value("model_name", "");
            entry["checkpoint"] = model.value("checkpoint", "");
            entry["type"] = model.value("type", "");
            entry["device"] = model.value("device", "");
            entry["recipe"] = model.value("recipe", "");
            entry["recipe_options"] = model.value("recipe_options", json::object());
            entry["status"] = model.value("status", "");
            entry["pinned"] = model.value("pinned", false);
            entry["pid"] = model.value("pid", 0);
            entry["backend_url"] = model.value("backend_url", "");
            out["models"].push_back(entry);
        }

        std::cout << out.dump(2) << std::endl;
        return 0;

    } catch (const json::exception& e) {
        return print_health_fetch_error(e);
    } catch (const HttpError& e) {
        return print_health_fetch_error(e);
    } catch (const std::exception& e) {
        return print_health_fetch_error(e);
    }
}

//Helper functions to calculate the total size of the models in a collection.
static double get_collection_component_size(const json& model) {
    if (model.contains("recipe") && model["recipe"].is_string() && model["recipe"].get<std::string>() == "cloud") {
        return UNKNOWN_MODEL_SIZE;
    }
    if (model.contains("size") &&
            model["size"].is_number()) {
        return model["size"].get<double>();
    }
    return UNKNOWN_MODEL_SIZE;
}

static std::vector<double> get_collection_sizes(const json& collection_components, const json& server_models) {
    std::vector<double> collection_sizes;
    double component_size = UNKNOWN_MODEL_SIZE;
    for (const auto component : collection_components){
        component_size = UNKNOWN_MODEL_SIZE;
        for (const auto& model : server_models) {
            if (model.contains("id") && model["id"].get<std::string>() == component) {
                component_size = get_collection_component_size(model);
                break;
            }
        }
        collection_sizes.push_back(component_size);
    }
    return collection_sizes;
}

static std::string model_size_to_str(const ModelInfo& model) {
    double size = UNKNOWN_MODEL_SIZE;
    bool is_aprox_size = false;

    for(double component_size : model.component_sizes) {
        if (component_size == UNKNOWN_MODEL_SIZE) {
            is_aprox_size = true;
        } else {
            size += component_size;
        }
    }
    std::ostringstream os;
    if (size == UNKNOWN_MODEL_SIZE) {
        os << "N/A";
    } else {
        if (is_aprox_size) {
            os << ">";
        }
        os << std::fixed << std::setprecision(2) << size;
    }
    return os.str();
}

std::vector<ModelInfo> LemonadeClient::get_models(bool show_all) const {
    std::vector<ModelInfo> models;

    try {
        std::string response = make_request("/api/v1/models?show_all=" + std::string(show_all ? "true" : "false"));
        auto json_response = json::parse(response);

        if (!json_response.contains("data") || !json_response["data"].is_array()) {
            return models;
        }

        for (const auto& model_item : json_response["data"]) {
            ModelInfo info;

            if (model_item.contains("id") && model_item["id"].is_string()) {
                info.id = model_item["id"].get<std::string>();
            }

            if (model_item.contains("checkpoint") && model_item["checkpoint"].is_string()) {
                info.checkpoint = model_item["checkpoint"].get<std::string>();
            }

            if (model_item.contains("recipe") && model_item["recipe"].is_string()) {
                info.recipe = model_item["recipe"].get<std::string>();
            }

            if (model_item.contains("downloaded") && model_item["downloaded"].is_boolean()) {
                info.downloaded = model_item["downloaded"].get<bool>();
            }

            if (model_item.contains("suggested") && model_item["suggested"].is_boolean()) {
                info.suggested = model_item["suggested"].get<bool>();
            }

            if (model_item.contains("labels") && model_item["labels"].is_array()) {
                for (const auto& label : model_item["labels"]) {
                    if (label.is_string()) {
                        info.labels.push_back(label.get<std::string>());
                    }
                }
            }
            if (model_item.contains("components") && model_item["components"].is_array() && !model_item["components"].empty()) {
                info.component_sizes=get_collection_sizes(model_item["components"], json_response["data"]);
            } else {
                info.component_sizes.push_back(get_collection_component_size(model_item));
            }

            if (!info.id.empty()) {
                models.push_back(info);
            }
        }

    } catch (const HttpError& e) {
        std::cerr << "Error listing models: " << extract_server_error_message(e) << std::endl;
        return {};
    } catch (const json::exception& e) {
        std::cerr << "Error parsing models JSON: " << e.what() << std::endl;
    }

    return models;
}

int LemonadeClient::list_models(bool show_all, const std::string& name_filter) const {
    try {
        std::vector<ModelInfo> models = get_models(show_all);

        if (!name_filter.empty()) {
            const std::regex filter_regex = build_name_filter_regex(name_filter);
            models.erase(
                std::remove_if(models.begin(), models.end(),
                    [&](const ModelInfo& m) {
                        return !std::regex_search(m.id, filter_regex) &&
                               !std::regex_search(strip_canonical_prefix(m.id), filter_regex);
                    }),
                models.end());
        }

        std::sort(models.begin(), models.end(),
            [](const ModelInfo& a, const ModelInfo& b) {
                const std::string bare_a = strip_canonical_prefix(a.id);
                const std::string bare_b = strip_canonical_prefix(b.id);
                const int bare_compare = bare_a.compare(bare_b);
                if (bare_compare != 0) return bare_compare < 0;

                const int source_compare = model_source_sort_rank(a.id) - model_source_sort_rank(b.id);
                if (source_compare != 0) return source_compare < 0;

                return a.id < b.id;
            });

        if (models.empty()) {
            std::cout << (show_all ? "No models available" : "No local models downloaded.") << std::endl;
            return 0;
        }

        // Helper lambda to print a formatted table of models.
        auto print_model_table = [](const std::vector<ModelInfo>& models) {
            std::cout << std::left << std::setw(40) << "Model Name"
                      << std::setw(15) << "Downloaded"
                      << std::setw(15) << "Size (GB)"
                      << "Details" << std::endl;
            std::cout << std::string(100, '-') << std::endl;

            // Model Name is the API id emitted verbatim by `/v1/models`. For each
            // bare name, the precedence-winning source (registered > imported >
            // builtin) shows as the bare name; any shadowed sources show as their
            // canonical id (user.NAME / extra.NAME / builtin.NAME). Either form is
            // valid input to `lemonade load`, `lemonade delete`, etc., so the
            // column is always copy-paste-safe.
            for (const auto& model : models) {
                std::string downloaded = model.downloaded ? "Yes" : "No";
                std::string details = model.recipe.empty() ? "-" : model.recipe;
                std::cout   << std::left << std::setw(40) << model.id
                            << std::setw(15) << downloaded;
                std::cout   << std::right << std::setw(8) << model_size_to_str(model) << std::setw(7) << " ";
                std::cout   << std::setw(20) << std::left << details << std::endl;
            }

            std::cout << std::string(100, '-') << std::endl;
        };

        if (!show_all) {
            print_model_table(models);
            return 0;
        }

        std::vector<ModelInfo> local_models;
        std::vector<ModelInfo> available_models;
        for (const auto& model : models) {
            if (model.downloaded) {
                local_models.push_back(model);
            } else {
                available_models.push_back(model);
            }
        }

        std::cout << "Local" << std::endl;
        if (local_models.empty()) {
            std::cout << "No local models downloaded." << std::endl;
        } else {
            print_model_table(local_models);
        }

        std::cout << std::endl;

        std::cout << "Available for Download" << std::endl;
        if (available_models.empty()) {
            std::cout << "No models available for download." << std::endl;
        } else {
            print_model_table(available_models);
        }

        return 0;

    } catch (const HttpError& e) {
        std::cerr << "Error listing models: " << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error listing models: " << e.what() << std::endl;
        return 1;
    }
}

// Helper function to parse SSE progress events
static std::string format_speed(double bytes_per_sec) {
    if (bytes_per_sec < 1024.0) {
        return std::to_string(static_cast<int>(bytes_per_sec)) + " B/s";
    } else if (bytes_per_sec < 1024.0 * 1024.0) {
        std::ostringstream oss;
        oss << std::fixed << std::setprecision(1) << (bytes_per_sec / 1024.0) << " KB/s";
        return oss.str();
    } else if (bytes_per_sec < 1024.0 * 1024.0 * 1024.0) {
        std::ostringstream oss;
        oss << std::fixed << std::setprecision(1) << (bytes_per_sec / (1024.0 * 1024.0)) << " MB/s";
        return oss.str();
    } else {
        std::ostringstream oss;
        oss << std::fixed << std::setprecision(1) << (bytes_per_sec / (1024.0 * 1024.0 * 1024.0)) << " GB/s";
        return oss.str();
    }
}

static bool parse_sse_progress(const std::string& event_data, StreamingRequestState& state) {
    std::string& last_file = state.last_file;
    int& last_percent = state.last_percent;
    std::string& error_message = state.error_message;
    bool& total_size_printed = state.total_size_printed;
    uint64_t& last_file_size = state.last_file_size;
    try {
        auto json_data = json::parse(event_data);

        if (json_data.contains("file") && json_data["file"].is_string()) {
            std::string file = json_data["file"].get<std::string>();
            int file_index = json_data.value("file_index", 0);
            int total_files = json_data.value("total_files", 0);
            // Use uint64_t explicitly to avoid JSON type inference issues with large numbers
            uint64_t bytes_downloaded = json_data.value("bytes_downloaded", (uint64_t)0);
            uint64_t bytes_total = json_data.value("bytes_total", (uint64_t)0);
            uint64_t bytes_prev = json_data.value("bytes_previously_downloaded", (uint64_t)0);
            uint64_t total_download_size = json_data.value("total_download_size", (uint64_t)0);

            // Print total download size once on first event
            if (!total_size_printed && total_download_size > 0 && total_files > 0) {
                double size_gb = total_download_size / (1024.0 * 1024.0 * 1024.0);
                std::cout << "Total: " << std::fixed << std::setprecision(1)
                          << size_gb << " GB, " << total_files << " files" << std::endl;
                total_size_printed = true;
            }

            if (file != last_file) {
                // First event for this file — remember its size for progress display
                last_file_size = bytes_total;

                // Add newline after progress bar (carriage-return based), but not after "(already downloaded)"
                if (!last_file.empty() && last_percent != 100) {
                    std::cout << std::endl;
                }
                std::cout << "[" << file_index << "/" << total_files << "] " << file;
                if (bytes_total > 0) {
                    std::cout << " (" << std::fixed << std::setprecision(1)
                            << (bytes_total / (1024.0 * 1024.0)) << " MB)";
                }

                // Check if already downloaded: bytes_prev equals the KNOWN file size
                bool is_already_downloaded = bytes_prev > 0 && bytes_prev == bytes_total;
                if (is_already_downloaded) {
                    std::cout << " (already downloaded)";
                    last_percent = 100;
                } else {
                    last_percent = -1;
                    state.file_start_time = std::chrono::steady_clock::now();
                    state.file_bytes_resumed = bytes_prev;
                }
                std::cout << std::endl;
                last_file = file;
            } else if (last_percent != 100 && bytes_prev > 0 && bytes_prev == last_file_size) {
                // Completion event: bytes_prev matches known file size (not a redirect artifact)
                std::cout << "\r" << std::string(80, ' ') << "\r  (already downloaded)" << std::endl;
                last_percent = 100;
            }

            // Show progress bar using the known file size as denominator
            if (last_percent != 100 && last_file_size > 0) {
                int display_percent = static_cast<int>((bytes_downloaded * 100) / last_file_size);
                if (display_percent > 100) display_percent = 100;
                if (display_percent != last_percent) {
                    // Calculate speed from session bytes only (exclude resumed bytes)
                    std::string speed_str;
                    auto elapsed = std::chrono::steady_clock::now() - state.file_start_time;
                    double elapsed_sec = std::chrono::duration<double>(elapsed).count();
                    if (elapsed_sec > 0.5 && bytes_downloaded > state.file_bytes_resumed) {
                        double speed = (bytes_downloaded - state.file_bytes_resumed) / elapsed_sec;
                        speed_str = " " + format_speed(speed);
                    }

                    std::cout << "\r  Progress: " << display_percent << "% ("
                            << std::fixed << std::setprecision(1)
                            << (bytes_downloaded / (1024.0 * 1024.0)) << "/"
                            << (last_file_size / (1024.0 * 1024.0)) << " MB)"
                            << speed_str << "    " << std::flush;
                    last_percent = display_percent;
                }
            }
        }

        if (json_data.contains("error") && json_data["error"].is_string()) {
            error_message = json_data["error"].get<std::string>();
        }

        return json_data.contains("complete");
    } catch (const json::exception&) {
        return false;
    }
}

int LemonadeClient::pull_model(const json& model_data, const std::string& display_name, bool upgrade) {
    try {
        // Validate that model field exists in model_data
        if (!model_data.contains("model_name") || !model_data["model_name"].is_string()) {
            std::cerr << "Error: 'model_name' field is required in model_data" << std::endl;
            return 1;
        }

        std::string model_name = model_data["model_name"].get<std::string>();
        std::string output_name = display_name.empty() ? model_name : display_name;
        std::cout << "Pulling model: " << output_name << std::endl;

        json request_body = model_data;
        request_body["stream"] = true;

        // Cache-first by default: an already-downloaded model is reused instead
        // of triggering a remote-registry update check (and a possible full
        // re-download). Only the explicit `lemonade pull` update flow opts into
        // an upgrade. An explicit field already in model_data wins.
        if (!request_body.contains("do_not_upgrade")) {
            request_body["do_not_upgrade"] = !upgrade;
        }

        std::string body = request_body.dump();

        StreamingRequestState state;

        make_request("/api/v1/pull", "POST", body, "application/json",
        [&](const std::string& event_type, const std::string& event_data) {
            if (event_type == "complete") {
                std::cout << std::endl;
                state.success = true;
            } else if (event_type == "error") {
                try {
                    auto error_json = json::parse(event_data);
                    if (error_json.contains("error") && error_json["error"].is_string()) {
                        state.error_message = error_json["error"].get<std::string>();
                    }
                    if (error_json.contains("code") && error_json["code"].is_string()) {
                        state.error_code = error_json["code"].get<std::string>();
                    }
                } catch (...) {
                    state.error_message = event_data;
                }
            } else {
                parse_sse_progress(event_data, state);
            }
        }, LONG_TIMEOUT_MS, LONG_TIMEOUT_MS);

        if (!state.success) {
            // Wire-protocol constant; server-side definition and contract live in
            // include/lemon/model_manager.h (kUnknownModelErrorCode). Keep in sync.
            if (state.error_code == "unknown_model") {
                state.error_message =
                    "No built-in model with the name '" + model_name + "' is registered.\n\n"
                    "If you meant a built-in model, run `lemonade list` to see available models.\n"
                    "If you meant to add a custom model from Hugging Face or ModelScope, run `lemonade pull CHECKPOINT`.";
            }

            if (!state.error_message.empty()) {
                throw std::runtime_error(state.error_message);
            }

            throw std::runtime_error("Model pull failed");
        }

        std::cout << "Model pulled successfully: " << output_name << std::endl;
        return 0;
    } catch (const HttpError& e) {
        std::cerr << "Error pulling model: " << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error pulling model: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::delete_model(const std::string& model_name) const {
    std::cout << "Deleting model: " << model_name << std::endl;

    try {
        json request_body = {{"model_name", model_name}};
        std::string response = make_request("/api/v1/delete", "POST", request_body.dump(), "application/json");

        auto response_json = json::parse(response);
        if (response_json.contains("status") && response_json["status"] == "success") {
            std::cout << "Model deleted successfully: " << model_name << std::endl;
            return 0;
        } else {
            std::cerr << "Failed to delete model" << std::endl;
            return 1;
        }

    } catch (const HttpError& e) {
        std::cerr << "Error deleting model: " << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const json::exception& e) {
        std::cerr << "Error parsing delete response JSON: " << e.what() << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error deleting model: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::cleanup_cache(bool dry_run) const {
    std::cout << (dry_run ? "Previewing" : "Running") << " cache cleanup..." << std::endl;

    try {
        json request_body = {{"dry_run", dry_run}};
        std::string response = make_request("/internal/cleanup-cache", "POST",
            request_body.dump(), "application/json", 30, 300);

        auto result = json::parse(response);

        if (result.contains("error")) {
            std::cerr << "Error: " << result["error"].value("message", "Unknown error") << std::endl;
            return 1;
        }

        auto orphaned = result.value("orphaned_files", json::array());
        size_t total_bytes = result.value("total_bytes", 0);

        if (orphaned.empty()) {
            std::cout << "No orphaned files found. Cache is clean." << std::endl;
            return 0;
        }

        for (const auto& file : orphaned) {
            std::string path = file.value("path", "");
            size_t size = file.value("size", 0);
            std::string model = file.value("model", "");
            double size_mb = size / (1024.0 * 1024.0);
            std::cout << "  " << path << " (" << std::fixed << std::setprecision(1) << size_mb << " MB)"
                      << " [from " << model << "]" << std::endl;
        }

        double total_mb = total_bytes / (1024.0 * 1024.0);
        if (dry_run) {
            std::cout << "\nWould free " << std::fixed << std::setprecision(1) << total_mb << " MB from "
                      << orphaned.size() << " file(s). Run without --dry-run to delete." << std::endl;
        } else {
            std::cout << "\nFreed " << std::fixed << std::setprecision(1) << total_mb << " MB from "
                      << orphaned.size() << " file(s)." << std::endl;
        }

        return 0;
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::load_model(const std::string& model_name, const nlohmann::json& recipe_options, bool save_options, std::optional<bool> pinned) const {
    std::cout << "Loading model: " << model_name << std::endl;

    try {
        json request_body = recipe_options;
        request_body["model_name"] = model_name;
        request_body["save_options"] = save_options;
        if (pinned.has_value()) {
            request_body["pinned"] = pinned.value();
        }

        // since load can trigger a pull but doesn't send the related streaming events, we want long read timeouts.
        make_request("/api/v1/load", "POST", request_body.dump(), "application/json", LONG_TIMEOUT_MS, LONG_TIMEOUT_MS);

        std::cout << "Model loaded successfully!" << std::endl;
        return 0;

    } catch (const HttpError& e) {
        std::cerr << "Error loading model: " << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error loading model: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::pin_model(const std::string& model_name, bool pinned) const {
    try {
        json request_body = {{"model_name", model_name}, {"pinned", pinned}};
        make_request("/internal/pin", "POST", request_body.dump(), "application/json");
        std::cout << "Model " << (pinned ? "pinned" : "unpinned") << " successfully!" << std::endl;
        return 0;
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::unload_model(const std::string& model_name) const {
    try {
        json request_body = {};

        if (model_name.empty()) {
            std::cout << "Unloading all models" << std::endl;
        } else {
            std::cout << "Unloading model: " << model_name << std::endl;
            request_body["model_name"] = model_name;
        }

        make_request("/api/v1/unload", "POST", request_body.dump(), "application/json");

        std::cout << "Model unloaded successfully!" << std::endl;
        return 0;

    } catch (const HttpError& e) {
        std::cerr << "Error unloading model: " << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error unloading model: " << e.what() << std::endl;
        return 1;
    }
}

nlohmann::json LemonadeClient::get_model_info(const std::string& model_name) const {
    try {
        std::string response = make_request("/api/v1/models/" + model_name);
        return json::parse(response);
    } catch (const HttpError& e) {
        std::cerr << "Error fetching model info: " << extract_server_error_message(e) << std::endl;
        return json{};
    } catch (const json::exception& e) {
        std::cerr << "Error parsing model info JSON: " << e.what() << std::endl;
        return json{};
    } catch (const std::exception& e) {
        std::cerr << "Error fetching model info: " << e.what() << std::endl;
        return json{};
    }
}

int LemonadeClient::list_recipes(bool show_all) const {
    try {
        std::string response = make_request("/api/v1/system-info");
        auto json_response = json::parse(response);

        std::vector<RecipeStatus> recipes;

        if (!json_response.contains("recipes") || !json_response["recipes"].is_object()) {
            std::cout << "No recipes available" << std::endl;
            return 0;
        }

        // Hidden unless --all (see /system-info's unavailable_recipes).
        std::set<std::string> unavailable_recipes;
        if (json_response.contains("unavailable_recipes") &&
            json_response["unavailable_recipes"].is_array()) {
            for (const auto& r : json_response["unavailable_recipes"]) {
                if (r.is_string()) unavailable_recipes.insert(r.get<std::string>());
            }
        }

        for (const auto& [recipe_name, recipe_data] : json_response["recipes"].items()) {
            if (!show_all && unavailable_recipes.count(recipe_name)) {
                continue;
            }

            RecipeStatus status;
            status.name = recipe_name;

            if (recipe_data.contains("backends") && recipe_data["backends"].is_object()) {
                for (const auto& [backend_name, backend_data] : recipe_data["backends"].items()) {
                    BackendStatus backend;
                    backend.name = backend_name;

                    if (backend_data.contains("state") && backend_data["state"].is_string()) {
                        backend.state = backend_data["state"].get<std::string>();
                    }

                    if (backend_data.contains("version") && backend_data["version"].is_string()) {
                        backend.version = backend_data["version"].get<std::string>();
                    }

                    if (backend_data.contains("message") && backend_data["message"].is_string()) {
                        backend.message = backend_data["message"].get<std::string>();
                    }

                    if (backend_data.contains("action") && backend_data["action"].is_string()) {
                        backend.action = backend_data["action"].get<std::string>();
                    }

                    status.backends.push_back(backend);
                }
            }

            recipes.push_back(status);
        }

        std::cout << std::left << std::setw(20) << "Recipe"
                  << std::setw(12) << "Backend"
                  << std::setw(16) << "Status"
                  << std::setw(46) << "Message/Version"
                  << "Action" << std::endl;
        std::cout << std::string(148, '-') << std::endl;

        for (const auto& recipe : recipes) {
            bool first_backend = true;

            if (recipe.backends.empty()) {
                if (show_all) {
                    std::cout << std::left << std::setw(20) << recipe.name
                            << std::setw(12) << "-"
                            << std::setw(16) << "unsupported"
                            << std::setw(46) << "No backend definitions"
                            << "-" << std::endl;
                }
            } else {
                for (const auto& backend : recipe.backends) {
                    std::string recipe_col = first_backend ? recipe.name : "";
                    std::string status_str = backend.state.empty() ? "unsupported" : backend.state;

                    std::string info_col;
                    if (status_str == "installed" && !backend.version.empty() && backend.version != "unknown") {
                        info_col = backend.version;
                    } else if (!backend.message.empty()) {
                        info_col = backend.message;
                    } else {
                        info_col = "-";
                    }
                    std::string action_col = backend.action.empty() ? "-" : backend.action;
                    if (show_all || status_str != "unsupported") {
                        std::cout << std::left << std::setw(20) << recipe_col
                                << std::setw(12) << backend.name
                                << std::setw(16) << status_str
                                << std::setw(46) << info_col
                                << " " << action_col << std::endl;

                        first_backend = false;
                    }
                }
            }
        }

        std::cout << std::string(148, '-') << std::endl;
        return 0;

    } catch (const HttpError& e) {
        std::cerr << "Error listing recipes: " << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error listing recipes: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::install_backend(const std::string& recipe, const std::string& backend, bool force) {
    std::cout << "Installing backend: " << recipe << ":" << backend << std::endl;

    try {
        json request_body = {{"recipe", recipe}, {"backend", backend}};
        request_body["stream"] = true;
        request_body["force"] = force;
        std::string body = request_body.dump();

        StreamingRequestState state;

        make_request("/api/v1/install", "POST", body, "application/json",
        [&](const std::string& event_type, const std::string& event_data) {
            if (event_type == "complete") {
                std::cout << std::endl;
                state.success = true;
            } else if (event_type == "error") {
                // Server sent an explicit error event
                try {
                    auto error_json = json::parse(event_data);
                    if (error_json.contains("error")) {
                        state.error_message = error_json["error"].get<std::string>();
                    }
                } catch (...) {
                    state.error_message = event_data;
                }
            } else {
                parse_sse_progress(event_data, state);
            }
        }, LONG_TIMEOUT_MS, LONG_TIMEOUT_MS);
        if (!state.success) {
            if (!state.error_message.empty()) {
                throw std::runtime_error(state.error_message);
            }
            throw std::runtime_error("Backend installation failed (no details from server)");
        }

        std::cout << "Backend installed successfully: " << recipe << ":" << backend << std::endl;
        return 0;
    } catch (const HttpError& e) {
        std::cerr << "Error: " << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::uninstall_backend(const std::string& recipe, const std::string& backend) {
    std::cout << "Uninstalling backend: " << recipe << ":" << backend << std::endl;

    try {
        json request_body = {{"recipe", recipe}, {"backend", backend}};
        std::string response = make_request("/api/v1/uninstall", "POST", request_body.dump(), "application/json");

        auto response_json = json::parse(response);
        if (response_json.contains("status") && response_json["status"] == "success") {
            std::cout << "Backend uninstalled successfully: " << recipe << ":" << backend << std::endl;
            return 0;
        } else {
            std::cerr << "Uninstall failed: " << response << std::endl;
            return 1;
        }

    } catch (const HttpError& e) {
        std::cerr << "Error uninstalling backend: " << extract_server_error_message(e)
                  << std::endl;
        return 1;
    } catch (const json::exception& e) {
        std::cerr << "Error parsing uninstall response JSON: " << e.what() << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error uninstalling backend: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::install_cloud_provider(const std::string& provider,
                                            const std::string& base_url,
                                            const std::string& api_key,
                                            bool allow_insecure_http,
                                            const std::optional<std::string>& auth_header_name,
                                            const std::optional<std::string>& auth_header_prefix,
                                            const std::optional<std::string>& wire_format) {
    std::cout << "Installing cloud provider: " << provider
              << " (" << base_url << ")" << std::endl;
    try {
        json body = {
            {"backend", "cloud"},
            {"provider", provider},
            {"base_url", base_url}
        };
        if (allow_insecure_http) {
            body["allow_insecure_http"] = true;
        }
        if (!api_key.empty()) {
            body["api_key"] = api_key;
        }
        if (auth_header_name) {
            body["auth_header_name"] = *auth_header_name;
        }
        if (auth_header_prefix) {
            body["auth_header_prefix"] = *auth_header_prefix;
        }
        if (wire_format) {
            body["wire_format"] = *wire_format;
        }
        std::string response = make_request("/api/v1/install", "POST",
                                             body.dump(), "application/json");
        auto response_json = json::parse(response);
        if (response_json.value("status", "") != "success") {
            std::cerr << "Install failed: " << response << std::endl;
            return 1;
        }
        std::cout << "Cloud provider installed: " << provider << std::endl;
        if (response_json.contains("auth_state")) {
            const auto& s = response_json["auth_state"];
            bool env = s.value("env_var_set", false);
            bool rt = s.value("runtime_key_set", false);
            std::cout << "  env var set: " << (env ? "yes" : "no")
                      << ", runtime key set: " << (rt ? "yes" : "no")
                      << std::endl;
        }
        if (response_json.contains("models_discovered")) {
            std::cout << "  models discovered: "
                      << response_json["models_discovered"].get<size_t>()
                      << std::endl;
        }
        print_response_warnings(response_json);
        return 0;
    } catch (const HttpError& e) {
        std::cerr << "Error installing cloud provider: "
                  << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error installing cloud provider: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::uninstall_cloud_provider(const std::string& provider) {
    std::cout << "Uninstalling cloud provider: " << provider << std::endl;
    try {
        json body = {{"backend", "cloud"}, {"provider", provider}};
        std::string response = make_request("/api/v1/uninstall", "POST",
                                             body.dump(), "application/json");
        auto response_json = json::parse(response);
        if (response_json.value("status", "") != "success") {
            std::cerr << "Uninstall failed: " << response << std::endl;
            return 1;
        }
        std::cout << "Cloud provider uninstalled: " << provider
                  << " (evicted "
                  << response_json.value("models_evicted", size_t{0})
                  << " models)" << std::endl;
        return 0;
    } catch (const HttpError& e) {
        std::cerr << "Error uninstalling cloud provider: "
                  << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error uninstalling cloud provider: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::cloud_auth(const std::string& provider,
                               const std::string& api_key,
                               bool allow_insecure_http) {
    try {
        json body = {{"provider", provider}, {"api_key", api_key}};
        if (allow_insecure_http) {
            body["allow_insecure_http"] = true;
        }
        std::string response = make_request("/api/v1/cloud/auth", "POST",
                                             body.dump(), "application/json");
        auto response_json = json::parse(response);
        std::cout << "Cloud auth set for: " << provider << std::endl;
        if (response_json.contains("models_discovered")) {
            std::cout << "  models discovered: "
                      << response_json["models_discovered"].get<size_t>()
                      << std::endl;
        }
        print_response_warnings(response_json);
        return 0;
    } catch (const HttpError& e) {
        // 409 (env conflict) and 404 (not installed) come through here with
        // a structured error body — extract_server_error_message pulls the
        // message field out.
        std::cerr << "Error setting cloud auth: "
                  << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error setting cloud auth: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::cloud_auth_clear(const std::string& provider) {
    try {
        std::string response = make_request("/api/v1/cloud/auth/" + provider,
                                             "DELETE", "", "");
        auto response_json = json::parse(response);
        bool cleared = response_json.value("cleared_runtime_key", false);
        std::cout << "Cloud auth cleared for: " << provider
                  << (cleared ? "" : " (no runtime key was set)") << std::endl;
        return 0;
    } catch (const HttpError& e) {
        std::cerr << "Error clearing cloud auth: "
                  << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error clearing cloud auth: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::cloud_list(bool json_output) const {
    try {
        std::string response = make_request("/api/v1/system-info", "GET");
        auto info = json::parse(response);
        json providers = json::array();
        if (info.contains("cloud") && info["cloud"].contains("providers") &&
            info["cloud"]["providers"].is_array()) {
            providers = info["cloud"]["providers"];
        }
        if (json_output) {
            std::cout << providers.dump() << std::endl;
            return 0;
        }
        if (providers.empty()) {
            std::cout << "No cloud providers installed." << std::endl;
            return 0;
        }
        std::cout << "Cloud providers:" << std::endl;
        for (const auto& p : providers) {
            std::cout << "  " << p.value("name", "")
                      << "  " << p.value("base_url", "")
                      << "  [env_var=" << p.value("env_var", "") << "]"
                      << std::endl;
            std::cout << "    auth: "
                      << "env_var_set=" << (p.value("env_var_set", false) ? "yes" : "no")
                      << ", runtime_key_set=" << (p.value("runtime_key_set", false) ? "yes" : "no")
                      << ", models_discovered=" << p.value("models_discovered", size_t{0})
                      << std::endl;
            const std::string header_name = p.value("auth_header_name", std::string("Authorization"));
            const std::string header_prefix = p.value("auth_header_prefix", std::string("Bearer "));
            if (header_name != "Authorization" || header_prefix != "Bearer ") {
                // Quoted so a trailing space or a deliberately empty prefix is
                // visible rather than indistinguishable from the default.
                std::cout << "    auth header: " << header_name
                          << ": \"" << header_prefix << "\"<key>" << std::endl;
            }
            const std::string wire_format = p.value("wire_format", std::string("openai"));
            if (wire_format != "openai") {
                std::cout << "    wire format: " << wire_format << std::endl;
            }
            print_response_warnings(p, "    ");
        }
        return 0;
    } catch (const HttpError& e) {
        std::cerr << "Error listing cloud providers: "
                  << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error listing cloud providers: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::alias_add(const std::string& alias, const std::string& target_model) const {
    try {
        json req_body = {{"alias", alias}, {"target", target_model}};
        std::string response = make_request("/internal/aliases", "POST", req_body.dump());
        auto res = json::parse(response);
        std::cout << "✓ Created alias '" << alias << "' -> '" << res.value("target", target_model) << "'" << std::endl;
        return 0;
    } catch (const HttpError& e) {
        std::cerr << "Error creating alias: " << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error creating alias: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::alias_remove(const std::string& alias) const {
    try {
        std::string path = "/internal/aliases/" + lemon::utils::url_encode(alias);
        make_request(path, "DELETE");
        std::cout << "✓ Removed alias '" << alias << "'" << std::endl;
        return 0;
    } catch (const HttpError& e) {
        std::cerr << "Error removing alias: " << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error removing alias: " << e.what() << std::endl;
        return 1;
    }
}

int LemonadeClient::alias_list() const {
    try {
        std::string response = make_request("/internal/aliases", "GET");
        auto res = json::parse(response);
        if (!res.contains("aliases") || res["aliases"].empty()) {
            std::cout << "No model aliases registered." << std::endl;
            return 0;
        }

        std::cout << "Registered model aliases:" << std::endl;
        for (const auto& item : res["aliases"]) {
            std::string alias = item.value("alias", "");
            std::string target = item.value("target", "");
            std::string recipe = item.value("recipe", "");
            bool downloaded = item.value("downloaded", false);

            std::cout << "  " << alias << " -> " << target
                      << " [" << recipe << ", downloaded=" << (downloaded ? "yes" : "no") << "]"
                      << std::endl;
        }
        return 0;
    } catch (const HttpError& e) {
        std::cerr << "Error listing aliases: " << extract_server_error_message(e) << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "Error listing aliases: " << e.what() << std::endl;
        return 1;
    }
}

} // namespace lemonade
