#include "dps/app.hpp"
#include "dps/fs_utils.hpp"
#include "dps/labels.hpp"

#include <algorithm>
#include <array>
#include <cctype>
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <random>
#include <sstream>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <openssl/rsa.h>
#include <openssl/sha.h>
#include <openssl/x509.h>
#include <zlib.h>
#include <mariadb/mysql.h>

#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <unistd.h>
#endif

extern "C" {
#ifdef _WIN32
#include "ow-crypt.h"
#else
#include <crypt.h>
#endif
}

namespace fs = std::filesystem;

namespace dps {

namespace {

const char *kToolPrivateKey =
    "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMiYuNW4K1rST7ZW"
    "YpWX6nEziXi5JveLPhDCLj0VZ/5/4dzBWrmoL/IcFZuHOBJtYHm965713kKC9gtw"
    "2EyVgkqmXLT3105jEUqzNizfThc6C2ZL6vMmzUZlooxNyaOC5mWthPZtwlqQihYW"
    "T2nW/wKp8fpTouXihQOCPjqdRoVFAgMBAAECgYEAg+1dcsHjUaIL5uG9iKEXAUhh"
    "21H6PMgJE8CB5I6VjJ3Sj/tijcP9dH/f/h/aUtQ4pRixRCRz/s5ev3uB5ixWOVOz"
    "Uhga/NwjURlkg1Katg/14tvd8iZsAzI7KyXGdUefC8+Rulc8SBx+BqYIZ1FTLJkg"
    "obisps6esaaNZekvS0ECQQDxPnfWuNfwfpYFK038AhEFqBiOBMsMrB/QX+O0sQ/Z"
    "31gWkNuu3Z+cz6PG0FEK0O8/jsXQ1/gVAreADptJAMzpAkEA1N3GFV1LuYYqkp3a"
    "y4DnmdI/LgrBUAjWSvTSDKfB8Ik2Y8I2zDS20EsEQMvQYtLxMOMHIEqeAVzmnZcS"
    "dH4L/QJAQM/elFJuuU7Y8SSUO/s2JYXmqukAwDPSDEJmw5m6P3dwjAd47b7e7dsf"
    "Df/Tdgtx62ppHNWY8dQcBoxmDbPoWQJBANBQBBROS9fQA4Od9Usn5/5xcSDWp51y"
    "OHv8ID8AQNvq+44ets+aWrl2YEAk9NZxRlWoJwYj34LH7muUNxxHaeUCQQCjtLPU"
    "din4XjlMMRSTW/EOgX4JgZ+tQaAtB32wQocPDDeHqkDusUdlEpnDPbmkE9Sbxf1p"
    "0mSxXgnIuT99niBd";

const char *kScriptPrivateKey =
    "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANp/KnuAZq609lnc"
    "jciVgt/xDLDEaKC+DSuSZc9o/fgLqweL/8Yu5iPAzoBQlO1t+jSkI2YuoFsKKK7O"
    "QFcSoz4Z5Lb7vOG7XJ4Re/6L2LJIU9tjJ3LznW5WKnuiqxs0ziIZilLkRE0P1di6"
    "FIZQUjmn2KQ2y/P7PuviJPat3y+vAgMBAAECgYEAmn64h0MvV/FVEA1Ho8E0HSzQ"
    "kF0Qrjg0D88gdhwDGFUHxfpUDMo0qKs4WLqh05CkDnzRGvt1H2p7gb8M94SSL5SX"
    "mCR2ZLiCQJy5tZpQNRUt9P67upLcDcKJ6YPujcuEdlo0pzKGGMZk8Uvh05S9lnwl"
    "yWJeEk4G7q5Aalz8vIECQQDwBVJH60Gd2St/GS4dCtXwHTO9HJZRaWEkaXPgVbXI"
    "vqv+jtD4HpzDCpgv/kUK0fnb4V/9/LM1SHOedVMfmh0fAkEA6QsChdHiU1lHfL1b"
    "Z3Uvxvi/Zk8i0ryzBaYudrzNmb7iJcssJPPty3KTjihgvw2W36GV+2GlQBEjOf2B"
    "2WgLcQJBAO66XDxsIbd+aWThBpycSm2one1aoagXyCcPO9HFbilcfHWUVwRybjkQ"
    "MI6LuOAqOPoaD//vd89nYJga2bJ09sECQFmXRPoDTVozqXr4JSqp75szyAliBQY1"
    "SzGxyI0XWodvzesvp6HxMQsU2ks9lKv+YnFI4qsIyAnQTNWfcwsMp9ECQDba0LJV"
    "xI0SIznsVtBb7T5Ir9ppLKx1QTNcytkjHxmQqWGy+57ojjQ6GV3z4fSwNGjIhsOT"
    "DFgz0gEcRr13bRk=";

std::string trim(const std::string &value) {
    std::size_t left = 0;
    while (left < value.size() && std::isspace(static_cast<unsigned char>(value[left]))) {
        ++left;
    }
    std::size_t right = value.size();
    while (right > left && std::isspace(static_cast<unsigned char>(value[right - 1]))) {
        --right;
    }
    return value.substr(left, right - left);
}

std::string join_path(const std::string &a, const std::string &b) {
    if (a.empty()) return b;
    if (b.empty()) return a;
    if (a.back() == '/' || a.back() == '\\') return a + b;
    return a + "/" + b;
}

std::string format_now() {
    const auto now = std::chrono::system_clock::now();
    const std::time_t time = std::chrono::system_clock::to_time_t(now);
    std::tm local{};
#ifdef _WIN32
    localtime_s(&local, &time);
#else
    localtime_r(&time, &local);
#endif
    std::ostringstream out;
    out << std::put_time(&local, "%Y-%m-%d %H:%M:%S");
    return out.str();
}

std::optional<std::tm> parse_time(const std::string &value) {
    std::tm tm{};
    std::istringstream full(value);
    full >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
    if (!full.fail()) return tm;
    std::istringstream short_value(value);
    short_value >> std::get_time(&tm, "%Y-%m-%d");
    if (!short_value.fail()) return tm;
    return std::nullopt;
}

std::string add_days_impl(const std::string &value, int days) {
    auto parsed = parse_time(value);
    std::time_t now_time = std::time(nullptr);
    std::tm tm{};
    if (parsed.has_value()) {
        tm = *parsed;
    } else {
#ifdef _WIN32
        localtime_s(&tm, &now_time);
#else
        localtime_r(&now_time, &tm);
#endif
    }
    std::time_t base = std::mktime(&tm);
    base += static_cast<std::time_t>(days) * 24 * 60 * 60;
#ifdef _WIN32
    localtime_s(&tm, &base);
#else
    localtime_r(&base, &tm);
#endif
    std::ostringstream out;
    out << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
    return out.str();
}

std::string to_json_bytes(const std::vector<unsigned char> &bytes) {
    std::ostringstream out;
    for (std::size_t i = 0; i < bytes.size(); ++i) {
        if (i != 0) out << ",";
        out << static_cast<int>(static_cast<signed char>(bytes[i]));
    }
    return out.str();
}

std::vector<std::string> split_csv_config(const std::string &path) {
    std::vector<std::string> result;
    std::ifstream input(path);
    std::string line;
    while (std::getline(input, line)) {
        line = trim(line);
        if (!line.empty()) result.push_back(line);
    }
    return result;
}

long long latest_mtime_ms(const fs::path &root) {
    if (!fs::exists(root)) return 0;
    long long latest = 0;
    for (const auto &entry : fs::recursive_directory_iterator(root)) {
        if (!entry.is_regular_file()) continue;
        const auto file_time = entry.last_write_time();
        const auto system_time = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
            file_time - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
        const auto time = std::chrono::duration_cast<std::chrono::milliseconds>(system_time.time_since_epoch()).count();
        if (time > latest) latest = time;
    }
    return latest;
}

Json map_to_json_object(const std::map<std::string, Json> &values) {
    Json result = Json::object();
    for (const auto &entry : values) result[entry.first] = entry.second;
    return result;
}

std::string to_lower_copy(std::string value) {
    std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
        return static_cast<char>(std::tolower(ch));
    });
    return value;
}

std::optional<std::array<int, 4>> parse_ipv4_octets(const std::string &value) {
    std::array<int, 4> octets{};
    std::size_t start = 0;
    for (std::size_t i = 0; i < octets.size(); ++i) {
        const std::size_t dot = value.find('.', start);
        const std::string part = value.substr(start, dot == std::string::npos ? std::string::npos : dot - start);
        if (part.empty() || part.size() > 3) return std::nullopt;
        if (!std::all_of(part.begin(), part.end(), [](unsigned char ch) { return std::isdigit(ch); })) return std::nullopt;
        const int number = std::stoi(part);
        if (number < 0 || number > 255) return std::nullopt;
        octets[i] = number;
        if (i + 1 == octets.size()) {
            if (dot != std::string::npos) return std::nullopt;
        } else {
            if (dot == std::string::npos) return std::nullopt;
            start = dot + 1;
        }
    }
    return octets;
}

bool is_local_purchase_ip(const std::string &value) {
    const auto octets = parse_ipv4_octets(value);
    if (!octets.has_value()) return false;
    const int a = (*octets)[0];
    const int b = (*octets)[1];
    if (a == 127) return true;
    if (a == 10) return true;
    if (a == 192 && b == 168) return true;
    if (a == 172 && b >= 16 && b <= 31) return true;
    return false;
}

long long duration_ms(std::chrono::steady_clock::time_point started,
                      std::chrono::steady_clock::time_point finished) {
    return std::chrono::duration_cast<std::chrono::milliseconds>(finished - started).count();
}

std::string sql_category(const std::string &sql) {
    const std::string lower = to_lower_copy(sql);
    if (lower.rfind("select", 0) == 0) {
        if (lower.find(" from sys_user ") != std::string::npos) return "select_sys_user";
        if (lower.find(" from user_and_server ") != std::string::npos) return "select_user_and_server";
        if (lower.find(" from user_server_project ") != std::string::npos) return "select_user_server_project";
        return "select_other";
    }
    if (lower.rfind("insert", 0) == 0) {
        if (lower.find(" into sys_user ") != std::string::npos) return "insert_sys_user";
        if (lower.find(" into user_and_server ") != std::string::npos) return "insert_user_and_server";
        if (lower.find(" into user_server_project ") != std::string::npos) return "insert_user_server_project";
        if (lower.find(" into log ") != std::string::npos) return "insert_log";
        return "insert_other";
    }
    if (lower.rfind("update", 0) == 0) {
        if (lower.find("update sys_user ") != std::string::npos) return "update_sys_user";
        if (lower.find("update user_and_server ") != std::string::npos) return "update_user_and_server";
        if (lower.find("update user_server_project ") != std::string::npos) return "update_user_server_project";
        return "update_other";
    }
    if (lower.rfind("delete", 0) == 0) {
        if (lower.find(" from user_and_server ") != std::string::npos) return "delete_user_and_server";
        if (lower.find(" from user_server_project ") != std::string::npos) return "delete_user_server_project";
        return "delete_other";
    }
    return "sql_other";
}

