// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/src/core/store.c
 *
 *  Content-addressed store. Every artifact lives at
 *  <root>/sha512/<hh>/<full-hex>/, where <hh> is the first two hex chars
 *  of the SHA-512 digest. Tarballs land first in <root>/tmp/<rand>/ and
 *  are then renamed into their final home; rename(2) on the same
 *  filesystem is atomic, so the store never has a partially-extracted
 *  object visible under its canonical path.
 *
 *  Copyright (C) 2026 Project Tick
 */

#include <ppm/store.h>
#include <ppm/archive.h>

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

#include <sodium.h>

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

struct ppm_store {
    char *root;
};

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

static ppm_error_t ensure_layout(const char *root) {
    /* root/, root/sha512/, root/tmp/, root/profiles/ */
    ppm_error_t err;
    if ((err = ppm_path_mkdirs(root, 0755))) return err;
    char *sub;
    if ((err = ppm_path_join(root, "sha512", &sub))) return err;
    err = ppm_path_mkdirs(sub, 0755); free(sub); if (err) return err;
    if ((err = ppm_path_join(root, "tmp", &sub))) return err;
    err = ppm_path_mkdirs(sub, 0755); free(sub); if (err) return err;
    if ((err = ppm_path_join(root, "profiles", &sub))) return err;
    err = ppm_path_mkdirs(sub, 0755); free(sub);
    return err;
}

static ppm_error_t object_dir(const ppm_store_t *s, const ppm_sha512_t *h,
                              char **out_path) {
    char hex[PPM_SHA512_HEX_BUFSZ];
    ppm_sha512_to_hex(h, hex);
    char *p = NULL;
    if (asprintf(&p, "%s/sha512/%c%c/%s", s->root, hex[0], hex[1], hex) < 0)
        return PPM_ERR_NOMEM;
    *out_path = p;
    return PPM_OK;
}

static ppm_error_t object_parent_dir(const ppm_store_t *s,
                                     const ppm_sha512_t *h, char **out_path) {
    char hex[PPM_SHA512_HEX_BUFSZ];
    ppm_sha512_to_hex(h, hex);
    char *p = NULL;
    if (asprintf(&p, "%s/sha512/%c%c", s->root, hex[0], hex[1]) < 0)
        return PPM_ERR_NOMEM;
    *out_path = p;
    return PPM_OK;
}

static ppm_error_t make_tmpdir(const ppm_store_t *s, char **out) {
    /* 16 hex chars of randomness is plenty for staging dir uniqueness. */
    unsigned char raw[8];
    randombytes_buf(raw, sizeof(raw));
    char tag[17];
    sodium_bin2hex(tag, sizeof(tag), raw, sizeof(raw));
    char *p = NULL;
    if (asprintf(&p, "%s/tmp/stage.%s", s->root, tag) < 0) return PPM_ERR_NOMEM;
    if (mkdir(p, 0755) != 0) {
        free(p);
        return PPM_ERR_IO;
    }
    *out = p;
    return PPM_OK;
}

/* -------------------------------------------------------------- public API */

ppm_error_t ppm_store_open(const char *root, ppm_store_t **out) {
    if (!root || !out) return PPM_ERR_INVALID;
    ppm_error_t err = ensure_layout(root);
    if (err) return err;
    ppm_store_t *s = calloc(1, sizeof(*s));
    if (!s) return PPM_ERR_NOMEM;
    s->root = strdup(root);
    if (!s->root) { free(s); return PPM_ERR_NOMEM; }
    *out = s;
    return PPM_OK;
}

void ppm_store_close(ppm_store_t *store) {
    if (!store) return;
    free(store->root);
    free(store);
}

bool ppm_store_has(const ppm_store_t *store, const ppm_sha512_t *hash) {
    if (!store || !hash) return false;
    char *path = NULL;
    if (object_dir(store, hash, &path) != PPM_OK) return false;
    int ok = ppm_path_is_dir(path);
    free(path);
    return ok != 0;
}

ppm_error_t ppm_store_path(const ppm_store_t *store,
                           const ppm_sha512_t *hash,
                           char **out_path) {
    if (!store || !hash || !out_path) return PPM_ERR_INVALID;
    return object_dir(store, hash, out_path);
}

