// =====================================================================
// ops.sx (app) — `dist app set`: admin mutation of an app's install
// policy over the persisted store (P4.6, subplan 04 Slice 5).
//
// Apps are CREATED by publish (find-or-create on slug); `app set` only
// edits ones that exist. It updates display_name, the iOS install mode
// (artifact_only / testflight / enterprise), the TestFlight URL, and the
// iOS bundle id, then revalidates the whole App — so the iOS policy
// preconditions (testflight needs a URL, enterprise needs a bundle id)
// abort the set as loud, machine-readable errors instead of leaving a
// policy the install page cannot honor.
//
// FAILURE CONTRACT (as everywhere): every abort happens before `db.save`,
// so a failed set never changes db.json.
// =====================================================================

#import "modules/std.sx";
#import "modules/std/fs.sx";
#import "modules/std/json.sx";
#import "../domain/platform.sx";
#import "../domain/app.sx";
#import "../domain/release.sx";
#import "../domain/artifact.sx";
#import "../domain/channel.sx";
#import "../domain/token.sx";
#import "../domain/audit.sx";
#import "../domain/validate.sx";
#import "../repo/repo.sx";
db   :: #import "../repo/db.sx";
jout :: #import "../json_out.sx";
pl   :: #import "../publish/publish.sx";

AppOpError :: error {
    Load,       // db.json absent or unreadable
    NotFound,   // no app with that slug
    Invalid,    // the requested policy fails validation
    Persist,    // db.json could not be re-written
}

// What `dist app set` wants to change; empty string = leave unchanged.
AppSetRequest :: struct {
    display_name: string = "";
    ios_mode: string = "";        // variant name, parsed here
    testflight_url: string = "";
    ios_bundle_id: string = "";
}

AppSetOutcome :: struct {
    app_id: string;
    slug: string;
    display_name: string;
    ios_mode: string;
    testflight_url: string;
    ios_bundle_id: string;
}

app_invalid_code :: (e: ValidationErr) -> string {
    if e == error.BadIosMode            { return "app.bad_ios_mode"; }
    if e == error.TestflightUrlRequired { return "app.testflight_url_required"; }
    if e == error.BundleIdRequired      { return "app.bundle_id_required"; }
    return "app.invalid";
}

app_invalid_message :: (e: ValidationErr) -> string {
    if e == error.BadIosMode            { return "--ios-mode must be one of: artifact_only testflight enterprise"; }
    if e == error.TestflightUrlRequired { return "ios_mode testflight requires --testflight-url"; }
    if e == error.BundleIdRequired      { return "ios_mode enterprise requires --ios-bundle-id"; }
    return "app fails domain validation";
}

run_app_set :: (store_dir: string, slug: string, req: AppSetRequest, fail_out: *jout.CliFailure) -> (AppSetOutcome, !AppOpError) {
    if !exists(path_join(store_dir, "db.json")) {
        fail_out.code    = "store.load";
        fail_out.message = concat("no db.json under the store (nothing published yet): ", store_dir);
        raise error.Load;
    }
    repo, le := db.load(store_dir);
    if le {
        fail_out.code    = "store.load";
        fail_out.message = concat("db.json under the store could not be loaded: ", store_dir);
        raise error.Load;
    }

    aq := repo.find_app_by_slug(slug);
    if aq == null {
        fail_out.code    = "app.unknown";
        fail_out.message = concat("no app with that slug in the store (publish creates apps): ", slug);
        raise error.NotFound;
    }
    a := aq!;

    if req.display_name.len > 0 { a.display_name = req.display_name; }
    if req.ios_mode.len > 0 {
        m, me := parse_ios_mode(req.ios_mode);
        if me {
            fail_out.code    = app_invalid_code(me);
            fail_out.message = app_invalid_message(me);
            raise error.Invalid;
        }
        a.ios_mode = m;
    }
    if req.testflight_url.len > 0 { a.testflight_url = req.testflight_url; }
    if req.ios_bundle_id.len > 0 {
        // replace-or-add the iOS bundle id (one per platform)
        found := false;
        i := 0;
        while i < a.bundle_ids.len {
            if a.bundle_ids.items[i].platform == .ios {
                a.bundle_ids.items[i].value = req.ios_bundle_id;
                found = true;
            }
            i += 1;
        }
        if !found {
            a.bundle_ids.append(BundleId.{ platform = .ios, value = req.ios_bundle_id },
                                repo.own_allocator);
        }
    }
    a.updated_at = pl.now_secs();

    verr := false;
    vcode := "";
    vmsg := "";
    validate_app(a) catch (e) {
        verr = true;
        vcode = app_invalid_code(e);
        vmsg = app_invalid_message(e);
    };
    if verr {
        fail_out.code    = vcode;
        fail_out.message = vmsg;
        raise error.Invalid;
    }

    repo.update_app(a);
    repo.create_audit_event(AuditEvent.{
        id = concat(concat("evt-app-set-", a.id), concat("-", int_to_string(a.updated_at))),
        actor = "cli", action = "app.update", target_type = "app",
        target_id = a.id, metadata = "", created_at = a.updated_at,
    });

    werr := false;
    db.save(@repo, store_dir) catch { werr = true; };
    if werr {
        fail_out.code    = "persist.save";
        fail_out.message = concat("db.json could not be written under the store: ", store_dir);
        raise error.Persist;
    }

    return AppSetOutcome.{
        app_id = a.id, slug = a.slug, display_name = a.display_name,
        ios_mode = db.ios_mode_str(a.ios_mode),
        testflight_url = a.testflight_url,
        ios_bundle_id = bundle_id_for(a, .ios),
    };
}

// `{"status":"updated","app":{id,slug,display_name,ios_mode,
// testflight_url,ios_bundle_id}}`.
write_app_set_json :: (o: *AppSetOutcome, dst: []u8) -> (i64, !JsonError) {
    gpa := GPA.init();
    root : Object = .{};
    root.put("status", .str("updated"), xx gpa);
    ao : Object = .{};
    ao.put("id", .str(o.app_id), xx gpa);
    ao.put("slug", .str(o.slug), xx gpa);
    ao.put("display_name", .str(o.display_name), xx gpa);
    ao.put("ios_mode", .str(o.ios_mode), xx gpa);
    ao.put("testflight_url", .str(o.testflight_url), xx gpa);
    ao.put("ios_bundle_id", .str(o.ios_bundle_id), xx gpa);
    root.put("app", .object(ao), xx gpa);
    rootv : Value = .object(root);
    n := try write_to_buffer(rootv, dst);
    return n;
}

app_set_human :: (o: *AppSetOutcome) -> string {
    s := concat("updated app ", concat(o.slug, concat(" (", concat(o.display_name, ")\n"))));
    s = concat(s, concat("  ios_mode: ", o.ios_mode));
    if o.testflight_url.len > 0 { s = concat(s, concat("  testflight: ", o.testflight_url)); }
    if o.ios_bundle_id.len > 0 { s = concat(s, concat("  ios bundle id: ", o.ios_bundle_id)); }
    return concat(s, "\n");
}
