// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/src/core/resolver.c
 *
 *  Dependency resolution under the one-version rule. The algorithm is
 *  intentionally simple and deterministic:
 *
 *    1. Maintain a `selected` map of name -> chosen version + manifest.
 *    2. Maintain a `constraints` map of name -> list of requirement
 *       strings accumulated from the root and from already-selected
 *       packages.
 *    3. Maintain a FIFO `pending` queue of names whose manifest still
 *       needs to be picked.
 *    4. For each pending name:
 *         a. Fetch every published version, newest first.
 *         b. Pick the first one that satisfies every accumulated
 *            constraint for that name. If the name is already selected,
 *            verify it still satisfies the new constraints (else
 *            CONFLICT).
 *         c. Load the published manifest. For each runtime dep, append
 *            its name to `pending` and its req string to `constraints`.
 *    5. When `pending` drains, materialise a deterministic lockfile
 *       (entries sorted by name) and return it.
 *
 *  On failure, `out_report` receives a human-readable explanation:
 *    - which package was being resolved,
 *    - the candidate versions seen,
 *    - the constraints in force.
 *
 *  This is not full PubGrub: there is no conflict-driven backjumping.
 *  In exchange, the implementation is short, deterministic, and easy
 *  to reason about. PubGrub semantics arrive in M5.5.
 *
 *  Copyright (C) 2026 Project Tick
 */

#include <ppm/resolver.h>
#include <ppm/registry.h>
#include <ppm/build.h>

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

#include "../util/log.h"
#include "version_req.h"

/* ---------------------------------------------------------------- helpers */

typedef struct {
    char            *name;
    ppm_version_t    version;
    ppm_manifest_t  *manifest; /* owned */
} selected_t;

typedef struct {
    char  *name;
    char **reqs;
    size_t reqs_len;
    size_t reqs_cap;
} constraints_t;

typedef struct {
    selected_t     *sel;
    size_t          sel_len, sel_cap;
    constraints_t  *con;
    size_t          con_len, con_cap;
    char          **pending;
    size_t          pending_head, pending_tail, pending_cap;
} state_t;

static int ensure_cap(void **p, size_t *cap, size_t need, size_t elem) {
    if (*cap >= need) return 0;
    size_t nc = *cap ? *cap * 2 : 8;
    while (nc < need) nc *= 2;
    void *np = realloc(*p, nc * elem);
    if (!np) return -1;
    *p = np;
    *cap = nc;
    return 0;
}

static selected_t *find_selected(state_t *s, const char *name) {
    for (size_t i = 0; i < s->sel_len; i++)
        if (strcmp(s->sel[i].name, name) == 0) return &s->sel[i];
    return NULL;
}

static constraints_t *find_constraints(state_t *s, const char *name) {
    for (size_t i = 0; i < s->con_len; i++)
        if (strcmp(s->con[i].name, name) == 0) return &s->con[i];
    return NULL;
}

static ppm_error_t add_constraint(state_t *s, const char *name, const char *req) {
    constraints_t *c = find_constraints(s, name);
    if (!c) {
        if (ensure_cap((void **)&s->con, &s->con_cap, s->con_len + 1,
                       sizeof(*s->con)) < 0)
            return PPM_ERR_NOMEM;
        c = &s->con[s->con_len++];
        c->name = strdup(name);
        c->reqs = NULL; c->reqs_len = 0; c->reqs_cap = 0;
        if (!c->name) return PPM_ERR_NOMEM;
    }
    if (ensure_cap((void **)&c->reqs, &c->reqs_cap, c->reqs_len + 1,
                   sizeof(*c->reqs)) < 0)
        return PPM_ERR_NOMEM;
    c->reqs[c->reqs_len] = strdup(req);
    if (!c->reqs[c->reqs_len]) return PPM_ERR_NOMEM;
    c->reqs_len++;
    return PPM_OK;
}

static ppm_error_t enqueue(state_t *s, const char *name) {
    if (ensure_cap((void **)&s->pending, &s->pending_cap, s->pending_tail + 1,
                   sizeof(*s->pending)) < 0)
        return PPM_ERR_NOMEM;
    s->pending[s->pending_tail] = strdup(name);
    if (!s->pending[s->pending_tail]) return PPM_ERR_NOMEM;
    s->pending_tail++;
    return PPM_OK;
}

