#include "app.h"

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "downloader.h"
#include "util.h"

#define MESSAGE_TTL_TICKS 30 /* ~ a few seconds at the UI's redraw rate */
#define DOWNLOAD_MAX_ATTEMPTS 3
#define DOWNLOAD_RETRY_DELAY_SECS 2

void app_init(App *app, const char *output_dir, const char *audio_format) {
    memset(app, 0, sizeof(*app));
    queue_init(&app->queue);
    pthread_mutex_init(&app->settings_lock, NULL);
    pthread_mutex_init(&app->work_lock, NULL);
    pthread_cond_init(&app->work_cond, NULL);
    pthread_mutex_init(&app->message_lock, NULL);

    safe_strcpy(app->output_dir, sizeof(app->output_dir), output_dir);
    safe_strcpy(app->audio_format, sizeof(app->audio_format), audio_format);
    app->playlist_folders = 1;
}

/* ---- settings ---------------------------------------------------------- */

void app_set_output_dir(App *app, const char *path) {
    pthread_mutex_lock(&app->settings_lock);
    safe_strcpy(app->output_dir, sizeof(app->output_dir), path);
    pthread_mutex_unlock(&app->settings_lock);
    app_notify(app, 0, "Now saving to %s", path);
}

void app_get_output_dir(App *app, char *out, size_t cap) {
    pthread_mutex_lock(&app->settings_lock);
    safe_strcpy(out, cap, app->output_dir);
    pthread_mutex_unlock(&app->settings_lock);
}

void app_set_playlist_folders(App *app, int enabled) {
    pthread_mutex_lock(&app->settings_lock);
    app->playlist_folders = enabled;
    pthread_mutex_unlock(&app->settings_lock);
}

int app_get_playlist_folders(App *app) {
    pthread_mutex_lock(&app->settings_lock);
    int v = app->playlist_folders;
    pthread_mutex_unlock(&app->settings_lock);
    return v;
}

static void app_get_audio_format(App *app, char *out, size_t cap) {
    pthread_mutex_lock(&app->settings_lock);
    safe_strcpy(out, cap, app->audio_format);
    pthread_mutex_unlock(&app->settings_lock);
}

/* ---- messages ------------------------------------------------------------ */

void app_notify(App *app, int is_error, const char *fmt, ...) {
    char buf[MESSAGE_CAP];
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);

    pthread_mutex_lock(&app->message_lock);
    safe_strcpy(app->message, sizeof(app->message), buf);
    app->message_ttl_ticks = MESSAGE_TTL_TICKS;
    app->message_is_error = is_error;
    pthread_mutex_unlock(&app->message_lock);
}

void app_get_message(App *app, char *out, size_t cap, int *is_error) {
    pthread_mutex_lock(&app->message_lock);
    if (app->message_ttl_ticks > 0) {
        safe_strcpy(out, cap, app->message);
        if (is_error) *is_error = app->message_is_error;
        app->message_ttl_ticks--;
    } else {
        if (cap > 0) out[0] = '\0';
        if (is_error) *is_error = 0;
    }
    pthread_mutex_unlock(&app->message_lock);
}

/* ---- work queue ---------------------------------------------------------- */

static void app_enqueue_work(App *app, int id) {
    pthread_mutex_lock(&app->work_lock);
    if (app->work_count < MAX_QUEUE_ITEMS) {
        app->work_ids[app->work_tail] = id;
        app->work_tail = (app->work_tail + 1) % MAX_QUEUE_ITEMS;
        app->work_count++;
        pthread_cond_signal(&app->work_cond);
    }
    pthread_mutex_unlock(&app->work_lock);
}

/* ---- resolving ------------------------------------------------------------ */

struct entry_ctx {
    App *app;
    int playlist_folders;
    char *playlist_title; /* shared buffer, populated by downloader_resolve as entries stream in */
};

static void entry_trampoline(void *ud, const char *url, const char *title) {
    struct entry_ctx *ctx = ud;
    char subdir[SUBDIR_CAP] = "";
    if (ctx->playlist_folders && ctx->playlist_title[0]) {
        sanitize_dirname(ctx->playlist_title, subdir, sizeof(subdir));
    }
    int id = queue_add(&ctx->app->queue, url, title, subdir);
    if (id >= 0) app_enqueue_work(ctx->app, id);
}

struct resolve_thread_arg {
    App *app;
    char url[URL_CAP];
};

static void *resolver_thread_fn(void *arg) {
    struct resolve_thread_arg *rta = arg;
    App *app = rta->app;

    struct entry_ctx ectx;
    ectx.app = app;
    ectx.playlist_folders = app_get_playlist_folders(app);
    char playlist_title[TITLE_CAP] = "";
    ectx.playlist_title = playlist_title;

    char err[DL_ERROR_CAP];
    int rc = downloader_resolve(rta->url, entry_trampoline, &ectx, playlist_title,
                                 sizeof(playlist_title), err, sizeof(err));
    if (rc != 0) {
        app_notify(app, 1, "Failed to resolve: %s", err);
    }

    free(rta);
    return NULL;
}

