#include "McpScreen.h"

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <cJSON.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <lwip/inet.h>
#include <lwip/sockets.h>

#define MCP_HTTP_PORT 80
#define MCP_HEADER_LIMIT 2048
#define MCP_JSON_BODY_LIMIT (256 * 1024)
#define MCP_RAW_BODY_LIMIT (512 * 1024)
#define MCP_RESPONSE_LIMIT (64 * 1024)

static const char* TAG = "McpServer";

static const char* TOOLS_JSON =
    "["
    "{"
      "\"name\":\"get_capabilities\","
      "\"description\":\"Get the Tactility display capabilities.\","
      "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}"
    "},"
    "{"
      "\"name\":\"clear_screen\","
      "\"description\":\"Clear the MCP drawing area to white (0) or black (1).\","
      "\"inputSchema\":{"
        "\"type\":\"object\","
        "\"properties\":{\"color\":{\"type\":\"integer\",\"enum\":[0,1]}},"
        "\"required\":[\"color\"]"
      "}"
    "},"
    "{"
      "\"name\":\"draw_text\","
      "\"description\":\"Draw text in the MCP drawing area at x,y coordinates.\","
      "\"inputSchema\":{"
        "\"type\":\"object\","
        "\"properties\":{"
          "\"text\":{\"type\":\"string\"},"
          "\"x\":{\"type\":\"integer\"},"
          "\"y\":{\"type\":\"integer\"},"
          "\"size\":{\"type\":\"integer\",\"enum\":[1,2],\"default\":1}"
        "},"
        "\"required\":[\"text\",\"x\",\"y\"]"
      "}"
    "},"
    "{"
      "\"name\":\"draw_image\","
      "\"description\":\"Draw a bridge-preprocessed binary PBM image.\","
      "\"inputSchema\":{"
        "\"type\":\"object\","
        "\"properties\":{"
          "\"pbm_base64\":{\"type\":\"string\"},"
          "\"x\":{\"type\":\"integer\",\"default\":0},"
          "\"y\":{\"type\":\"integer\",\"default\":0},"
          "\"dither\":{\"type\":\"boolean\",\"default\":true}"
        "},"
        "\"required\":[\"pbm_base64\"]"
      "}"
    "},"
    "{"
      "\"name\":\"draw_color_bmp\","
      "\"description\":\"Draw an uncompressed 24-bit or 32-bit BMP image.\","
      "\"inputSchema\":{"
        "\"type\":\"object\","
        "\"properties\":{"
          "\"bmp_base64\":{\"type\":\"string\"},"
          "\"x\":{\"type\":\"integer\",\"default\":0},"
          "\"y\":{\"type\":\"integer\",\"default\":0}"
        "},"
        "\"required\":[\"bmp_base64\"]"
      "}"
    "},"
    "{"
      "\"name\":\"draw_raw_rgb565\","
      "\"description\":\"Draw big-endian raw RGB565 pixels.\","
      "\"inputSchema\":{"
        "\"type\":\"object\","
        "\"properties\":{"
          "\"rgb565_base64\":{\"type\":\"string\"},"
          "\"x\":{\"type\":\"integer\"},"
          "\"y\":{\"type\":\"integer\"},"
          "\"w\":{\"type\":\"integer\"},"
          "\"h\":{\"type\":\"integer\"}"
        "},"
        "\"required\":[\"rgb565_base64\",\"x\",\"y\",\"w\",\"h\"]"
      "}"
    "},"
    "{"
      "\"name\":\"get_screenshot\","
      "\"description\":\"Capture the MCP framebuffer as a PBM image.\","
      "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}"
    "}"
    "]";

static int base64_value(char character) {
    if (character >= 'A' && character <= 'Z') return character - 'A';
    if (character >= 'a' && character <= 'z') return character - 'a' + 26;
    if (character >= '0' && character <= '9') return character - '0' + 52;
    if (character == '+') return 62;
    if (character == '/') return 63;
    return -1;
}