bool path_prefix_matches(const fs::path &root, const fs::path &target) {
    auto root_it = root.begin();
    auto target_it = target.begin();
    for (; root_it != root.end(); ++root_it, ++target_it) {
        if (target_it == target.end()) return false;
        if (to_lower_copy(path_to_utf8_string(*root_it)) != to_lower_copy(path_to_utf8_string(*target_it))) {
            return false;
        }
    }
    return true;
}

std::string current_executable_dir() {
#ifdef _WIN32
    std::array<char, MAX_PATH> buffer{};
    const DWORD size = GetModuleFileNameA(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
    if (size == 0 || size == buffer.size()) return {};
    return fs::path(std::string(buffer.data(), size)).parent_path().string();
#else
    std::array<char, 4096> buffer{};
    const ssize_t size = readlink("/proc/self/exe", buffer.data(), buffer.size());
    if (size <= 0 || static_cast<std::size_t>(size) >= buffer.size()) return {};
    return fs::path(std::string(buffer.data(), static_cast<std::size_t>(size))).parent_path().string();
#endif
}

#ifdef _WIN32
constexpr int kBcryptBufferSize = 64;
#else
constexpr int kBcryptSaltBufferSize = CRYPT_GENSALT_OUTPUT_SIZE;
constexpr std::size_t kBcryptWorkBufferSize = sizeof(struct crypt_data);
#endif

std::string base64_encode(const std::string &input, bool url_safe, bool pad_output) {
    static const char *kBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    std::string output;
    int value = 0;
    int bits = -6;
    for (const unsigned char ch : input) {
        value = (value << 8) + ch;
        bits += 8;
        while (bits >= 0) {
            output.push_back(kBase64[(value >> bits) & 0x3F]);
            bits -= 6;
        }
    }
    if (bits > -6) output.push_back(kBase64[((value << 8) >> (bits + 8)) & 0x3F]);
    if (pad_output) {
        while (output.size() % 4 != 0) output.push_back('=');
    }
    if (url_safe) {
        std::replace(output.begin(), output.end(), '+', '-');
        std::replace(output.begin(), output.end(), '/', '_');
        if (!pad_output) {
            while (!output.empty() && output.back() == '=') output.pop_back();
        }
    }
    return output;
}

std::optional<std::string> base64_decode(std::string input, bool url_safe) {
    if (url_safe) {
        std::replace(input.begin(), input.end(), '-', '+');
        std::replace(input.begin(), input.end(), '_', '/');
    }
    while (input.size() % 4 != 0) input.push_back('=');

    static const std::string kBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    std::array<int, 256> lookup{};
    lookup.fill(-1);
    for (std::size_t i = 0; i < kBase64.size(); ++i) lookup[static_cast<unsigned char>(kBase64[i])] = static_cast<int>(i);

    std::string output;
    int value = 0;
    int bits = -8;
    for (const unsigned char ch : input) {
        if (ch == '=') break;
        if (lookup[ch] == -1) return std::nullopt;
        value = (value << 6) + lookup[ch];
        bits += 6;
        if (bits >= 0) {
            output.push_back(static_cast<char>((value >> bits) & 0xFF));
            bits -= 8;
        }
    }
    return output;
}

bool fill_random_bytes(std::array<unsigned char, 16> &buffer) {
    return RAND_bytes(buffer.data(), static_cast<int>(buffer.size())) == 1;
}

std::string bcrypt_hash_impl(const std::string &password) {
    std::array<unsigned char, 16> raw{};
    if (!fill_random_bytes(raw)) return "";

#ifdef _WIN32
    std::array<char, kBcryptBufferSize> salt{};
#else
    std::array<char, kBcryptSaltBufferSize> salt{};
#endif
    if (crypt_gensalt_rn("$2b$", 12, reinterpret_cast<const char *>(raw.data()),
                         static_cast<int>(raw.size()), salt.data(),
                         static_cast<int>(salt.size())) == nullptr) {
        return "";
    }

#ifdef _WIN32
    std::array<char, kBcryptBufferSize> hash{};
#else
    std::array<char, kBcryptWorkBufferSize> hash{};
#endif
    if (crypt_rn(password.c_str(), salt.data(), hash.data(), static_cast<int>(hash.size())) == nullptr) return "";
    return std::string(hash.data());
}

bool bcrypt_verify_impl(const std::string &password, const std::string &encoded) {
#ifdef _WIN32
    std::array<char, kBcryptBufferSize> hash{};
#else
    std::array<char, kBcryptWorkBufferSize> hash{};
#endif
    char *result = crypt_rn(password.c_str(), encoded.c_str(), hash.data(), static_cast<int>(hash.size()));
    return result != nullptr && encoded == result;
}

}  // namespace

LegacyCrypto::LegacyCrypto(std::string jwt_secret) : jwt_secret_(std::move(jwt_secret)) {}

std::string LegacyCrypto::base64_url_encode(const std::string &input) const {
    return base64_encode(input, true, false);
}

std::string LegacyCrypto::base64_url_decode(const std::string &input) const {
    return base64_decode(input, true).value_or("");
}

std::string LegacyCrypto::hmac_sha256(const std::string &message) const {
    unsigned char digest[EVP_MAX_MD_SIZE];
    unsigned int digest_size = 0;
    HMAC(EVP_sha256(), jwt_secret_.data(), static_cast<int>(jwt_secret_.size()),
         reinterpret_cast<const unsigned char *>(message.data()), message.size(),
         digest, &digest_size);
    return std::string(reinterpret_cast<char *>(digest), digest_size);
}

std::string LegacyCrypto::sha256_hex(const std::string &message) const {
    unsigned char digest[SHA256_DIGEST_LENGTH];
    SHA256(reinterpret_cast<const unsigned char *>(message.data()), message.size(), digest);
    std::ostringstream out;
    out << std::hex << std::setfill('0');
    for (unsigned char byte : digest) out << std::setw(2) << static_cast<int>(byte);
    return out.str();
}

std::string LegacyCrypto::bcrypt_hash(const std::string &password) const {
    return bcrypt_hash_impl(password);
}

std::string LegacyCrypto::rsa_private_encrypt_der_b64(const std::string &der_b64, const std::string &plain) const {
    const std::string der = base64_decode(der_b64, false).value_or("");
    if (der.empty()) return "";
    const unsigned char *ptr = reinterpret_cast<const unsigned char *>(der.data());
    RSA *rsa = d2i_RSAPrivateKey(nullptr, &ptr, static_cast<long>(der.size()));
    if (rsa == nullptr) return "";
    std::vector<unsigned char> encrypted(RSA_size(rsa));
    const int size = RSA_private_encrypt(static_cast<int>(plain.size()), reinterpret_cast<const unsigned char *>(plain.data()), encrypted.data(), rsa, RSA_PKCS1_PADDING);
    RSA_free(rsa);
    if (size <= 0) return "";
    std::string raw(reinterpret_cast<char *>(encrypted.data()), static_cast<std::size_t>(size));
    return base64_encode(raw, false, true);
}

std::string LegacyCrypto::issue_jwt(const std::string &username, long long expiration_seconds) const {
    Json header = Json::object();
    header["alg"] = "HS256";
    header["typ"] = "JWT";
    Json payload = Json::object();
    const auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
    payload["sub"] = username;
    payload["username"] = username;
    Json roles = Json::array();
    roles.push_back("ROLE_ALL");
    payload["authorities"] = roles;
    payload["iat"] = static_cast<long long>(now);
    payload["exp"] = static_cast<long long>(now + expiration_seconds);
    const std::string token_body = base64_url_encode(header.dump()) + "." + base64_url_encode(payload.dump());
    return token_body + "." + base64_url_encode(hmac_sha256(token_body));
}

std::optional<std::string> LegacyCrypto::validate_jwt(const std::string &token) const {
    const std::size_t first = token.find('.');
    const std::size_t second = token.find('.', first == std::string::npos ? first : first + 1);
    if (first == std::string::npos || second == std::string::npos) return std::nullopt;
    const std::string body = token.substr(0, second);
    if (base64_url_encode(hmac_sha256(body)) != token.substr(second + 1)) return std::nullopt;
    const Json payload = Json::parse(base64_url_decode(token.substr(first + 1, second - first - 1)));
    const long long exp = payload["exp"].to_int_or(0);
    const auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
    if (now >= exp) return std::nullopt;
    return payload["sub"].to_string_or("");
}

std::string LegacyCrypto::hash_password(const std::string &password) const { return bcrypt_hash(password); }

bool LegacyCrypto::verify_password(const std::string &password, const std::string &encoded) const {
    return encoded.rfind("$2", 0) == 0 && bcrypt_verify_impl(password, encoded);
}

std::string LegacyCrypto::rsa_private_encrypt_tool(const std::string &plain) const { return rsa_private_encrypt_der_b64(kToolPrivateKey, plain); }
std::string LegacyCrypto::rsa_private_encrypt_script(const std::string &plain) const { return rsa_private_encrypt_der_b64(kScriptPrivateKey, plain); }

std::string LegacyCrypto::gzip_base64(const std::string &plain) const {
    z_stream stream{};
    if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) return "";
    stream.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(plain.data()));
    stream.avail_in = static_cast<uInt>(plain.size());
    std::string compressed;
    std::array<char, 4096> buffer{};
    int ret = Z_OK;
    while (ret == Z_OK) {
        stream.next_out = reinterpret_cast<Bytef *>(buffer.data());
        stream.avail_out = static_cast<uInt>(buffer.size());
        ret = deflate(&stream, Z_FINISH);
        compressed.append(buffer.data(), buffer.size() - stream.avail_out);
    }
    deflateEnd(&stream);
    if (ret != Z_STREAM_END) return "";
    return base64_encode(compressed, false, true);
}