void app_submit_url(App *app, const char *url) {
    struct resolve_thread_arg *rta = malloc(sizeof(*rta));
    rta->app = app;
    safe_strcpy(rta->url, sizeof(rta->url), url);

    app_notify(app, 0, "Resolving %s...", url);

    pthread_t t;
    pthread_create(&t, NULL, resolver_thread_fn, rta);
    pthread_detach(t);
}

/* ---- downloading ----------------------------------------------------------- */

struct progress_ud {
    Queue *q;
    int id;
};

static void progress_trampoline(void *ud, float percent, const char *speed, const char *eta) {
    struct progress_ud *p = ud;
    queue_set_progress(p->q, p->id, percent, speed, eta);
}

static void process_item(App *app, int id) {
    QueueItem item;
    if (!queue_get(&app->queue, id, &item)) return;
    if (item.status == STATUS_DONE) return;

    queue_set_status(&app->queue, id, STATUS_DOWNLOADING, "");

    char output_dir[OUTPUT_DIR_CAP];
    char audio_format[AUDIO_FORMAT_CAP];
    app_get_output_dir(app, output_dir, sizeof(output_dir));
    app_get_audio_format(app, audio_format, sizeof(audio_format));

    char target_dir[OUTPUT_DIR_CAP + SUBDIR_CAP + 2];
    if (item.subdir[0]) {
        snprintf(target_dir, sizeof(target_dir), "%s/%s", output_dir, item.subdir);
    } else {
        safe_strcpy(target_dir, sizeof(target_dir), output_dir);
    }

    struct progress_ud pud = {.q = &app->queue, .id = id};
    char err[DL_ERROR_CAP];
    int rc = -1;

    /* YouTube occasionally returns a transient HTTP 403 on the signed media
     * URL, especially under concurrent downloads; a fresh extraction
     * (a brand new yt-dlp invocation) gets a new URL and usually succeeds.
     * Retry a couple of times with a short backoff before giving up. */
    for (int attempt = 0; attempt < DOWNLOAD_MAX_ATTEMPTS; attempt++) {
        if (attempt > 0) sleep(DOWNLOAD_RETRY_DELAY_SECS);
        rc = downloader_download(item.url, target_dir, audio_format, progress_trampoline, &pud,
                                  err, sizeof(err));
        if (rc == 0) break;
    }

    if (rc == 0) {
        queue_set_status(&app->queue, id, STATUS_DONE, NULL);
    } else {
        queue_set_status(&app->queue, id, STATUS_ERROR, err);
    }
}

static void *worker_thread_fn(void *arg) {
    App *app = arg;
    for (;;) {
        pthread_mutex_lock(&app->work_lock);
        while (app->work_count == 0 && !app->shutdown) {
            pthread_cond_wait(&app->work_cond, &app->work_lock);
        }
        if (app->shutdown && app->work_count == 0) {
            pthread_mutex_unlock(&app->work_lock);
            break;
        }
        int id = app->work_ids[app->work_head];
        app->work_head = (app->work_head + 1) % MAX_QUEUE_ITEMS;
        app->work_count--;
        pthread_mutex_unlock(&app->work_lock);

        process_item(app, id);
    }
    return NULL;
}

void app_start_workers(App *app) {
    for (int i = 0; i < CONCURRENT_DOWNLOADS; i++) {
        pthread_create(&app->workers[i], NULL, worker_thread_fn, app);
    }
}

void app_shutdown(App *app) {
    pthread_mutex_lock(&app->work_lock);
    app->shutdown = 1;
    pthread_cond_broadcast(&app->work_cond);
    pthread_mutex_unlock(&app->work_lock);

    for (int i = 0; i < CONCURRENT_DOWNLOADS; i++) {
        pthread_join(app->workers[i], NULL);
    }
}

/* ---- queue actions ----------------------------------------------------------- */

void app_clear_finished(App *app) {
    int n = queue_clear_finished(&app->queue);
    if (n > 0) {
        app_notify(app, 0, "Cleared %d finished item(s)", n);
    } else {
        app_notify(app, 0, "Nothing to clear");
    }
}

void app_remove_item(App *app, int id) {
    queue_remove(&app->queue, id);
}

void app_retry_item(App *app, int id) {
    QueueItem item;
    if (!queue_get(&app->queue, id, &item)) return;
    if (item.status != STATUS_ERROR) return;
    queue_reset_for_retry(&app->queue, id);
    app_enqueue_work(app, id);
}