static uint8_t* base64_decode(const char* input, size_t* output_size) {
    if (input == NULL || output_size == NULL) {
        return NULL;
    }

    const char* payload = input;
    const char* marker = strstr(input, ";base64,");
    if (marker != NULL) {
        payload = marker + 8;
    }

    size_t input_size = strlen(payload);
    uint8_t* output = malloc((input_size / 4) * 3 + 3);
    if (output == NULL) {
        return NULL;
    }

    uint32_t accumulator = 0;
    int bits = 0;
    size_t written = 0;
    for (size_t index = 0; index < input_size; ++index) {
        char character = payload[index];
        if (character == '=') {
            break;
        }
        int value = base64_value(character);
        if (value < 0) {
            if (character == '\r' || character == '\n' ||
                character == ' ' || character == '\t') {
                continue;
            }
            free(output);
            return NULL;
        }
        accumulator = (accumulator << 6) | (uint32_t)value;
        bits += 6;
        if (bits >= 8) {
            bits -= 8;
            output[written++] = (uint8_t)((accumulator >> bits) & 0xFF);
        }
    }
    *output_size = written;
    return output;
}

static void close_socket(int* socket_fd) {
    if (socket_fd != NULL && *socket_fd >= 0) {
        close(*socket_fd);
        *socket_fd = -1;
    }
}

static bool send_all(int socket_fd, const char* data, size_t length) {
    size_t sent_total = 0;
    while (sent_total < length) {
        int sent = send(socket_fd, data + sent_total, length - sent_total, 0);
        if (sent <= 0) {
            return false;
        }
        sent_total += (size_t)sent;
    }
    return true;
}

static cJSON* make_error(cJSON* id, int code, const char* message) {
    cJSON* root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "jsonrpc", "2.0");

    cJSON* error = cJSON_AddObjectToObject(root, "error");
    cJSON_AddNumberToObject(error, "code", code);
    cJSON_AddStringToObject(error, "message", message);

    if (id != NULL) {
        cJSON_AddItemToObject(root, "id", cJSON_Duplicate(id, true));
    } else {
        cJSON_AddNullToObject(root, "id");
    }
    return root;
}

static cJSON* make_tool_result(cJSON* id, const char* text) {
    cJSON* root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "jsonrpc", "2.0");

    cJSON* result = cJSON_AddObjectToObject(root, "result");
    cJSON* content = cJSON_AddArrayToObject(result, "content");
    cJSON* item = cJSON_CreateObject();
    cJSON_AddStringToObject(item, "type", "text");
    cJSON_AddStringToObject(item, "text", text);
    cJSON_AddItemToArray(content, item);

    if (id != NULL) {
        cJSON_AddItemToObject(root, "id", cJSON_Duplicate(id, true));
    } else {
        cJSON_AddNullToObject(root, "id");
    }
    return root;
}

