/*
 * Non-compilable C-like reconstruction of /games/pokemon_pro/conagent.
 *
 * This is not a Ghidra C export. Ghidra imported the binary but did not create
 * normal functions for most stripped text during this pass. The code below is
 * manually reconstructed from Ghidra imports, objdump ranges, and strings.
 */

typedef struct AgentConfig AgentConfig;
typedef struct Log Log;
typedef struct GameClient GameClient;
typedef struct HttpClient HttpClient;
typedef struct GameMessage GameMessage;
typedef struct HttpRequest HttpRequest;
typedef struct TransferRequest TransferRequest;

static volatile bool g_shutdown;

int conagent_main(int argc, char **argv)
{
    AgentConfig cfg;
    Log *log;
    GameClient *game;

    cfg = default_agent_config();
    parse_agent_args(argc, argv, &cfg);

    install_signal_handler(SIGTERM, request_shutdown);
    install_signal_handler(SIGINT, request_shutdown);

    log = create_log_channel(&cfg);
    if (log == NULL) {
        if (!cfg.silent) {
            fwrite("WARNING: Failed to create agent log file.\n", 1, 42, stderr);
            fwrite("WARNING: Failed to create agent log channel.\n", 1, 45, stderr);
        }
    }

    log_info(log, "Stern Pinball Connectivity Agent %s", "1.0.16 (Linux,ARM64,GNU)");
    log_info(log, "Using libagentcmn %s", "libagent 1.0.16 (Linux,ARM64,GNU)");
    log_agent_configuration(log, &cfg);

    game = game_client_create(&cfg, log);
    if (game == NULL) {
        log_fatal(log, "Failed to create Game process interface.");
        destroy_log_channel(log);
        return 0;
    }

    game_client_start(game, log);

    while (!g_shutdown) {
        game_client_update(game, log);
        usleep(16000);
    }

    game_client_stop(game, log);
    destroy_log_channel(log);
    return 0;
}

GameClient *game_client_create(const AgentConfig *cfg, Log *log)
{
    GameClient *gc;

    gc = allocate_game_client_state();
    if (gc == NULL) {
        log_error(log, "Failed to allocate memory for game_client_state_t.");
        return NULL;
    }

    gc->socket_path = cfg->game_process_host ? cfg->game_process_host
                                             : "/usr/local/spike/agent.uds";
    gc->state = NOT_CONNECTED;
    gc->next_rx_id = 0;
    gc->next_tx_id = 0;

    gc->http = http_client_create(cfg, log);
    if (gc->http == NULL) {
        log_error(log, "Failed to create HTTP client to access back-end server.");
        free(gc);
        return NULL;
    }

    init_arena(&gc->json_arena, "game_client_json");
    init_arena(&gc->action_arena, "game_client_action");
    init_arena(&gc->config_arena, "game_client_config");
    return gc;
}

void game_client_update(GameClient *gc, Log *log)
{
    GameMessage msg;

    if (gc->state == NOT_CONNECTED) {
        if (agent_client_connect(gc->socket_path)) {
            log_info(log, "Successfully connected to Game process. Resetting state.");
            gc->state = NEGOTIATE_PROTOCOL;
            send_protocol_request(gc);
        }
        return;
    }

    while (receive_game_message(gc, &msg)) {
        GameAction action = decode_game_message(gc, &msg);
        process_game_action(gc, log, action);
    }

    if (gc->state == COMMS_ENABLED) {
        maybe_send_heartbeat(gc, log);
        http_client_update(gc->http, log);
    }
}

