#include "dps/http.hpp"

#include <algorithm>
#include <array>
#include <cerrno>
#include <chrono>
#include <cctype>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <thread>

#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
using socket_t = SOCKET;
static constexpr socket_t kInvalidSocket = INVALID_SOCKET;
#else
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
using socket_t = int;
static constexpr socket_t kInvalidSocket = -1;
#endif

namespace dps {

namespace {

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

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 now_text() {
    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();
}

bool is_text_content_type(const std::string &content_type) {
    const std::string lower = to_lower(content_type);
    return lower.find("json") != std::string::npos || lower.find("text/") != std::string::npos ||
           lower.find("x-www-form-urlencoded") != std::string::npos || lower.find("xml") != std::string::npos;
}

bool is_mostly_printable(const std::string &text) {
    if (text.empty()) {
        return true;
    }
    std::size_t printable = 0;
    for (unsigned char ch : text) {
        if (std::isprint(ch) || std::isspace(ch)) {
            ++printable;
        }
    }
    return printable * 100 / text.size() >= 90;
}

std::string truncate_text(const std::string &text, int max_body) {
    if (max_body <= 0 || text.size() <= static_cast<std::size_t>(max_body)) {
        return text;
    }
    return text.substr(0, static_cast<std::size_t>(max_body)) + "...<truncated>";
}

std::string request_header_summary(const Request &request) {
    std::ostringstream out;
    const std::string content_type = request.header("content-type");
    out << "content_type=" << (content_type.empty() ? "-" : content_type);
    out << " auth=" << (request.header("authorization").empty() ? "absent" : "present");
    return out.str();
}

void log_request_summary(const LoggingConfig &logging, const Request &request) {
    if (!logging.http_summary) {
        return;
    }
    std::clog << "[" << now_text() << "] REQ ip=" << request.remote_addr
              << " method=" << request.method
              << " path=" << request.path
              << " query=" << (request.query.empty() ? "-" : request.raw_target.substr(request.path.size()))
              << " body_bytes=" << request.body.size()
              << " " << request_header_summary(request)
              << std::endl;
    if (logging.http_dump && !request.body.empty()) {
        const std::string content_type = request.header("content-type");
        if ((content_type.empty() || is_text_content_type(content_type)) && is_mostly_printable(request.body)) {
            std::clog << "[" << now_text() << "] REQ_BODY "
                      << truncate_text(request.body, logging.http_max_body)
                      << std::endl;
        }
    }
}

void log_response_summary(const LoggingConfig &logging, const Request &request, const Response &response, long long cost_ms) {
    if (!logging.http_summary) {
        return;
    }
    const std::size_t body_bytes = response.has_binary() ? response.binary_body.size() : response.body.size();
    std::clog << "[" << now_text() << "] RESP ip=" << request.remote_addr
              << " method=" << request.method
              << " path=" << request.path
              << " status=" << response.status
              << " content_type=" << response.content_type
              << " body_bytes=" << body_bytes
              << " binary=" << (response.has_binary() ? "yes" : "no")
              << " cost_ms=" << cost_ms
              << std::endl;
    if (logging.http_dump && !response.has_binary() && !response.body.empty() &&
        is_text_content_type(response.content_type) && is_mostly_printable(response.body)) {
        std::clog << "[" << now_text() << "] RESP_BODY "
                  << truncate_text(response.body, logging.http_max_body)
                  << std::endl;
    }
}

std::vector<std::string> split_path(const std::string &path) {
    std::vector<std::string> result;
    std::string current;
    for (char ch : path) {
        if (ch == '/') {
            if (!current.empty()) {
                result.push_back(current);
                current.clear();
            }
            continue;
        }
        current.push_back(ch);
    }
    if (!current.empty()) {
        result.push_back(current);
    }
    return result;
}

bool match_route(const std::string &pattern, const std::string &path, std::map<std::string, std::string> *params) {
    const auto lhs = split_path(pattern);
    const auto rhs = split_path(path);
    if (lhs.size() != rhs.size()) {
        return false;
    }
    for (std::size_t i = 0; i < lhs.size(); ++i) {
        const std::string &expected = lhs[i];
        const std::string actual = url_decode(rhs[i]);
        if (expected.size() >= 2 && expected.front() == '{' && expected.back() == '}') {
            (*params)[expected.substr(1, expected.size() - 2)] = actual;
            continue;
        }
        if (expected != actual) {
            return false;
        }
    }
    return true;
}

void close_socket(socket_t sock) {
#ifdef _WIN32
    closesocket(sock);
#else
    close(sock);
#endif
}

bool parse_ipv4_address(const std::string &host, in_addr *addr) {
    if (host.empty() || host == "0.0.0.0") {
        addr->s_addr = htonl(INADDR_ANY);
        return true;
    }
#ifdef _WIN32
    return InetPtonA(AF_INET, host.c_str(), addr) == 1;
#else
    return inet_pton(AF_INET, host.c_str(), addr) == 1;
#endif
}

std::string ipv4_address_to_string(const in_addr &addr) {
    char buffer[INET_ADDRSTRLEN] = {};
#ifdef _WIN32
    if (InetNtopA(AF_INET, const_cast<in_addr *>(&addr), buffer, static_cast<DWORD>(sizeof(buffer))) == nullptr) {
        return "";
    }
#else
    if (inet_ntop(AF_INET, &addr, buffer, sizeof(buffer)) == nullptr) {
        return "";
    }
#endif
    return buffer;
}

bool send_all(socket_t sock, const unsigned char *data, std::size_t size) {
    std::size_t sent = 0;
    while (sent < size) {
#ifdef _WIN32
        const int count = send(sock, reinterpret_cast<const char *>(data + sent), static_cast<int>(size - sent), 0);
#else
        const auto count = send(sock, data + sent, size - sent, 0);
#endif
        if (count <= 0) {
            return false;
        }
        sent += static_cast<std::size_t>(count);
    }
    return true;
}

std::string reason_phrase(int status) {
    switch (status) {
        case 200: return "OK";
        case 201: return "Created";
        case 400: return "Bad Request";
        case 401: return "Unauthorized";
        case 403: return "Forbidden";
        case 404: return "Not Found";
        case 405: return "Method Not Allowed";
        case 500: return "Internal Server Error";
        case 501: return "Not Implemented";
        default: return "OK";
    }
}

std::string make_http_message(const Response &response) {
    std::ostringstream out;
    const std::size_t content_length = response.has_binary() ? response.binary_body.size() : response.body.size();
    out << "HTTP/1.1 " << response.status << " " << reason_phrase(response.status) << "\r\n";
    out << "Connection: close\r\n";
    out << "Content-Length: " << content_length << "\r\n";
    out << "Content-Type: " << response.content_type << "\r\n";
    for (const auto &header : response.headers) {
        out << header.first << ": " << header.second << "\r\n";
    }
    out << "\r\n";
    return out.str();
}

Response bad_request(const std::string &message) {
    Response response;
    response.status = 400;
    response.body = "{\"code\":400,\"message\":\"" + message + "\",\"data\":null}";
    return response;
}

std::string read_request(socket_t client) {
    std::string data;
    std::array<char, 4096> buffer{};
    std::size_t header_end = std::string::npos;
    std::size_t content_length = 0;
    while (true) {
#ifdef _WIN32
        const int bytes = recv(client, buffer.data(), static_cast<int>(buffer.size()), 0);
#else
        const auto bytes = recv(client, buffer.data(), buffer.size(), 0);
#endif
        if (bytes <= 0) {
            break;
        }
        data.append(buffer.data(), static_cast<std::size_t>(bytes));
        if (header_end == std::string::npos) {
            header_end = data.find("\r\n\r\n");
            if (header_end != std::string::npos) {
                const std::string headers = data.substr(0, header_end + 4);
                const std::size_t pos = to_lower(headers).find("content-length:");
                if (pos != std::string::npos) {
                    std::size_t value_start = headers.find(':', pos);
                    std::size_t value_end = headers.find("\r\n", value_start);
                    content_length = static_cast<std::size_t>(
                        std::stoll(trim(headers.substr(value_start + 1, value_end - value_start - 1))));
                }
            }
        }
        if (header_end != std::string::npos && data.size() >= header_end + 4 + content_length) {
            break;
        }
    }
    return data;
}

Request parse_request(const std::string &raw, const std::string &remote_addr) {
    const std::size_t header_end = raw.find("\r\n\r\n");
    if (header_end == std::string::npos) {
        throw std::runtime_error("invalid request");
    }
    std::istringstream lines(raw.substr(0, header_end));
    std::string line;
    if (!std::getline(lines, line)) {
        throw std::runtime_error("missing request line");
    }
    if (!line.empty() && line.back() == '\r') {
        line.pop_back();
    }
    std::istringstream request_line(line);
    Request request;
    std::string version;
    request_line >> request.method >> request.raw_target >> version;
    request.remote_addr = remote_addr;
    const std::size_t query_pos = request.raw_target.find('?');
    request.path = query_pos == std::string::npos ? request.raw_target : request.raw_target.substr(0, query_pos);
    if (query_pos != std::string::npos) {
        request.query = parse_query_string(request.raw_target.substr(query_pos + 1));
    }

    while (std::getline(lines, line)) {
        if (!line.empty() && line.back() == '\r') {
            line.pop_back();
        }
        const std::size_t pos = line.find(':');
        if (pos == std::string::npos) {
            continue;
        }
        request.headers[to_lower(trim(line.substr(0, pos)))] = trim(line.substr(pos + 1));
    }
    request.body = raw.substr(header_end + 4);
    return request;
}

}  // namespace

std::string Request::header(const std::string &name) const {
    const auto it = headers.find(to_lower(name));
    return it == headers.end() ? "" : it->second;
}

std::string Request::query_value(const std::string &name) const {
    const auto it = query.find(name);
    return it == query.end() ? "" : it->second;
}

std::string Request::path_param(const std::string &name) const {
    const auto it = path_params.find(name);
    return it == path_params.end() ? "" : it->second;
}

bool Response::has_binary() const {
    return !binary_body.empty();
}

void Router::add(const std::string &method, const std::string &pattern, bool requires_auth, Handler handler) {
    routes_.push_back(Route{to_lower(method), pattern, requires_auth, std::move(handler)});
}

Response Router::dispatch(Request request) const {
    const std::string method = to_lower(request.method);
    for (const auto &route : routes_) {
        if (route.method != method) {
            continue;
        }
        std::map<std::string, std::string> params;
        if (!match_route(route.pattern, request.path, &params)) {
            continue;
        }
        request.path_params = std::move(params);
        return route.handler(request);
    }
    Response response;
    response.status = 404;
    response.body = "{\"code\":404,\"message\":\"not found\",\"data\":null}";
    return response;
}

HttpServer::HttpServer(std::string host, int port, LoggingConfig logging, const Router &router)
    : host_(std::move(host)), port_(port), logging_(std::move(logging)), router_(router) {}

int HttpServer::run() const {
#ifdef _WIN32
    WSADATA wsa_data{};
    if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) {
        std::cerr << "WSAStartup failed\n";
        return 1;
    }
#endif

