//
// Async HTTP. Each request spawns a worker thread that runs libcurl's blocking
// easy-perform; the main loop polls `thread_is_done` each frame via
// `http_pump()` and fires the request's `on_done` callback on the main thread
// when the response is ready.
//
// All UI state mutation happens in the on_done callback, so handlers don't
// need to think about threading at all — just JSON-parse and write to
// `app.library` / `app.current_track` / etc.
//
// Per-request threads (vs a shared queue) keep this small. Library browsing
// and track downloads rarely have more than ~3 in flight; if we start
// flooding we'll graduate to a thread pool.
//

#import "Thread";

Http_Task :: struct {
    method: string;
    url:    string;
    body:   string;
    auth:   string;

    response: Http_Response;

    // Bookkeeping. The worker writes `done` after `response` is fully
    // populated; the main thread reads via thread_is_done.
    done:      bool;

    thread:    Thread;
    user_data: *void;
    on_done:   #type (task: *Http_Task);
}

http_in_flight: [..] *Http_Task;

http_submit :: (
    method:    string,
    path:      string,
    body:      string = "",
    on_done:   #type (task: *Http_Task) = null,
    user_data: *void = null,
) -> *Http_Task {
    task := New(Http_Task);
    task.method    = copy_string(method);
    task.url       = copy_string(tprint("%/%", app.jellyfin.server_url, trim_left(path, "/")));
    task.body      = copy_string(body);
    task.auth      = copy_string(build_auth_header(*app.jellyfin));
    task.on_done   = on_done;
    task.user_data = user_data;

    if !thread_init(*task.thread, http_thread_proc) {
        log_error("http_submit: thread_init failed");
        free_task(task);
        return null;
    }
    task.thread.data = task;
    thread_start(*task.thread);

    array_add(*http_in_flight, task);
    return task;
}

//
// Called every frame from the main loop. Drains finished tasks.
//
http_pump :: () {
    i := 0;
    while i < http_in_flight.count {
        task := http_in_flight[i];
        if thread_is_done(*task.thread, 0) {
            if task.on_done  task.on_done(task);
            array_unordered_remove_by_index(*http_in_flight, i);
            free_task(task);
        } else {
            i += 1;
        }
    }
}

#scope_file

http_thread_proc :: (thread: *Thread) -> s64 {
    task := cast(*Http_Task) thread.data;
    task.response = perform_curl_blocking(task);
    // Memory-ordering note: thread_is_done synchronizes with the worker's
    // exit, so by the time the main thread sees the thread as finished, the
    // response store is visible too. No explicit barrier needed.
    task.done = true;
    return 0;
}

free_task :: (task: *Http_Task) {
    free(task.method);
    free(task.url);
    free(task.body);
    free(task.auth);
    free(task.response.body);
    thread_deinit(*task.thread);
    free(task);
}

Write_Buffer :: struct {
    builder: String_Builder;
}

write_callback :: (contents: *u8, count: u64, size: u64, user: *Write_Buffer) -> u64 #c_call {
    total := count * size;
    push_context {
        append(*user.builder, contents, cast(s64) total);
    }
    return total;
}

perform_curl_blocking :: (task: *Http_Task) -> Http_Response {
    using Curl;
    response: Http_Response;

    handle := curl_easy_init();
    if !handle {
        log_error("http: curl_easy_init failed");
        return response;
    }
    defer curl_easy_cleanup(handle);

    curl_easy_setopt(handle, CURLoption.URL, temp_c_string(task.url));

    // Jellyfin's /Audio/{id}/universal endpoint redirects to the actual
    // transcoded stream. Without FOLLOWLOCATION we just get the 302 body
    // (empty) and a 200/302 status, which downstream silently looks like
    // "successful 0-byte download".
    curl_easy_setopt(handle, CURLoption.FOLLOWLOCATION, cast(s64) 1);
    curl_easy_setopt(handle, CURLoption.MAXREDIRS,      cast(s64) 5);

    buf: Write_Buffer;
    curl_easy_setopt(handle, CURLoption.WRITEFUNCTION, write_callback);
    curl_easy_setopt(handle, CURLoption.WRITEDATA, *buf);

    headers: *curl_slist;
    headers = curl_slist_append(headers, temp_c_string(tprint("X-Emby-Authorization: %", task.auth)));
    headers = curl_slist_append(headers, temp_c_string("Accept: application/json"));
    if task.method == "POST" || task.method == "PUT" {
        headers = curl_slist_append(headers, temp_c_string("Content-Type: application/json"));
        curl_easy_setopt(handle, CURLoption.POSTFIELDS, temp_c_string(task.body));
        curl_easy_setopt(handle, CURLoption.POSTFIELDSIZE, cast(s64) task.body.count);
    } else if task.method != "GET" {
        curl_easy_setopt(handle, CURLoption.CUSTOMREQUEST, temp_c_string(task.method));
    }
    curl_easy_setopt(handle, CURLoption.HTTPHEADER, headers);
    defer curl_slist_free_all(headers);

    err := curl_easy_perform(handle);
    if err != .OK {
        msg := to_string(curl_easy_strerror(err));
        log_error("curl % %: %", task.method, task.url, msg);
        return response;
    }

    code: s64;
    curl_easy_getinfo(handle, CURLINFO.RESPONSE_CODE, *code);
    response.status_code = code;
    response.body        = builder_to_string(*buf.builder);
    response.ok          = code >= 200 && code < 300;
    return response;
}