static cJSON* handle_tool_call(McpScreenState* state, cJSON* id, cJSON* params) {
    if (!cJSON_IsObject(params)) {
        return make_error(id, -32602, "Invalid params");
    }

    cJSON* name_item = cJSON_GetObjectItemCaseSensitive(params, "name");
    cJSON* arguments = cJSON_GetObjectItemCaseSensitive(params, "arguments");
    if (!cJSON_IsString(name_item) || name_item->valuestring == NULL) {
        return make_error(id, -32602, "Missing tool name");
    }
    if (arguments != NULL && !cJSON_IsObject(arguments)) {
        return make_error(id, -32602, "Tool arguments must be an object");
    }

    const char* name = name_item->valuestring;
    mcp_ui_set_last_tool(state, name);

    if (strcmp(name, "get_capabilities") == 0) {
        char capabilities[320];
        uint16_t width = state->draw_width > 0 ? state->draw_width : state->display_width;
        uint16_t height = state->draw_height > 0 ? state->draw_height : state->display_height;
        snprintf(
            capabilities,
            sizeof(capabilities),
            "{\"color\":true,\"width\":%u,\"height\":%u,"
            "\"formats\":[\"rgb565_base64\",\"bmp_base64\",\"pbm_base64\"],"
            "\"platform\":\"tactility\",\"appVersion\":\"0.1.0\","
            "\"uiVisible\":%s}",
            width,
            height,
            state->visible ? "true" : "false"
        );
        return make_tool_result(id, capabilities);
    }

    if (strcmp(name, "clear_screen") == 0) {
        cJSON* color_item = cJSON_GetObjectItemCaseSensitive(arguments, "color");
        if (!cJSON_IsNumber(color_item) ||
            (color_item->valueint != 0 && color_item->valueint != 1)) {
            return make_error(id, -32602, "color must be 0 or 1");
        }
        if (!mcp_ui_clear(state, color_item->valueint)) {
            return make_error(id, -32010, "MCP Screen app is not visible");
        }
        return make_tool_result(id, "Screen cleared.");
    }

    if (strcmp(name, "draw_text") == 0) {
        cJSON* text_item = cJSON_GetObjectItemCaseSensitive(arguments, "text");
        cJSON* x_item = cJSON_GetObjectItemCaseSensitive(arguments, "x");
        cJSON* y_item = cJSON_GetObjectItemCaseSensitive(arguments, "y");
        cJSON* size_item = cJSON_GetObjectItemCaseSensitive(arguments, "size");

        if (!cJSON_IsString(text_item) || text_item->valuestring == NULL ||
            !cJSON_IsNumber(x_item) || !cJSON_IsNumber(y_item)) {
            return make_error(id, -32602, "draw_text requires text, x, and y");
        }
        if (strlen(text_item->valuestring) > 512) {
            return make_error(id, -32602, "text exceeds 512 characters");
        }

        int size = cJSON_IsNumber(size_item) ? size_item->valueint : 1;
        if (size != 1 && size != 2) {
            return make_error(id, -32602, "size must be 1 or 2");
        }

        if (!mcp_ui_draw_text(
                state,
                text_item->valuestring,
                x_item->valueint,
                y_item->valueint,
                size)) {
            return make_error(id, -32010, "MCP Screen app is not visible");
        }

        char message[160];
        snprintf(
            message,
            sizeof(message),
            "Successfully drew text at (%d, %d) with size %d.",
            x_item->valueint,
            y_item->valueint,
            size
        );
        return make_tool_result(id, message);
    }

    if (strcmp(name, "draw_raw_rgb565") == 0) {
        cJSON* encoded_item = cJSON_GetObjectItemCaseSensitive(arguments, "rgb565_base64");
        cJSON* x_item = cJSON_GetObjectItemCaseSensitive(arguments, "x");
        cJSON* y_item = cJSON_GetObjectItemCaseSensitive(arguments, "y");
        cJSON* width_item = cJSON_GetObjectItemCaseSensitive(arguments, "w");
        cJSON* height_item = cJSON_GetObjectItemCaseSensitive(arguments, "h");
        if (!cJSON_IsString(encoded_item) || !cJSON_IsNumber(x_item) ||
            !cJSON_IsNumber(y_item) || !cJSON_IsNumber(width_item) ||
            !cJSON_IsNumber(height_item)) {
            return make_error(id, -32602, "draw_raw_rgb565 requires rgb565_base64, x, y, w, and h");
        }

        size_t decoded_size = 0;
        uint8_t* decoded = base64_decode(encoded_item->valuestring, &decoded_size);
        if (decoded == NULL) {
            return make_error(id, -32602, "Invalid RGB565 base64 data");
        }
        bool drawn = mcp_ui_draw_rgb565_be(
            state,
            decoded,
            decoded_size,
            x_item->valueint,
            y_item->valueint,
            width_item->valueint,
            height_item->valueint
        );
        free(decoded);
        return drawn
            ? make_tool_result(id, "Raw RGB565 data displayed successfully.")
            : make_error(id, -32010, "Failed to draw RGB565 data");
    }

    if (strcmp(name, "draw_color_bmp") == 0) {
        cJSON* encoded_item = cJSON_GetObjectItemCaseSensitive(arguments, "bmp_base64");
        cJSON* x_item = cJSON_GetObjectItemCaseSensitive(arguments, "x");
        cJSON* y_item = cJSON_GetObjectItemCaseSensitive(arguments, "y");
        if (!cJSON_IsString(encoded_item)) {
            return make_error(id, -32602, "Missing bmp_base64");
        }
        size_t decoded_size = 0;
        uint8_t* decoded = base64_decode(encoded_item->valuestring, &decoded_size);
        if (decoded == NULL) {
            return make_error(id, -32602, "Invalid BMP base64 data");
        }
        bool drawn = mcp_ui_draw_bmp(
            state,
            decoded,
            decoded_size,
            cJSON_IsNumber(x_item) ? x_item->valueint : 0,
            cJSON_IsNumber(y_item) ? y_item->valueint : 0
        );
        free(decoded);
        return drawn
            ? make_tool_result(id, "Color BMP image displayed successfully.")
            : make_error(id, -32602, "Unsupported or invalid BMP image");
    }

    if (strcmp(name, "draw_image") == 0) {
        cJSON* encoded_item = cJSON_GetObjectItemCaseSensitive(arguments, "pbm_base64");
        cJSON* x_item = cJSON_GetObjectItemCaseSensitive(arguments, "x");
        cJSON* y_item = cJSON_GetObjectItemCaseSensitive(arguments, "y");
        if (!cJSON_IsString(encoded_item)) {
            return make_error(
                id,
                -32602,
                "draw_image requires bridge-preprocessed pbm_base64"
            );
        }
        size_t decoded_size = 0;
        uint8_t* decoded = base64_decode(encoded_item->valuestring, &decoded_size);
        if (decoded == NULL) {
            return make_error(id, -32602, "Invalid PBM base64 data");
        }
        bool drawn = mcp_ui_draw_pbm(
            state,
            decoded,
            decoded_size,
            cJSON_IsNumber(x_item) ? x_item->valueint : 0,
            cJSON_IsNumber(y_item) ? y_item->valueint : 0
        );
        free(decoded);
        return drawn
            ? make_tool_result(id, "Image displayed successfully.")
            : make_error(id, -32602, "Unsupported or invalid PBM image");
    }

    if (strcmp(name, "get_screenshot") == 0) {
        char* encoded = mcp_ui_get_screenshot_pbm_base64(state);
        if (encoded == NULL) {
            return make_error(id, -32010, "Screenshot capture failed");
        }
        size_t result_size = strlen(encoded) + 17;
        char* result = malloc(result_size);
        if (result == NULL) {
            free(encoded);
            return make_error(id, -32603, "Out of memory");
        }
        snprintf(result, result_size, "__PBM_BASE64__:%s", encoded);
        cJSON* response = make_tool_result(id, result);
        free(result);
        free(encoded);
        return response;
    }

    return make_error(id, -32602, "Unknown tool");
}