    socket_t server = socket(AF_INET, SOCK_STREAM, 0);
    if (server == kInvalidSocket) {
        std::cerr << "socket creation failed\n";
        return 1;
    }

    int reuse = 1;
#ifdef _WIN32
    setsockopt(server, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char *>(&reuse), sizeof(reuse));
#else
    setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
#endif

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(static_cast<unsigned short>(port_));
    if (!parse_ipv4_address(host_, &addr.sin_addr)) {
        std::cerr << "invalid listen host: " << host_ << "\n";
        close_socket(server);
        return 1;
    }

    if (bind(server, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) != 0) {
        std::cerr << "bind failed\n";
        close_socket(server);
        return 1;
    }
    if (listen(server, 64) != 0) {
        std::cerr << "listen failed\n";
        close_socket(server);
        return 1;
    }

    std::clog << "Listening on " << host_ << ":" << port_ << std::endl;
    while (true) {
        sockaddr_in client_addr{};
#ifdef _WIN32
        int client_len = sizeof(client_addr);
#else
        socklen_t client_len = sizeof(client_addr);
#endif
        socket_t client = accept(server, reinterpret_cast<sockaddr *>(&client_addr), &client_len);
        if (client == kInvalidSocket) {
            continue;
        }
        std::thread([this, client, client_addr]() {
            const std::string remote = ipv4_address_to_string(client_addr.sin_addr);
            try {
                const std::string raw = read_request(client);
                if (raw.empty()) {
                    close_socket(client);
                    return;
                }
                Request request = parse_request(raw, remote);
                log_request_summary(logging_, request);
                const auto started = std::chrono::steady_clock::now();
                const Response response = router_.dispatch(request);
                const auto finished = std::chrono::steady_clock::now();
                const auto cost_ms = std::chrono::duration_cast<std::chrono::milliseconds>(finished - started).count();
                log_response_summary(logging_, request, response, cost_ms);
                const std::string headers = make_http_message(response);
                send_all(client, reinterpret_cast<const unsigned char *>(headers.data()), headers.size());
                if (response.has_binary()) {
                    send_all(client, response.binary_body.data(), response.binary_body.size());
                } else {
                    send_all(client, reinterpret_cast<const unsigned char *>(response.body.data()), response.body.size());
                }
            } catch (const std::exception &ex) {
                const Response response = bad_request(ex.what());
                Request fallback;
                fallback.remote_addr = remote;
                fallback.method = "UNKNOWN";
                fallback.path = "-";
                log_response_summary(logging_, fallback, response, 0);
                const std::string headers = make_http_message(response);
                send_all(client, reinterpret_cast<const unsigned char *>(headers.data()), headers.size());
                send_all(client, reinterpret_cast<const unsigned char *>(response.body.data()), response.body.size());
            }
            close_socket(client);
        }).detach();
    }