void process_game_action(GameClient *gc, Log *log, GameAction action)
{
    switch (action.type) {
    case AGREE_PROTOCOL_VERSION:
        if (action.protocol_version == EXPECTED_PROTOCOL_VERSION) {
            log_info(log, "Negotiated protocol version %d.", action.protocol_version);
            gc->state = AWAIT_CONFIG;
        } else {
            log_error(log, "Expected protocol version %d, got unsupported version %d.",
                      EXPECTED_PROTOCOL_VERSION, action.protocol_version);
            disconnect_game(gc);
        }
        break;

    case CONFIG_SECURITY:
        require(action.has_device_credentials,
                "CONFIG_SECURITY message missing device credentials.");
        store_security_config(gc, action.security);
        break;

    case CONFIG_NETWORK:
        validate_network_config(action.network);
        http_client_configure(gc->http, action.network);
        break;

    case CONFIG_TITLE:
        require(action.serial_number, "CONFIG_TITLE message missing machine serial number.");
        require(action.machine_configuration_number,
                "CONFIG_TITLE message missing machine configuration number.");
        require(action.code_version, "CONFIG_TITLE message missing code version identifier.");
        require(action.game_title, "CONFIG_TITLE message missing game title identifier.");
        require(action.game_model, "CONFIG_TITLE message missing game model identifier.");
        store_title_config(gc, action.title);
        break;

    case COMMS_ENABLE:
        if (gc->state == NEGOTIATE_PROTOCOL) {
            log_error(log, "Cannot enable communications during protocol version negotiation");
            break;
        }
        if (!have_required_config(gc)) {
            log_error(log, "Cannot enable communications while waiting for required configuration data");
            break;
        }
        if (!http_sync_api_token(gc->http, log)) {
            log_error(log, "Failed to retrieve the API token from the backend server");
            break;
        }
        gc->state = COMMS_ENABLED;
        break;

    case COMMS_DISABLE:
        gc->state = COMMS_DISABLED;
        break;

    case MACHINE_REGISTER:
        api_game_register(gc, log, &action.register_machine);
        break;

    case SET_MACHINE_UUID:
        gc->machine_uuid = action.machine_uuid;
        log_info(log, "Machine UUID updated to %s.", gc->machine_uuid);
        break;

    case PLAYER_AUTHENTICATE:
        api_player_auth(gc, log, action.user_uuid);
        break;

    case PING_REQUEST:
        api_ping(gc, log);
        break;

    case GAME_TO_SERVER_POST:
        if (!valid_game_api_endpoint(action.endpoint)) {
            nack(action.id, "The API endpoint supplied with GAME_TO_SERVER_POST request is invalid");
            break;
        }
        api_game_to_server_post(gc, log, action.endpoint, action.json_payload);
        break;

    case GAME_TO_SERVER_GET:
        if (!valid_game_api_endpoint(action.endpoint)) {
            nack(action.id, "The API endpoint supplied with GAME_TO_SERVER_GET request is invalid");
            break;
        }
        api_game_to_server_get(gc, log, action.endpoint, action.args);
        break;

    case FILE_TRANSFER_REQUEST:
        http_client_queue_file_transfer(gc->http, &action.transfer);
        break;

    case DISABLE_INTERFACE_REQUEST:
        disable_network_interface(log, action.interface_name);
        break;

    case CONFIGURE_INTERFACE_REQUEST:
        reconfigure_network_interface(log, &action.network_interface);
        break;

    case QUERY_SERVER_TIME:
        http_client_query_server_time(gc->http, log, action.server_host);
        break;

    case CHECK_FOR_UPDATES:
        api_version_upgrades_available(gc, log);
        break;
    }
}