static cJSON* handle_rpc(McpScreenState* state, const char* body) {
    cJSON* request = cJSON_Parse(body);
    if (request == NULL) {
        return make_error(NULL, -32700, "Parse error");
    }

    cJSON* id = cJSON_GetObjectItemCaseSensitive(request, "id");
    cJSON* jsonrpc = cJSON_GetObjectItemCaseSensitive(request, "jsonrpc");
    cJSON* method = cJSON_GetObjectItemCaseSensitive(request, "method");
    cJSON* params = cJSON_GetObjectItemCaseSensitive(request, "params");

    if (!cJSON_IsObject(request) ||
        !cJSON_IsString(jsonrpc) ||
        strcmp(jsonrpc->valuestring, "2.0") != 0 ||
        !cJSON_IsString(method)) {
        cJSON* response = make_error(id, -32600, "Invalid Request");
        cJSON_Delete(request);
        return response;
    }

    cJSON* response = NULL;
    if (strcmp(method->valuestring, "tools/list") == 0) {
        cJSON* tools = cJSON_Parse(TOOLS_JSON);
        if (tools == NULL) {
            response = make_error(id, -32603, "Failed to construct tool list");
        } else {
            response = cJSON_CreateObject();
            cJSON_AddStringToObject(response, "jsonrpc", "2.0");
            cJSON* result = cJSON_AddObjectToObject(response, "result");
            cJSON_AddItemToObject(result, "tools", tools);
            if (id != NULL) {
                cJSON_AddItemToObject(response, "id", cJSON_Duplicate(id, true));
            } else {
                cJSON_AddNullToObject(response, "id");
            }
        }
    } else if (strcmp(method->valuestring, "tools/call") == 0) {
        response = handle_tool_call(state, id, params);
    } else {
        response = make_error(id, -32601, "Method not found");
    }

    cJSON_Delete(request);
    return response;
}