    return 0;
}

std::string url_decode(const std::string &value) {
    std::ostringstream out;
    for (std::size_t i = 0; i < value.size(); ++i) {
        const char ch = value[i];
        if (ch == '+' ) {
            out << ' ';
        } else if (ch == '%' && i + 2 < value.size()) {
            const std::string hex = value.substr(i + 1, 2);
            out << static_cast<char>(std::strtol(hex.c_str(), nullptr, 16));
            i += 2;
        } else {
            out << ch;
        }
    }
    return out.str();
}

std::map<std::string, std::string> parse_query_string(const std::string &query) {
    std::map<std::string, std::string> values;
    std::size_t start = 0;
    while (start < query.size()) {
        const std::size_t amp = query.find('&', start);
        const std::string token = query.substr(start, amp == std::string::npos ? std::string::npos : amp - start);
        const std::size_t eq = token.find('=');
        if (eq == std::string::npos) {
            values[url_decode(token)] = "";
        } else {
            values[url_decode(token.substr(0, eq))] = url_decode(token.substr(eq + 1));
        }
        if (amp == std::string::npos) {
            break;
        }
        start = amp + 1;
    }
    return values;
}

std::string guess_content_type(const std::string &path) {
    const auto lower = to_lower(path);
    if (lower.size() >= 4 && lower.substr(lower.size() - 4) == ".png") return "image/png";
    if (lower.size() >= 4 && lower.substr(lower.size() - 4) == ".jpg") return "image/jpeg";
    if (lower.size() >= 5 && lower.substr(lower.size() - 5) == ".jpeg") return "image/jpeg";
    if (lower.size() >= 4 && lower.substr(lower.size() - 4) == ".mp4") return "video/mp4";
    if (lower.size() >= 4 && lower.substr(lower.size() - 4) == ".zip") return "application/zip";
    if (lower.size() >= 7 && lower.substr(lower.size() - 7) == ".tar.gz") return "application/gzip";
    if (lower.size() >= 4 && lower.substr(lower.size() - 4) == ".txt") return "text/plain; charset=utf-8";
    if (lower.size() >= 5 && lower.substr(lower.size() - 5) == ".json") return "application/json; charset=utf-8";
    if (lower.size() >= 3 && lower.substr(lower.size() - 3) == ".sh") return "application/octet-stream";
    return "application/octet-stream";
}

}  // namespace dps
