// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/src/core/manifest.c
 *
 *  Copyright (C) 2026 Project Tick
 */

#include <ppm/manifest.h>

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

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

/*
 * Minimal SemVer 2.0.0 parser. Accepts MAJOR.MINOR.PATCH with optional
 * "-PRE" and "+BUILD" tails. The full grammar will be enforced when the
 * resolver lands; for now we accept enough to make tests meaningful.
 */
ppm_error_t ppm_version_parse(const char *s, ppm_version_t *out) {
    if (!s || !out) return PPM_ERR_INVALID;

    *out = (ppm_version_t){0};

    char *end = NULL;
    errno = 0;
    unsigned long maj = strtoul(s, &end, 10);
    if (errno || end == s || *end != '.') return PPM_ERR_PARSE;
    s = end + 1;

    unsigned long min = strtoul(s, &end, 10);
    if (errno || end == s || *end != '.') return PPM_ERR_PARSE;
    s = end + 1;

    unsigned long pat = strtoul(s, &end, 10);
    if (errno || end == s) return PPM_ERR_PARSE;
    s = end;

    out->major = (uint32_t)maj;
    out->minor = (uint32_t)min;
    out->patch = (uint32_t)pat;

    if (*s == '-') {
        const char *start = ++s;
        while (*s && *s != '+') s++;
        out->pre = strndup(start, (size_t)(s - start));
        if (!out->pre) return PPM_ERR_NOMEM;
    }
    if (*s == '+') {
        out->build = strdup(s + 1);
        if (!out->build) return PPM_ERR_NOMEM;
    }
    return PPM_OK;
}

void ppm_version_dispose(ppm_version_t *v) {
    if (!v) return;
    free(v->pre);
    free(v->build);
    v->pre = NULL;
    v->build = NULL;
}

int ppm_version_cmp(const ppm_version_t *a, const ppm_version_t *b) {
    if (a->major != b->major) return (a->major < b->major) ? -1 : 1;
    if (a->minor != b->minor) return (a->minor < b->minor) ? -1 : 1;
    if (a->patch != b->patch) return (a->patch < b->patch) ? -1 : 1;

    /* A version without prerelease outranks one with prerelease. */
    if (!a->pre && b->pre) return 1;
    if (a->pre && !b->pre) return -1;
    if (!a->pre && !b->pre) return 0;
    return strcmp(a->pre, b->pre);
}

ppm_error_t ppm_manifest_validate(const ppm_manifest_t *m,
                                  ppm_manifest_mode_t mode) {
    if (!m) return PPM_ERR_INVALID;
    if (!m->name || !*m->name) {
        ppm_error("manifest: [package].name is empty");
        return PPM_ERR_PARSE;
    }
    if (!isalnum((unsigned char)m->name[0])) {
        ppm_error("manifest: name must start with a letter or digit (got '%s')",
                  m->name);
        return PPM_ERR_PARSE;
    }
    for (const char *p = m->name; *p; ++p) {
        if (!isalnum((unsigned char)*p) && *p != '-' && *p != '_' && *p != '.') {
            ppm_error("manifest: invalid character '%c' in name '%s'", *p, m->name);
            return PPM_ERR_PARSE;
        }
    }
    if (m->version.major == 0 && m->version.minor == 0 && m->version.patch == 0
        && !m->version.pre) {
        /* 0.0.0 without prerelease tag is suspicious; warn but accept. */
        ppm_warn("manifest: version 0.0.0 is unusual; consider 0.1.0");
    }
    if (mode == PPM_MANIFEST_PUBLISHED) {
        if (!m->source.url || !*m->source.url) {
            ppm_error("manifest: published manifests require [source].url");
            return PPM_ERR_PARSE;
        }
        int hash_set = 0;
        for (size_t i = 0; i < PPM_SHA512_BYTES; i++) {
            if (m->source.hash.bytes[i]) { hash_set = 1; break; }
        }
        if (!hash_set) {
            ppm_error("manifest: published manifests require [source].hash");
            return PPM_ERR_PARSE;
        }
    }
    return PPM_OK;
}

void ppm_manifest_free(ppm_manifest_t *m) {
    if (!m) return;
    free(m->name);
    ppm_version_dispose(&m->version);
    free(m->build.system);
    free(m->build.script);
    free(m->summary);
    free(m->license);
    free(m->homepage);
    if (m->authors) {
        for (char **p = m->authors; *p; ++p) free(*p);
        free(m->authors);
    }
    free(m->source.url);
    free(m->source.sig_url);
    for (size_t i = 0; i < m->deps_len; i++) {
        free(m->deps[i].name);
        free(m->deps[i].spec);
    }
    free(m->deps);
    for (size_t i = 0; i < m->build_deps_len; i++) {
        free(m->build_deps[i].name);
        free(m->build_deps[i].spec);
    }
    free(m->build_deps);
    free(m);
}

