// Reverse-proxy routes for the 4 sidecar services. Uses cpp-httplib as the
// upstream client (single-header, no extra system packages).
//
// Each prefix is registered explicitly with CROW_ROUTE so Crow can extract
// the `<path>` tail at compile time. Upstream URLs are read from env at
// startup; missing envs make that prefix return 503.
//
// We deliberately do NOT enable cpp-httplib's OpenSSL support: all upstreams
// are internal HTTP services on the same docker network. The macro must not
// be defined at all (defining it to 0 still triggers the openssl/err.h
// include because httplib.h tests with #ifdef, not the value).

#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#undef CPPHTTPLIB_OPENSSL_SUPPORT
#endif
#include <httplib.h>

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

#include <cstdlib>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>

namespace routes {

namespace {

struct Upstream {
    std::string scheme;
    std::string host;
    int         port = 0;
    std::string path_prefix; // e.g. "/rest/v1" — appended before forwarded path.
};

std::optional<Upstream> parse_upstream(const std::string& url) {
    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);
    if (slash != std::string::npos) {
        up.path_prefix = rest.substr(slash);
        // Trim trailing slash so concatenation stays clean.
        while (up.path_prefix.size() > 1 && up.path_prefix.back() == '/') up.path_prefix.pop_back();
    }
    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;
    auto resp = crow::response(503, j);
    resp.add_header("Content-Type", "application/json");
    return resp;
}

httplib::Headers forward_headers(const crow::request& req) {
    httplib::Headers h;
    for (const auto& kv : req.headers) {
        if (hop_by_hop().count(to_lower(kv.first))) 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) {
        if (hop_by_hop().count(to_lower(kv.first))) 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& tail) {
    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);
    auto headers = forward_headers(req);

    std::string upstream_path = up.path_prefix + "/" + tail;
    auto qpos = req.raw_url.find('?');
    if (qpos != std::string::npos) {
        upstream_path += req.raw_url.substr(qpos);
    }

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

    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 UpstreamSet {
    std::optional<Upstream> diffusion;
    std::optional<Upstream> demand;
    std::optional<Upstream> lang;
    std::optional<Upstream> slack;
    std::optional<Upstream> files;
    std::string              files_anon_key;
};
const UpstreamSet& upstreams() {
    static UpstreamSet s;
    static std::once_flag once;
    std::call_once(once, [] {
        s.diffusion = env_upstream("CXAI_DIFFUSION_UPSTREAM");
        s.demand    = env_upstream("CXAI_DEMAND_UPSTREAM");
        s.lang      = env_upstream("CXAI_LANG_UPSTREAM");
        s.slack     = env_upstream("CXAI_SLACK_UPSTREAM");
        s.files     = env_upstream("CXAI_FILES_UPSTREAM");
        if (const char* k = std::getenv("FILES_ANON_KEY")) s.files_anon_key = k;
        else if (const char* k = std::getenv("CXAI_FILES_ANON_KEY")) s.files_anon_key = k;
    });
    return s;
}

// Files-specific forwarder: injects PostgREST auth headers from FILES_ANON_KEY.
crow::response forward_files(const Upstream& up, const crow::request& req,
                             const std::string& tail) {
    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);
    auto headers = forward_headers(req);
    const auto& key = upstreams().files_anon_key;
    if (!key.empty()) {
        // PostgREST expects both "apikey" and a bearer token.
        headers.erase("apikey");
        headers.erase("Authorization");
        headers.emplace("apikey", key);
        headers.emplace("Authorization", std::string("Bearer ") + key);
    }
    std::string upstream_path = up.path_prefix + "/" + tail;
    auto qpos = req.raw_url.find('?');
    if (qpos != std::string::npos) {
        upstream_path += req.raw_url.substr(qpos);
    }
    std::string content_type = "application/json";
    auto it = req.headers.find("Content-Type");
    if (it != req.headers.end()) content_type = it->second;

    switch (req.method) {
        case crow::HTTPMethod::Get:
            return from_httplib(cli.Get(upstream_path.c_str(), headers), "files");
        case crow::HTTPMethod::Post:
            return from_httplib(
                cli.Post(upstream_path.c_str(), headers, req.body, content_type.c_str()),
                "files");
        case crow::HTTPMethod::Patch:
            return from_httplib(
                cli.Patch(upstream_path.c_str(), headers, req.body, content_type.c_str()),
                "files");
        case crow::HTTPMethod::Delete:
            return from_httplib(cli.Delete(upstream_path.c_str(), headers), "files");
        case crow::HTTPMethod::Head:
            return from_httplib(cli.Head(upstream_path.c_str(), headers), "files");
        default:
            return crow::response(405, "method not allowed via files proxy");
    }
}

#define PROXY_PREFIX(APP, PREFIX, NAME, UPGETTER)                                          \
    CROW_ROUTE((APP), PREFIX).methods(                                                     \
        crow::HTTPMethod::Get, crow::HTTPMethod::Post, crow::HTTPMethod::Put,              \
        crow::HTTPMethod::Delete, crow::HTTPMethod::Patch, crow::HTTPMethod::Head)         \
    ([](const crow::request& req) {                                                        \
        const auto& up = UPGETTER();                                                       \
        if (!up) return service_unavailable(NAME);                                         \
        return forward(*up, NAME, req, "");                                                \
    });                                                                                    \
    CROW_ROUTE((APP), PREFIX "/<path>").methods(                                           \
        crow::HTTPMethod::Get, crow::HTTPMethod::Post, crow::HTTPMethod::Put,              \
        crow::HTTPMethod::Delete, crow::HTTPMethod::Patch, crow::HTTPMethod::Head)         \
    ([](const crow::request& req, std::string tail) {                                      \
        const auto& up = UPGETTER();                                                       \
        if (!up) return service_unavailable(NAME);                                         \
        return forward(*up, NAME, req, tail);                                              \
    });

const std::optional<Upstream>& up_diffusion() { return upstreams().diffusion; }
const std::optional<Upstream>& up_demand()    { return upstreams().demand; }
const std::optional<Upstream>& up_lang()      { return upstreams().lang; }
const std::optional<Upstream>& up_slack()     { return upstreams().slack; }

} // anonymous namespace