static int find_header_end(const char* buffer, int length) {
    for (int i = 0; i <= length - 4; ++i) {
        if (buffer[i] == '\r' && buffer[i + 1] == '\n' &&
            buffer[i + 2] == '\r' && buffer[i + 3] == '\n') {
            return i + 4;
        }
    }
    return -1;
}

static int parse_content_length(const char* headers) {
    const char* cursor = headers;
    while ((cursor = strstr(cursor, "\r\n")) != NULL) {
        cursor += 2;
        if (strncasecmp(cursor, "Content-Length:", 15) == 0) {
            return atoi(cursor + 15);
        }
    }
    return 0;
}

static void send_http_response(int client, int status, const char* content_type, const char* body) {
    const char* status_text = status == 200 ? "OK" :
                              status == 404 ? "Not Found" :
                              status == 405 ? "Method Not Allowed" :
                              status == 413 ? "Payload Too Large" :
                              "Bad Request";
    char header[320];
    int body_length = body == NULL ? 0 : (int)strlen(body);
    int header_length = snprintf(
        header,
        sizeof(header),
        "HTTP/1.1 %d %s\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %d\r\n"
        "Connection: close\r\n"
        "Access-Control-Allow-Origin: *\r\n"
        "\r\n",
        status,
        status_text,
        content_type,
        body_length
    );
    send_all(client, header, (size_t)header_length);
    if (body_length > 0) {
        send_all(client, body, (size_t)body_length);
    }
}

static int query_integer(const char* path, const char* name, int default_value) {
    const char* query = strchr(path, '?');
    if (query == NULL) {
        return default_value;
    }
    query++;
    size_t name_size = strlen(name);
    while (*query != '\0') {
        if (strncmp(query, name, name_size) == 0 && query[name_size] == '=') {
            return atoi(query + name_size + 1);
        }
        query = strchr(query, '&');
        if (query == NULL) {
            break;
        }
        query++;
    }
    return default_value;
}