std::vector<unsigned char> LegacyCrypto::makecode(std::vector<unsigned char> bytes, const std::vector<int> &key) const {
    for (std::size_t i = 0; i < bytes.size(); ++i) {
        const int k1 = key[i % key.size()];
        const int k2 = key[(i + 18) % key.size()];
        const int value = static_cast<signed char>(bytes[i]);
        bytes[i] = static_cast<unsigned char>((((value + k1) ^ k2) ^ k1) + 1);
    }
    return bytes;
}

std::string LegacyCrypto::xor_encrypt(const std::string &plain, const std::string &key) const {
    if (key.empty()) return plain;
    std::string output = plain;
    for (std::size_t i = 0; i < plain.size(); ++i) output[i] = static_cast<char>(plain[i] ^ key[i % key.size()]);
    return output;
}

struct Database::Impl {
    MYSQL *connection = nullptr;
    DatabaseConfig config;
    std::string error;
    mutable std::recursive_mutex mutex;
};

bool set_mysql_bool_option(MYSQL *connection, enum mysql_option option, bool value, std::string *error) {
    my_bool flag = value ? 1 : 0;
    if (mysql_options(connection, option, &flag) == 0) return true;
    if (error != nullptr) *error = mysql_error(connection);
    return false;
}

bool set_mysql_string_option(MYSQL *connection, enum mysql_option option, const std::string &value, std::string *error) {
    if (value.empty()) return true;
    if (mysql_options(connection, option, value.c_str()) == 0) return true;
    if (error != nullptr) *error = mysql_error(connection);
    return false;
}

bool configure_database_tls(MYSQL *connection, const DatabaseConfig &config, std::string *error) {
    const std::string mode = to_lower_copy(config.ssl_mode.empty() ? "preferred" : config.ssl_mode);
    if (mode == "disable") {
        if (!set_mysql_bool_option(connection, MYSQL_OPT_SSL_ENFORCE, false, error)) return false;
        if (!set_mysql_bool_option(connection, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, false, error)) return false;
        return true;
    }
    if (mode == "preferred") {
        if (!set_mysql_bool_option(connection, MYSQL_OPT_SSL_ENFORCE, false, error)) return false;
        if (!set_mysql_bool_option(connection, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, false, error)) return false;
        return true;
    }
    if (mode == "required") {
        if (!set_mysql_bool_option(connection, MYSQL_OPT_SSL_ENFORCE, true, error)) return false;
        if (!set_mysql_bool_option(connection, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, false, error)) return false;
        return true;
    }
    if (mode == "verify_ca" || mode == "verify_identity") {
        if (!set_mysql_bool_option(connection, MYSQL_OPT_SSL_ENFORCE, true, error)) return false;
        if (!set_mysql_bool_option(connection, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, true, error)) return false;
        if (!set_mysql_string_option(connection, MYSQL_OPT_SSL_CA, config.ssl_ca, error)) return false;
        return true;
    }
    if (error != nullptr) *error = "unsupported database.ssl_mode: " + config.ssl_mode;
    return false;
}

bool configure_database_plugins(MYSQL *connection, const DatabaseConfig &config, std::string *error) {
    std::string plugin_dir = config.plugin_dir;
    if (plugin_dir.empty()) {
        const std::string exe_dir = current_executable_dir();
        if (!exe_dir.empty()) {
            const fs::path bundled_dir = fs::path(exe_dir) / "mariadb-plugin";
            if (fs::exists(bundled_dir) && fs::is_directory(bundled_dir)) {
                plugin_dir = bundled_dir.string();
            }
        }
    }
    if (plugin_dir.empty()) return true;
    return set_mysql_string_option(connection, MYSQL_PLUGIN_DIR, plugin_dir, error);
}

Database::Database(const DatabaseConfig &config) : impl_(new Impl{}) { impl_->config = config; }

Database::~Database() {
    std::lock_guard<std::recursive_mutex> lock(impl_->mutex);
    if (impl_->connection != nullptr) {
        mysql_close(impl_->connection);
        impl_->connection = nullptr;
    }
}

bool Database::connect() {
    std::lock_guard<std::recursive_mutex> lock(impl_->mutex);
    if (impl_->connection != nullptr) {
        impl_->error.clear();
        return true;
    }

    impl_->error.clear();
    impl_->connection = mysql_init(nullptr);
    if (impl_->connection == nullptr) {
        impl_->error = "mysql_init failed";
        return false;
    }
    if (!configure_database_tls(impl_->connection, impl_->config, &impl_->error)) {
        mysql_close(impl_->connection);
        impl_->connection = nullptr;
        return false;
    }
    if (!configure_database_plugins(impl_->connection, impl_->config, &impl_->error)) {
        mysql_close(impl_->connection);
        impl_->connection = nullptr;
        return false;
    }
    if (mysql_real_connect(impl_->connection, impl_->config.host.c_str(),
                           impl_->config.user.c_str(), impl_->config.password.c_str(),
                           impl_->config.name.c_str(), impl_->config.port, nullptr, 0) == nullptr) {
        impl_->error = mysql_error(impl_->connection);
        mysql_close(impl_->connection);
        impl_->connection = nullptr;
        return false;
    }
    if (mysql_set_character_set(impl_->connection, "utf8mb4") != 0) {
        impl_->error = mysql_error(impl_->connection);
        mysql_close(impl_->connection);
        impl_->connection = nullptr;
        return false;
    }
    impl_->error.clear();
    return true;
}

bool Database::is_connected() const {
    std::lock_guard<std::recursive_mutex> lock(impl_->mutex);
    return impl_->connection != nullptr;
}

std::string Database::last_error() const {
    std::lock_guard<std::recursive_mutex> lock(impl_->mutex);
    return impl_->error;
}

std::string Database::escape(const std::string &value) const {
    std::lock_guard<std::recursive_mutex> lock(impl_->mutex);
    if (impl_->connection == nullptr && !const_cast<Database *>(this)->connect()) return value;
    if (impl_->connection == nullptr) return value;
    std::string escaped;
    escaped.resize(value.size() * 2 + 1);
    const unsigned long size = mysql_real_escape_string(impl_->connection, escaped.data(),
                                                        value.data(),
                                                        static_cast<unsigned long>(value.size()));
    escaped.resize(size);
    return escaped;
}

bool Database::execute(const std::string &sql) {
    const std::string category = sql_category(sql);
    const auto wait_started = std::chrono::steady_clock::now();
    std::unique_lock<std::recursive_mutex> lock(impl_->mutex);
    const auto lock_acquired = std::chrono::steady_clock::now();
    if (impl_->connection == nullptr && !const_cast<Database *>(this)->connect()) return false;
    impl_->error.clear();
    const auto query_started = std::chrono::steady_clock::now();
    if (mysql_query(impl_->connection, sql.c_str()) != 0) {
        impl_->error = mysql_error(impl_->connection);
        const auto query_finished = std::chrono::steady_clock::now();
        std::clog << "[db] execute category=" << category
                  << " db_lock_wait_ms=" << duration_ms(wait_started, lock_acquired)
                  << " db_query_ms=" << duration_ms(query_started, query_finished)
                  << " db_fetch_ms=0"
                  << " ok=no"
                  << " db_error=" << impl_->error
                  << std::endl;
        return false;
    }
    const auto query_finished = std::chrono::steady_clock::now();
    std::clog << "[db] execute category=" << category
              << " db_lock_wait_ms=" << duration_ms(wait_started, lock_acquired)
              << " db_query_ms=" << duration_ms(query_started, query_finished)
              << " db_fetch_ms=0"
              << " ok=yes"
              << std::endl;
    return true;
}

std::vector<std::map<std::string, std::string>> Database::query(const std::string &sql) {
    std::vector<std::map<std::string, std::string>> rows;
    const std::string category = sql_category(sql);
    const auto wait_started = std::chrono::steady_clock::now();
    std::unique_lock<std::recursive_mutex> lock(impl_->mutex);
    const auto lock_acquired = std::chrono::steady_clock::now();
    if (impl_->connection == nullptr && !connect()) return rows;
    impl_->error.clear();
    const auto query_started = std::chrono::steady_clock::now();
    if (mysql_query(impl_->connection, sql.c_str()) != 0) {
        impl_->error = mysql_error(impl_->connection);
        const auto query_finished = std::chrono::steady_clock::now();
        std::clog << "[db] query category=" << category
                  << " db_lock_wait_ms=" << duration_ms(wait_started, lock_acquired)
                  << " db_query_ms=" << duration_ms(query_started, query_finished)
                  << " db_fetch_ms=0"
                  << " rows=0"
                  << " ok=no"
                  << " db_error=" << impl_->error
                  << std::endl;
        return rows;
    }
    const auto query_finished = std::chrono::steady_clock::now();
    const auto fetch_started = std::chrono::steady_clock::now();
    MYSQL_RES *result = mysql_store_result(impl_->connection);
    const auto fetch_finished = std::chrono::steady_clock::now();
    if (result == nullptr) {
        std::clog << "[db] query category=" << category
                  << " db_lock_wait_ms=" << duration_ms(wait_started, lock_acquired)
                  << " db_query_ms=" << duration_ms(query_started, query_finished)
                  << " db_fetch_ms=" << duration_ms(fetch_started, fetch_finished)
                  << " rows=0"
                  << " ok=yes"
                  << std::endl;
        return rows;
    }
    const int count = mysql_num_fields(result);
    MYSQL_FIELD *fields = mysql_fetch_fields(result);
    MYSQL_ROW row = nullptr;
    while ((row = mysql_fetch_row(result)) != nullptr) {
        unsigned long *lengths = mysql_fetch_lengths(result);
        std::map<std::string, std::string> item;
        for (int i = 0; i < count; ++i) item[fields[i].name] = row[i] == nullptr ? "" : std::string(row[i], lengths[i]);
        rows.push_back(std::move(item));
    }
    mysql_free_result(result);
    const auto rows_finished = std::chrono::steady_clock::now();
    std::clog << "[db] query category=" << category
              << " db_lock_wait_ms=" << duration_ms(wait_started, lock_acquired)
              << " db_query_ms=" << duration_ms(query_started, query_finished)
              << " db_fetch_ms=" << duration_ms(fetch_started, rows_finished)
              << " rows=" << rows.size()
              << " ok=yes"
              << std::endl;
    return rows;
}

