// Reverse-proxy routes for the 4 sidecar services. Uses cpp-httplib as the
// upstream client (single-header, no extra system packages).
//
// Upstream URLs are read from env at startup; missing envs make that prefix
// return 503 so the SPA can render a graceful "service unavailable" state.

#define CPPHTTPLIB_OPENSSL_SUPPORT 0
#include <httplib.h>

#include "crow.h"
#include "routes.h"

#include <cstdlib>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>

namespace routes {

namespace {

struct Upstream {
    std::string scheme;   // "http" or "https"
    std::string host;
    int         port = 0;
};

std::optional<Upstream> parse_upstream(const std::string& url) {
    // Expect: http(s)://host[:port]
    if (url.empty()) return std::nullopt;
    Upstream up;
    std::string rest;
    if (url.rfind("https://", 0) == 0) {
        up.scheme = "https";
        rest = url.substr(8);
        up.port = 443;
    } else if (url.rfind("http://", 0) == 0) {
        up.scheme = "http";
        rest = url.substr(7);
        up.port = 80;
    } else {
        return std::nullopt;
    }
    auto slash = rest.find('/');
    std::string hostport = slash == std::string::npos ? rest : rest.substr(0, slash);
    auto colon = hostport.find(':');
    if (colon == std::string::npos) {
        up.host = hostport;
    } else {
        up.host = hostport.substr(0, colon);
        try { up.port = std::stoi(hostport.substr(colon + 1)); } catch (...) {}
    }
    if (up.host.empty()) return std::nullopt;
    return up;
}

std::optional<Upstream> env_upstream(const char* var) {
    const char* v = std::getenv(var);
    if (!v || !*v) return std::nullopt;
    return parse_upstream(std::string(v));
}

const std::unordered_set<std::string>& hop_by_hop() {
    static const std::unordered_set<std::string> h = {
        "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
        "te", "trailers", "transfer-encoding", "upgrade", "host",
        "content-length",
    };
    return h;
}

std::string to_lower(std::string s) {
    for (auto& c : s) c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
    return s;
}

crow::response service_unavailable(const std::string& name) {
    crow::json::wvalue j;
    j["error"] = "upstream_unavailable";
    j["service"] = name;
    j["hint"] = "set CXAI_" + name + "_UPSTREAM env var (e.g. http://cxai-" + to_lower(name) + ":8101)";
    auto resp = crow::response(503, j);
    resp.add_header("Content-Type", "application/json");
    return resp;
}

// Strip the "/api/<service>" prefix and return the remainder (always starts with '/').
std::string strip_prefix(const std::string& url, const std::string& prefix) {
    if (url.size() < prefix.size() || url.compare(0, prefix.size(), prefix) != 0) {
        return "/";
    }
    std::string rest = url.substr(prefix.size());
    if (rest.empty() || rest.front() != '/') rest = "/" + rest;
    return rest;
}

// Build an httplib::Client per request (cheap; keeps things thread-safe).
httplib::Client make_client(const Upstream& up) {
    httplib::Client cli(up.host, up.port);
    cli.set_connection_timeout(5, 0);
    cli.set_read_timeout(120, 0);
    cli.set_write_timeout(30, 0);
    return cli;
}

httplib::Headers forward_headers(const crow::request& req) {
    httplib::Headers h;
    for (const auto& kv : req.headers) {
        std::string k = to_lower(kv.first);
        if (hop_by_hop().count(k)) continue;
        h.emplace(kv.first, kv.second);
    }
    return h;
}

crow::response from_httplib(const httplib::Result& r, const std::string& service) {
    if (!r) {
        crow::json::wvalue j;
        j["error"] = "upstream_error";
        j["service"] = service;
        j["detail"] = httplib::to_string(r.error());
        auto resp = crow::response(502, j);
        resp.add_header("Content-Type", "application/json");
        return resp;
    }
    crow::response out(r->status, r->body);
    for (const auto& kv : r->headers) {
        std::string k = to_lower(kv.first);
        if (hop_by_hop().count(k)) continue;
        out.add_header(kv.first, kv.second);
    }
    return out;
}

crow::response forward(const Upstream& up, const std::string& service,
                       const crow::request& req, const std::string& path) {
    auto cli = make_client(up);
    auto headers = forward_headers(req);
    std::string upstream_path = path;
    if (!req.url_params.get(nullptr)) {
        // url_params has no direct stringify; reconstruct from raw url if present.
    }
    // Crow stores the query string inside `req.raw_url`; append it.
    auto qpos = req.raw_url.find('?');
    if (qpos != std::string::npos) {
        upstream_path += req.raw_url.substr(qpos);
    }

    auto content_type_it = req.headers.find("Content-Type");
    std::string content_type = content_type_it != req.headers.end()
                                   ? content_type_it->second
                                   : std::string("application/octet-stream");

    switch (req.method) {
        case crow::HTTPMethod::GET:
            return from_httplib(cli.Get(upstream_path.c_str(), headers), service);
        case crow::HTTPMethod::DELETE:
            return from_httplib(cli.Delete(upstream_path.c_str(), headers), service);
        case crow::HTTPMethod::HEAD:
            return from_httplib(cli.Head(upstream_path.c_str(), headers), service);
        case crow::HTTPMethod::POST:
            return from_httplib(
                cli.Post(upstream_path.c_str(), headers, req.body, content_type.c_str()),
                service);
        case crow::HTTPMethod::PUT:
            return from_httplib(
                cli.Put(upstream_path.c_str(), headers, req.body, content_type.c_str()),
                service);
        case crow::HTTPMethod::PATCH:
            return from_httplib(
                cli.Patch(upstream_path.c_str(), headers, req.body, content_type.c_str()),
                service);
        default:
            return crow::response(405, "method not allowed via proxy");
    }
}

struct ProxyMount {
    std::string         service;     // "diffusion" / "demand" / "lang" / "slack"
    std::string         env_var;     // "CXAI_DIFFUSION_UPSTREAM" / ...
    std::string         prefix;      // "/api/diffusion"
    std::optional<Upstream> upstream;
};

std::vector<ProxyMount>& mounts() {
    static std::vector<ProxyMount> m = {
        {"diffusion", "CXAI_DIFFUSION_UPSTREAM", "/api/diffusion", std::nullopt},
        {"demand",    "CXAI_DEMAND_UPSTREAM",    "/api/demand",    std::nullopt},
        {"lang",      "CXAI_LANG_UPSTREAM",      "/api/lang",      std::nullopt},
        {"slack",     "CXAI_SLACK_UPSTREAM",     "/api/slack",     std::nullopt},
    };
    static std::once_flag once;
    std::call_once(once, [] {
        for (auto& mt : m) mt.upstream = env_upstream(mt.env_var.c_str());
    });
    return m;
}

void install_one(crow::SimpleApp& app, ProxyMount& mount) {
    // Crow `<path>` matches multiple segments; we register one catch-all per
    // mount, plus an exact-prefix variant for the bare `/api/<svc>` URL.
    static const std::vector<crow::HTTPMethod> methods = {
        crow::HTTPMethod::GET,    crow::HTTPMethod::POST,  crow::HTTPMethod::PUT,
        crow::HTTPMethod::DELETE, crow::HTTPMethod::PATCH, crow::HTTPMethod::HEAD,
    };

    auto handler = [&mount](const crow::request& req) -> crow::response {
        if (!mount.upstream) return service_unavailable(mount.service);
        std::string path = strip_prefix(req.url, mount.prefix);
        return forward(*mount.upstream, mount.service, req, path);
    };

    // Sub-paths: /api/<svc>/<...>
    app.route_dynamic(mount.prefix + "/<path>")
        .methods(methods.data(), methods.size())(
            [handler](const crow::request& req, const std::string&) {
                return handler(req);
            });
    // Bare prefix (no trailing path)
    app.route_dynamic(mount.prefix)
        .methods(methods.data(), methods.size())(handler);
}

} // anonymous namespace

void register_proxy(crow::SimpleApp& app) {
    // Health-summary endpoint, lists configured proxies.
    CROW_ROUTE(app, "/api/services")
    ([]() {
        crow::json::wvalue r;
        std::vector<crow::json::wvalue> arr;
        for (const auto& m : mounts()) {
            crow::json::wvalue item;
            item["service"] = m.service;
            item["prefix"]  = m.prefix;
            item["configured"] = static_cast<bool>(m.upstream);
            if (m.upstream) {
                item["upstream_host"] = m.upstream->host;
                item["upstream_port"] = m.upstream->port;
                item["upstream_scheme"] = m.upstream->scheme;
            }
            arr.push_back(std::move(item));
        }
        r["services"] = std::move(arr);
        return r;
    });

    for (auto& m : mounts()) install_one(app, m);
}

} // namespace routes
