#include <Tactility/LogMessages.h>
#include <Tactility/StringUtils.h>
#include <Tactility/file/File.h>
#include <Tactility/network/HttpdReq.h>

#include <tactility/log.h>

#include <memory>
#include <ranges>
#include <sstream>

#ifdef ESP_PLATFORM

namespace tt::network {

constexpr auto* TAG = "HttpdReq";

bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value) {
    size_t header_size = httpd_req_get_hdr_value_len(request, name.c_str());
    if (header_size == 0) {
        httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "header missing");
        return false;
    }

    auto header_buffer = std::make_unique<char[]>(header_size + 1);
    if (header_buffer == nullptr) {
        LOG_E(TAG, LOG_MESSAGE_ALLOC_FAILED);
        httpd_resp_send_500(request);
        return false;
    }

    if (httpd_req_get_hdr_value_str(request, name.c_str(), header_buffer.get(), header_size + 1) != ESP_OK) {
        httpd_resp_send_500(request);
        return false;
    }

    value = header_buffer.get();
    return true;
}

bool getMultiPartBoundaryOrSendError(httpd_req_t* request, std::string& boundary) {
    std::string content_type_header;
    if (!getHeaderOrSendError(request, "Content-Type", content_type_header)) {
        return false;
    }

    auto boundary_index = content_type_header.find("boundary=");
    if (boundary_index == std::string::npos) {
        httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "boundary not found in Content-Type");
        return false;
    }

    boundary = content_type_header.substr(boundary_index + 9);
    return true;
}

bool getQueryOrSendError(httpd_req_t* request, std::string& query) {
    size_t buffer_length = httpd_req_get_url_query_len(request);
    if (buffer_length == 0) {
        httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified");
        return false;
    }

    auto buffer = std::make_unique<char[]>(buffer_length + 1);
    if (buffer.get() == nullptr || httpd_req_get_url_query_str(request, buffer.get(), buffer_length + 1) != ESP_OK) {
        httpd_resp_send_500(request);
        return false;
    }

    query = buffer.get();

    return true;
}

std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead) {
    assert(length > 0);
    bytesRead = 0;

    // We have to use malloc() because make_unique() throws an exception
    // and we don't have exceptions enabled in the compiler settings
    auto* buffer = static_cast<char*>(malloc(length));
    if (buffer == nullptr) {
        LOG_E(TAG, "Out of memory (failed to allocated %u bytes)", (unsigned)length);
        return nullptr;
    }

    constexpr int MAX_TIMEOUT_RETRIES = 5;
    int timeout_retries = 0;
    while (bytesRead < length) {
        size_t read_size = length - bytesRead;
        int bytes_received = httpd_req_recv(request, buffer + bytesRead, read_size);
        if (bytes_received == HTTPD_SOCK_ERR_TIMEOUT) {
            // Timeout - retry with backoff
            timeout_retries++;
            if (timeout_retries >= MAX_TIMEOUT_RETRIES) {
                LOG_W(TAG, "Recv timeout after %d retries, read %u/%u bytes", timeout_retries, (unsigned)bytesRead, (unsigned)length);
                free(buffer);
                return nullptr;
            }
            LOG_W(TAG, "Recv timeout, retry %d/%d", timeout_retries, MAX_TIMEOUT_RETRIES);
            vTaskDelay(pdMS_TO_TICKS(100 * timeout_retries)); // Exponential backoff
            continue;
        }
        if (bytes_received <= 0) {
            LOG_W(TAG, "Received error %d after reading %u/%u bytes", bytes_received, (unsigned)bytesRead, (unsigned)length);
            free(buffer);
            return nullptr;
        }

        // Successful read - reset timeout counter
        timeout_retries = 0;
        bytesRead += bytes_received;
    }

    return std::unique_ptr<char[]>(buffer);
}

std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator) {
    size_t read_index = 0;
    std::stringstream result;
    while (!result.str().ends_with(terminator)) {
        char buffer;
        size_t bytes_read = httpd_req_recv(request, &buffer, 1);
        if (bytes_read <= 0) {
            return "";
        } else {
            read_index += bytes_read;
        }

        result << buffer;
    }

    return result.str();
}

std::map<std::string, std::string> parseContentDisposition(const std::vector<std::string>& input) {
    std::map<std::string, std::string> result;
    static std::string prefix = "Content-Disposition: ";

    // Find header
    auto content_disposition_header = std::ranges::find_if(input, [](const std::string& header) {
        return header.starts_with(prefix);
    });

    // Header not found
    if (content_disposition_header == input.end()) {
        return result;
    }

    auto parseable = content_disposition_header->substr(prefix.size());
    auto parts = string::split(parseable, "; ");
    for (const auto& part : parts) {
        auto key_value = string::split(part, "=");
        if (key_value.size() == 2) {
            // Trim trailing newlines
            auto value = string::trim(key_value[1], "\r\n");
            if (value.size() > 2) {
                result[key_value[0]] = value.substr(1, value.size() - 2);
            } else {
                result[key_value[0]] = "";
            }
        }
    }

    return result;
}

bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead) {
    size_t bytes_read;
    auto buffer = receiveByteArray(request, toRead.length(), bytes_read);
    if (buffer == nullptr || bytes_read != toRead.length()) {
        httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "failed to read discardable data");
        return false;
    }

    if (memcmp(buffer.get(), toRead.c_str(), bytes_read) != 0) {
        httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "discardable data mismatch");
        return false;
    }

    return true;
}

size_t receiveFile(httpd_req_t* request, size_t length, const std::string& filePath) {
    constexpr auto BUFFER_SIZE = 512;
    char buffer[BUFFER_SIZE];
    size_t bytes_received = 0;

    file::FileMutexGuard guard(filePath);

    auto* file = fopen(filePath.c_str(), "wb");
    if (file == nullptr) {
        LOG_E(TAG, "Failed to open file for writing: %s", filePath.c_str());
        return 0;
    }

    while (bytes_received < length) {
        auto expected_chunk_size = std::min<size_t>(BUFFER_SIZE, length - bytes_received);
        size_t receive_chunk_size = httpd_req_recv(request, buffer, expected_chunk_size);
        if (receive_chunk_size <= 0) {
            LOG_E(TAG, "Receive failed, got 0 bytes but expected %zu more", length - bytes_received);
            break;
        }
        if (fwrite(buffer, 1, receive_chunk_size, file) != receive_chunk_size) {
            LOG_E(TAG, "Failed to write all bytes");
            break;
        }
        bytes_received += receive_chunk_size;
    }

    // Write file
    fclose(file);
    return bytes_received;
}

}

#endif // ESP_PLATFORM