long long Database::last_insert_id() const {
    std::lock_guard<std::recursive_mutex> lock(impl_->mutex);
    return impl_->connection == nullptr ? 0 : static_cast<long long>(mysql_insert_id(impl_->connection));
}
ProjectCatalog::ProjectCatalog(const PathConfig &paths) : paths_(paths) { reload(); }

Json ProjectCatalog::parse_json_file(const std::string &path) const {
    const std::string text = read_text_file(path);
    if (text.empty()) return Json::object();
    try {
        return Json::parse(text);
    } catch (...) {
        return Json::object();
    }
}

std::vector<std::string> ProjectCatalog::read_script_list(const std::string &root, const std::vector<std::string> &items) const {
    std::vector<std::string> result;
    for (const auto &item : items) {
        const std::string content = read_text_file(join_path(root, item));
        if (!content.empty()) result.push_back(content);
    }
    return result;
}

std::vector<std::pair<std::string, std::string>> ProjectCatalog::read_named_scripts(const std::string &root, const std::vector<std::string> &items) const {
    std::vector<std::pair<std::string, std::string>> result;
    for (const auto &item : items) {
        const std::string path = join_path(root, item);
        const std::string content = read_text_file(path);
        if (!content.empty()) result.emplace_back(path, content);
    }
    return result;
}

const ProjectCatalog::VersionData *ProjectCatalog::find_version(const std::string &requested) const {
    if (!requested.empty()) {
        const auto found = versions_.find(requested);
        if (found != versions_.end()) return &found->second;
    }
    const auto latest = versions_.find(newest_version_);
    return latest == versions_.end() ? nullptr : &latest->second;
}

void ProjectCatalog::load_custom_client_scripts(VersionData &data) {
    const fs::path root(paths_.client_user_script_root);
    if (!fs::exists(root)) return;
    for (const auto &entry : fs::directory_iterator(root)) {
        if (!entry.is_directory()) continue;
        const fs::path config = entry.path() / "config";
        if (!fs::exists(config)) continue;
        const auto scripts = split_csv_config(config.string());
        data.custom_client_scripts[entry.path().filename().string()] = read_script_list(entry.path().string(), scripts);
    }
}

void ProjectCatalog::load_client_versions() {
    const fs::path root(paths_.script_root);
    if (!fs::exists(root)) return;
    for (const auto &entry : fs::directory_iterator(root)) {
        if (!entry.is_directory()) continue;
        const fs::path config_file = entry.path() / "FileConfig.json";
        if (!fs::exists(config_file)) continue;
        VersionData data;
        const std::string dir_name = entry.path().filename().string();
        data.name = dir_name;
        if (dir_name.rfind("Script", 0) == 0) {
            data.name = dir_name.substr(6);
            if (!data.name.empty() && (data.name.front() == '_' || data.name.front() == '-')) data.name.erase(data.name.begin());
        }
        if (data.name.empty()) data.name = dir_name;
        const Json root_json = parse_json_file(config_file.string());
        if (root_json.contains("BaseScript") && root_json["BaseScript"].is_array()) {
            std::vector<std::string> names;
            for (const auto &item : root_json["BaseScript"].as_array()) names.push_back(item.to_string_or(""));
            data.base_scripts = read_script_list(entry.path().string(), names);
        }
        if (root_json.contains("CSBaseScript") && root_json["CSBaseScript"].is_array()) {
            std::vector<std::string> names;
            for (const auto &item : root_json["CSBaseScript"].as_array()) names.push_back(item.to_string_or(""));
            data.cs_base_scripts = read_script_list(entry.path().string(), names);
        }
        if (root_json.contains("ProjectScript") && root_json["ProjectScript"].is_object()) {
            for (const auto &project_entry : root_json["ProjectScript"].as_object()) {
                ProjectDefinition project;
                project.name = project_entry.first;
                const Json &project_json = project_entry.second;
                if (project_json.contains("Script") && project_json["Script"].is_array()) {
                    for (const auto &item : project_json["Script"].as_array()) project.script_paths.push_back(item.to_string_or(""));
                }
                if (project_json.contains("Info") && project_json["Info"].is_object())
                    project.info = project_json["Info"].as_object();
                else if (project_json.contains("info") && project_json["info"].is_object())
                    project.info = project_json["info"].as_object();
                project.price = project_json["price"].to_int_or(0);
                project.is_private = project_json["Private"].to_bool_or(false);
                project.img_path = project_json["imgPath"].to_string_or("");
                data.projects[project.name] = project;
                data.project_scripts[project.name] = read_script_list(entry.path().string(), project.script_paths);
            }
        }
        const std::string manifest_path = join_path(join_path(paths_.file_root, "DP-S_Script"), "Script" + data.name + "/_DPS_/FileConfig.json");
        data.dps_manifest = parse_json_file(manifest_path);
        load_custom_client_scripts(data);
        const std::string version_name = data.name;
        versions_[data.name] = std::move(data);
        newest_version_ = std::max(newest_version_, version_name);
    }
}

void ProjectCatalog::reload() {
    versions_.clear();
    newest_version_.clear();
    load_client_versions();
}

std::optional<ProjectDefinition> ProjectCatalog::get_project(const std::string &name, const std::string &version) const {
    const VersionData *data = find_version(version);
    if (data == nullptr) return std::nullopt;
    const auto it = data->projects.find(name);
    return it == data->projects.end() ? std::optional<ProjectDefinition>{} : std::optional<ProjectDefinition>{it->second};
}

Json ProjectCatalog::public_project_info() const {
    Json result = Json::object();
    const VersionData *data = find_version("");
    if (data == nullptr) return result;
    for (const auto &entry : data->projects) {
        if (entry.second.is_private) continue;
        Json info = map_to_json_object(entry.second.info);
        info["ProjectPrice"] = entry.second.price;
        result[entry.first] = info;
    }
    return result;
}

Json ProjectCatalog::all_project_info() const {
    Json result = Json::object();
    const VersionData *data = find_version("");
    if (data == nullptr) return result;
    for (const auto &entry : data->projects) {
        Json info = map_to_json_object(entry.second.info);
        info["ProjectPrice"] = entry.second.price;
        result[entry.first] = info;
    }
    return result;
}

int ProjectCatalog::project_price(const std::string &name) const {
    const auto project = get_project(name);
    return project.has_value() ? project->price : 0;
}

std::string ProjectCatalog::project_img_path(const std::string &name) const {
    const auto project = get_project(name);
    return project.has_value() ? project->img_path : "";
}

std::vector<std::string> ProjectCatalog::base_script_contents(const std::string &version, bool cs_mode) const {
    const VersionData *data = find_version(version);
    if (data == nullptr) return {};
    return cs_mode && !data->cs_base_scripts.empty() ? data->cs_base_scripts : data->base_scripts;
}

std::vector<std::string> ProjectCatalog::project_script_contents(const std::string &project_name, const std::string &version) const {
    const VersionData *data = find_version(version);
    if (data == nullptr) return {};
    const auto it = data->project_scripts.find(project_name);
    return it == data->project_scripts.end() ? std::vector<std::string>{} : it->second;
}

std::vector<std::string> ProjectCatalog::custom_client_scripts(const std::string &ip, const std::string &version) const {
    const VersionData *data = find_version(version);
    if (data == nullptr) return {};
    const auto it = data->custom_client_scripts.find(ip);
    return it == data->custom_client_scripts.end() ? std::vector<std::string>{} : it->second;
}

std::vector<std::pair<std::string, std::string>> ProjectCatalog::dps_service_scripts(const std::string &ip) const {
    std::vector<std::pair<std::string, std::string>> result;
    const std::string base_root = join_path(paths_.file_root, "dpsScript");
    std::string base_name = "base";
    const std::vector<std::string> special_ips = {"112.12.194.152", "202.189.14.166", "45.125.44.2", "110.42.110.159", "202.189.14.111"};
    if (std::find(special_ips.begin(), special_ips.end(), ip) != special_ips.end()) base_name = "base3";
    const fs::path base_config = fs::path(base_root) / base_name / "config";
    if (fs::exists(base_config)) {
        const auto names = split_csv_config(base_config.string());
        auto items = read_named_scripts((fs::path(base_root) / base_name).string(), names);
        result.insert(result.end(), items.begin(), items.end());
    }
    const fs::path common = fs::path(base_root) / u8"user" / u8"通用" / "main.nut";
    if (fs::exists(common)) result.emplace_back(common.string(), read_text_file(common.string()));
    const fs::path custom_root = fs::path(base_root) / "user" / ip;
    const fs::path custom_config = custom_root / "config";
    if (fs::exists(custom_config)) {
        const auto names = split_csv_config(custom_config.string());
        auto items = read_named_scripts(custom_root.string(), names);
        result.insert(result.end(), items.begin(), items.end());
    }
    return result;
}

std::vector<std::pair<std::string, std::string>> ProjectCatalog::dps_service_scripts_new(const std::string &ip, const std::string &version) const {
    std::vector<std::pair<std::string, std::string>> result;
    const VersionData *data = find_version(version);
    if (data == nullptr) return result;
    const fs::path custom_root = fs::path(paths_.file_root) / "dpsScript" / "user" / ip;
    const fs::path custom_config = custom_root / "config";
    if (fs::exists(custom_config)) {
        const auto names = split_csv_config(custom_config.string());
        auto items = read_named_scripts(custom_root.string(), names);
        result.insert(result.end(), items.begin(), items.end());
    }
    return result;
}

std::string ProjectCatalog::newest_version() const { return newest_version_; }

long long ProjectCatalog::base_update_time(const std::string &version) const {
    const VersionData *data = find_version(version);
    if (data == nullptr) return 0;
    const fs::path root = fs::path(paths_.file_root) / "DP-S_Script" / ("Script" + data->name) / "_DPS_";
    return latest_mtime_ms(root);
}

Application::Application(AppConfig config)
    : config_(std::move(config)), crypto_(config_.jwt.secret), database_(config_.database), catalog_(config_.paths) {
    if (!database_.connect()) {
        throw std::runtime_error("failed to connect database: " + database_.last_error());
    }
    std::clog << "[startup] database_connected host=" << config_.database.host
              << " port=" << config_.database.port
              << " name=" << config_.database.name
              << " user=" << config_.database.user
              << " ssl_mode=" << config_.database.ssl_mode
              << std::endl;
    register_routes();
}