static int satisfies_all(const constraints_t *c, const ppm_version_t *v) {
    if (!c) return 1;
    for (size_t i = 0; i < c->reqs_len; i++) {
        if (!ppm_version_req_match(c->reqs[i], v)) return 0;
    }
    return 1;
}

static void state_free(state_t *s) {
    for (size_t i = 0; i < s->sel_len; i++) {
        free(s->sel[i].name);
        ppm_manifest_free(s->sel[i].manifest);
    }
    free(s->sel);
    for (size_t i = 0; i < s->con_len; i++) {
        free(s->con[i].name);
        for (size_t j = 0; j < s->con[i].reqs_len; j++) free(s->con[i].reqs[j]);
        free(s->con[i].reqs);
    }
    free(s->con);
    for (size_t i = s->pending_head; i < s->pending_tail; i++)
        free(s->pending[i]);
    free(s->pending);
}

/* Build a textual report listing what we tried and what blocked us. */
static char *make_report(state_t *s, const char *name,
                         const ppm_version_t *seen, size_t seen_len) {
    char *buf = NULL;
    size_t cap = 1024, len = 0;
    buf = malloc(cap);
    if (!buf) return NULL;
    len += (size_t)snprintf(buf + len, cap - len,
        "could not satisfy '%s'\n", name);

    constraints_t *c = find_constraints(s, name);
    if (c && c->reqs_len) {
        len += (size_t)snprintf(buf + len, cap - len, "  constraints:\n");
        for (size_t i = 0; i < c->reqs_len; i++) {
            if (len + 256 > cap) {
                cap *= 2;
                char *nb = realloc(buf, cap);
                if (!nb) { free(buf); return NULL; }
                buf = nb;
            }
            len += (size_t)snprintf(buf + len, cap - len,
                                    "    %s\n", c->reqs[i]);
        }
    }
    if (seen_len) {
        len += (size_t)snprintf(buf + len, cap - len,
            "  candidate versions tried (newest first):\n");
        for (size_t i = 0; i < seen_len; i++) {
            if (len + 64 > cap) {
                cap *= 2;
                char *nb = realloc(buf, cap);
                if (!nb) { free(buf); return NULL; }
                buf = nb;
            }
            len += (size_t)snprintf(buf + len, cap - len,
                                    "    %u.%u.%u\n",
                                    seen[i].major, seen[i].minor, seen[i].patch);
        }
    }
    return buf;
}

/* ------------------------------------------------------------------ main */

