// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/src/cmd/install.c
 *
 *  End-to-end install: load ppm.toml, augment with new requirements
 *  from argv, resolve, download every tarball, ingest into the store,
 *  commit a new generation through the transaction layer, persist
 *  ppm.lock.
 *
 *  Copyright (C) 2026 Project Tick
 */

#include <ppm/manifest.h>
#include <ppm/lockfile.h>
#include <ppm/resolver.h>
#include <ppm/store.h>
#include <ppm/registry.h>
#include <ppm/transaction.h>
#include <ppm/crypto.h>
#include <ppm/build.h>

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

#include "../cli.h"
#include "../util/log.h"
#include "../util/path.h"
#include "../util/http.h"
#include "common.h"

static int append_dependency(const char *name, const char *spec) {
    FILE *f = fopen("ppm.toml", "a");
    if (!f) return -1;
    fprintf(f, "%s = \"%s\"\n", name, spec);
    fflush(f);
    fclose(f);
    return 0;
}

static int ensure_dependency_listed(const char *name, const char *spec,
                                    int *added_out) {
    *added_out = 0;
    ppm_manifest_t *m = NULL;
    if (ppm_manifest_load("ppm.toml", &m) != PPM_OK) return -1;
    int present = 0;
    for (size_t i = 0; i < m->deps_len; i++) {
        if (strcmp(m->deps[i].name, name) == 0) { present = 1; break; }
    }
    ppm_manifest_free(m);
    if (present) return 0;

    FILE *r = fopen("ppm.toml", "r");
    if (!r) return -1;
    char buf[8192]; size_t n = fread(buf, 1, sizeof(buf) - 1, r);
    buf[n] = '\0'; fclose(r);

    if (!strstr(buf, "[dependencies]")) {
        FILE *a = fopen("ppm.toml", "a");
        if (!a) return -1;
        fputs("\n[dependencies]\n", a);
        fclose(a);
    }
    if (append_dependency(name, spec) != 0) return -1;
    *added_out = 1;
    return 0;
}

/*
 * For a given package's runtime dependency names, build parallel
 * arrays of (name, hash, store_path) by looking them up in the
 * lockfile. Caller frees with free_dep_paths().
 *
 * Every dependency must already be present in the store at this
 * point; we rely on the caller to install them in topological order.
 */
static ppm_error_t collect_dep_paths(ppm_store_t *s,
                                     const ppm_lockfile_t *lock,
                                     const ppm_manifest_t *mf,
                                     char ***out_names,
                                     ppm_sha512_t **out_hashes,
                                     char ***out_paths,
                                     size_t *out_count) {
    size_t n = mf->deps_len + mf->build_deps_len;
    *out_count = 0;
    *out_names = NULL;
    *out_hashes = NULL;
    *out_paths = NULL;
    if (n == 0) return PPM_OK;

    char         **names  = calloc(n, sizeof(*names));
    ppm_sha512_t  *hashes = calloc(n, sizeof(*hashes));
    char         **paths  = calloc(n, sizeof(*paths));
    if (!names || !hashes || !paths) {
        free(names); free(hashes); free(paths);
        return PPM_ERR_NOMEM;
    }

    size_t out_i = 0;
    #define COLLECT(arr, arr_len) \
        for (size_t i = 0; i < (arr_len); i++) { \
            const char *dep_name = (arr)[i].name; \
            const ppm_lock_entry_t *dep = NULL; \
            for (size_t j = 0; j < lock->entries_len; j++) { \
                if (strcmp(lock->entries[j].name, dep_name) == 0) { \
                    dep = &lock->entries[j]; break; \
                } \
            } \
            if (!dep) { \
                ppm_error("install: dependency '%s' missing from lockfile", dep_name); \
                for (size_t k = 0; k < out_i; k++) { free(names[k]); free(paths[k]); } \
                free(names); free(hashes); free(paths); \
                return PPM_ERR_INTERNAL; \
            } \
            char *p = NULL; \
            ppm_error_t e = ppm_store_path(s, &dep->hash, &p); \
            if (e) { \
                for (size_t k = 0; k < out_i; k++) { free(names[k]); free(paths[k]); } \
                free(names); free(hashes); free(paths); \
                return e; \
            } \
            names[out_i]  = strdup(dep_name); \
            hashes[out_i] = dep->hash; \
            paths[out_i]  = p; \
            if (!names[out_i]) { \
                free(p); \
                for (size_t k = 0; k < out_i; k++) { free(names[k]); free(paths[k]); } \
                free(names); free(hashes); free(paths); \
                return PPM_ERR_NOMEM; \
            } \
            out_i++; \
        }
    COLLECT(mf->deps,       mf->deps_len)
    COLLECT(mf->build_deps, mf->build_deps_len)
    #undef COLLECT

    *out_names  = names;
    *out_hashes = hashes;
    *out_paths  = paths;
    *out_count  = out_i;
    return PPM_OK;
}