Response Application::json_response(int status, const Json &body) const {
    Response response;
    response.status = status;
    response.body = body.dump();
    return response;
}

Response Application::common_success(const Json &data, const std::string &message, int status) const {
    Json result = Json::object();
    result["code"] = 200;
    result["message"] = message;
    result["data"] = data;
    return json_response(status, result);
}

Response Application::common_failed(int code, const std::string &message, int http_status) const {
    Json result = Json::object();
    result["code"] = code;
    result["message"] = message;
    result["data"] = Json();
    return json_response(http_status, result);
}

Response Application::file_response(const fs::path &path, const std::string &download_name) const {
    if (!fs::exists(path) || !fs::is_regular_file(path)) return common_failed(404, "file not found", 404);
    Response response;
    response.status = 200;
    response.content_type = guess_content_type(path_to_utf8_string(path));
    response.binary_body = read_binary_file(path);
    const std::string name = download_name.empty() ? path_to_utf8_string(path.filename()) : download_name;
    response.headers["Content-Disposition"] = "attachment; filename=\"" + name + "\"";
    return response;
}
std::string Application::request_ip(const Request &request) const {
    if (!config_.server.trust_proxy) return request.remote_addr;
    const std::string forwarded = request.header(config_.server.proxy_header);
    if (!forwarded.empty()) {
        const std::size_t comma = forwarded.find(',');
        const std::string ip = trim(forwarded.substr(0, comma));
        if (!ip.empty()) return ip;
    }
    return request.remote_addr;
}

std::vector<int> Application::random_key() const {
    std::vector<int> key(20, 1);
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> dist(1, 9);
    for (int &item : key) item = dist(gen);
    return key;
}

std::string Application::format_date_now() const { return format_now(); }
std::string Application::add_days(const std::string &iso_date, int days) const { return add_days_impl(iso_date, days); }

std::string Application::make_random_name() const {
    static const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> dist(0, static_cast<int>(sizeof(chars) - 2));
    std::string output;
    for (int i = 0; i < 8; ++i) output.push_back(chars[dist(gen)]);
    return output;
}

Json Application::parse_body_or_empty(const Request &request) const {
    if (request.body.empty()) return Json::object();
    try {
        return Json::parse(request.body);
    } catch (...) {
        Json result = Json::object();
        for (const auto &entry : parse_query_string(request.body)) result[entry.first] = entry.second;
        return result;
    }
}

std::optional<std::string> Application::current_user(const Request &request) const {
    if (request.current_user.has_value() && !request.current_user->empty()) return request.current_user;
    return std::nullopt;
}
std::optional<User> Application::find_user_by_username(const std::string &username) {
    const auto lookup_started = std::chrono::steady_clock::now();
    std::clog << "[auth] user_lookup_start username=" << username << std::endl;
    const std::string sql =
        "SELECT id, username, password, gold, level, qq, name, img, client_key, server_key FROM sys_user WHERE username='" +
        database_.escape(username) + "' LIMIT 1";
    const auto rows = database_.query(sql);
    const auto lookup_finished = std::chrono::steady_clock::now();
    std::clog << "[auth] user_lookup_done"
              << " username=" << username
              << " rows=" << rows.size()
              << " db_error=" << (database_.last_error().empty() ? "-" : database_.last_error())
              << " cost_ms=" << duration_ms(lookup_started, lookup_finished)
              << std::endl;
    if (rows.empty()) return std::nullopt;
    const Row &row = rows.front();
    User user;
    user.id = row.count("id") ? std::stoi(row.at("id")) : 0;
    user.username = row.at("username");
    user.password = row.at("password");
    user.gold = row.count("gold") ? std::stoi(row.at("gold")) : 0;
    user.level = row.count("level") ? std::stoi(row.at("level")) : 1;
    user.qq = row.count("qq") ? row.at("qq") : "";
    user.name = row.count("name") ? row.at("name") : "";
    user.img = row.count("img") ? row.at("img") : "";
    user.client_key = row.count("client_key") ? row.at("client_key") : "";
    user.server_key = row.count("server_key") ? row.at("server_key") : "";
    return user;
}

bool Application::username_exists(const std::string &username) { return find_user_by_username(username).has_value(); }

bool Application::create_user(const std::string &username, const std::string &password, const std::string &qq) {
    const std::string encoded_password = crypto_.hash_password(password);
    if (encoded_password.empty()) return false;
    const std::string sql =
        "INSERT INTO sys_user (username, password, create_time, update_time, gold, qq, name) VALUES ('" +
        database_.escape(username) + "','" + database_.escape(encoded_password) +
        "', NOW(), NOW(), 0, '" + database_.escape(qq) + "', '" + database_.escape(make_random_name()) + "')";
    return database_.execute(sql);
}

bool Application::update_password(const std::string &username, const std::string &qq, const std::string &password) {
    auto user = find_user_by_username(username);
    if (!user.has_value() || user->qq != qq) return false;
    const std::string encoded_password = crypto_.hash_password(password);
    if (encoded_password.empty()) return false;
    return database_.execute("UPDATE sys_user SET password='" + database_.escape(encoded_password) +
                             "', update_time=NOW() WHERE username='" + database_.escape(username) + "'");
}

bool Application::update_user_row(const User &user) {
    const std::string sql =
        "UPDATE sys_user SET gold=" + std::to_string(user.gold) + ", name='" + database_.escape(user.name) +
        "', img='" + database_.escape(user.img) + "', client_key='" + database_.escape(user.client_key) +
        "', server_key='" + database_.escape(user.server_key) + "', update_time=NOW() WHERE id=" + std::to_string(user.id);
    return database_.execute(sql);
}

std::vector<UserAndServer> Application::servers_for_user(const std::string &username) {
    std::vector<UserAndServer> result;
    const auto rows = database_.query("SELECT user_name, server_ip, account, password, port FROM user_and_server WHERE user_name='" + database_.escape(username) + "'");
    for (const auto &row : rows) result.push_back(UserAndServer{row.at("user_name"), row.at("server_ip"), row.at("account"), row.at("password"), row.at("port")});
    return result;
}

std::optional<UserAndServer> Application::find_server(const std::string &username, const std::string &ip) {
    const auto rows = database_.query("SELECT user_name, server_ip, account, password, port FROM user_and_server WHERE user_name='" + database_.escape(username) + "' AND server_ip='" + database_.escape(ip) + "' LIMIT 1");
    if (rows.empty()) return std::nullopt;
    const Row &row = rows.front();
    return UserAndServer{row.at("user_name"), row.at("server_ip"), row.at("account"), row.at("password"), row.at("port")};
}

bool Application::upsert_server(const UserAndServer &server) {
    if (find_server(server.user_name, server.server_ip).has_value()) {
        return database_.execute("UPDATE user_and_server SET account='" + database_.escape(server.account) +
                                 "', password='" + database_.escape(server.password) + "', port='" + database_.escape(server.port) +
                                 "' WHERE user_name='" + database_.escape(server.user_name) + "' AND server_ip='" + database_.escape(server.server_ip) + "'");
    }
    return database_.execute("INSERT INTO user_and_server (user_name, server_ip, account, password, port) VALUES ('" +
                             database_.escape(server.user_name) + "','" + database_.escape(server.server_ip) + "','" +
                             database_.escape(server.account) + "','" + database_.escape(server.password) + "','" + database_.escape(server.port) + "')");
}

bool Application::delete_server(const std::string &username, const std::string &ip) {
    return database_.execute("DELETE FROM user_and_server WHERE user_name='" + database_.escape(username) + "' AND server_ip='" + database_.escape(ip) + "'");
}

std::vector<UserServerProject> Application::projects_for_ip(const std::string &ip, bool require_open) {
    std::vector<UserServerProject> result;
    std::string sql =
        "SELECT server_ip, project_name, user_name, open, DATE_FORMAT(expiration_date, '%Y-%m-%d %H:%i:%s') AS expiration_date FROM user_server_project WHERE server_ip='" +
        database_.escape(ip) + "' AND expiration_date >= NOW()";
    if (require_open) sql += " AND open=1";
    const auto rows = database_.query(sql);
    for (const auto &row : rows) result.push_back(UserServerProject{row.at("server_ip"), row.at("project_name"), row.at("user_name"), row.at("expiration_date"), row.count("open") ? std::stoi(row.at("open")) : 1});
    return result;
}

std::vector<UserServerProject> Application::projects_for_user(const std::string &username) {
    std::vector<UserServerProject> result;
    const auto rows = database_.query(
        "SELECT server_ip, project_name, user_name, open, DATE_FORMAT(expiration_date, '%Y-%m-%d %H:%i:%s') AS expiration_date FROM user_server_project WHERE user_name='" +
        database_.escape(username) + "' AND expiration_date >= NOW()");
    for (const auto &row : rows) result.push_back(UserServerProject{row.at("server_ip"), row.at("project_name"), row.at("user_name"), row.at("expiration_date"), row.count("open") ? std::stoi(row.at("open")) : 1});
    return result;
}

std::optional<UserServerProject> Application::find_project_binding(const std::string &ip, const std::string &project_name) {
    const auto rows = database_.query(
        "SELECT server_ip, project_name, user_name, open, DATE_FORMAT(expiration_date, '%Y-%m-%d %H:%i:%s') AS expiration_date FROM user_server_project WHERE server_ip='" +
        database_.escape(ip) + "' AND project_name='" + database_.escape(project_name) + "' LIMIT 1");
    if (rows.empty()) return std::nullopt;
    const Row &row = rows.front();
    return UserServerProject{row.at("server_ip"), row.at("project_name"), row.at("user_name"), row.at("expiration_date"), row.count("open") ? std::stoi(row.at("open")) : 1};
}

bool Application::add_or_extend_project(const std::string &ip, const std::string &project_name, int days, const std::string &username) {
    const auto existing = find_project_binding(ip, project_name);
    if (!existing.has_value()) {
        return database_.execute("INSERT INTO user_server_project (server_ip, project_name, expiration_date, user_name, open) VALUES ('" + database_.escape(ip) +
                                 "','" + database_.escape(project_name) + "', DATE_ADD(NOW(), INTERVAL " + std::to_string(days) + " DAY), '" +
                                 database_.escape(username) + "', 1)");
    }
    const std::string next = add_days(existing->expiration_date, days);
    return database_.execute("UPDATE user_server_project SET expiration_date='" + database_.escape(next) + "', user_name='" + database_.escape(username) +
                             "' WHERE server_ip='" + database_.escape(ip) + "' AND project_name='" + database_.escape(project_name) + "'");
}