/* -------------------- TOML → manifest mapping ----------------------------- */

static ppm_error_t dup_str(const char *src, char **out) {
    if (!src) { *out = NULL; return PPM_OK; }
    char *s = strdup(src);
    if (!s) return PPM_ERR_NOMEM;
    *out = s;
    return PPM_OK;
}

/* "sha512:..." -> 64 raw bytes. Anything else is rejected. */
static ppm_error_t parse_source_hash(const char *spec, ppm_sha512_t *out) {
    if (!spec) return PPM_ERR_PARSE;
    static const char prefix[] = "sha512:";
    if (strncmp(spec, prefix, sizeof(prefix) - 1) != 0) {
        ppm_error("manifest: source.hash must start with \"sha512:\"");
        return PPM_ERR_PARSE;
    }
    return ppm_sha512_from_hex(spec + sizeof(prefix) - 1, out);
}

static ppm_error_t parse_dep_table(const ppm_toml_node_t *tbl,
                                   ppm_req_t **out, size_t *out_len) {
    *out = NULL;
    *out_len = 0;
    if (!tbl) return PPM_OK;
    if (tbl->kind != PPM_TOML_TABLE) return PPM_ERR_PARSE;
    if (tbl->u.tbl.len == 0) return PPM_OK;

    ppm_req_t *deps = calloc(tbl->u.tbl.len, sizeof(*deps));
    if (!deps) return PPM_ERR_NOMEM;

    for (size_t i = 0; i < tbl->u.tbl.len; i++) {
        const ppm_toml_pair_t *pair = &tbl->u.tbl.pairs[i];
        if (pair->value->kind != PPM_TOML_STRING) {
            ppm_error("manifest: dependency '%s' must be a version string",
                      pair->key);
            for (size_t j = 0; j < i; j++) {
                free(deps[j].name);
                free(deps[j].spec);
            }
            free(deps);
            return PPM_ERR_PARSE;
        }
        deps[i].name = strdup(pair->key);
        deps[i].spec = strdup(pair->value->u.str);
        if (!deps[i].name || !deps[i].spec) {
            for (size_t j = 0; j <= i; j++) {
                free(deps[j].name);
                free(deps[j].spec);
            }
            free(deps);
            return PPM_ERR_NOMEM;
        }
    }
    *out = deps;
    *out_len = tbl->u.tbl.len;
    return PPM_OK;
}

static ppm_error_t parse_authors(const ppm_toml_node_t *arr, char ***out) {
    *out = NULL;
    if (!arr) return PPM_OK;
    if (arr->kind != PPM_TOML_ARRAY) return PPM_ERR_PARSE;

    char **authors = calloc(arr->u.arr.len + 1, sizeof(char *));
    if (!authors) return PPM_ERR_NOMEM;

    for (size_t i = 0; i < arr->u.arr.len; i++) {
        if (arr->u.arr.items[i]->kind != PPM_TOML_STRING) {
            for (size_t j = 0; j < i; j++) free(authors[j]);
            free(authors);
            return PPM_ERR_PARSE;
        }
        authors[i] = strdup(arr->u.arr.items[i]->u.str);
        if (!authors[i]) {
            for (size_t j = 0; j < i; j++) free(authors[j]);
            free(authors);
            return PPM_ERR_NOMEM;
        }
    }
    authors[arr->u.arr.len] = NULL;
    *out = authors;
    return PPM_OK;
}

