// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/src/core/lockfile.c
 *
 *  ppm.lock format (TOML, schema 1). Entries are emitted as numbered
 *  sub-tables to stay within our minimal TOML subset:
 *
 *      schema          = "1"
 *      registry_commit = "<sha1-hex>"
 *
 *      [package.0]
 *      name       = "hello"
 *      version    = "1.0.0"
 *      hash       = "sha512:..."
 *      source_url = "https://..."
 *
 *  Copyright (C) 2026 Project Tick
 */

#include <ppm/lockfile.h>

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

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

static ppm_error_t parse_hash(const char *s, ppm_sha512_t *out) {
    static const char prefix[] = "sha512:";
    if (!s || strncmp(s, prefix, sizeof(prefix) - 1) != 0) return PPM_ERR_PARSE;
    return ppm_sha512_from_hex(s + sizeof(prefix) - 1, out);
}

static ppm_error_t parse_one_package(const ppm_toml_node_t *tbl,
                                     ppm_lock_entry_t *out) {
    const char *name    = ppm_toml_get_str(tbl, "name");
    const char *version = ppm_toml_get_str(tbl, "version");
    const char *hash    = ppm_toml_get_str(tbl, "hash");
    const char *url     = ppm_toml_get_str(tbl, "source_url");
    if (!name || !version || !hash || !url) {
        ppm_error("lockfile: package entry missing required fields");
        return PPM_ERR_PARSE;
    }
    ppm_error_t err = ppm_version_parse(version, &out->version);
    if (err) return err;
    err = parse_hash(hash, &out->hash);
    if (err) { ppm_version_dispose(&out->version); return err; }
    out->name = strdup(name);
    out->source_url = strdup(url);
    if (!out->name || !out->source_url) {
        ppm_version_dispose(&out->version);
        free(out->name);
        free(out->source_url);
        return PPM_ERR_NOMEM;
    }
    return PPM_OK;
}

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

    FILE *f = fopen(path, "rb");
    if (!f) return (errno == ENOENT) ? PPM_ERR_NOT_FOUND : 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_toml_node_t *root = NULL;
    char *terr = NULL;
    ppm_error_t err = ppm_toml_parse(buf, &root, &terr);
    free(buf);
    if (err) {
        ppm_error("lockfile: %s", terr ? terr : ppm_strerror(err));
        free(terr);
        return err;
    }

    ppm_lockfile_t *lock = calloc(1, sizeof(*lock));
    if (!lock) { ppm_toml_free(root); return PPM_ERR_NOMEM; }
    lock->schema = 1;

    const char *schema = ppm_toml_get_str(root, "schema");
    if (!schema || strcmp(schema, "1") != 0) {
        ppm_error("lockfile: unsupported schema '%s'", schema ? schema : "(missing)");
        ppm_toml_free(root);
        free(lock);
        return PPM_ERR_PARSE;
    }
    const char *commit = ppm_toml_get_str(root, "registry_commit");
    if (commit) lock->registry_commit = strdup(commit);

    const ppm_toml_node_t *pkg_root = ppm_toml_get_table(root, "package");
    size_t count = 0;
    if (pkg_root) {
        for (size_t i = 0; i < pkg_root->u.tbl.len; i++) {
            if (pkg_root->u.tbl.pairs[i].value->kind == PPM_TOML_TABLE) count++;
        }
    }

    if (count) {
        lock->entries = calloc(count, sizeof(*lock->entries));
        if (!lock->entries) {
            ppm_lockfile_free(lock);
            ppm_toml_free(root);
            return PPM_ERR_NOMEM;
        }
        size_t out_i = 0;
        for (size_t i = 0; i < pkg_root->u.tbl.len; i++) {
            const ppm_toml_node_t *v = pkg_root->u.tbl.pairs[i].value;
            if (v->kind != PPM_TOML_TABLE) continue;
            err = parse_one_package(v, &lock->entries[out_i]);
            if (err) {
                lock->entries_len = out_i;
                ppm_lockfile_free(lock);
                ppm_toml_free(root);
                return err;
            }
            out_i++;
        }
        lock->entries_len = out_i;
    }

    ppm_toml_free(root);
    *out = lock;
    return PPM_OK;
}

ppm_error_t ppm_lockfile_save(const char *path, const ppm_lockfile_t *lock) {
    if (!path || !lock) return PPM_ERR_INVALID;

    char *tmp;
    if (asprintf(&tmp, "%s.tmp", path) < 0) return PPM_ERR_NOMEM;

    FILE *f = fopen(tmp, "w");
    if (!f) { free(tmp); return PPM_ERR_IO; }

    fputs("# ppm.lock — generated, do not edit by hand.\n", f);
    fputs("schema = \"1\"\n", f);
    if (lock->registry_commit && *lock->registry_commit) {
        fprintf(f, "registry_commit = \"%s\"\n", lock->registry_commit);
    }
    fputc('\n', f);

    for (size_t i = 0; i < lock->entries_len; i++) {
        const ppm_lock_entry_t *e = &lock->entries[i];
        char hex[PPM_SHA512_HEX_BUFSZ];
        ppm_sha512_to_hex(&e->hash, hex);
        fprintf(f, "[package.%zu]\n", i);
        fprintf(f, "name       = \"%s\"\n", e->name);
        fprintf(f, "version    = \"%u.%u.%u\"\n",
                e->version.major, e->version.minor, e->version.patch);
        fprintf(f, "hash       = \"sha512:%s\"\n", hex);
        fprintf(f, "source_url = \"%s\"\n\n", e->source_url);
    }

    if (fflush(f) != 0) { fclose(f); unlink(tmp); free(tmp); return PPM_ERR_IO; }
    int fd = fileno(f);
    if (fd >= 0) (void)ppm_path_fdatasync(fd);
    fclose(f);

    if (rename(tmp, path) != 0) {
        unlink(tmp);
        free(tmp);
        return PPM_ERR_IO;
    }
    free(tmp);
    return PPM_OK;
}

void ppm_lockfile_free(ppm_lockfile_t *lock) {
    if (!lock) return;
    free(lock->registry_commit);
    for (size_t i = 0; i < lock->entries_len; i++) {
        free(lock->entries[i].name);
        free(lock->entries[i].source_url);
        ppm_version_dispose(&lock->entries[i].version);
        if (lock->entries[i].deps) {
            for (char **p = lock->entries[i].deps; *p; ++p) free(*p);
            free(lock->entries[i].deps);
        }
    }
    free(lock->entries);
    free(lock);
}
