// =====================================================================
// manifest.sx — the v0 publish manifest: typed model + parse + validate
// (subplan 03, "Manifest Shape").
//
// A manifest (`examples/dist.json`) describes ONE publish: an app slug, a
// version, a target channel, and a list of artifacts (one binary per
// platform). It is parsed via `std.json` and walked into the typed
// `Manifest` below — every malformed / missing / wrong-type field surfaces
// as a machine-readable `ManifestErr`, NEVER a silent default.
//
// PATH RESOLUTION: an artifact `path` is interpreted RELATIVE TO THE
// DIRECTORY CONTAINING THE MANIFEST FILE. `load_manifest` resolves each
// path against `dirname(manifest_path)` before the on-disk existence check,
// so a manifest at `examples/dist.json` with `"path": "fixtures/x.apk"`
// references `examples/fixtures/x.apk`. `validate_manifest` takes that base
// directory explicitly so the resolution rule is visible at the call site.
//
// OWNERSHIP / LIFETIME (long-lived-container rule): `parse_manifest` COPIES
// every string field into the caller-provided `alloc` (via `dup_str`), so a
// returned `Manifest` — and its artifact `List` — is self-contained and
// outlives both the source bytes and the transient `std.json` value tree.
// The caller owns `alloc` (typically an Arena) and frees the whole manifest
// by dropping it.
// =====================================================================

#import "modules/std.sx";
#import "modules/std/json.sx";
// Also reached through an alias so the json reader is called as `jsonp.parse`
// — a bare `parse` would bind to `std.cli`'s `parse` once both modules share
// one program (the `dist` CLI), which returns a different type.
jsonp :: #import "modules/std/json.sx";
#import "modules/std/fs.sx";
#import "../domain/platform.sx";
#import "../domain/validate.sx";

// Typed manifest failures. One distinct tag per failure class so a caller
// (and, later, the CLI) can report a precise reason instead of a bare bool.
//   BadJson         — the bytes were not valid JSON at all.
//   WrongType       — a field was present but the wrong JSON type.
//   MissingField    — a required field was absent or an empty string.
//   UnknownPlatform — an artifact `platform` id did not name a Platform.
//   MissingArtifact — an artifact `path` did not exist on disk.
//   Io              — the manifest file itself could not be read.
ManifestErr :: error {
    BadJson,
    WrongType,
    MissingField,
    UnknownPlatform,
    MissingArtifact,
    Io,
}

// One artifact entry. `platform` + `path` are required; `filename`,
// `content_type`, and `metadata` are optional overrides that default to ""
// when absent. `metadata` is an opaque string mirroring Artifact.metadata.
//
// `size` / `sha256` are OPTIONAL declared expectations the publisher checks
// the on-disk file against: present -> that value IS the expectation;
// absent (`size == -1` / `sha256 == ""`) -> the publisher derives the
// expectation from the stored object, so a no-declaration manifest still
// validates trivially.
ManifestArtifact :: struct {
    platform: Platform;
    path: string;
    filename: string;
    content_type: string;
    metadata: string;
    size: i64 = -1;
    sha256: string = "";
}

// A single publish: which app/version/channel, and the artifacts that make
// it up. Strings are owned by the allocator passed to `parse_manifest`.
Manifest :: struct {
    app: string;
    version: string;
    channel: string;
    artifacts: List(ManifestArtifact);
}

// ── value-tree helpers (strict; copy strings into `alloc`) ────────────

// Copy `s` into `alloc`-owned, null-terminated storage so the manifest
// survives the source bytes / parse scratch being dropped.
dup_str :: (s: string, alloc: Allocator) -> string {
    raw : [*]u8 = xx alloc.alloc_bytes(s.len + 1);
    if s.len > 0 { memcpy(raw, s.ptr, s.len); }
    raw[s.len] = 0;
    return string.{ ptr = raw, len = s.len };
}

obj_find :: (o: Object, key: string) -> ?Value {
    i := 0;
    while i < o.len {
        if o.items[i].key == key { return o.items[i].val; }
        i += 1;
    }
    return null;
}

// Required string field, copied into `alloc`. Absent or empty -> MissingField;
// present-but-not-a-string -> WrongType.
req_str_nonempty :: (o: Object, key: string, alloc: Allocator) -> (string, !ManifestErr) {
    v := obj_find(o, key);
    if v == null { raise error.MissingField; }
    val := v!;
    if val != .str { raise error.WrongType; }
    if val.str.len == 0 { raise error.MissingField; }
    return dup_str(val.str, alloc);
}