void register_proxy(crow::SimpleApp& app) {

    CROW_ROUTE(app, "/api/services")
    ([]() {
        crow::json::wvalue r;
        auto add = [&](const char* name, const std::optional<Upstream>& up,
                       const char* prefix) {
            crow::json::wvalue item;
            item["service"]    = std::string(name);
            item["prefix"]     = std::string(prefix);
            item["configured"] = static_cast<bool>(up);
            if (up) {
                item["upstream_host"]   = up->host;
                item["upstream_port"]   = up->port;
                item["upstream_scheme"] = up->scheme;
            }
            return item;
        };
        std::vector<crow::json::wvalue> arr;
        arr.push_back(add("diffusion", upstreams().diffusion, "/api/diffusion"));
        arr.push_back(add("demand",    upstreams().demand,    "/api/demand"));
        arr.push_back(add("lang",      upstreams().lang,      "/api/lang"));
        arr.push_back(add("slack",     upstreams().slack,     "/api/slack"));
        arr.push_back(add("files",     upstreams().files,     "/api/files"));
        r["services"] = std::move(arr);
        return r;
    });

    PROXY_PREFIX(app, "/api/diffusion", "diffusion", up_diffusion)
    PROXY_PREFIX(app, "/api/demand",    "demand",    up_demand)
    PROXY_PREFIX(app, "/api/lang",      "lang",      up_lang)
    PROXY_PREFIX(app, "/api/slack",     "slack",     up_slack)

    // Files: dedicated routes that inject PostgREST auth headers.
    CROW_ROUTE(app, "/api/files").methods(
        crow::HTTPMethod::Get, crow::HTTPMethod::Post, crow::HTTPMethod::Patch,
        crow::HTTPMethod::Delete, crow::HTTPMethod::Head)
    ([](const crow::request& req) {
        const auto& up = upstreams().files;
        if (!up) return service_unavailable("files");
        return forward_files(*up, req, "");
    });
    CROW_ROUTE(app, "/api/files/<path>").methods(
        crow::HTTPMethod::Get, crow::HTTPMethod::Post, crow::HTTPMethod::Patch,
        crow::HTTPMethod::Delete, crow::HTTPMethod::Head)
    ([](const crow::request& req, std::string tail) {
        const auto& up = upstreams().files;
        if (!up) return service_unavailable("files");
        return forward_files(*up, req, tail);
    });
}

} // namespace routes