ppm_error_t ppm_resolve(const ppm_manifest_t      *root,
                        ppm_registry_t            *registry,
                        const ppm_resolver_opts_t *opts,
                        ppm_lockfile_t           **out_lock,
                        char                     **out_report) {
    (void)opts; /* offline / prefer_newer not used yet */
    if (!root || !registry || !out_lock) return PPM_ERR_INVALID;
    if (out_report) *out_report = NULL;

    state_t s = {0};
    ppm_error_t err = PPM_OK;

    /*
     * Seed with the root's direct dependencies. Both [dependencies]
     * and [build-dependencies] become resolver inputs; we do not
     * distinguish at this layer (the install pipeline can choose to
     * skip build-deps after the build finishes, but the resolver
     * needs them in the lockfile so their store paths are computable
     * deterministically).
     */
    for (size_t i = 0; i < root->deps_len; i++) {
        if ((err = add_constraint(&s, root->deps[i].name, root->deps[i].spec)))
            goto fail;
        if ((err = enqueue(&s, root->deps[i].name))) goto fail;
    }
    for (size_t i = 0; i < root->build_deps_len; i++) {
        if ((err = add_constraint(&s, root->build_deps[i].name,
                                       root->build_deps[i].spec))) goto fail;
        if ((err = enqueue(&s, root->build_deps[i].name))) goto fail;
    }

    while (s.pending_head < s.pending_tail) {
        char *name = s.pending[s.pending_head++];

        /* Already selected? Just verify it still satisfies all constraints. */
        selected_t *prev = find_selected(&s, name);
        if (prev) {
            constraints_t *cc = find_constraints(&s, name);
            if (!satisfies_all(cc, &prev->version)) {
                if (out_report) *out_report = make_report(&s, name, &prev->version, 1);
                err = PPM_ERR_UNSAT;
                free(name);
                goto fail;
            }
            free(name);
            continue;
        }

        ppm_version_t *versions = NULL;
        size_t versions_len = 0;
        err = ppm_registry_versions(registry, name, &versions, &versions_len);
        if (err == PPM_ERR_NOT_FOUND) {
            ppm_error("resolver: package '%s' not found in registry", name);
            if (out_report) *out_report = make_report(&s, name, NULL, 0);
            free(name);
            goto fail;
        }
        if (err) { free(name); goto fail; }
        if (versions_len == 0) {
            ppm_error("resolver: '%s' has no published versions", name);
            if (out_report) *out_report = make_report(&s, name, NULL, 0);
            free(versions);
            free(name);
            err = PPM_ERR_UNSAT;
            goto fail;
        }

        constraints_t *cc = find_constraints(&s, name);
        ppm_manifest_t *chosen_mf = NULL;
        ppm_version_t   chosen_v = {0};
        int picked = 0;
        for (size_t i = 0; i < versions_len; i++) {
            if (!satisfies_all(cc, &versions[i])) continue;
            ppm_manifest_t *mf = NULL;
            if (ppm_registry_manifest(registry, name, &versions[i], &mf) != PPM_OK)
                continue;
            chosen_mf = mf;
            chosen_v  = versions[i];
            picked = 1;
            break;
        }
        if (!picked) {
            if (out_report) *out_report = make_report(&s, name, versions, versions_len);
            free(versions);
            free(name);
            err = PPM_ERR_UNSAT;
            goto fail;
        }

        /* Record the selection. */
        if (ensure_cap((void **)&s.sel, &s.sel_cap, s.sel_len + 1,
                       sizeof(*s.sel)) < 0) {
            ppm_manifest_free(chosen_mf);
            free(versions);
            free(name);
            err = PPM_ERR_NOMEM;
            goto fail;
        }
        s.sel[s.sel_len].name     = name; /* take ownership */
        s.sel[s.sel_len].version  = chosen_v;
        s.sel[s.sel_len].manifest = chosen_mf;
        s.sel_len++;

        /* Push the transitive runtime + build deps. */
        for (size_t i = 0; i < chosen_mf->deps_len; i++) {
            if ((err = add_constraint(&s, chosen_mf->deps[i].name,
                                      chosen_mf->deps[i].spec))) goto fail;
            if (!find_selected(&s, chosen_mf->deps[i].name) &&
                (err = enqueue(&s, chosen_mf->deps[i].name))) goto fail;
        }
        for (size_t i = 0; i < chosen_mf->build_deps_len; i++) {
            if ((err = add_constraint(&s, chosen_mf->build_deps[i].name,
                                      chosen_mf->build_deps[i].spec))) goto fail;
            if (!find_selected(&s, chosen_mf->build_deps[i].name) &&
                (err = enqueue(&s, chosen_mf->build_deps[i].name))) goto fail;
        }

        free(versions);
    }

    /* Sort selections by name for a deterministic lockfile. */
    for (size_t i = 1; i < s.sel_len; i++) {
        for (size_t j = i; j > 0 && strcmp(s.sel[j - 1].name, s.sel[j].name) > 0; j--) {
            selected_t tmp = s.sel[j - 1];
            s.sel[j - 1] = s.sel[j];
            s.sel[j] = tmp;
        }
    }

    /* Materialise the lockfile. */
    ppm_lockfile_t *lock = calloc(1, sizeof(*lock));
    if (!lock) { err = PPM_ERR_NOMEM; goto fail; }
    lock->schema = 1;
    char *commit = NULL;
    if (ppm_registry_head_commit(registry, &commit) == PPM_OK) {
        lock->registry_commit = commit;
    }
    lock->entries_len = s.sel_len;
    lock->entries = calloc(s.sel_len, sizeof(*lock->entries));
    if (s.sel_len && !lock->entries) {
        ppm_lockfile_free(lock);
        err = PPM_ERR_NOMEM;
        goto fail;
    }
    /*
     * First pass: copy name/version/source_url and pre-fill the hash
     * for binary packages (source-tarball SHA-512 is independent of
     * anything else). Derivation hashes depend on their deps' hashes
     * and are filled in by a second pass below.
     */
    for (size_t i = 0; i < s.sel_len; i++) {
        lock->entries[i].name = strdup(s.sel[i].name);
        lock->entries[i].version = s.sel[i].version;
        lock->entries[i].version.pre = NULL;
        lock->entries[i].version.build = NULL;
        lock->entries[i].source_url = strdup(s.sel[i].manifest->source.url);
        if (!lock->entries[i].name || !lock->entries[i].source_url) {
            ppm_lockfile_free(lock);
            err = PPM_ERR_NOMEM;
            goto fail;
        }
        if (!s.sel[i].manifest->build.has_build) {
            lock->entries[i].hash = s.sel[i].manifest->source.hash;
        }
    }

    /*
     * Second pass: derivation hashes, computed in topological order.
     * A derivation's output hash includes the hashes of its runtime
     * dependencies, so we must fill them in deps-first.
     *
     * State: 0=todo, 1=on-stack, 2=done. Iterative DFS over a stack
     * of selected-array indices. Small N → O(n²) lookups are fine.
     */
    char  *state = calloc(s.sel_len, 1);
    size_t *stk  = malloc(s.sel_len * sizeof(*stk));
    if ((s.sel_len && (!state || !stk))) {
        free(state); free(stk);
        ppm_lockfile_free(lock);
        err = PPM_ERR_NOMEM;
        goto fail;
    }

    for (size_t start = 0; start < s.sel_len; start++) {
        if (state[start]) continue;
        size_t top = 0;
        stk[top++] = start;
        state[start] = 1;
        while (top) {
            size_t cur = stk[top - 1];
            const ppm_manifest_t *mf = s.sel[cur].manifest;
            size_t pushed = 0;

            /* Walk [dependencies] + [build-dependencies] uniformly. The
             * derivation hash treats them the same: a change in *any*
             * input (runtime or build-time) invalidates the cache. */
            #define WALK_DEPS(arr, arr_len) \
                for (size_t k = 0; k < (arr_len); k++) { \
                    ssize_t di = -1; \
                    for (size_t j = 0; j < s.sel_len; j++) { \
                        if (strcmp(s.sel[j].name, (arr)[k].name) == 0) { \
                            di = (ssize_t)j; break; \
                        } \
                    } \
                    if (di < 0) continue; \
                    if (state[di] == 1) { \
                        ppm_error("resolver: cycle at '%s' ↔ '%s'", \
                                  s.sel[cur].name, (arr)[k].name); \
                        free(state); free(stk); \
                        ppm_lockfile_free(lock); \
                        err = PPM_ERR_CONFLICT; \
                        goto fail; \
                    } \
                    if (state[di] == 0) { \
                        stk[top++] = (size_t)di; \
                        state[di]  = 1; \
                        pushed++; \
                    } \
                }
            WALK_DEPS(mf->deps,       mf->deps_len)
            WALK_DEPS(mf->build_deps, mf->build_deps_len)
            #undef WALK_DEPS

            if (pushed == 0) {
                /* All deps done. Compute this entry's hash. */
                if (mf->build.has_build) {
                    size_t n = mf->deps_len + mf->build_deps_len;
                    char **dnames = NULL;
                    ppm_sha512_t *dhashes = NULL;
                    if (n) {
                        dnames  = calloc(n, sizeof(*dnames));
                        dhashes = calloc(n, sizeof(*dhashes));
                        if (!dnames || !dhashes) {
                            free(dnames); free(dhashes);
                            free(state); free(stk);
                            ppm_lockfile_free(lock);
                            err = PPM_ERR_NOMEM;
                            goto fail;
                        }
                        size_t w = 0;
                        for (size_t k = 0; k < mf->deps_len; k++, w++) {
                            const char *dn = mf->deps[k].name;
                            dnames[w] = (char *)dn;
                            for (size_t j = 0; j < s.sel_len; j++) {
                                if (strcmp(s.sel[j].name, dn) == 0) {
                                    dhashes[w] = lock->entries[j].hash;
                                    break;
                                }
                            }
                        }
                        for (size_t k = 0; k < mf->build_deps_len; k++, w++) {
                            const char *dn = mf->build_deps[k].name;
                            dnames[w] = (char *)dn;
                            for (size_t j = 0; j < s.sel_len; j++) {
                                if (strcmp(s.sel[j].name, dn) == 0) {
                                    dhashes[w] = lock->entries[j].hash;
                                    break;
                                }
                            }
                        }
                    }
                    err = ppm_build_output_hash(mf,
                                                (const char *const *)dnames,
                                                dhashes, n,
                                                &lock->entries[cur].hash);
                    free(dnames); free(dhashes);
                    if (err) {
                        free(state); free(stk);
                        ppm_lockfile_free(lock);
                        goto fail;
                    }
                }
                state[cur] = 2;
                top--;
            }
        }
    }
    free(state);
    free(stk);
    *out_lock = lock;
    state_free(&s);
    return PPM_OK;

fail:
    state_free(&s);
    return err;
}
