#include "downloader.h"

#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#include "util.h"

typedef void (*line_cb_t)(void *ud, const char *line);

struct stderr_reader_args {
    int fd;
    char *buf;
    size_t cap;
};

static void *stderr_reader_thread(void *arg) {
    struct stderr_reader_args *a = arg;
    size_t used = 0;
    char chunk[512];
    ssize_t n;
    while ((n = read(a->fd, chunk, sizeof(chunk))) > 0) {
        if (a->cap > 0 && used < a->cap - 1) {
            size_t room = a->cap - 1 - used;
            size_t take = (size_t)n < room ? (size_t)n : room;
            memcpy(a->buf + used, chunk, take);
            used += take;
            a->buf[used] = '\0';
        }
        /* otherwise keep draining and discard, so the child never blocks on a full pipe */
    }
    return NULL;
}

/* Forks + execs argv, streams stdout lines to stdout_cb, captures stderr into err.
 * Returns 0 if the child exited with status 0, -1 otherwise. */
static int run_process(char *const argv[], line_cb_t stdout_cb, void *stdout_ud,
                        char *err, size_t err_cap) {
    int out_pipe[2], err_pipe[2];
    if (pipe(out_pipe) != 0 || pipe(err_pipe) != 0) {
        snprintf(err, err_cap, "pipe() failed: %s", strerror(errno));
        return -1;
    }

    pid_t pid = fork();
    if (pid < 0) {
        snprintf(err, err_cap, "fork() failed: %s", strerror(errno));
        return -1;
    }

    if (pid == 0) {
        dup2(out_pipe[1], STDOUT_FILENO);
        dup2(err_pipe[1], STDERR_FILENO);
        close(out_pipe[0]);
        close(out_pipe[1]);
        close(err_pipe[0]);
        close(err_pipe[1]);
        execvp(argv[0], argv);
        _exit(127);
    }

    close(out_pipe[1]);
    close(err_pipe[1]);

    if (err_cap > 0) err[0] = '\0';
    struct stderr_reader_args sargs = {.fd = err_pipe[0], .buf = err, .cap = err_cap};
    pthread_t err_thread;
    pthread_create(&err_thread, NULL, stderr_reader_thread, &sargs);

    FILE *out_f = fdopen(out_pipe[0], "r");
    if (out_f) {
        char *line = NULL;
        size_t linecap = 0;
        ssize_t len;
        while ((len = getline(&line, &linecap, out_f)) >= 0) {
            while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
                line[--len] = '\0';
            }
            if (stdout_cb) stdout_cb(stdout_ud, line);
        }
        free(line);
        fclose(out_f);
    } else {
        close(out_pipe[0]);
    }

    pthread_join(err_thread, NULL);
    close(err_pipe[0]);

    int status = 0;
    waitpid(pid, &status, 0);
    if (WIFEXITED(status) && WEXITSTATUS(status) == 0) return 0;
    return -1;
}

/* ---- resolve -------------------------------------------------------- */

#define RESOLVE_TITLE_CAP 512
#define RESOLVE_URL_CAP 2048

struct resolve_ctx {
    entry_cb_t on_entry;
    void *user_data;
    char *playlist_title;
    size_t playlist_title_cap;
    int got_playlist_title;
    int entry_count;
};

static void resolve_line_cb(void *ud, const char *line) {
    struct resolve_ctx *ctx = ud;

    const char *sep1 = strchr(line, '\x1f');
    if (!sep1) return;
    const char *sep2 = strchr(sep1 + 1, '\x1f');
    if (!sep2) return;

    char title[RESOLVE_TITLE_CAP];
    char url[RESOLVE_URL_CAP];
    size_t title_len = (size_t)(sep1 - line);
    size_t url_len = (size_t)(sep2 - (sep1 + 1));
    if (title_len >= sizeof(title)) title_len = sizeof(title) - 1;
    if (url_len >= sizeof(url)) url_len = sizeof(url) - 1;
    memcpy(title, line, title_len);
    title[title_len] = '\0';
    memcpy(url, sep1 + 1, url_len);
    url[url_len] = '\0';

    const char *playlist_title_field = sep2 + 1;
    if (!ctx->got_playlist_title && *playlist_title_field) {
        safe_strcpy(ctx->playlist_title, ctx->playlist_title_cap, playlist_title_field);
        ctx->got_playlist_title = 1;
    }

    if (*url && ctx->on_entry) {
        ctx->on_entry(ctx->user_data, url, *title ? title : url);
        ctx->entry_count++;
    }
}