bool Application::update_project_open(const std::string &ip, const std::string &project_name, int open_value) {
    return database_.execute("UPDATE user_server_project SET open=" + std::to_string(open_value) + " WHERE server_ip='" + database_.escape(ip) +
                             "' AND project_name='" + database_.escape(project_name) + "'");
}

bool Application::insert_log(const std::string &username, int type, const std::string &content) {
    return database_.execute("INSERT INTO `log` (`name`, `log`, `type`, `time`) VALUES ('" + database_.escape(username) + "','" +
                             database_.escape(content) + "'," + std::to_string(type) + ", NOW())");
}

void Application::register_routes() {
    using Member = Response (Application::*)(const Request &);
    auto add = [this](const std::string &method, const std::string &path, bool auth, Member member) {
        router_.add(method, path, auth, [this, auth, member](const Request &request) {
            Request copy = request;
            if (auth) {
                const std::string header = copy.header("authorization");
                if (header.rfind("Bearer ", 0) != 0) return common_failed(401, "暂未登录或 token 已经过期", 401);
                const auto user = crypto_.validate_jwt(header.substr(7));
                if (!user.has_value() || user->empty()) return common_failed(401, "暂未登录或 token 已经过期", 401);
                copy.current_user = user.value();
            }
            return (this->*member)(copy);
        });
    };

    add("POST", "/login", false, &Application::handle_login);
    add("POST", "/register", false, &Application::handle_register);
    add("POST", "/changePassword", false, &Application::handle_change_password);
    add("POST", "/rindro/getInfo", true, &Application::handle_get_info);
    add("POST", "/rindro/addServer", true, &Application::handle_add_server);
    add("POST", "/rindro/deleteServer", true, &Application::handle_delete_server);
    add("POST", "/rindro/getServerList", true, &Application::handle_get_server_list);
    add("POST", "/rindro/getServer", true, &Application::handle_get_server);
    add("POST", "/rindro/getProjectInfoList", true, &Application::handle_get_project_info_list);
    add("POST", "/rindro/getUserProList", true, &Application::handle_get_user_project_list);
    add("POST", "/rindro/getServerProList", true, &Application::handle_get_server_project_list);
    add("POST", "/rindro/buy", true, &Application::handle_buy);
    add("GET", "/rindro/addgold", false, &Application::handle_add_gold);
    add("POST", "/rindro/transfer", true, &Application::handle_transfer);
    add("POST", "/rindro/setKey", true, &Application::handle_set_key);
    add("POST", "/rindro/setproopen", true, &Application::handle_set_project_open);
    add("POST", "/rindro/download", false, &Application::handle_rindro_download);
    add("GET", "/rindro/download/sh", false, &Application::handle_get_version);
    add("GET", "/rindro/download/c", false, &Application::handle_get_version);
    add("GET", "/rindro/download/d", false, &Application::handle_get_version);
    add("GET", "/rindro/download/djson", false, &Application::handle_get_version);
    add("GET", "/rindro/download/dsh", false, &Application::handle_get_version);
    add("GET", "/rindro/download/ddsh", false, &Application::handle_get_version);
    add("GET", "/rindro/download/jdk", false, &Application::handle_get_version);
    add("GET", "/rindro/download/jar", false, &Application::handle_get_version);
    add("POST", "/rindro/download/all", false, &Application::handle_get_version);
    add("GET", "/rindro/download/gr", false, &Application::handle_get_version);
    add("GET", "/rindro/getversion", false, &Application::handle_get_version);
    add("GET", "/rindro/getimg/{project}/{img}", false, &Application::handle_get_version);
    add("GET", "/rindro/getthumb/{project}/{img}", false, &Application::handle_get_version);
    add("GET", "/rindro/getmp42/{project}/{img}", false, &Application::handle_get_version);
    add("GET", "/rindro/getmp4/{project}/{img}", false, &Application::handle_get_version);
    add("GET", "/dps/getversion", false, &Application::handle_get_version);
    add("GET", "/dps/getclientversion", false, &Application::handle_get_version);
    add("GET", "/dps/getlist", false, &Application::handle_get_version);
    add("GET", "/dps/getadvertisement", false, &Application::handle_get_version);
    add("POST", "/dps/download/{colonPath}", false, &Application::handle_get_version);
    add("POST", "/dps/download2/{colonPath}", false, &Application::handle_get_version);
    add("GET", "/dps/download3/{colonPath}", false, &Application::handle_get_version);
    add("GET", "/script/client", false, &Application::handle_script_client);
    add("GET", "/script/client2", false, &Application::handle_script_client2);
    add("GET", "/script/getzdy", false, &Application::handle_script_getzdy);
    add("POST", "/script/getpro", false, &Application::handle_script_getpro);
    add("GET", "/script/getservice", false, &Application::handle_script_getservice);
    add("GET", "/script/getBaseUpdateTime", false, &Application::handle_script_base_update_time);
    add("GET", "/script/getBaseSutZip", false, &Application::handle_script_base_sut);
    add("GET", "/script/getservicenew", false, &Application::handle_script_getservice_new);
    add("GET", "/api/videos/{project}/{videoName}", false, &Application::handle_video_placeholder);
    add("GET", "/zf/callback", false, &Application::handle_payment_placeholder);
    add("POST", "/zf/create", true, &Application::handle_payment_placeholder);
}
Response Application::handle_login(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const std::string username = body["username"].to_string_or("");
    const std::string password = body["password"].to_string_or("");
    const std::string ip = request_ip(request);
    const auto login_started = std::chrono::steady_clock::now();
    std::clog << "[auth] login_start"
              << " ip=" << ip
              << " username=" << username
              << std::endl;
    const auto lookup_started = std::chrono::steady_clock::now();
    const auto user = find_user_by_username(username);
    const auto lookup_finished = std::chrono::steady_clock::now();
    std::clog << "[auth] login_lookup_done"
              << " ip=" << ip
              << " username=" << username
              << " has_user=" << (user.has_value() ? "yes" : "no")
              << " cost_ms=" << duration_ms(lookup_started, lookup_finished)
              << std::endl;
    if (!user.has_value()) {
        const std::string db_error = database_.last_error();
        if (!db_error.empty()) {
            std::clog << "[auth] login_failed reason=db_query_failed"
                      << " ip=" << ip
                      << " username=" << username
                      << " db_error=" << db_error
                      << " total_cost_ms=" << duration_ms(login_started, std::chrono::steady_clock::now())
                      << std::endl;
        } else {
            std::clog << "[auth] login_failed reason=user_not_found"
                      << " ip=" << ip
                      << " username=" << username
                      << " total_cost_ms=" << duration_ms(login_started, std::chrono::steady_clock::now())
                      << std::endl;
        }
        return common_failed(401, "用户名或密码错误", 401);
    }
    if (user->password.rfind("$2", 0) != 0) {
        std::clog << "[auth] login_failed reason=stored_password_not_bcrypt"
                  << " ip=" << ip
                  << " username=" << username
                  << " stored_password_len=" << user->password.size()
                  << " stored_password_prefix=" << user->password.substr(0, std::min<std::size_t>(4, user->password.size()))
                  << " total_cost_ms=" << duration_ms(login_started, std::chrono::steady_clock::now())
                  << std::endl;
        return common_failed(401, "用户名或密码错误", 401);
    }
    std::clog << "[auth] password_verify_start"
              << " ip=" << ip
              << " username=" << username
              << std::endl;
    const auto verify_started = std::chrono::steady_clock::now();
    const bool password_ok = crypto_.verify_password(password, user->password);
    const auto verify_finished = std::chrono::steady_clock::now();
    std::clog << "[auth] password_verify_done"
              << " ip=" << ip
              << " username=" << username
              << " ok=" << (password_ok ? "yes" : "no")
              << " cost_ms=" << duration_ms(verify_started, verify_finished)
              << std::endl;
    if (!password_ok) {
        std::clog << "[auth] login_failed reason=password_mismatch"
                  << " ip=" << ip
                  << " username=" << username
                  << " stored_password_len=" << user->password.size()
                  << " stored_password_prefix=" << user->password.substr(0, std::min<std::size_t>(4, user->password.size()))
                  << " total_cost_ms=" << duration_ms(login_started, std::chrono::steady_clock::now())
                  << std::endl;
        return common_failed(401, "用户名或密码错误", 401);
    }
    std::clog << "[auth] login_success"
              << " ip=" << ip
              << " username=" << username
              << " total_cost_ms=" << duration_ms(login_started, std::chrono::steady_clock::now())
              << std::endl;
    Json data = Json::object();
    data["token"] = "Bearer " + crypto_.issue_jwt(user->username, config_.jwt.expiration_seconds);
    data["username"] = user->username;
    Json roles = Json::array();
    roles.push_back("ROLE_ALL");
    data["roles"] = roles;
    return json_response(200, data);
}

Response Application::handle_register(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const std::string username = body["username"].to_string_or("");
    const std::string password = body["password"].to_string_or("");
    const std::string qq = body["qq"].to_string_or("");
    if (username.empty()) return common_failed(201, "用户名为空", 201);
    if (password.empty()) return common_failed(201, "密码为空", 201);
    if (qq.empty()) return common_failed(201, "安全口令为空", 201);
    if (username_exists(username)) return common_failed(201, "用户名已存在", 201);
    return create_user(username, password, qq) ? common_success("注册成功") : common_failed(500, "注册失败");
}

Response Application::handle_change_password(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const std::string username = body["username"].to_string_or("");
    const std::string password = body["password"].to_string_or("");
    const std::string qq = body["qq"].to_string_or("");
    if (username.empty()) return common_failed(201, "用户名为空", 201);
    if (password.empty()) return common_failed(201, "密码为空", 201);
    if (qq.empty()) return common_failed(201, "安全口令为空", 201);
    return update_password(username, qq, password) ? common_success("修改成功") : common_failed(500, "验证信息错误");
}

Response Application::handle_get_info(const Request &request) {
    const auto user = find_user_by_username(current_user(request).value_or(""));
    if (!user.has_value()) return common_failed(404, "用户不存在", 404);
    Json body = Json::object();
    body["userpoints"] = user->gold;
    body["username"] = user->username;
    body["displayName"] = user->name;
    body["useravatar"] = user->img;
    body["level"] = user->level;
    body["uservip"] = membership_label(user->level);
    body["usercash"] = Json();
    return common_success(body);
}