static void handle_http_client(McpScreenState* state, int client) {
    struct timeval timeout = { .tv_sec = 0, .tv_usec = 250000 };
    setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
    setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));

    char* headers = calloc(1, MCP_HEADER_LIMIT + 1);
    if (headers == NULL) {
        send_http_response(client, 400, "text/plain", "Out of memory");
        return;
    }

    int received = 0;
    int header_end = -1;
    while (state->running && received < MCP_HEADER_LIMIT) {
        int count = recv(client, headers + received, MCP_HEADER_LIMIT - received, 0);
        if (count <= 0) {
            free(headers);
            return;
        }
        received += count;
        header_end = find_header_end(headers, received);
        if (header_end >= 0) {
            break;
        }
    }

    if (!state->running) {
        free(headers);
        return;
    }

    if (header_end < 0) {
        send_http_response(client, 400, "text/plain", "Invalid HTTP headers");
        free(headers);
        return;
    }

    headers[header_end - 4] = '\0';
    char method[12] = {0};
    char path[128] = {0};
    if (sscanf(headers, "%11s %127s", method, path) != 2) {
        send_http_response(client, 400, "text/plain", "Invalid request line");
        free(headers);
        return;
    }

    if (strcmp(method, "POST") != 0) {
        send_http_response(client, 405, "text/plain", "POST required");
        free(headers);
        return;
    }

    bool is_mcp = strcmp(path, "/api/mcp") == 0;
    bool is_raw = strncmp(path, "/api/screen/raw", 15) == 0;
    if (!is_mcp && !is_raw) {
        send_http_response(client, 404, "text/plain", "Not found");
        free(headers);
        return;
    }

    int body_limit = is_raw ? MCP_RAW_BODY_LIMIT : MCP_JSON_BODY_LIMIT;
    int content_length = parse_content_length(headers);
    if (content_length <= 0 || content_length > body_limit) {
        send_http_response(
            client,
            content_length > body_limit ? 413 : 400,
            "text/plain",
            "Invalid Content-Length"
        );
        free(headers);
        return;
    }

    uint8_t* body = malloc((size_t)content_length + (is_mcp ? 1 : 0));
    if (body == NULL) {
        send_http_response(client, 400, "text/plain", "Out of memory");
        free(headers);
        return;
    }

    int body_received = received - header_end;
    if (body_received > content_length) {
        body_received = content_length;
    }
    memcpy(body, headers + header_end, body_received);
    free(headers);

    while (state->running && body_received < content_length) {
        int count = recv(client, body + body_received, content_length - body_received, 0);
        if (count <= 0) {
            send_http_response(client, 400, "text/plain", "Incomplete request body");
            free(body);
            return;
        }
        body_received += count;
    }

    if (!state->running || body_received < content_length) {
        free(body);
        return;
    }

    if (is_raw) {
        int x = query_integer(path, "x", 0);
        int y = query_integer(path, "y", 0);
        int width = query_integer(path, "w", state->draw_width);
        int height = query_integer(path, "h", state->draw_height);
        bool valid_size = width > 0 && height > 0 &&
                          (size_t)width * height * 2 == (size_t)content_length;
        bool drawn = valid_size && mcp_ui_draw_rgb565_be(
            state,
            body,
            content_length,
            x,
            y,
            width,
            height
        );
        send_http_response(
            client,
            drawn ? 200 : 400,
            "text/plain",
            drawn ? "OK" : "Invalid RGB565 payload"
        );
    } else {
        body[content_length] = '\0';
        cJSON* response = handle_rpc(state, (const char*)body);
        char* response_text = cJSON_PrintUnformatted(response);
        if (response_text != NULL && strlen(response_text) <= MCP_RESPONSE_LIMIT) {
            send_http_response(client, 200, "application/json", response_text);
        } else {
            send_http_response(client, 400, "text/plain", "Response serialization failed");
        }
        cJSON_free(response_text);
        cJSON_Delete(response);
    }

    free(body);
}