static void free_dep_paths(char **names, ppm_sha512_t *hashes,
                           char **paths, size_t n) {
    for (size_t i = 0; i < n; i++) {
        free(names[i]);
        free(paths[i]);
    }
    free(names);
    free(hashes);
    free(paths);
}

/*
 * Materialise one lockfile entry into the store. If the registry says
 * this package has a [build] section, we build from source and the
 * resulting CAS path is keyed by the derivation hash. Otherwise we
 * treat e->hash as the SHA-512 of a binary tarball and download it.
 *
 * The caller MUST have already materialised every transitive dependency
 * of `e` before this call — see install_in_topo_order() below.
 */
static ppm_error_t materialise_entry(ppm_store_t *s, ppm_registry_t *reg,
                                     const ppm_lockfile_t *lock,
                                     const ppm_lock_entry_t *e) {
    if (ppm_store_has(s, &e->hash)) return PPM_OK;

    ppm_manifest_t *mf = NULL;
    ppm_error_t err = ppm_registry_manifest(reg, e->name, &e->version, &mf);
    if (err) return err;

    if (mf->build.has_build) {
        char         **dnames  = NULL;
        ppm_sha512_t  *dhashes = NULL;
        char         **dpaths  = NULL;
        size_t         dcount  = 0;

        err = collect_dep_paths(s, lock, mf, &dnames, &dhashes, &dpaths, &dcount);
        if (err) { ppm_manifest_free(mf); return err; }

        ppm_sha512_t out_hash;
        err = ppm_build_run(s, mf,
                            (const char *const *)dnames,
                            dhashes,
                            (const char *const *)dpaths,
                            dcount,
                            &out_hash);
        free_dep_paths(dnames, dhashes, dpaths, dcount);
        ppm_manifest_free(mf);
        if (err) return err;
        if (!ppm_sha512_equal(&out_hash, &e->hash)) {
            ppm_error("install: lockfile hash for '%s' does not match build output",
                      e->name);
            return PPM_ERR_HASH_MISMATCH;
        }
        return PPM_OK;
    }
    ppm_manifest_free(mf);

    /* Binary path: download tarball and ingest. */
    char *tarball;
    if (asprintf(&tarball, "%s/tmp/dl-%s.tar.zst",
                 ppm_cmd_store_dir(), e->name) < 0) {
        return PPM_ERR_NOMEM;
    }
    ppm_info("downloading %s %u.%u.%u from %s",
             e->name, e->version.major, e->version.minor, e->version.patch,
             e->source_url);
    err = ppm_http_download(e->source_url, tarball);
    if (err) { free(tarball); return err; }
    err = ppm_store_ingest_tarball(s, tarball, &e->hash);
    unlink(tarball);
    free(tarball);
    return err;
}

/*
 * Walk the lockfile in topological order — every package is
 * materialised only after all its declared runtime dependencies. We
 * re-read each manifest from the registry to know the deps. The
 * lockfile is small (a few dozen entries at most), so an O(n²) DFS
 * is fine.
 */
static ppm_error_t install_in_topo_order(ppm_store_t *s, ppm_registry_t *reg,
                                         const ppm_lockfile_t *lock) {
    if (lock->entries_len == 0) return PPM_OK;

    char *state = calloc(lock->entries_len, 1); /* 0=todo 1=onstack 2=done */
    if (!state) return PPM_ERR_NOMEM;

    /* DFS stack — simple iterative version using indices. */
    size_t *stack = malloc(lock->entries_len * sizeof(*stack));
    if (!stack) { free(state); return PPM_ERR_NOMEM; }

    ppm_error_t err = PPM_OK;
    for (size_t start = 0; start < lock->entries_len; start++) {
        if (state[start] != 0) continue;
        size_t top = 0;
        stack[top++] = start;
        state[start] = 1;

        while (top > 0) {
            size_t cur = stack[top - 1];
            const ppm_lock_entry_t *e = &lock->entries[cur];

            /* Fetch the manifest to discover deps. */
            ppm_manifest_t *mf = NULL;
            err = ppm_registry_manifest(reg, e->name, &e->version, &mf);
            if (err) goto done;

            size_t pushed = 0;
            #define TOPO_WALK(arr, arr_len) \
                for (size_t k = 0; k < (arr_len); k++) { \
                    ssize_t didx = -1; \
                    for (size_t j = 0; j < lock->entries_len; j++) { \
                        if (strcmp(lock->entries[j].name, (arr)[k].name) == 0) { \
                            didx = (ssize_t)j; break; \
                        } \
                    } \
                    if (didx < 0) continue; \
                    if (state[didx] == 1) { \
                        ppm_error("install: cycle detected at '%s' ↔ '%s'", \
                                  e->name, (arr)[k].name); \
                        ppm_manifest_free(mf); \
                        err = PPM_ERR_CONFLICT; \
                        goto done; \
                    } \
                    if (state[didx] == 0) { \
                        stack[top++] = (size_t)didx; \
                        state[didx]  = 1; \
                        pushed++; \
                    } \
                }
            TOPO_WALK(mf->deps,       mf->deps_len)
            TOPO_WALK(mf->build_deps, mf->build_deps_len)
            #undef TOPO_WALK
            ppm_manifest_free(mf);

            if (pushed == 0) {
                /* All deps already done — now materialise this one. */
                err = materialise_entry(s, reg, lock, e);
                if (err) goto done;
                state[cur] = 2;
                top--;
            }
        }
    }

done:
    free(state);
    free(stack);
    return err;
}