static ppm_error_t build_from_doc(const ppm_toml_node_t *root,
                                  ppm_manifest_t **out) {
    const ppm_toml_node_t *pkg = ppm_toml_get_table(root, "package");
    if (!pkg) {
        ppm_error("manifest: missing required [package] section");
        return PPM_ERR_PARSE;
    }

    ppm_manifest_t *m = calloc(1, sizeof(*m));
    if (!m) return PPM_ERR_NOMEM;

    ppm_error_t err;
    const char *name = ppm_toml_get_str(pkg, "name");
    if (!name) {
        ppm_error("manifest: [package].name is required");
        err = PPM_ERR_PARSE;
        goto fail;
    }
    if ((err = dup_str(name, &m->name))) goto fail;

    const char *ver = ppm_toml_get_str(pkg, "version");
    if (!ver) {
        ppm_error("manifest: [package].version is required");
        err = PPM_ERR_PARSE;
        goto fail;
    }
    if ((err = ppm_version_parse(ver, &m->version))) {
        ppm_error("manifest: [package].version '%s' is not valid SemVer", ver);
        goto fail;
    }

    if ((err = dup_str(ppm_toml_get_str(pkg, "summary"),  &m->summary)))  goto fail;
    if ((err = dup_str(ppm_toml_get_str(pkg, "license"),  &m->license)))  goto fail;
    if ((err = dup_str(ppm_toml_get_str(pkg, "homepage"), &m->homepage))) goto fail;
    if ((err = parse_authors(ppm_toml_get_array(pkg, "authors"), &m->authors)))
        goto fail;

    const ppm_toml_node_t *src = ppm_toml_get_table(root, "source");
    if (src) {
        const char *url = ppm_toml_get_str(src, "url");
        if ((err = dup_str(url, &m->source.url))) goto fail;
        const char *sig = ppm_toml_get_str(src, "sig_url");
        if ((err = dup_str(sig, &m->source.sig_url))) goto fail;
        const char *hash = ppm_toml_get_str(src, "hash");
        if (hash) {
            if ((err = parse_source_hash(hash, &m->source.hash))) goto fail;
        }
    }

    /* Optional [build] section — turns this manifest into a derivation. */
    const ppm_toml_node_t *bld = ppm_toml_get_table(root, "build");
    if (bld) {
        const char *script = ppm_toml_get_str(bld, "script");
        if (!script || !*script) {
            ppm_error("manifest: [build] section requires a non-empty 'script'");
            err = PPM_ERR_PARSE;
            goto fail;
        }
        if ((err = dup_str(script, &m->build.script))) goto fail;
        const char *sys = ppm_toml_get_str(bld, "system");
        if ((err = dup_str(sys, &m->build.system))) goto fail;
        m->build.has_build = 1;
    }

    if ((err = parse_dep_table(ppm_toml_get_table(root, "dependencies"),
                               &m->deps, &m->deps_len))) goto fail;
    if ((err = parse_dep_table(ppm_toml_get_table(root, "build-dependencies"),
                               &m->build_deps, &m->build_deps_len))) goto fail;

    *out = m;
    return PPM_OK;

fail:
    ppm_manifest_free(m);
    return err;
}

ppm_error_t ppm_manifest_parse(const char *buf, size_t len, ppm_manifest_t **out) {
    if (!buf || !out) return PPM_ERR_INVALID;

    /* The parser wants a NUL-terminated string. Copy if not already terminated. */
    char *owned = NULL;
    const char *src = buf;
    if (len > 0 && buf[len - 1] != '\0') {
        owned = strndup(buf, len);
        if (!owned) return PPM_ERR_NOMEM;
        src = owned;
    } else if (len == 0) {
        src = "";
    }

    ppm_toml_node_t *root = NULL;
    char *err_msg = NULL;
    ppm_error_t err = ppm_toml_parse(src, &root, &err_msg);
    free(owned);
    if (err) {
        ppm_error("manifest: %s", err_msg ? err_msg : ppm_strerror(err));
        free(err_msg);
        return err;
    }

    err = build_from_doc(root, out);
    ppm_toml_free(root);
    return err;
}

ppm_error_t ppm_manifest_load(const char *path, ppm_manifest_t **out) {
    if (!path || !out) return PPM_ERR_INVALID;

    FILE *f = fopen(path, "rb");
    if (!f) {
        ppm_error("manifest: cannot open '%s': %s", path, strerror(errno));
        return PPM_ERR_IO;
    }
    if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return PPM_ERR_IO; }
    long sz = ftell(f);
    if (sz < 0) { fclose(f); return PPM_ERR_IO; }
    if (fseek(f, 0, SEEK_SET) != 0) { fclose(f); return PPM_ERR_IO; }

    char *buf = malloc((size_t)sz + 1);
    if (!buf) { fclose(f); return PPM_ERR_NOMEM; }
    size_t n = fread(buf, 1, (size_t)sz, f);
    fclose(f);
    if (n != (size_t)sz) { free(buf); return PPM_ERR_IO; }
    buf[n] = '\0';

    ppm_error_t err = ppm_manifest_parse(buf, n, out);
    free(buf);
    return err;
}
