// =====================================================================
// remote.sx — `dist ci publish --server --token` (subplan 03, Slice 3):
// the PLAN.md CI contract in remote mode, against a running distd.
//
// Same manifest front half as local publish (parse, required fields,
// on-disk artifact existence), then over HTTP:
//
//   per artifact:  read bytes -> POST /api/upload -> the server answers
//                  the content digest; a manifest-DECLARED sha256 is
//                  checked against it (corruption in transit is loud);
//   then:          POST /api/apps/<slug>/releases naming the uploaded
//                  digests -> the server runs the same commit pipeline
//                  as a local publish.
//
// The server's 200 response IS the publish outcome: it is parsed back
// into a `PublishOutcome`, so `--json` and human rendering reuse the
// P3.4a paths unchanged. Any non-2xx is surfaced by lifting the server's
// JSON error object (code + message) into the CLI failure — the remote
// and local failure surfaces are the same contract.
// =====================================================================

#import "modules/std.sx";
#import "modules/std/json.sx";
#import "modules/std/fs.sx";
#import "../domain/platform.sx";
#import "../manifest/manifest.sx";
#import "publish.sx";
mani :: #import "../manifest/manifest.sx";
pl   :: #import "publish.sx";
jout :: #import "../json_out.sx";
cl   :: #import "../client/http_client.sx";
dbr  :: #import "../repo/db.sx";
jrem :: #import "modules/std/json.sx";

RemoteErr :: error {
    Manifest,   // manifest failed to load / parse / validate
    Url,        // --server is not http://<ipv4-or-localhost>:<port>
    Transport,  // connect / send / unparseable response
    Server,     // the server answered a JSON error (code passed through)
}

// ── response helpers ──────────────────────────────────────────────────

rm_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;
}

rm_str :: (o: Object, key: string) -> string {
    vq := rm_find(o, key);
    if vq == null { return ""; }
    v := vq!;
    if v != .str { return ""; }
    return v.str;
}

rm_int :: (o: Object, key: string) -> i64 {
    vq := rm_find(o, key);
    if vq == null { return 0; }
    v := vq!;
    if v != .int_ { return 0; }
    return v.int_;
}

// Lift a server JSON error body into the CLI failure (falling back to the
// raw status when the body isn't the error shape).
server_failure :: (resp: cl.HttpResponse, fail_out: *jout.CliFailure) {
    fail_out.code    = "server.error";
    fail_out.message = concat("server answered HTTP ", int_to_string(resp.status));
    v, pe := jrem.parse(resp.body, context.allocator);
    if pe { return; }
    if v != .object { return; }
    eq := rm_find(v.object, "error");
    if eq == null { return; }
    ev := eq!;
    if ev != .object { return; }
    code := rm_str(ev.object, "code");
    msg  := rm_str(ev.object, "message");
    if code.len > 0 { fail_out.code = code; }
    if msg.len > 0  { fail_out.message = msg; }
}

// ── pipeline ──────────────────────────────────────────────────────────

run_remote_publish :: (manifest_path: string, server: string, token: string, fail_out: *jout.CliFailure) -> (PublishOutcome, !RemoteErr) {
    alloc := context.allocator;

    m, me := mani.load_manifest(manifest_path, alloc);
    if me {
        fail_out.code    = pl.manifest_code(me);
        fail_out.message = pl.manifest_message(me, manifest_path);
        raise error.Manifest;
    }
    base_dir := dirname(manifest_path);

    srv, ue := cl.parse_server_url(server);
    if ue {
        if ue == error.Tls {
            fail_out.code    = "server.tls_unsupported";
            fail_out.message = "https:// is not supported by the client shim; terminate TLS at a reverse proxy and target http://<host>:<port>";
        } else {
            fail_out.code    = "server.bad_url";
            fail_out.message = concat("--server must be http://<ipv4-or-localhost>:<port>, got: ", server);
        }
        raise error.Url;
    }

    // 1. Upload every artifact; collect the server-confirmed digests.
    keys : List(string) = .{};
    sizes : List(i64) = .{};
    i := 0;
    while i < m.artifacts.len {
        ma := m.artifacts.items[i];
        src := path_join(base_dir, ma.path);
        sb := read_file(src);
        if sb == null {
            fail_out.code    = "store.read";
            fail_out.message = concat("artifact file could not be read: ", src);
            raise error.Transport;
        }
        bytes := sb!;

        resp, te := cl.http_post(srv, "/api/upload", token, "application/octet-stream", bytes);
        if te {
            fail_out.code    = "server.unreachable";
            fail_out.message = concat("upload could not reach the server: ", server);
            raise error.Transport;
        }
        if resp.status != 200 {
            server_failure(resp, fail_out);
            raise error.Server;
        }
        uv, upe := jrem.parse(resp.body, alloc);
        key := "";
        if !upe {
            if uv == .object { key = rm_str(uv.object, "sha256"); }
        }
        if key.len != 64 {
            fail_out.code    = "server.bad_response";
            fail_out.message = "upload response did not carry a sha256 key";
            raise error.Transport;
        }
        // Trust-but-verify a declared digest: the server hashed what
        // ARRIVED; a mismatch means corruption (or the wrong file) and
        // must abort before any release names the object.
        if ma.sha256.len > 0 and ma.sha256 != key {
            fail_out.code    = "remote.digest_mismatch";
            fail_out.message = concat("uploaded bytes hash differently than the manifest declares: ", src);
            raise error.Server;
        }
        keys.append(key, alloc);
        sizes.append(bytes.len, alloc);
        i += 1;
    }

    // 2. Publish the release over the uploaded objects.
    body := try release_request_body(@m, keys, sizes, base_dir);
    rresp, re := cl.http_post(srv, concat(concat("/api/apps/", m.app), "/releases"), token, "application/json", body);
    if re {
        fail_out.code    = "server.unreachable";
        fail_out.message = concat("release publish could not reach the server: ", server);
        raise error.Transport;
    }
    if rresp.status != 200 {
        server_failure(rresp, fail_out);
        raise error.Server;
    }

    o, oe := outcome_from_response(rresp.body);
    if oe {
        fail_out.code    = "server.bad_response";
        fail_out.message = "publish response was not the expected JSON shape";
        raise error.Transport;
    }
    return o;
}