ppm_error_t ppm_store_ingest_tarball(ppm_store_t        *store,
                                     const char         *tarball_path,
                                     const ppm_sha512_t *expected) {
    if (!store || !tarball_path || !expected) return PPM_ERR_INVALID;

    /* 1. Hash the tarball and compare against the expected digest. */
    ppm_sha512_t actual;
    ppm_error_t err = ppm_sha512_file(tarball_path, &actual);
    if (err) {
        ppm_error("store: cannot hash '%s': %s",
                  tarball_path, ppm_strerror(err));
        return err;
    }
    if (!ppm_sha512_equal(&actual, expected)) {
        char want[PPM_SHA512_HEX_BUFSZ], got[PPM_SHA512_HEX_BUFSZ];
        ppm_sha512_to_hex(expected, want);
        ppm_sha512_to_hex(&actual,   got);
        ppm_error("store: hash mismatch for '%s'\n  expected: %s\n  got:      %s",
                  tarball_path, want, got);
        return PPM_ERR_HASH_MISMATCH;
    }

    /* Fast path: already in the store. */
    if (ppm_store_has(store, expected)) return PPM_OK;

    /* 2. Make sure the bucket parent (sha512/<aa>/) exists. */
    char *parent = NULL;
    if ((err = object_parent_dir(store, expected, &parent))) return err;
    if ((err = ppm_path_mkdirs(parent, 0755))) { free(parent); return err; }

    /* 3. Stage extract into tmp/stage.<rand>/payload/. */
    char *stage = NULL;
    if ((err = make_tmpdir(store, &stage))) { free(parent); return err; }

    char *payload = NULL;
    if ((err = ppm_path_join(stage, "payload", &payload))) goto cleanup_stage;
    if (mkdir(payload, 0755) != 0) { err = PPM_ERR_IO; goto cleanup_payload; }

    if ((err = ppm_archive_extract(tarball_path, payload))) goto cleanup_payload;

    /* 4. fsync the staging dir then atomically rename into place. */
    if ((err = ppm_path_fsync_dir(payload))) goto cleanup_payload;

    char *final_path = NULL;
    if ((err = object_dir(store, expected, &final_path))) goto cleanup_payload;

    if (rename(payload, final_path) != 0) {
        if (errno == EEXIST || errno == ENOTEMPTY) {
            /* Lost a race against another ppm. Use theirs, drop ours. */
            err = PPM_OK;
        } else {
            ppm_error("store: rename(%s -> %s) failed: %s",
                      payload, final_path, strerror(errno));
            err = PPM_ERR_IO;
        }
        free(final_path);
        goto cleanup_payload;
    }
    free(final_path);

    /* 5. Best-effort fsync of the bucket so the new entry is durable. */
    ppm_path_fsync_dir(parent);

cleanup_payload:
    free(payload);
cleanup_stage:
    ppm_path_rmrf(stage); /* nukes anything left behind */
    free(stage);
    free(parent);
    return err;
}

ppm_error_t ppm_store_ingest_dir(ppm_store_t        *store,
                                 const char         *dir_path,
                                 const ppm_sha512_t *hash) {
    if (!store || !dir_path || !hash) return PPM_ERR_INVALID;

    /* Fast path: someone else won the race. */
    if (ppm_store_has(store, hash)) {
        ppm_path_rmrf(dir_path);
        return PPM_OK;
    }

    char *parent;
    ppm_error_t err = object_parent_dir(store, hash, &parent);
    if (err) return err;
    if ((err = ppm_path_mkdirs(parent, 0755))) { free(parent); return err; }

    char *final_path;
    if ((err = object_dir(store, hash, &final_path))) { free(parent); return err; }

    /* fsync the source dir so its contents are durable before we
     * publish it under a stable name. */
    ppm_path_fsync_dir(dir_path);

    if (rename(dir_path, final_path) != 0) {
        if (errno == EEXIST || errno == ENOTEMPTY) {
            /* Race-loss: another ppm published the same hash. Drop ours. */
            ppm_path_rmrf(dir_path);
            err = PPM_OK;
        } else {
            ppm_error("store: rename(%s -> %s) failed: %s",
                      dir_path, final_path, strerror(errno));
            err = PPM_ERR_IO;
        }
    } else {
        ppm_path_fsync_dir(parent);
    }
    free(final_path);
    free(parent);
    return err;
}

ppm_error_t ppm_store_verify(ppm_store_t *store, int *out_bad_count) {
    if (!store || !out_bad_count) return PPM_ERR_INVALID;
    *out_bad_count = 0;

    /*
     * Walking sha512/<aa>/<full>/ and re-hashing every object requires us
     * to define a canonical tarball-from-tree representation, which we
     * have not nailed down yet. For now, verify only that:
     *   - every entry in sha512/<aa>/ has a 128-char hex directory name,
     *   - that name corresponds to a valid SHA-512 hex string.
     *
     * Full re-hashing arrives once M3 (transactions) records the canonical
     * tarball hash alongside the unpacked tree.
     */
    char *base = NULL;
    ppm_error_t err = ppm_path_join(store->root, "sha512", &base);
    if (err) return err;

    DIR *d = opendir(base);
    if (!d) { free(base); return PPM_OK; } /* empty store is fine */

    struct dirent *de;
    while ((de = readdir(d)) != NULL) {
        if (de->d_name[0] == '.') continue;
        char *bucket;
        if (ppm_path_join(base, de->d_name, &bucket) != PPM_OK) continue;
        DIR *bd = opendir(bucket);
        free(bucket);
        if (!bd) continue;
        struct dirent *be;
        while ((be = readdir(bd)) != NULL) {
            if (be->d_name[0] == '.') continue;
            if (strlen(be->d_name) != PPM_SHA512_HEX_BYTES) {
                (*out_bad_count)++;
                continue;
            }
            ppm_sha512_t parsed;
            if (ppm_sha512_from_hex(be->d_name, &parsed) != PPM_OK)
                (*out_bad_count)++;
        }
        closedir(bd);
    }
    closedir(d);
    free(base);
    return PPM_OK;
}