// Optional string override, copied into `alloc`. Absent -> "" (a documented
// default, not a silent one); present-but-not-a-string -> WrongType.
opt_str :: (o: Object, key: string, alloc: Allocator) -> (string, !ManifestErr) {
    v := obj_find(o, key);
    if v == null { return ""; }
    val := v!;
    if val != .str { raise error.WrongType; }
    return dup_str(val.str, alloc);
}

// Optional integer field. Absent -> `default` (a documented default, not a
// silent one); present-but-not-an-integer -> WrongType.
opt_int :: (o: Object, key: string, default: i64) -> (i64, !ManifestErr) {
    v := obj_find(o, key);
    if v == null { return default; }
    val := v!;
    if val != .int_ { raise error.WrongType; }
    return val.int_;
}

// Required array field. Absent -> MissingField; present-but-not-an-array ->
// WrongType.
req_arr :: (o: Object, key: string) -> (Array, !ManifestErr) {
    v := obj_find(o, key);
    if v == null { raise error.MissingField; }
    val := v!;
    if val != .array { raise error.WrongType; }
    return val.array;
}

req_obj :: (v: Value) -> (Object, !ManifestErr) {
    if v != .object { raise error.WrongType; }
    return v.object;
}

// Walk one artifact object into a typed ManifestArtifact. `platform` must be
// present, a string, and a known Platform (reusing the P2.1 `parse_platform`
// — an unrecognized id surfaces as UnknownPlatform, never a default).
artifact_from_json :: (o: Object, alloc: Allocator) -> (ManifestArtifact, !ManifestErr) {
    a : ManifestArtifact = .{};

    pv := obj_find(o, "platform");
    if pv == null { raise error.MissingField; }
    pval := pv!;
    if pval != .str { raise error.WrongType; }
    if pval.str.len == 0 { raise error.MissingField; }
    plat, pe := parse_platform(pval.str);
    if pe { raise error.UnknownPlatform; }
    a.platform = plat;

    a.path         = try req_str_nonempty(o, "path", alloc);
    a.filename     = try opt_str(o, "filename", alloc);
    a.content_type = try opt_str(o, "content_type", alloc);
    a.metadata     = try opt_str(o, "metadata", alloc);
    a.size         = try opt_int(o, "size", -1);
    a.sha256       = try opt_str(o, "sha256", alloc);
    return a;
}

// ── public API ───────────────────────────────────────────────────────

// Parse manifest bytes into a typed `Manifest`. PURE: no filesystem access.
// Surfaces malformed JSON (BadJson), a non-object root or wrong-typed field
// (WrongType), a missing/empty required field (MissingField), and an unknown
// artifact platform (UnknownPlatform). All strings are copied into `alloc`.
parse_manifest :: (src: string, alloc: Allocator) -> (Manifest, !ManifestErr) {
    root, pe := jsonp.parse(src, alloc);
    if pe { raise error.BadJson; }
    if root != .object { raise error.WrongType; }
    ro := root.object;

    m : Manifest = .{};
    m.app     = try req_str_nonempty(ro, "app", alloc);
    m.version = try req_str_nonempty(ro, "version", alloc);
    m.channel = try req_str_nonempty(ro, "channel", alloc);

    arts := try req_arr(ro, "artifacts");
    i := 0;
    while i < arts.len {
        ao := try req_obj(arts.items[i]);
        a := try artifact_from_json(ao, alloc);
        m.artifacts.append(a, alloc);
        i += 1;
    }
    return m;
}

// Filesystem validation of an already-parsed manifest: every artifact `path`,
// resolved against `base_dir`, must exist on disk — a missing file surfaces
// as MissingArtifact. (Required-field/non-empty/platform checks are enforced
// by `parse_manifest`; this pass adds only the on-disk existence check.)
validate_manifest :: (m: Manifest, base_dir: string) -> !ManifestErr {
    i := 0;
    while i < m.artifacts.len {
        a := m.artifacts.items[i];
        full := path_join(base_dir, a.path);
        if !exists(full) { raise error.MissingArtifact; }
        i += 1;
    }
    return;
}

// Read, parse, and validate a manifest file. Artifact paths resolve relative
// to the manifest's own directory (`dirname(path)`). Unreadable file -> Io;
// otherwise the parse/validate errors above.
load_manifest :: (path: string, alloc: Allocator) -> (Manifest, !ManifestErr) {
    cpath := dup_str(path, alloc);          // null-terminated for the libc read
    src := read_file(cpath);
    if src == null { raise error.Io; }
    bytes := src!;
    m := try parse_manifest(bytes, alloc);
    try validate_manifest(m, dirname(path));
    return m;
}