Response Application::handle_add_server(const Request &request) {
    const Json body = parse_body_or_empty(request);
    UserAndServer row{current_user(request).value_or(""), body["ip"].to_string_or(""), body["username"].to_string_or(""), body["password"].to_string_or(""), body["port"].to_string_or("")};
    return upsert_server(row) ? common_success("添加成功") : common_failed(500, "添加失败");
}

Response Application::handle_delete_server(const Request &request) {
    const Json body = parse_body_or_empty(request);
    return delete_server(current_user(request).value_or(""), body["ip"].to_string_or("")) ? common_success("删除成功") : common_failed(500, "删除失败");
}

Response Application::handle_get_server_list(const Request &request) {
    Json result = Json::array();
    for (const auto &server : servers_for_user(current_user(request).value_or(""))) {
        Json item = Json::object();
        item["serverIp"] = server.server_ip;
        item["userName"] = server.account;
        item["password"] = server.password;
        item["port"] = server.port;
        result.push_back(item);
    }
    return common_success(result);
}

Response Application::handle_get_server(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const auto server = find_server(current_user(request).value_or(""), body["ip"].to_string_or(""));
    if (!server.has_value()) return common_success(Json());
    Json item = Json::object();
    item["userName"] = server->user_name;
    item["serverIp"] = server->server_ip;
    item["account"] = server->account;
    item["password"] = server->password;
    item["port"] = server->port;
    return common_success(item);
}

Response Application::handle_get_project_info_list(const Request &) {
    const Json data = catalog_.public_project_info();
    return common_success(data);
}

Response Application::handle_get_user_project_list(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const std::string ip = body["ip"].to_string_or("");
    const Json all = catalog_.all_project_info();
    Json result = Json::object();
    for (const auto &binding : projects_for_ip(ip)) {
        Json info = all[binding.project_name];
        if (info.is_null()) continue;
        info["endTime"] = binding.expiration_date;
        info["open"] = binding.open;
        result[binding.project_name] = info;
    }
    return common_success(result);
}

Response Application::handle_get_server_project_list(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const std::string ip = body["ip"].to_string_or("");
    if (!find_server(current_user(request).value_or(""), ip).has_value()) return common_success(Json());
    Json items = Json::array();
    for (const auto &binding : projects_for_ip(ip)) {
        Json item = Json::object();
        item["serverIp"] = binding.server_ip;
        item["projectName"] = binding.project_name;
        item["expirationDate"] = binding.expiration_date;
        item["userName"] = binding.user_name;
        item["open"] = binding.open;
        items.push_back(item);
    }
    return common_success(items);
}

Response Application::handle_buy(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const std::string username = current_user(request).value_or("");
    const std::string ip = body["ip"].to_string_or("");
    const std::string project_name = body["projectName"].to_string_or("");
    if (is_local_purchase_ip(ip)) {
        std::clog << "[buy] rejected_local_ip"
                  << " username=" << username
                  << " ip=" << ip
                  << " project=" << project_name
                  << std::endl;
        return common_failed(500, "本地IP不允许购买");
    }
    if (!find_server(username, ip).has_value()) return common_failed(500, "账号下不存在此服务器");
    auto user = find_user_by_username(username);
    if (!user.has_value()) return common_failed(500, "用户不存在");
    const int price = catalog_.project_price(project_name);
    if (price > 0 && user->gold < price) return common_failed(500, "积分不足");
    if (price > 0) {
        user->gold -= price;
        update_user_row(*user);
    }
    if (!add_or_extend_project(ip, project_name, 31, username)) return common_failed(500, "购买失败");
    insert_log(username, price > 0 ? 3 : 2, "购买项目 " + project_name + " IP " + ip);
    Json result = Json::object();
    result["info"] = "购买成功";
    const auto binding = find_project_binding(ip, project_name);
    result["endTime"] = binding.has_value() ? binding->expiration_date : add_days(format_date_now(), 31);
    return common_success(result);
}

Response Application::handle_add_gold(const Request &request) {
    const std::string username = request.query_value("userName");
    const int requested = request.query_value("gold").empty() ? 0 : std::stoi(request.query_value("gold"));
    auto user = find_user_by_username(username);
    if (!user.has_value()) return common_failed(500, "充值失败");
    int gold = requested;
    if (gold == 200) gold = 220;
    if (gold == 500) gold = 570;
    user->gold += gold;
    update_user_row(*user);
    insert_log(username, 1, "充值 " + std::to_string(gold) + " 成功");
    return common_success("充值成功");
}

Response Application::handle_transfer(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const std::string from = current_user(request).value_or("");
    const std::string to = body["username"].to_string_or("");
    const int gold = body["gold"].to_int_or(0);
    std::lock_guard<std::mutex> lock(transfer_mutex_);
    auto from_user = find_user_by_username(from);
    auto to_user = find_user_by_username(to);
    if (!from_user.has_value() || !to_user.has_value()) return common_success("被转让用户不存在，请检查用户名");
    if (from_user->gold < gold) return common_success("余额不足");
    from_user->gold -= gold;
    to_user->gold += gold;
    update_user_row(*from_user);
    update_user_row(*to_user);
    insert_log(from, 4, from + " 向 " + to + " 转让 " + std::to_string(gold) + " 积分");
    return common_success("转让完毕");
}

Response Application::handle_set_key(const Request &request) {
    auto user = find_user_by_username(current_user(request).value_or(""));
    if (!user.has_value()) return common_success("请重新登录后再试");
    const Json body = parse_body_or_empty(request);
    if (body.contains("clientkey")) user->client_key = body["clientkey"].to_string_or(user->client_key);
    if (body.contains("serverkey")) user->server_key = body["serverkey"].to_string_or(user->server_key);
    return update_user_row(*user) ? common_success("设置成功") : common_success("设置失败");
}

Response Application::handle_set_project_open(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const std::string ip = body["ip"].to_string_or("");
    const std::string project = body["projectName"].to_string_or("");
    const int open = body["open"].to_int_or(0);
    if (!find_server(current_user(request).value_or(""), ip).has_value()) return common_success("账号下不存在此项目，请检查项目所有权是否为该账号");
    return update_project_open(ip, project, open) ? common_success("操作成功") : common_success("账号下不存在此项目，请检查项目所有权是否为该账号");
}

Response Application::handle_rindro_download(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const std::string project_name = trim(body["projectName"].to_string_or(""));
    if (project_name.empty()) return common_failed(404, "file not found", 404);

    const fs::path target = path_from_utf8(config_.paths.file_root) / "download" / "profile" / path_from_utf8(project_name + ".zip");
    if (!fs::exists(target) || !fs::is_regular_file(target)) {
        std::clog << "[download] profile package missing project=" << project_name
                  << " path=" << path_to_utf8_string(target) << '\n';
        return common_failed(404, "file not found", 404);
    }
    return file_response(target, project_name + ".zip");
}

Response Application::handle_static_download(const std::string &disk_path, const std::string &download_name) const { return file_response(disk_path, download_name); }

Response Application::handle_get_image(const Request &request, bool) {
    const std::string project_name = request.path_param("project");
    const std::string img = request.path_param("img");
    const std::string img_path = catalog_.project_img_path(project_name);
    if (img_path.empty()) return common_failed(404, "file not found", 404);
    return file_response(join_path(join_path(config_.paths.file_root, "Script/Project/" + img_path + "/img"), img + ".png"));
}

Response Application::handle_get_mp42(const Request &request) {
    const std::string project_name = request.path_param("project");
    const std::string img = request.path_param("img");
    const std::string img_path = catalog_.project_img_path(project_name);
    if (img_path.empty()) return common_failed(404, "file not found", 404);
    return file_response(join_path(join_path(config_.paths.file_root, "Script/Project/" + img_path + "/vid"), img + ".mp4"));
}
Response Application::handle_get_version(const Request &request) {
    const std::string path = request.path;
    if (path == "/rindro/download/sh") return file_response(join_path(config_.paths.file_root, "re.sh"), "re.sh");
    if (path == "/rindro/download/c") return file_response(join_path(config_.paths.file_root, "download/ndpsm_svr"), "ndpsm_svr");
    if (path == "/rindro/download/d") return file_response(join_path(config_.paths.file_root, "download/DPS_Login_Gateway"), "DPS_Login_Gateway");
    if (path == "/rindro/download/djson") return file_response(join_path(config_.paths.file_root, "download/config.json"), "config.json");
    if (path == "/rindro/download/dsh") return file_response(join_path(config_.paths.file_root, "download/DpsLoginRestartSh"), "DpsLoginRestartSh");
    if (path == "/rindro/download/ddsh") return file_response(join_path(config_.paths.file_root, "download/DpsLoginDownloadSh"), "DpsLoginDownloadSh");
    if (path == "/rindro/download/jdk") return file_response(join_path(config_.paths.file_root, "jdk"), "jdk");
    if (path == "/rindro/download/jar") return file_response(join_path(config_.paths.file_root, "download/RT.tar.gz"), "RT.tar.gz");
    if (path == "/rindro/download/all") {
        try {
            const Json body = parse_body_or_empty(request);
            const Json map = Json::parse(read_text_file(config_.paths.file_map));
            const std::string target = map[body["key"].to_string_or("")].to_string_or("");
            if (target.empty() || !fs::exists(target) || !fs::is_regular_file(target)) return common_failed(404, "无下载文件", 404);
            return file_response(target);
        } catch (...) {
            return common_failed(404, "无下载文件", 404);
        }
    }
    if (path == "/rindro/download/gr") {
        try {
            const Json map = Json::parse(read_text_file(config_.paths.file_map));
            const std::string target = map[request.query_value("body")].to_string_or("");
            if (target.empty() || !fs::exists(target) || !fs::is_regular_file(target)) return common_failed(404, "无下载文件", 404);
            return file_response(target);
        } catch (...) {
            return common_failed(404, "无下载文件", 404);
        }
    }
    if (path == "/rindro/getversion") {
        Response response;
        response.content_type = "text/plain; charset=utf-8";
        response.body = read_text_file(join_path(config_.paths.file_root, "download/version"));
        return response;
    }
    if (path.rfind("/rindro/getimg/", 0) == 0) return handle_get_image(request, false);
    if (path.rfind("/rindro/getthumb/", 0) == 0) return handle_get_image(request, true);
    if (path.rfind("/rindro/getmp42/", 0) == 0 || path.rfind("/rindro/getmp4/", 0) == 0) return handle_get_mp42(request);
    if (path == "/dps/getversion") return handle_dps_version("Application/Proj.ifo");
    if (path == "/dps/getclientversion") return handle_dps_version("Marketplace/Proj.ifo");
    if (path == "/dps/getlist") return handle_dps_list("Script", "Proj.ifo");
    if (path == "/dps/getadvertisement") return handle_dps_list("Advertisement", "info.ifo");
    if (path.rfind("/dps/download/", 0) == 0) return handle_dps_download(request, join_path(config_.paths.dps_root, "Script"));
    if (path.rfind("/dps/download2/", 0) == 0) return handle_dps_download(request, join_path(config_.paths.dps_root, "Application"));
    if (path.rfind("/dps/download3/", 0) == 0) return handle_dps_download(request, join_path(config_.paths.dps_root, "Advertisement"));
    return common_failed(404, "not found", 404);
}