bool http_sync_api_token(HttpClient *http, Log *log)
{
    char url[MAX_URL];
    char body[MAX_JSON];
    char response[MAX_RESPONSE];
    json_t *json;

    if (!http->server_host) {
        log_error(log, "http_sync_api_token missing back-end server hostname or IP address.");
        return false;
    }
    if (!http->api_username) {
        log_error(log, "http_sync_api_token missing API token username.");
        return false;
    }
    if (!http->api_password) {
        log_error(log, "http_sync_api_token missing API token password.");
        return false;
    }

    snprintf(url, sizeof(url), "https://%s/api-token-auth/", http->server_host);
    snprintf(body, sizeof(body), "{\"username\":\"%s\",\"password\":\"%s\"}",
             http->api_username, http->api_password);

    CURL *curl = curl_easy_init();
    if (curl == NULL) {
        log_error(log, "Failed to allocate cURL request object.");
        return false;
    }

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, strlen(body));
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, "Content-Type: application/json");
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "Spike-Connectivity-Agent/1.0");

    if (curl_easy_perform(curl) != CURLE_OK) {
        log_error(log, "http_sync_api_token: Failed to retrieve API token");
        curl_easy_cleanup(curl);
        return false;
    }

    json = parse_json(response);
    if (!json_is_object(json)) {
        log_error(log, "http_sync_api_token: Expected json_type_object");
        curl_easy_cleanup(curl);
        return false;
    }
    if (!json_get_string(json, "token", http->api_token, sizeof(http->api_token))) {
        log_error(log, "http_sync_api_token: Failed to find the token field.");
        curl_easy_cleanup(curl);
        return false;
    }

    curl_easy_cleanup(curl);
    return true;
}

HttpResult http_execute_api(HttpClient *http, HttpRequest *req, Log *log)
{
    char url[MAX_URL];
    char auth[MAX_HEADER];
    char device_token[MAX_HEADER];
    char device_refresh[MAX_HEADER];

    if (req->query_len > 0) {
        snprintf(url, sizeof(url), "https://%s%s/?%.*s",
                 http->server_host, req->endpoint, req->query_len, req->query);
    } else {
        snprintf(url, sizeof(url), "https://%s%s/", http->server_host, req->endpoint);
    }

    snprintf(auth, sizeof(auth), "Authorization: Token %s", http->api_token);
    snprintf(device_token, sizeof(device_token), "Device-Token: %s", http->device_token);
    snprintf(device_refresh, sizeof(device_refresh), "Device-Refresh: %s", http->device_refresh);

    curl_easy_reset(req->curl);
    curl_easy_setopt(req->curl, CURLOPT_URL, url);
    curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, auth);
    curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, device_token);
    curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, device_refresh);
    curl_easy_setopt(req->curl, CURLOPT_USERAGENT, "Spike-Connectivity-Agent/1.0");

    if (req->method == HTTP_PUT) {
        curl_easy_setopt(req->curl, CURLOPT_CUSTOMREQUEST, "PUT");
    } else if (req->method == HTTP_DELETE) {
        curl_easy_setopt(req->curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    } else if (req->method == HTTP_POST) {
        curl_easy_setopt(req->curl, CURLOPT_POSTFIELDS, req->body);
        curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, "Content-Type: application/json");
    }

    CURLcode rc = curl_easy_perform(req->curl);
    if (rc != CURLE_OK) {
        log_error(log, "curl_easy_perform returned failure code %d.", rc);
        return HTTP_RESULT_CURL_ERROR;
    }

    read_response_headers(req, "Message-Number: ", "App-Status-Code: ");
    return classify_http_response(req);
}

void disable_network_interface(Log *log, const char *ifname)
{
    char path[128];
    char cmd[256];

    if (!valid_interface_name(ifname)) {
        log_error(log, "Cannot reconfigure network interface; no interface name supplied.");
        return;
    }

    snprintf(path, sizeof(path), "/tmp/network_interface.%s", ifname);
    if (!file_exists(path)) {
        log_info(log, "Skipping disablement of network interface %s since interface configuration file %s does not exist.",
                 ifname, path);
        return;
    }

    snprintf(cmd, sizeof(cmd), "ifdown -i %s %s", path, ifname);
    log_info(log, "Executing command \"%s\".", cmd);
    int rc = system(cmd);
    if (rc != 0) {
        log_error(log, "The command \"%s\" failed (exit code %d).", cmd, rc);
    }
}