// The JSON body for POST /api/apps/<slug>/releases, naming uploaded
// objects by digest plus the manifest's per-artifact metadata.
release_request_body :: (m: *Manifest, keys: List(string), sizes: List(i64), base_dir: string) -> (string, !RemoteErr) {
    gpa := GPA.init();
    root : Object = .{};
    root.put("version", .str(m.version), xx gpa);
    root.put("channel", .str(m.channel), xx gpa);
    arr : Array = .{};
    i := 0;
    while i < m.artifacts.len {
        ma := m.artifacts.items[i];
        ao : Object = .{};
        ao.put("platform", .str(dbr.platform_str(ma.platform)), xx gpa);
        ao.put("sha256", .str(keys.items[i]), xx gpa);
        ao.put("size_bytes", .int_(sizes.items[i]), xx gpa);
        fname := if ma.filename.len > 0 then ma.filename else basename(path_join(base_dir, ma.path));
        ao.put("filename", .str(fname), xx gpa);
        if ma.content_type.len > 0 { ao.put("content_type", .str(ma.content_type), xx gpa); }
        if ma.metadata.len > 0 { ao.put("metadata", .str(ma.metadata), xx gpa); }
        arr.add(.object(ao), xx gpa);
        i += 1;
    }
    root.put("artifacts", .array(arr), xx gpa);

    buf : [*]u8 = xx context.allocator.alloc_bytes(65536);
    rootv : Value = .object(root);
    werr := false;
    n := write_to_buffer(rootv, string.{ ptr = buf, len = 65536 }) catch { werr = true; 0 };
    if werr { raise error.Transport; }
    return string.{ ptr = buf, len = n };
}


// Parse the server's publish response back into the CLI's outcome shape
// so both render paths (json / human) are shared with local publish.
outcome_from_response :: (body: string) -> (PublishOutcome, !RemoteErr) {
    alloc := context.allocator;
    v, pe := jrem.parse(body, alloc);
    if pe { raise error.Transport; }
    if v != .object { raise error.Transport; }
    ro := v.object;
    if rm_str(ro, "status") != "published" { raise error.Transport; }

    relq := rm_find(ro, "release");
    if relq == null { raise error.Transport; }
    relv := relq!;
    if relv != .object { raise error.Transport; }
    rel := relv.object;

    o : PublishOutcome = .{
        release_id = rm_str(rel, "id"),
        app_id     = rm_str(rel, "app_id"),
        version    = rm_str(rel, "version"),
        channel    = rm_str(rel, "channel"),
    };

    aq := rm_find(ro, "artifacts");
    if aq == null { raise error.Transport; }
    av := aq!;
    if av != .array { raise error.Transport; }
    arr := av.array;
    i := 0;
    while i < arr.len {
        if arr.items[i] != .object { raise error.Transport; }
        ao := arr.items[i].object;
        o.artifacts.append(PublishedArtifact.{
            id = rm_str(ao, "id"),
            platform_name = rm_str(ao, "platform"),
            size_bytes = rm_int(ao, "size_bytes"),
            sha256 = rm_str(ao, "sha256"),
            url = rm_str(ao, "url"),
        }, alloc);
        i += 1;
    }
    return o;
}