Response Application::handle_dps_version(const std::string &relative_file) const {
    Response response;
    response.content_type = "text/plain; charset=utf-8";
    response.body = read_text_file(join_path(config_.paths.dps_root, relative_file));
    return response;
}

Response Application::handle_dps_list(const std::string &relative_dir, const std::string &metadata_file) const {
    Json list = Json::array();
    const fs::path root = fs::path(config_.paths.dps_root) / relative_dir;
    if (fs::exists(root)) {
        for (const auto &entry : fs::directory_iterator(root)) {
            if (!entry.is_directory()) continue;
            const std::string text = read_text_file((entry.path() / metadata_file).string());
            if (text.empty()) continue;
            try { list.push_back(Json::parse(text)); } catch (...) { list.push_back(text); }
        }
    }
    Json result = Json::object();
    result["list"] = list;
    Response response;
    response.content_type = "application/json; charset=utf-8";
    response.body = result.dump();
    return response;
}

Response Application::handle_dps_download(const Request &request, const std::string &base_dir) const {
    std::string relative = request.path_param("colonPath");
    relative = url_decode(relative);
    std::replace(relative.begin(), relative.end(), ':', '/');
    const fs::path root = fs::weakly_canonical(fs::path(base_dir));
    const fs::path target = fs::weakly_canonical(root / path_from_utf8(relative));
    if (!path_prefix_matches(root, target)) return common_failed(403, "forbidden", 403);
    return file_response(target);
}

Response Application::handle_script_client(const Request &request) {
    const std::string ip = request.query_value("ip");
    const std::string version = request.query_value("ve").empty() ? "1.0" : request.query_value("ve");
    const auto key = random_key();
    bool cs_mode = false;
    std::vector<std::string> scripts;
    const auto projects = projects_for_ip(ip, true);
    for (const auto &binding : projects) if (binding.project_name == "cs") cs_mode = true;
    auto base = catalog_.base_script_contents(version, cs_mode);
    scripts.insert(scripts.end(), base.begin(), base.end());
    for (const auto &binding : projects) {
        if (binding.project_name == "cs") continue;
        auto extra = catalog_.project_script_contents(binding.project_name, version);
        scripts.insert(scripts.end(), extra.begin(), extra.end());
    }
    auto custom = catalog_.custom_client_scripts(ip, version);
    scripts.insert(scripts.end(), custom.begin(), custom.end());
    std::ostringstream payload;
    for (const auto &script : scripts) payload << to_json_bytes(crypto_.makecode(std::vector<unsigned char>(script.begin(), script.end()), key)) << "$$$$$";
    std::string key_string;
    for (int item : key) key_string.push_back(static_cast<char>('0' + item));
    Json result = Json::object();
    result["getBaseScriptStr"] = payload.str();
    result["key"] = crypto_.rsa_private_encrypt_script(key_string);
    Response response;
    response.content_type = "text/plain; charset=utf-8";
    response.body = crypto_.gzip_base64(result.dump());
    return response;
}

Response Application::handle_script_client2(const Request &request) {
    const std::string ip = request.query_value("ip");
    const std::string version = request.query_value("ve").empty() ? "1.0" : request.query_value("ve");
    const auto key = random_key();
    Json values = Json::array();
    auto base = catalog_.base_script_contents(version, false);
    for (const auto &script : base) values.push_back(to_json_bytes(crypto_.makecode(std::vector<unsigned char>(script.begin(), script.end()), key)));
    for (const auto &binding : projects_for_ip(ip, true)) {
        auto extra = catalog_.project_script_contents(binding.project_name, version);
        for (const auto &script : extra) values.push_back(to_json_bytes(crypto_.makecode(std::vector<unsigned char>(script.begin(), script.end()), key)));
    }
    std::string key_string;
    for (int item : key) key_string.push_back(static_cast<char>('0' + item));
    Json result = Json::object();
    result["getBaseScriptStr"] = values;
    result["key"] = crypto_.rsa_private_encrypt_script(key_string);
    return json_response(200, result);
}

Response Application::handle_script_getzdy(const Request &request) {
    const std::string version = request.query_value("ve").empty() ? "1.0" : request.query_value("ve");
    Json result = Json::object();
    Json list = Json::array();
    for (const auto &script : catalog_.custom_client_scripts(request_ip(request), version)) list.push_back(script);
    result["getBaseScriptStr"] = list;
    Response response;
    response.content_type = "application/json; charset=utf-8";
    response.body = result.dump();
    return response;
}

Response Application::handle_script_getpro(const Request &request) {
    const Json body = parse_body_or_empty(request);
    const std::string prot = body["prot"].to_string_or(request.query_value("prot"));
    const std::string mac = body["mac"].to_string_or(request.query_value("mac"));
    const std::string stamp_text = body["l"].is_number() ? std::to_string(body["l"].as_int64()) : body["l"].to_string_or(request.query_value("l"));
    const long long stamp = stamp_text.empty() ? 0 : std::stoll(stamp_text);
    const std::string s = body["s"].to_string_or(request.query_value("s"));
    const std::string ip = request_ip(request);
    const long long now = static_cast<long long>(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count());
    if (std::llabs(now - stamp) > 5LL * 60 * 1000 || prot.empty() || mac.empty()) {
        Response response; response.content_type = "text/plain; charset=utf-8"; response.body = ""; return response;
    }
    {
        std::lock_guard<std::mutex> lock(script_mutex_);
        if (ip_and_mac_.count(ip) == 0) ip_and_mac_[ip] = mac;
    }
    const auto bindings = projects_for_ip(ip, true);
    if (bindings.empty()) {
        Response response; response.content_type = "text/plain; charset=utf-8"; response.body = ""; return response;
    }
    std::ostringstream names;
    for (const auto &binding : bindings) names << binding.project_name << ",";
    Json result = Json::object();
    result["pro"] = names.str();
    result["validate"] = now;
    result["ce"] = s;
    result["key"] = make_random_name();
    result["ip"] = ip;
    Response response;
    response.content_type = "text/plain; charset=utf-8";
    response.body = crypto_.rsa_private_encrypt_tool(result.dump());
    return response;
}

Response Application::handle_script_getservice(const Request &request) {
    const std::string ip = request_ip(request);
    const auto key = random_key();
    auto scripts = catalog_.dps_service_scripts(ip);
    for (const auto &binding : projects_for_ip(ip, true)) {
        const fs::path root = fs::path(config_.paths.file_root) / "dpsScript" / "project" / binding.project_name;
        const fs::path config = root / "config";
        if (!fs::exists(config)) continue;
        for (const auto &item : split_csv_config(config.string())) {
            const std::string path = (root / item).string();
            const std::string content = read_text_file(path);
            if (!content.empty()) scripts.emplace_back(path, content);
        }
    }
    std::ostringstream payload;
    for (const auto &entry : scripts) {
        payload << entry.first << "$$$$$" << to_json_bytes(crypto_.makecode(std::vector<unsigned char>(entry.second.begin(), entry.second.end()), key)) << "$$$$$";
    }
    std::string key_string;
    for (int item : key) key_string.push_back(static_cast<char>('0' + item));
    Json result = Json::object();
    result["getBaseScriptStr"] = payload.str();
    result["key"] = crypto_.rsa_private_encrypt_script(key_string);
    Response response;
    response.content_type = "text/plain; charset=utf-8";
    response.body = crypto_.gzip_base64(result.dump());
    return response;
}

Response Application::handle_script_base_update_time(const Request &request) {
    Response response;
    response.content_type = "text/plain; charset=utf-8";
    response.body = std::to_string(catalog_.base_update_time(request.query_value("ve").empty() ? "1.0" : request.query_value("ve")));
    return response;
}

Response Application::handle_script_base_sut(const Request &) { return file_response(join_path(config_.paths.file_root, "sut.tar.gz"), "sut.tar.gz"); }

Response Application::handle_script_getservice_new(const Request &request) {
    const std::string version = request.query_value("ve").empty() ? "1.0" : request.query_value("ve");
    const std::string ip = request_ip(request);
    const auto bindings = projects_for_ip(ip, true);
    if (bindings.empty()) {
        Response response; response.content_type = "text/plain; charset=utf-8"; response.body = "没有项目"; return response;
    }
    const auto key = random_key();
    std::ostringstream payload;
    for (const auto &binding : bindings) {
        auto scripts = catalog_.project_script_contents(binding.project_name, version);
        for (const auto &script : scripts) payload << binding.project_name << "$$$$$" << to_json_bytes(crypto_.makecode(std::vector<unsigned char>(script.begin(), script.end()), key)) << "$$$$$";
    }
    std::string key_string;
    for (int item : key) key_string.push_back(static_cast<char>('0' + item));
    Json result = Json::object();
    result["getBaseScriptStr"] = payload.str();
    result["key"] = crypto_.rsa_private_encrypt_script(key_string);
    Response response;
    response.content_type = "text/plain; charset=utf-8";
    response.body = crypto_.gzip_base64(result.dump());
    return response;
}

Response Application::handle_video_placeholder(const Request &) { return common_failed(501, "video service is deferred in v1", 501); }
Response Application::handle_payment_placeholder(const Request &) { return common_failed(501, "payment service is deferred in v1", 501); }

int Application::run() {
    HttpServer server(config_.server.host, config_.server.port, config_.logging, router_);
    return server.run();
}

}  // namespace dps