int ppm_cmd_install(int argc, char **argv) {
    if (ppm_crypto_init() != PPM_OK) return 1;
    if (ppm_cmd_require_root_or_writable_store() != PPM_OK) return 1;

    if (access("ppm.toml", F_OK) != 0) {
        ppm_info("no ppm.toml found; creating a minimal one");
        FILE *f = fopen("ppm.toml", "w");
        if (!f) { ppm_error("cannot create ppm.toml: %s", strerror(errno)); return 1; }
        fputs("[package]\nname = \"workspace\"\nversion = \"0.0.0\"\n", f);
        fclose(f);
    }

    for (int i = 1; i < argc; i++) {
        const char *name = argv[i];
        const char *eq   = strchr(name, '@');
        char *only_name  = NULL;
        const char *spec = "*";
        if (eq) {
            only_name = strndup(name, (size_t)(eq - name));
            spec = eq + 1;
            name = only_name;
        }
        int added = 0;
        if (ensure_dependency_listed(name, spec, &added) != 0) {
            ppm_error("cannot update ppm.toml");
            free(only_name);
            return 1;
        }
        if (added) ppm_info("added %s = \"%s\" to ppm.toml", name, spec);
        free(only_name);
    }

    ppm_store_t *store = NULL;
    if (ppm_cmd_open_store(&store) != PPM_OK) {
        ppm_error("could not open store");
        return 1;
    }
    ppm_registry_t *reg = NULL;
    if (ppm_cmd_open_registry(&reg) != PPM_OK) {
        ppm_error("could not open registry (set PPM_REGISTRY_URL or clone manually)");
        ppm_store_close(store);
        return 1;
    }

    ppm_manifest_t *root = NULL;
    if (ppm_manifest_load("ppm.toml", &root) != PPM_OK) {
        ppm_registry_close(reg);
        ppm_store_close(store);
        return 1;
    }

    ppm_lockfile_t *lock = NULL;
    char *report = NULL;
    ppm_resolver_opts_t opts = {0};
    ppm_error_t err = ppm_resolve(root, reg, &opts, &lock, &report);
    if (err) {
        if (report) {
            fputs(report, stderr);
            free(report);
        } else {
            ppm_error("resolve failed: %s", ppm_strerror(err));
        }
        ppm_manifest_free(root);
        ppm_registry_close(reg);
        ppm_store_close(store);
        return 1;
    }

    ppm_info("resolved %zu package(s)", lock->entries_len);

    err = install_in_topo_order(store, reg, lock);
    if (err) {
        ppm_error("install: %s", ppm_strerror(err));
        ppm_lockfile_free(lock);
        ppm_manifest_free(root);
        ppm_registry_close(reg);
        ppm_store_close(store);
        return 1;
    }

    ppm_txn_t *txn = NULL;
    if ((err = ppm_txn_begin(store, "default", &txn))) {
        ppm_error("txn begin: %s", ppm_strerror(err));
        ppm_lockfile_free(lock);
        ppm_manifest_free(root);
        ppm_registry_close(reg);
        ppm_store_close(store);
        return 1;
    }
    for (size_t i = 0; i < lock->entries_len; i++) {
        if ((err = ppm_txn_stage(txn, &lock->entries[i]))) {
            ppm_error("txn stage '%s': %s",
                      lock->entries[i].name, ppm_strerror(err));
            ppm_txn_abort(txn);
            ppm_lockfile_free(lock);
            ppm_manifest_free(root);
            ppm_registry_close(reg);
            ppm_store_close(store);
            return 1;
        }
    }
    if ((err = ppm_txn_commit(txn))) {
        ppm_error("txn commit: %s", ppm_strerror(err));
        ppm_txn_abort(txn);
        ppm_lockfile_free(lock);
        ppm_manifest_free(root);
        ppm_registry_close(reg);
        ppm_store_close(store);
        return 1;
    }
    ppm_txn_abort(txn);

    if ((err = ppm_lockfile_save("ppm.lock", lock))) {
        ppm_warn("could not save ppm.lock: %s", ppm_strerror(err));
    }

    ppm_info("installed %zu package(s) into the 'default' profile",
             lock->entries_len);

    ppm_lockfile_free(lock);
    ppm_manifest_free(root);
    ppm_registry_close(reg);
    ppm_store_close(store);
    return 0;
}