static void http_task(void* argument) {
    McpScreenState* state = argument;
    state->http_exited = false;
    int server = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    if (server < 0) {
        ESP_LOGE(TAG, "Failed to create HTTP socket: errno=%d", errno);
        state->http_ready = false;
        mcp_ui_set_server_status(state, "MCP socket error");
        goto suspend_for_owner;
    }
    state->http_socket = server;

    int reuse = 1;
    setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    struct sockaddr_in address = {
        .sin_family = AF_INET,
        .sin_port = htons(MCP_HTTP_PORT),
        .sin_addr.s_addr = htonl(INADDR_ANY)
    };

    if (bind(server, (struct sockaddr*)&address, sizeof(address)) != 0 ||
        listen(server, 3) != 0) {
        ESP_LOGE(TAG, "Failed to bind/listen on TCP %d: errno=%d", MCP_HTTP_PORT, errno);
        state->http_ready = false;
        mcp_ui_set_server_status(state, "MCP bind error");
        close_socket(&state->http_socket);
        goto suspend_for_owner;
    }

    int flags = fcntl(server, F_GETFL, 0);
    if (flags < 0 || fcntl(server, F_SETFL, flags | O_NONBLOCK) < 0) {
        ESP_LOGE(TAG, "Failed to make HTTP listener non-blocking: errno=%d", errno);
        state->http_ready = false;
        mcp_ui_set_server_status(state, "MCP socket config error");
        close_socket(&state->http_socket);
        goto suspend_for_owner;
    }

    ESP_LOGI(TAG, "HTTP MCP server listening on port %d", MCP_HTTP_PORT);
    state->http_ready = true;
    mcp_ui_set_server_status(state, "MCP :80 ready");

    while (state->running) {
        struct sockaddr_storage source;
        socklen_t source_length = sizeof(source);
        int client = accept(server, (struct sockaddr*)&source, &source_length);
        if (client < 0) {
            if (!state->running) {
                break;
            }
            vTaskDelay(pdMS_TO_TICKS(50));
            continue;
        }
        state->client_socket = client;
        handle_http_client(state, client);
        close_socket(&state->client_socket);
    }

    close_socket(&state->client_socket);
    close_socket(&state->http_socket);
    state->http_ready = false;
    
suspend_for_owner:
    /*
     * External-app code must not continue after Tactility unloads the ELF.
     * Signal completion, then suspend in FreeRTOS code. onDestroy owns the
     * final vTaskDelete() and will not return until the worker is gone.
     */
    state->http_exited = true;
    vTaskSuspend(NULL);
    vTaskDelete(NULL);
}

bool mcp_services_start(McpScreenState* state) {
    if (state == NULL || state->running) {
        ESP_LOGE(TAG, "Service start rejected state=%p running=%d", state, state != NULL && state->running);
        return false;
    }

    /*
     * onHide only requests shutdown because it runs in Tactility's GUI
     * lifecycle. Reap the worker after it has reached its safe suspended
     * state before starting a new foreground server.
     */
    if (state->http_task != NULL) {
        if (!state->http_exited) {
            ESP_LOGE(TAG, "Previous HTTP worker is still stopping");
            return false;
        }
        vTaskDelete(state->http_task);
        state->http_task = NULL;
    }

    state->running = true;
    state->http_exited = false;
    state->http_socket = -1;
    state->client_socket = -1;
    BaseType_t http_result = xTaskCreate(
        http_task,
        "mcp_http",
        8192,
        state,
        4,
        &state->http_task
    );
    /*
     * UDP discovery requires lwip_recvfrom/lwip_sendto. Tactility 0.7.0-dev
     * currently does not export those symbols to external ELF apps. The
     * existing host bridge remains compatible through its HTTP subnet-scan
     * fallback. Re-enable discovery when those symbols are exported.
     */
    state->discovery_ready = false;
    state->discovery_task = NULL;

    if (http_result != pdPASS) {
        ESP_LOGE(TAG, "Failed to create MCP service tasks");
        mcp_services_stop(state);
        return false;
    }
    return true;
}

void mcp_services_hide(McpScreenState* state) {
    if (state == NULL) {
        return;
    }

    state->running = false;
    state->http_ready = false;
    state->discovery_ready = false;
}

void mcp_services_stop(McpScreenState* state) {
    if (state == NULL) {
        return;
    }

    mcp_services_hide(state);

    for (int attempt = 0; attempt < 80; ++attempt) {
        if (state->http_task == NULL || state->http_exited) {
            break;
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }

    if (state->http_task != NULL) {
        /*
         * Normal path: the non-blocking worker has closed its sockets and
         * suspended itself. The bounded fallback guarantees that no
         * external-ELF instruction survives onDestroy.
         */
        vTaskDelete(state->http_task);
        state->http_task = NULL;
    }
    close_socket(&state->http_socket);
    close_socket(&state->client_socket);
    close_socket(&state->discovery_socket);
    state->http_exited = true;
}