bool reconfigure_network_interface(Log *log, const NetworkInterfaceConfig *cfg)
{
    char path[128];
    char cmd[256];
    FILE *fp;

    validate_network_interface_config(cfg);

    snprintf(path, sizeof(path), "/tmp/network_interface.%s", cfg->ifname);
    fp = fopen(path, "w");
    if (fp == NULL) {
        log_error(log, "Failed to open network interface configuration file \"%s\".", path);
        return false;
    }

    if (cfg->dhcp) {
        fprintf(fp, "iface %s inet dhcp\n", cfg->ifname);
    } else {
        fprintf(fp, "iface %s inet static\n", cfg->ifname);
        fprintf(fp, "address %s\n", cfg->address);
        fprintf(fp, "netmask %s\n", cfg->netmask);
        fprintf(fp, "gateway %s\n", cfg->gateway);
        if (cfg->secondary_dns) {
            fprintf(fp, "dns-nameservers %s %s\n", cfg->primary_dns, cfg->secondary_dns);
        } else {
            fprintf(fp, "dns-nameservers %s\n", cfg->primary_dns);
        }
    }

    fflush(fp);
    fclose(fp);

    if (cfg->store_only) {
        log_info(log, "Not actually reconfiguring network interface %s; store-only flag is set.",
                 cfg->ifname);
        return true;
    }

    snprintf(cmd, sizeof(cmd), "ifup -i %s %s", path, cfg->ifname);
    log_info(log, "Executing command \"%s\".", cmd);
    return system(cmd) == 0;
}

void *agent_client_rx_thread(void *arg)
{
    GameClient *gc = arg;

    pthread_setname_np(pthread_self(), "agent_client_rx");
    log_info(gc->log, "Client receive thread started normally.");

    while (!gc->rx_shutdown) {
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(gc->socket_fd, &readfds);

        int rc = select(gc->socket_fd + 1, &readfds, NULL, NULL, NULL);
        if (rc < 0) {
            if (errno == EAGAIN || errno == EINTR) {
                continue;
            }
            gc->last_error = errno;
            break;
        }

        if (FD_ISSET(gc->socket_fd, &readfds)) {
            ssize_t n = recv(gc->socket_fd, gc->rxbuf, sizeof(gc->rxbuf), 0);
            if (n == 0) {
                log_info(gc->log, "Client receive thread saw client disconnect.");
                break;
            }
            if (n < 0) {
                gc->last_error = errno;
                break;
            }

            ParserStatus st = agent_parse_frame(&gc->parser, gc->rxbuf, n);
            if (st == PARSER_COMPLETE) {
                push_received_message(gc, &gc->parser.message);
            } else if (st == PARSER_ERROR) {
                log_error(gc->log, "Encountered error decoding message frame");
                reset_stream(gc);
            }
        }
    }

    log_info(gc->log, "Client receive thread terminated.");
    return NULL;
}

void cloudwatch_put_log_events(CloudWatchLogger *cw, const char *message)
{
    /*
     * Builds an AWS Signature Version 4 request:
     *   service: logs
     *   region: us-west-1
     *   target: Logs_20140328.PutLogEvents
     *   URL: https://logs.us-west-1.amazonaws.com
     *
     * The binary imports SHA256_* and HMAC and contains strings for the
     * canonical request, signed headers, x-amz-date, and nextSequenceToken.
     */
    char payload[MAX_CLOUDWATCH_PAYLOAD];
    char authorization[MAX_HEADER];

    build_put_log_events_json(payload, cw->log_group, cw->log_stream,
                              cw->next_sequence_token, message);
    aws4_sign_request(authorization, cw->access_key, cw->secret_key,
                      "us-west-1", "logs", payload);

    curl_easy_setopt(cw->curl, CURLOPT_URL, "https://logs.us-west-1.amazonaws.com");
    curl_easy_setopt(cw->curl, CURLOPT_HTTPHEADER, "x-amz-target: Logs_20140328.PutLogEvents");
    curl_easy_setopt(cw->curl, CURLOPT_HTTPHEADER, authorization);
    curl_easy_setopt(cw->curl, CURLOPT_POSTFIELDS, payload);

    if (curl_easy_perform(cw->curl) != CURLE_OK) {
        log_error(cw->log, "CLOUD LOG POST: curl_easy_perform() failed");
    }
}
