#include <SQLiteCpp/Database.h>
#include <SQLiteCpp/Statement.h>
#include "../../Radarcape_Core/cpp-httplib/httplib.h"
#include <algorithm>
#include <asio/co_spawn.hpp>
#include <asio/thread_pool.hpp>
#include <asio/use_awaitable.hpp>
#include "Global.h"
#include "Performance_Monitor.h"
#if defined(ASIO_HAS_FILE)
#include <asio/random_access_file.hpp>
#include <asio/read_at.hpp>
#else
#include <fstream>
#endif
#include <cctype>
#include <cstdint>
#include <ctime>
#include <filesystem>
#include <format>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <sys/stat.h>
#include <vector>
namespace {
constexpr std::uint64_t kMaxTileFileSize = 32ull * 1024ull * 1024ull;
enum class Tile_Load_Status { Ready, Not_Found, Not_Modified };
struct Tile_Request {
    std::uint32_t z;
    std::uint32_t x;
    std::uint32_t y;
    std::string extension;
};
struct Tile_Cache_Request {
    std::string if_none_match;
    std::string if_modified_since;
    bool include_body;
};
struct File_Cache_Info {
    std::uint64_t size;
    std::time_t modified_time;
    std::string etag;
    std::string last_modified;
};
struct Tile_Load_Result {
    Tile_Load_Status status;
    std::string path;
    std::string content;
    std::string content_type;
    File_Cache_Info cache_info;
};
asio::thread_pool& tile_file_pool() {
    static asio::thread_pool pool(4);
    return pool;
}
std::string format_http_date(std::time_t time) {
    std::tm tm{};
#ifdef _WIN32
    gmtime_s(&tm, &time);
#else
    gmtime_r(&time, &tm);
#endif
    char buffer[64];
    auto size = std::strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S GMT", &tm);
    return std::string(buffer, size);
}
std::optional<File_Cache_Info> get_file_cache_info(const std::filesystem::path& path) {
#ifdef _WIN32
    struct _stati64 info{};
    if (_wstati64(path.c_str(), &info) != 0 || (info.st_mode & _S_IFMT) != _S_IFREG || info.st_size < 0) {
        return std::nullopt;
    }
#else
    struct stat info{};
    if (::stat(path.c_str(), &info) != 0 || !S_ISREG(info.st_mode) || info.st_size < 0) {
        return std::nullopt;
    }
#endif
    File_Cache_Info result;
    result.size = static_cast<std::uint64_t>(info.st_size);
    result.modified_time = info.st_mtime;
    result.etag = std::format("W/\"{:x}-{:x}\"", static_cast<std::uint64_t>(result.modified_time), result.size);
    result.last_modified = format_http_date(result.modified_time);
    return result;
}
File_Cache_Info make_mbtiles_cache_info(const File_Cache_Info& file_info, const Tile_Request& request,
                                        std::uint64_t content_size) {
    auto result = file_info;
    result.size = content_size;
    result.etag = std::format("W/\"{:x}-{:x}-{}-{}-{}-{:x}\"", static_cast<std::uint64_t>(result.modified_time),
                              file_info.size, request.z, request.x, request.y, content_size);
    return result;
}
std::string_view trim_http_ows(std::string_view value) {
    while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
        value.remove_prefix(1);
    }
    while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) {
        value.remove_suffix(1);
    }
    return value;
}
std::string_view remove_weak_prefix(std::string_view etag) {
    if (etag.starts_with("W/")) {
        etag.remove_prefix(2);
    }
    return etag;
}
bool etag_matches(std::string_view values, std::string_view current_etag) {
    while (!values.empty()) {
        auto pos = values.find(',');
        auto value = trim_http_ows(values.substr(0, pos));
        if (value == "*" || remove_weak_prefix(value) == remove_weak_prefix(current_etag)) {
            return true;
        }
        if (pos == std::string_view::npos) {
            break;
        }
        values.remove_prefix(pos + 1);
    }
    return false;
}
bool is_not_modified(const Tile_Cache_Request& request, const File_Cache_Info& info) {
    if (!request.if_none_match.empty()) {
        return etag_matches(request.if_none_match, info.etag);
    }
    return !request.if_modified_since.empty() && request.if_modified_since == info.last_modified;
}
void add_tile_cache_headers(const drogon::HttpResponsePtr& response, const File_Cache_Info& info,
                            bool include_content_length) {
    response->addHeader("Cache-Control", "public, max-age=86400");
    response->addHeader("ETag", info.etag);
    response->addHeader("Last-Modified", info.last_modified);
    if (include_content_length) {
        response->addHeader("Content-Length", std::to_string(info.size));
    }
}
bool is_unsigned_path_component(std::string_view value) {
    if (value.empty()) {
        return false;
    }
    return std::ranges::all_of(value, [](unsigned char ch) { return std::isdigit(ch) != 0; });
}
std::uint32_t parse_uint32_component(std::string_view value) {
    std::uint64_t result = 0;
    for (char ch : value) {
        result = result * 10 + static_cast<std::uint64_t>(ch - '0');
        if (result > std::numeric_limits<std::uint32_t>::max()) {
            throw std::out_of_range("tile coordinate is too large");
        }
    }
    return static_cast<std::uint32_t>(result);
}
std::optional<Tile_Request> parse_tile_request(std::string_view path, std::string_view prefix,
                                               std::string_view default_extension) {
    if (!path.starts_with(prefix)) {
        return std::nullopt;
    }
    path.remove_prefix(prefix.size());
    auto first = path.find('/');
    auto second = first == std::string_view::npos ? std::string_view::npos : path.find('/', first + 1);
    if (first == std::string_view::npos || second == std::string_view::npos ||
        path.find('/', second + 1) != std::string_view::npos) {
        return std::nullopt;
    }
    auto z = path.substr(0, first);
    auto x = path.substr(first + 1, second - first - 1);
    auto y_with_extension = path.substr(second + 1);
    auto dot = y_with_extension.rfind('.');
    auto y = dot == std::string_view::npos ? y_with_extension : y_with_extension.substr(0, dot);
    auto extension =
    dot == std::string_view::npos ? std::string(default_extension) : std::string(y_with_extension.substr(dot));
    if (!is_unsigned_path_component(z) || !is_unsigned_path_component(x) || !is_unsigned_path_component(y) ||
        extension != default_extension) {
        return std::nullopt;
    }
    return Tile_Request{parse_uint32_component(z), parse_uint32_component(x), parse_uint32_component(y), extension};
}
class Tile_Path_Resolver {
public:
    explicit Tile_Path_Resolver(const Map_Tile_Config& config) :
        config_(config), directories_(config.get_true_directories()) {
        if (config_.path_layout != "z/x/y" && config_.path_layout != "z/y/x") {
            throw std::invalid_argument("invalid tile path_layout: " + config_.path_layout);
        }
        if (config_.extension.empty() || config_.extension.front() != '.') {
            throw std::invalid_argument("invalid tile extension: " + config_.extension);
        }
    }
    std::optional<std::filesystem::path> find_file(const Tile_Request& request) const {
        auto relative = relative_path(request);
        for (const auto& directory : directories_) {
            auto path = Psc::path_from_utf8(directory) / relative;
            if (std::filesystem::is_regular_file(path)) {
                return path;
            }
        }
        return std::nullopt;
    }
    const Map_Tile_Config& config() const {
        return config_;
    }
private:
    std::filesystem::path relative_path(const Tile_Request& request) const {
        auto z = std::to_string(request.z);
        auto x = std::to_string(request.x);
        auto y = std::to_string(request.y) + config_.extension;
        if (config_.path_layout == "z/x/y") {
            return std::filesystem::path(z) / x / y;
        }
        return std::filesystem::path(z) / std::to_string(request.y) / (std::to_string(request.x) + config_.extension);
    }
    Map_Tile_Config config_;
    std::vector<std::string> directories_;
};
class Mbtiles_File {
public:
    explicit Mbtiles_File(std::string path) : path_(std::move(path)) {
        auto file_info = get_file_cache_info(Psc::path_from_utf8(path_));
        if (!file_info) {
            std::string str = "找不到地图:" + VAR_STR_1(path_);
            Psc::fail_fast(str);
        }
        db_ = std::make_unique<SQLite::Database>(path_, SQLite::OPEN_READONLY);
    }
    std::optional<Tile_Load_Result> load(const Tile_Request& request, const Tile_Cache_Request& cache_request,
                                         std::string_view content_type);
private:
    std::string path_;
    std::unique_ptr<SQLite::Database> db_;
    std::mutex mtx_;
};
std::optional<Tile_Load_Result> Mbtiles_File::load(const Tile_Request& request, const Tile_Cache_Request& cache_request,
                                                   std::string_view content_type) {
    auto file_info = get_file_cache_info(Psc::path_from_utf8(path_));
    if (!file_info) {
        return std::nullopt;
    }
    std::lock_guard lock(mtx_);
    SQLite::Statement statement(
    *db_, "SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ? LIMIT 1");
    statement.bind(1, static_cast<int>(request.z));
    statement.bind(2, static_cast<int>(request.x));
    statement.bind(3, static_cast<int>(request.y));
    if (!statement.executeStep()) {
        return std::nullopt;
    }
    auto column = statement.getColumn(0);
    auto bytes = column.getBytes();
    if (bytes < 0 || static_cast<std::uint64_t>(bytes) > kMaxTileFileSize) {
        throw std::runtime_error("mbtiles tile is too large: " + path_);
    }
    auto cache_info = make_mbtiles_cache_info(*file_info, request, static_cast<std::uint64_t>(bytes));
    if (is_not_modified(cache_request, cache_info)) {
        return Tile_Load_Result{
        Tile_Load_Status::Not_Modified, path_, {}, std::string(content_type), std::move(cache_info)};
    }
    std::string content;
    if (cache_request.include_body) {
        auto data = static_cast<const char*>(column.getBlob());
        content.assign(data, static_cast<std::size_t>(bytes));
    }
    return Tile_Load_Result{Tile_Load_Status::Ready, path_, std::move(content), std::string(content_type),
                            std::move(cache_info)};
}
class Tile_Source {
public:
    explicit Tile_Source(Map_Tile_Config config) : config_(std::move(config)) {
        if (config_.extension.empty() || config_.extension.front() != '.') {
            throw std::invalid_argument("invalid tile extension: " + config_.extension);
        }
        if (config_.file_format == Map_Tile_File_Format::directory) {
            resolver_ = std::make_unique<Tile_Path_Resolver>(config_);
        }
        else if (config_.file_format == Map_Tile_File_Format::mbtiles) {
            for (auto& file : config_.get_true_files()) {
                mbtiles_files_.push_back(std::make_unique<Mbtiles_File>(std::move(file)));
            }
        }
        else {
            throw std::invalid_argument("invalid tile file_format: " + Psc::to_string(config_.file_format));
        }
    }
    asio::awaitable<Tile_Load_Result> load(const Tile_Request& request, const Tile_Cache_Request& cache_request) const {
        if (request.z < config_.minimum_level || request.z > config_.maximum_level) {
            co_return Tile_Load_Result{Tile_Load_Status::Not_Found};
        }
        if (config_.file_format == Map_Tile_File_Format::mbtiles) {
            co_return co_await load_mbtiles_tile(request, cache_request);
        }
        co_return co_await load_directory_tile(request, cache_request);
    }
    const Map_Tile_Config& config() const {
        return config_;
    }
private:
    asio::awaitable<Tile_Load_Result> load_directory_tile(const Tile_Request& request,
                                                          const Tile_Cache_Request& cache_request) const {
        auto path = resolver_->find_file(request);
        if (!path) {
            co_return Tile_Load_Result{Tile_Load_Status::Not_Found};
        }
        auto cache_info = get_file_cache_info(*path);
        if (!cache_info) {
            co_return Tile_Load_Result{Tile_Load_Status::Not_Found};
        }
        if (cache_info->size > kMaxTileFileSize || cache_info->size > std::numeric_limits<std::size_t>::max()) {
            throw std::runtime_error("tile file is too large: " + path->generic_string());
        }
        auto generic_path = path->generic_string();
        auto content_type = httplib::detail::find_content_type(generic_path, {}, "application/octet-stream");
        if (is_not_modified(cache_request, *cache_info)) {
            co_return Tile_Load_Result{
            Tile_Load_Status::Not_Modified, std::move(generic_path), {}, std::move(content_type), *cache_info};
        }
        std::string content;
        if (cache_request.include_body) {
#if defined(ASIO_HAS_FILE)
            auto executor = co_await asio::this_coro::executor;
            asio::random_access_file file(executor, path->generic_string(), asio::file_base::read_only);
            content.resize(static_cast<std::size_t>(cache_info->size));
            if (!content.empty()) {
                co_await asio::async_read_at(file, 0, asio::buffer(content.data(), content.size()),
                                             asio::use_awaitable);
            }
#else
            content = co_await read_file_fallback(*path, static_cast<std::size_t>(cache_info->size));
#endif
        }
        co_return Tile_Load_Result{Tile_Load_Status::Ready, std::move(generic_path), std::move(content),
                                   std::move(content_type), *cache_info};
    }
    asio::awaitable<Tile_Load_Result> load_mbtiles_tile(Tile_Request request, Tile_Cache_Request cache_request) const {
        auto content_type =
        httplib::detail::find_content_type("tile" + config_.extension, {}, "application/octet-stream");
        co_return co_await asio::co_spawn(
        tile_file_pool(),
        [this, request, cache_request = std::move(cache_request),
         content_type = std::move(content_type)]() -> asio::awaitable<Tile_Load_Result> {
            for (const auto& file : mbtiles_files_) {
                auto result = file->load(request, cache_request, content_type);
                if (result) {
                    co_return std::move(*result);
                }
            }
            co_return Tile_Load_Result{Tile_Load_Status::Not_Found};
        },
        asio::use_awaitable);
    }
    Map_Tile_Config config_;
    std::unique_ptr<Tile_Path_Resolver> resolver_;
    std::vector<std::unique_ptr<Mbtiles_File>> mbtiles_files_;
#if !defined(ASIO_HAS_FILE)
    static asio::awaitable<std::string> read_file_fallback(std::filesystem::path path, std::size_t size) {
        co_return co_await asio::co_spawn(
        tile_file_pool(),
        [path = std::move(path), size]() -> asio::awaitable<std::string> {
            std::ifstream file(path, std::ios::binary);
            if (!file) {
                throw std::runtime_error("failed to open tile file: " + path.generic_string());
            }
            std::string content(size, '\0');
            if (!content.empty()) {
                file.read(content.data(), static_cast<std::streamsize>(content.size()));
                if (file.gcount() != static_cast<std::streamsize>(content.size())) {
                    throw std::runtime_error("failed to read complete tile file: " + path.generic_string());
                }
            }
            co_return content;
        },
        asio::use_awaitable);
    }
#endif
};
drogon::HttpResponsePtr make_tile_not_found_response(std::string_view request_path, bool include_body) {
    auto response = drogon::HttpResponse::newHttpResponse();
    JSON ret = JSON::object();
    ret.append({"LOG_POS", LOG_POS});
    ret.append({"not_found_path", std::string(request_path)});
    response->setStatusCode(drogon::k404NotFound);
    response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
    if (include_body) {
        response->setBody(ret.to_json_string());
    }
    response->addHeader("Cache-Control", "no-store");
    return response;
}
drogon::HttpResponsePtr make_tile_not_modified_response(const File_Cache_Info& info) {
    auto response = drogon::HttpResponse::newHttpResponse();
    response->setStatusCode(drogon::k304NotModified);
    response->setContentTypeCode(drogon::CT_NONE);
    add_tile_cache_headers(response, info, false);
    return response;
}
drogon::HttpResponsePtr make_tile_ok_response(Tile_Load_Result&& result, bool include_body) {
    auto response = drogon::HttpResponse::newHttpResponse();
    response->setStatusCode(drogon::k200OK);
    response->setContentTypeString(result.content_type);
    add_tile_cache_headers(response, result.cache_info, !include_body);
    if (include_body) {
        response->setBody(std::move(result.content));
    }
    return response;
}
drogon::HttpResponsePtr make_tile_response(Tile_Load_Result&& result, std::string_view request_path,
                                           bool include_body) {
    switch (result.status) {
    case Tile_Load_Status::Ready:
        return make_tile_ok_response(std::move(result), include_body);
    case Tile_Load_Status::Not_Modified:
        return make_tile_not_modified_response(result.cache_info);
    case Tile_Load_Status::Not_Found:
        return make_tile_not_found_response(request_path, include_body);
    }
    return make_tile_not_found_response(request_path, include_body);
}
drogon::HttpResponsePtr make_tile_read_failed_response(const std::exception& error, bool include_body) {
    auto response = drogon::HttpResponse::newHttpResponse();
    response->setStatusCode(drogon::k500InternalServerError);
    response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
    if (include_body) {
        response->setBody(Psc::JSON::object({{"error", Psc::platform_2_utf8(error.what())}}).to_json_string());
    }
    return response;
}
Tile_Cache_Request make_cache_request(const drogon::HttpRequestPtr& req) {
    return Tile_Cache_Request{req->getHeader("if-none-match"), req->getHeader("if-modified-since"),
                              req->method() != drogon::Head};
}
void register_tile_handler(std::string_view regex, std::string_view prefix, std::shared_ptr<Tile_Source> source) {
    drogon::app().registerHandlerViaRegex(
    std::string(regex),
    [prefix = std::string(prefix), source = std::move(source)](
    const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) mutable {
        auto cache_request = make_cache_request(req);
        auto request = parse_tile_request(req->path(), prefix, source->config().extension);
        if (!request) {
            callback(make_tile_not_found_response(req->path(), cache_request.include_body));
            return;
        }
        drogon::async_run([callback = std::move(callback), source, request = std::move(*request),
                           cache_request = std::move(cache_request), path = req->path()]() mutable -> drogon::Task<> {
            try {
                auto result = co_await Ecap_Coro::to_drogon(source->load(request, cache_request));
                callback(make_tile_response(std::move(result), path, cache_request.include_body));
            }
            catch (const std::exception& error) {
                if (server_logger) {
                    server_logger->error("tiles", {}, LOG_POS + " " + Psc::platform_2_utf8(error.what()));
                }
                callback(make_tile_read_failed_response(error, cache_request.include_body));
            }
            co_return;
        });
    },
    {drogon::Get, drogon::Head});
}
void register_tile_source_handler(const Map_Tile_Source_Config& config) {
    if (!config.local()) {
        return;
    }
    auto source = std::make_shared<Tile_Source>(config.resource);
    auto prefix = "/map/tile_sources/" + config.key + "/";
    auto regex = "^" + prefix + ".*$";
    register_tile_handler(regex, prefix, source);
}
void register_map_resources_handler() {
    drogon::app().registerHandlerViaRegex(
    R"(^/map/resources$)",
    [](const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
        auto global = Global::instance();
        auto response = drogon::HttpResponse::newHttpResponse();
        if (req->method() == drogon::Post) {
            auto parsed = Psc::try_parse_json(req->body().data());
            if (!parsed.has_value()) {
                response->setStatusCode(drogon::k400BadRequest);
                response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
                response->setBody("invalid current_imagery_key");
                callback(response);
                return;
            }
            auto key = parsed.value().try_get_string("current_imagery_key");
            const bool valid_key = key.has_value() && global->map_resources_config.read([&](const auto& config) {
                return config.is_imagery_key(key.value());
            });
            if (!valid_key) {
                response->setStatusCode(drogon::k400BadRequest);
                response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
                response->setBody("invalid current_imagery_key");
                callback(response);
                return;
            }
            global->map_resources_config.write([&](auto& config) {
                config.current_imagery_key = key.value();
            });
            Global::save(Config_Section::map_resources);
        }
        response->setStatusCode(drogon::k200OK);
        response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
        response->setBody(global->map_resources_config.read([](const auto& config) {
            return config.to_public_json().to_json_string();
        }));
        response->addHeader("Cache-Control", "no-store");
        callback(response);
    },
    {drogon::Get, drogon::Post});
}
void register_map_graphics_handler() {
    drogon::app().registerHandlerViaRegex(
    R"(^/map/graphics$)",
    [](const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
        auto global = Global::instance();
        auto response = drogon::HttpResponse::newHttpResponse();
        if (req->method() == drogon::Post) {
            auto parsed = Psc::try_parse_json(req->body().data());
            if (!parsed.has_value()) {
                response->setStatusCode(drogon::k400BadRequest);
                response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
                response->setBody("invalid cesium_graphics");
                callback(response);
                return;
            }
            try {
                global->cesium_graphics_config.from_base_json(&parsed.value(), false);
                Global::save(Config_Section::cesium_graphics);
            }
            catch (const std::exception& error) {
                response->setStatusCode(drogon::k400BadRequest);
                response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
                response->setBody(Psc::platform_2_utf8(error.what()));
                callback(response);
                return;
            }
        }
        response->setStatusCode(drogon::k200OK);
        response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
        response->setBody(global->cesium_graphics_config.to_base_json().to_json_string());
        response->addHeader("Cache-Control", "no-store");
        callback(response);
    },
    {drogon::Get, drogon::Post});
}
Psc::JSON make_map_view_response(Global* global) {
    auto view = global->map_view_config.read([](const auto& config) {
        return config;
    });
    global->map_resources_config.read([&](const auto& config) {
        const auto first_key = [&](Map_Tile_Type type) {
            const auto item = std::ranges::find_if(config.tile_sources, [type](const auto& source) {
                return source.tile_type == type;
            });
            return item == config.tile_sources.end() ? std::string{} : item->key;
        };
        if(!config.is_imagery_key(view.map2d.current_imagery_key))
            view.map2d.current_imagery_key = config.current_imagery_key;
        if(!config.is_imagery_key(view.map3d.current_imagery_key))
            view.map3d.current_imagery_key = config.current_imagery_key;
        if(!view.map3d.current_terrain_key.empty() && !config.is_terrain_key(view.map3d.current_terrain_key))
            view.map3d.current_terrain_key = first_key(Map_Tile_Type::terrain);
    });
    auto ret = view.to_base_json();
    global->map_resources_config.read([&](const auto& config) {
        ret.append({"current_imagery_key", config.current_imagery_key});
    });
    return ret;
}
bool is_multipart_request(const drogon::HttpRequestPtr& req) {
    auto content_type = req->getHeader("content-type");
    return content_type.find("multipart/form-data") != std::string::npos;
}
std::string normalize_upload_extension(std::string extension) {
    std::ranges::transform(extension, extension.begin(),
                           [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
    if (!extension.empty() && extension.front() != '.') {
        extension.insert(extension.begin(), '.');
    }
    return extension;
}
std::filesystem::path web_model_directory(Global* global) {
    return std::filesystem::path(global->web_server_config.get_true_webapp()) / "model";
}
std::filesystem::path source_model_directory(Global* global) {
    return std::filesystem::path(global->web_server_config.get_true_webapp()).parent_path() / "model";
}
bool is_safe_aircraft_model_key(std::string_view key) {
    return !key.empty() &&
    std::ranges::all_of(key, [](unsigned char ch) { return std::isalnum(ch) != 0 || ch == '_'; });
}
std::string model_file_name(std::string_view model_type, std::string_view aircraft_model_key) {
    if (model_type == "aircraft") {
        return "aircraft-custom.glb";
    }
    if (model_type == "aircraft_type") {
        if (!is_safe_aircraft_model_key(aircraft_model_key)) {
            throw std::invalid_argument("invalid aircraft_model_key");
        }
        auto normalized_key = std::string(aircraft_model_key);
        std::ranges::replace(normalized_key, '_', '-');
        return "aircraft-" + normalized_key + ".glb";
    }
    if (model_type == "base_station") {
        return "base-station-custom.glb";
    }
    if (model_type == "device") {
        return "device-custom.glb";
    }
    throw std::invalid_argument("invalid model_type");
}
void save_uploaded_model_file(const drogon::HttpFile& upload, std::string_view model_type,
                              std::string_view aircraft_model_key, Global* global) {
    auto extension = normalize_upload_extension(std::string(upload.getFileExtension()));
    if (extension != ".glb") {
        throw std::invalid_argument("model file must be .glb");
    }
    if (model_type == "aircraft_type") {
        const bool exists = global->map_model_config.read([&](const auto& config) {
            return config.aircraft_models.contains(std::string(aircraft_model_key));
        });
        if (!exists) {
            throw std::invalid_argument("unknown aircraft_model_key");
        }
    }
    auto upload_dir = std::filesystem::path(drogon::app().getUploadPath());
    std::filesystem::create_directories(upload_dir);
    auto temp_name = std::string(model_type) + "_" + std::string(aircraft_model_key) + "_model_upload.tmp";
    if (upload.saveAs(temp_name) != 0) {
        throw std::runtime_error("cannot save uploaded model");
    }
    auto temp_path = upload_dir / temp_name;
    auto file_name = model_file_name(model_type, aircraft_model_key);
    auto web_dir = web_model_directory(global);
    auto source_dir = source_model_directory(global);
    std::filesystem::create_directories(web_dir);
    std::filesystem::create_directories(source_dir);
    std::filesystem::copy_file(temp_path, web_dir / file_name, std::filesystem::copy_options::overwrite_existing);
    std::filesystem::copy_file(temp_path, source_dir / file_name, std::filesystem::copy_options::overwrite_existing);
    std::filesystem::remove(temp_path);
    auto url = "/ui/model/" + file_name;
    global->map_model_config.write([&](auto& config) {
        if (model_type == "aircraft") {
            config.aircraft_model.url = url;
        }
        else if (model_type == "aircraft_type") {
            config.aircraft_models[std::string(aircraft_model_key)].url = url;
        }
        else if (model_type == "base_station") {
            config.base_station_model.url = url;
        }
        else {
            config.device_model.url = url;
        }
    });
}
void handle_map_model_upload(const drogon::HttpRequestPtr& req, Global* global) {
    drogon::MultiPartParser parser;
    if (parser.parse(req) != 0) {
        throw std::invalid_argument("invalid multipart model upload");
    }
    auto model_type = parser.getOptionalParameter<std::string>("model_type");
    if (!model_type || parser.getFiles().size() != 1) {
        throw std::invalid_argument("model_type or file");
    }
    auto aircraft_model_key = parser.getOptionalParameter<std::string>("aircraft_model_key").value_or("");
    save_uploaded_model_file(parser.getFiles().front(), *model_type, aircraft_model_key, global);
    Global::save(Config_Section::map_models);
}
void handle_map_model_config_update(const drogon::HttpRequestPtr& req, Global* global) {
    auto parsed = Psc::try_parse_json(req->body().data());
    if (!parsed.has_value()) {
        throw std::invalid_argument("invalid map_models");
    }
    global->map_model_config.write([&](auto& config) {
        config.from_base_json(&parsed.value(), false);
    });
    Global::save(Config_Section::map_models);
}
void register_map_models_handler() {
    drogon::app().registerHandlerViaRegex(
    R"(^/map/models$)",
    [](const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
        auto global = Global::instance();
        auto response = drogon::HttpResponse::newHttpResponse();
        try {
            if (req->method() == drogon::Post) {
                if (is_multipart_request(req)) {
                    handle_map_model_upload(req, global);
                }
                else {
                    handle_map_model_config_update(req, global);
                }
            }
            response->setStatusCode(drogon::k200OK);
            response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
            response->setBody(global->map_model_config.read([](const auto& config) {
                return config.to_base_json().to_json_string();
            }));
        }
        catch (const std::exception& error) {
            response->setStatusCode(drogon::k400BadRequest);
            response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
            response->setBody(Psc::platform_2_utf8(error.what()));
        }
        response->addHeader("Cache-Control", "no-store");
        callback(response);
    },
    {drogon::Get, drogon::Post});
}
void register_map_view_handler() {
    drogon::app().registerHandlerViaRegex(
    R"(^/map/view$)",
    [](const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
        auto global = Global::instance();
        auto response = drogon::HttpResponse::newHttpResponse();
        if (req->method() == drogon::Post) {
            auto parsed = Psc::try_parse_json(req->body().data());
            if (!parsed.has_value()) {
                response->setStatusCode(drogon::k400BadRequest);
                response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
                response->setBody("invalid map_view");
                callback(response);
                return;
            }
            auto key = parsed.value().try_get_string("current_imagery_key");
            const bool valid_key = key.has_value() && global->map_resources_config.read([&](const auto& config) {
                return config.is_imagery_key(key.value());
            });
            if (!valid_key) {
                response->setStatusCode(drogon::k400BadRequest);
                response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
                response->setBody("invalid current_imagery_key");
                callback(response);
                return;
            }
            try {
                auto next_view = global->map_view_config.read([](const auto& config) {
                    return config;
                });
                next_view.from_base_json(&parsed.value(), false);
                global->map_resources_config.read([&](const auto& config) {
                    if (!config.is_imagery_key(next_view.map2d.current_imagery_key) ||
                        !config.is_imagery_key(next_view.map3d.current_imagery_key)) {
                        throw std::invalid_argument("invalid current_imagery_key");
                    }
                    if (!next_view.map3d.current_terrain_key.empty() &&
                        !config.is_terrain_key(next_view.map3d.current_terrain_key)) {
                        throw std::invalid_argument("invalid current_terrain_key");
                    }
                });
                global->map_resources_config.write([&](auto& config) {
                    config.current_imagery_key = key.value();
                });
                global->map_view_config.write([&](auto& config) {
                    config = std::move(next_view);
                });
                Global::save(Config_Section::map_resources);
                Global::save(Config_Section::map_view);
            }
            catch (const std::exception& error) {
                response->setStatusCode(drogon::k400BadRequest);
                response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
                response->setBody(Psc::platform_2_utf8(error.what()));
                callback(response);
                return;
            }
        }
        response->setStatusCode(drogon::k200OK);
        response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
        response->setBody(make_map_view_response(global).to_json_string());
        response->addHeader("Cache-Control", "no-store");
        callback(response);
    },
    {drogon::Get, drogon::Post});
}
} // namespace
void Global::init_tiles() {
    register_map_resources_handler();
    register_map_graphics_handler();
    register_map_models_handler();
    register_map_view_handler();
    map_resources_config.read([](const auto& config) {
        for (const auto& source : config.tile_sources) {
            register_tile_source_handler(source);
        }
    });
}