int downloader_resolve(const char *url, entry_cb_t on_entry, void *user_data,
                        char *playlist_title, size_t playlist_title_cap,
                        char *err, size_t err_cap) {
    if (playlist_title_cap > 0) playlist_title[0] = '\0';

    static const char *template = "%(title)s\x1f%(webpage_url,url)s\x1f%(playlist_title|)s";
    char *argv[] = {
        "yt-dlp", "--flat-playlist", "--skip-download",
        "--no-cache-dir", /* concurrent yt-dlp invocations sharing ~/.cache/yt-dlp can race */
        "--print", (char *)template,
        (char *)url, NULL,
    };

    struct resolve_ctx ctx = {
        .on_entry = on_entry,
        .user_data = user_data,
        .playlist_title = playlist_title,
        .playlist_title_cap = playlist_title_cap,
        .got_playlist_title = 0,
        .entry_count = 0,
    };

    char proc_err[DL_ERROR_CAP];
    run_process(argv, resolve_line_cb, &ctx, proc_err, sizeof(proc_err));

    if (ctx.entry_count == 0) {
        if (*proc_err) {
            snprintf(err, err_cap, "%s", proc_err);
        } else {
            snprintf(err, err_cap, "Nothing downloadable found at %s", url);
        }
        return -1;
    }
    return 0;
}

/* ---- download --------------------------------------------------------- */

struct download_ctx {
    progress_cb_t cb;
    void *user_data;
};

static void download_line_cb(void *ud, const char *line) {
    struct download_ctx *ctx = ud;
    if (strncmp(line, "[download]", 10) != 0) return;

    float percent;
    char speed[64];
    char eta[32];
    int n = sscanf(line, "[download] %f%% of %*s at %63s ETA %31s", &percent, speed, eta);
    if (n == 3 && ctx->cb) {
        ctx->cb(ctx->user_data, percent, speed, eta);
    }
}

int downloader_download(const char *url, const char *output_dir, const char *audio_format,
                         progress_cb_t cb, void *user_data,
                         char *err, size_t err_cap) {
    if (mkdir_p(output_dir) != 0) {
        snprintf(err, err_cap, "Could not create %s: %s", output_dir, strerror(errno));
        return -1;
    }

    char outtmpl[2200];
    snprintf(outtmpl, sizeof(outtmpl), "%s/%%(title)s.%%(ext)s", output_dir);

    char *argv[] = {
        "yt-dlp",
        "-f", "bestaudio/best",
        "-x", "--audio-format", (char *)audio_format,
        "--audio-quality", "0",
        "--embed-thumbnail",
        "--embed-metadata",
        "--no-cache-dir", /* concurrent yt-dlp invocations sharing ~/.cache/yt-dlp can race */
        "--newline",
        "-o", outtmpl,
        (char *)url,
        NULL,
    };

    struct download_ctx ctx = {.cb = cb, .user_data = user_data};

    char proc_err[DL_ERROR_CAP];
    int rc = run_process(argv, download_line_cb, &ctx, proc_err, sizeof(proc_err));
    if (rc != 0) {
        if (*proc_err) {
            snprintf(err, err_cap, "%s", proc_err);
        } else {
            snprintf(err, err_cap, "yt-dlp exited with an error");
        }
        return -1;
    }
    return 0;
}
