//! PiCloud command-line client.
//!
//! Thin client over the existing admin + execute HTTP surface — the
//! server gains nothing for the CLI; the CLI is just a developer
//! ergonomics layer over endpoints the dashboard already uses.

use std::path::PathBuf;
use std::process::ExitCode;

use clap::{Args, Parser, Subcommand, ValueEnum};

mod client;
mod cmds;
mod config;
mod discover;
mod linkstate;
mod manifest;
mod output;

use crate::output::OutputMode;

#[derive(Parser)]
#[command(name = "pic", version, about = "PiCloud command-line client")]
struct Cli {
    /// Output format for `ls` / `show` / `whoami` / `logs` commands.
    /// TSV stays pipe-friendly; JSON is `jq`-ready.
    #[arg(long, value_enum, global = true, default_value_t = OutputMode::Tsv)]
    output: OutputMode,

    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    /// Authenticate with the server. Default flow prompts for username
    /// + password and saves the returned session token; `--token` skips
    ///   the password exchange and persists a bearer string directly (use
    ///   this for long-lived API keys minted via `pic api-keys mint`).
    Login(LoginArgs),

    /// Revoke the saved session server-side and delete the local
    /// credentials file. Idempotent.
    Logout,

    /// Print the principal the saved token resolves to.
    Whoami,

    /// App management.
    Apps {
        #[command(subcommand)]
        cmd: AppsCmd,
    },

    /// Group (org-tree) management.
    Groups {
        #[command(subcommand)]
        cmd: GroupsCmd,
    },

    /// §7 multi-repo ownership projects (read-only).
    Projects {
        #[command(subcommand)]
        cmd: ProjectsCmd,
    },

    /// Script management.
    Scripts {
        #[command(subcommand)]
        cmd: ScriptsCmd,
    },

    /// Long-lived bearer API key management.
    #[command(name = "api-keys")]
    ApiKeys {
        #[command(subcommand)]
        cmd: ApiKeysCmd,
    },

    /// Tail recent execution logs for a script.
    Logs(LogsArgs),

    /// Top-level alias for `pic scripts invoke <id>`.
    Invoke(InvokeArgs),

    /// Top-level alias for `pic scripts deploy <file> --app <slug>`.
    Deploy(DeployArgs),

    /// Route management — bind script ids to HTTP method+host+path.
    Routes {
        #[command(subcommand)]
        cmd: RoutesCmd,
    },

    /// Admin user management — list / create / patch / delete the
    /// per-instance admin accounts. All endpoints require
    /// `instance:admin` or `instance:owner`.
    Admins {
        #[command(subcommand)]
        cmd: AdminsCmd,
    },

    /// Trigger management — list / create / delete the triggers that
    /// fire scripts on KV mutations, cron schedules, dead-letter rows,
    /// etc. `create-from-json` is the escape hatch for kinds the CLI
    /// doesn't expose a per-kind wrapper for (docs/files/pubsub/email/
    /// queue) — pass the body JSON inline, via `@<file>`, or `-` to
    /// read from stdin. Note: a `pubsub` trigger runs a script when a
    /// message is published; to let an *external* client subscribe to a
    /// topic over SSE, register it with `pic topics` instead.
    Triggers {
        #[command(subcommand)]
        cmd: TriggersCmd,
    },

    /// Durable DAG workflows (v1.2). Definitions are authored declaratively
    /// (`[[workflows]]` in `picloud.toml` → `pic apply`); this surface starts
    /// runs and reads run history.
    Workflows {
        #[command(subcommand)]
        cmd: WorkflowsCmd,
    },

    /// Pub/sub topic registry — control which topics external clients
    /// can subscribe to over SSE (`/realtime/topics/{name}`). Publishing
    /// from a script needs no registration; only outside subscribers do.
    Topics {
        #[command(subcommand)]
        cmd: TopicsCmd,
    },

    /// App end-user management — list / inspect the users who registered
    /// via `users::*` in scripts, mint reset tokens, revoke sessions.
    /// Distinct from `pic admins` (instance accounts).
    Users {
        #[command(subcommand)]
        cmd: UsersCmd,
    },

    /// Dead-letter management — inspect, replay, and resolve rows the
    /// dispatcher wrote after exhausting a trigger's retries.
    #[command(name = "dead-letters")]
    DeadLetters {
        #[command(subcommand)]
        cmd: DeadLettersCmd,
    },

    /// Per-app secret storage — list names, set values from stdin,
    /// delete by name. Values never leave the server after `set`.
    Secrets {
        #[command(subcommand)]
        cmd: SecretsCmd,
    },

    /// App membership — list members and grant / change / revoke their
    /// per-app role (app_admin | editor | viewer).
    Members {
        #[command(subcommand)]
        cmd: MembersCmd,
    },

    /// Config vars (Phase 3) — set / list / delete group- or app-owned
    /// env-scoped vars. Values inherit down the group tree; an app value
    /// overrides an inherited one (proximity wins).
    Vars {
        #[command(subcommand)]
        cmd: VarsCmd,
    },

    /// Extension points (§5.5) — read-only view of the module names a node
    /// marks as overridable. Authored declaratively via the manifest
    /// `extension_points = [...]`; this lists them + (for an app) each one's
    /// resolved provider.
    ExtensionPoints {
        #[command(subcommand)]
        cmd: ExtensionPointsCmd,
    },

    /// Shared group collections (§11.6) — read-only view of the KV collection
    /// names a group offers as cross-app-shared. Authored declaratively via the
    /// `[group]` manifest `collections = [...]`; scripts read/write them with
    /// `kv::shared_collection("name")`.
    Collections {
        #[command(subcommand)]
        cmd: CollectionsCmd,
    },

    /// Per-app opt-out of inherited group templates (§11 tail). Authoring is
    /// declarative via the `[suppress]` manifest block; this lists what an app
    /// declines.
    Suppress {
        #[command(subcommand)]
        cmd: SuppressCmd,
    },

    /// Files inspection — list a collection's blobs, download bytes, or
    /// delete a file. Read + delete only; writes go through scripts.
    Files {
        #[command(subcommand)]
        cmd: FilesCmd,
    },

    /// Queue inspection — list queues with depth counts, or drill into
    /// one queue's stats + registered consumer. Read-only.
    Queues {
        #[command(subcommand)]
        cmd: QueuesCmd,
    },

    /// KV inspection — list keys in a collection or fetch one value.
    /// Read-only; writes go through `kv::set` in scripts.
    Kv {
        #[command(subcommand)]
        cmd: KvCmd,
    },

    /// Docs inspection for a group's shared-docs collection (§11.6).
    /// Read-only; writes go through `docs::shared_collection(...)` in scripts.
    Docs {
        #[command(subcommand)]
        cmd: DocsCmd,
    },

    /// Reconcile the live app to a `picloud.toml` manifest in one
    /// transaction (creates + updates; `--prune` also deletes resources
    /// absent from the manifest).
    Apply(ApplyArgs),

    /// Diff a `picloud.toml` manifest against the live app and print the
    /// changes (create / update / no-op / delete). Read-only.
    Plan(PlanArgs),

    /// Export an app's current server state into a `picloud.toml` manifest
    /// (+ `scripts/<name>.rhai` sources) for declarative management with
    /// `pic plan` / `pic apply`.
    Pull(PullArgs),

    /// Scaffold a new declarative project: a `picloud.toml` (minimal working
    /// app), `scripts/hello.rhai`, and a `.gitignore`. Offline; deploy with
    /// `pic plan` then `pic apply`.
    Init(InitArgs),

    /// Show an app's resolved configuration. `--effective` lists secrets with
    /// values masked (`<set>`/`<unset>`), cross-referenced against the manifest.
    Config(ConfigArgs),
}

#[derive(Args)]
struct ApplyArgs {
    /// Path to the manifest.
    #[arg(long, default_value = "picloud.toml")]
    file: PathBuf,
    /// Phase 5: apply a whole project TREE — every `picloud.toml` under this
    /// directory (groups + apps) reconciled in one atomic transaction.
    /// Mutually exclusive with `--file`.
    #[arg(long, conflicts_with = "file")]
    dir: Option<PathBuf>,
    /// Delete live scripts/routes/triggers absent from the manifest.
    /// Secrets are never pruned.
    #[arg(long)]
    prune: bool,
    /// Skip the `--prune` confirmation prompt. Required to prune
    /// non-interactively (CI).
    #[arg(long)]
    yes: bool,
    /// Skip the bound-plan staleness check (apply even if the app changed
    /// since the last `pic plan`). Also skips the first-run preview + blast-
    /// radius confirmation prompt (like `--yes`), so a scripted `--force` apply
    /// never blocks on a TTY question.
    #[arg(long)]
    force: bool,
    /// §7 multi-repo ownership: reassign a group node owned by another
    /// `[project]` to this manifest's project. Requires group-admin on the
    /// node; without it an apply to an owned node is refused (409).
    #[arg(long)]
    takeover: bool,
    /// Merge the `picloud.<env>.toml` overlay (per-env slug + secrets) on
    /// top of the base manifest before applying.
    #[arg(long)]
    env: Option<String>,
    /// §6 M2: reparent a group whose server parent diverges from the
    /// manifest's directory nesting (default: refuse on divergence).
    #[arg(long, conflicts_with = "adopt_server_structure")]
    force_local_structure: bool,
    /// §6 M2: accept the server's group placement on a structural
    /// divergence and reconcile content in place (no reparent).
    #[arg(long)]
    adopt_server_structure: bool,
    /// §3 M3: approve applying to a confirm-required environment
    /// (`[project.environments]`). Repeatable; each must match the `--env`.
    #[arg(long = "approve")]
    approve: Vec<String>,
}

#[derive(Args)]
struct PlanArgs {
    /// Path to the manifest.
    #[arg(long, default_value = "picloud.toml")]
    file: PathBuf,
    /// Phase 5: plan a whole project TREE — every `picloud.toml` under this
    /// directory (groups + apps). Mutually exclusive with `--file`.
    #[arg(long, conflicts_with = "file")]
    dir: Option<PathBuf>,
    /// Merge the `picloud.<env>.toml` overlay (per-env slug + secrets) on
    /// top of the base manifest before diffing.
    #[arg(long)]
    env: Option<String>,
}

#[derive(Args)]
struct PullArgs {
    /// App slug or id to export.
    app: String,
    /// Directory to write `picloud.toml` + `scripts/` into.
    #[arg(long, default_value = ".")]
    dir: PathBuf,
    /// Overwrite an existing `picloud.toml` (and colliding `scripts/*.rhai`).
    #[arg(long)]
    force: bool,
}

#[derive(Args)]
struct InitArgs {
    /// App slug for the new project. Defaults to a slug derived from the
    /// target directory's name.
    slug: Option<String>,
    /// Directory to scaffold into.
    #[arg(long, default_value = ".")]
    dir: PathBuf,
    /// Display name for the app. Defaults to a title-cased slug.
    #[arg(long)]
    name: Option<String>,
    /// Overwrite an existing `picloud.toml`.
    #[arg(long)]
    force: bool,
}

#[derive(Args)]
struct ConfigArgs {
    /// Path to the manifest.
    #[arg(long, default_value = "picloud.toml")]
    file: PathBuf,
    /// Show the effective (resolved) config with secrets masked.
    #[arg(long)]
    effective: bool,
    /// With `--effective`, also print each resolved var's `merged_from`
    /// provenance — the ordered (depth, scope) layers it merged from.
    #[arg(long)]
    explain: bool,
    /// Resolve against the `picloud.<env>.toml` overlay (per-env slug +
    /// secrets), matching `pic plan --env` / `pic apply --env`.
    #[arg(long)]
    env: Option<String>,
}

#[derive(Subcommand)]
enum KvCmd {
    /// List keys in a collection. Exactly one of `--app` (per-app KV) or
    /// `--group` (a group's §11.6 shared KV).
    Ls {
        #[arg(long, conflicts_with = "group")]
        app: Option<String>,
        #[arg(long)]
        group: Option<String>,
        #[arg(long)]
        collection: String,
        #[arg(long, default_value_t = 100)]
        limit: u32,
    },
    /// Fetch one key's value (printed as JSON). `--app` or `--group`.
    Get {
        #[arg(long, conflicts_with = "group")]
        app: Option<String>,
        #[arg(long)]
        group: Option<String>,
        #[arg(long)]
        collection: String,
        key: String,
    },
}

#[derive(Subcommand)]
enum DocsCmd {
    /// List document ids in a group's shared-docs collection.
    Ls {
        #[arg(long)]
        group: String,
        #[arg(long)]
        collection: String,
        #[arg(long, default_value_t = 100)]
        limit: u32,
    },
    /// Fetch one document's data (printed as JSON).
    Get {
        #[arg(long)]
        group: String,
        #[arg(long)]
        collection: String,
        id: String,
    },
}

#[derive(Subcommand)]
enum MembersCmd {
    /// List app members.
    Ls {
        #[arg(long)]
        app: String,
    },
    /// Grant a user a role on the app.
    Add {
        #[arg(long)]
        app: String,
        #[arg(long = "user")]
        user_id: String,
        /// `app_admin` | `editor` | `viewer`.
        #[arg(long)]
        role: String,
    },
    /// Change an existing member's role.
    Set {
        #[arg(long)]
        app: String,
        #[arg(long = "user")]
        user_id: String,
        #[arg(long)]
        role: String,
    },
    /// Remove a member.
    Rm {
        #[arg(long)]
        app: String,
        #[arg(long = "user")]
        user_id: String,
    },
}

#[derive(Subcommand)]
enum FilesCmd {
    /// List files in a collection. Exactly one of `--app` (per-app files) or
    /// `--group` (a group's §11.6 shared files).
    Ls {
        #[arg(long, conflicts_with = "group")]
        app: Option<String>,
        #[arg(long)]
        group: Option<String>,
        #[arg(long)]
        collection: String,
        #[arg(long, default_value_t = 100)]
        limit: u32,
    },
    /// Download a file's bytes (to `--out <path>` or stdout). `--app` or `--group`.
    Get {
        #[arg(long, conflicts_with = "group")]
        app: Option<String>,
        #[arg(long)]
        group: Option<String>,
        #[arg(long)]
        collection: String,
        #[arg(long = "id")]
        file_id: String,
        #[arg(long)]
        out: Option<PathBuf>,
    },
    /// Delete a file. Per-app only — shared-collection blobs are not
    /// operator-deletable (writes go through scripts).
    Rm {
        #[arg(long)]
        app: String,
        #[arg(long)]
        collection: String,
        #[arg(long = "id")]
        file_id: String,
    },
}

#[derive(Subcommand)]
enum QueuesCmd {
    /// List queues with depth counts.
    Ls {
        #[arg(long)]
        app: String,
    },
    /// Show one queue's stats + consumer.
    Show {
        #[arg(long)]
        app: String,
        queue_name: String,
    },
}

#[derive(Args)]
struct LoginArgs {
    /// Override the URL prompt non-interactively. Also reads
    /// `PICLOUD_URL`.
    #[arg(long)]
    url: Option<String>,

    /// Skip the username + password exchange and persist a bearer
    /// directly (validated against `/auth/me` first). Pass `--token -`
    /// to read it from stdin; an inline value is rejected (it leaks into
    /// shell history / `ps`). For CI, set `PICLOUD_TOKEN` instead.
    #[arg(long)]
    token: Option<String>,

    /// Username for non-interactive password login. Pair with
    /// `--password-stdin`. Omit both for the interactive prompt.
    #[arg(long)]
    username: Option<String>,

    /// Read the password from stdin instead of prompting (CI path). The
    /// password is never accepted inline — that leaks into shell history,
    /// `ps`, and /proc.
    #[arg(long = "password-stdin")]
    password_stdin: bool,
}

#[derive(Subcommand)]
enum AppsCmd {
    /// List apps the caller can see.
    Ls,

    /// Create a new app.
    Create {
        slug: String,
        #[arg(long)]
        name: Option<String>,
        #[arg(long)]
        description: Option<String>,
        /// Parent group (slug or id). Defaults to the instance root.
        #[arg(long)]
        group: Option<String>,
    },

    /// Show a single app, including the caller's role in it.
    Show { ident: String },

    /// Delete an app. Without `--force`, the server rejects if the app
    /// still owns scripts.
    Delete {
        ident: String,
        #[arg(long)]
        force: bool,
    },

    /// Manage an app's domain claims. A non-default app's routes only
    /// match once it claims the request `Host` — without a claim every
    /// route 404s.
    Domains {
        #[command(subcommand)]
        cmd: DomainsCmd,
    },
}

#[derive(Subcommand)]
enum ProjectsCmd {
    /// List all projects + how many group nodes each owns.
    Ls,
}

#[derive(Subcommand)]
enum GroupsCmd {
    /// List all groups (flat).
    Ls,
    /// Render the group hierarchy as an indented tree.
    Tree,
    /// Create a new group. Omit `--parent` for a root-level group.
    Create {
        slug: String,
        #[arg(long)]
        name: Option<String>,
        #[arg(long)]
        description: Option<String>,
        /// Parent group (slug or id).
        #[arg(long)]
        parent: Option<String>,
    },
    /// Show a group with its path, subgroups, and apps.
    Show { ident: String },
    /// Rename a group (name/description only — the slug is frozen).
    Rename {
        ident: String,
        #[arg(long)]
        name: Option<String>,
        #[arg(long)]
        description: Option<String>,
    },
    /// Move a group under a new parent (`--to` slug/id, or omit for root).
    Reparent {
        ident: String,
        #[arg(long)]
        to: Option<String>,
    },
    /// Delete a group. Refused (409) if non-empty unless `--recursive`,
    /// which deletes child groups leaf-first (apps are never auto-deleted).
    Rm {
        ident: String,
        #[arg(long)]
        recursive: bool,
    },
    /// Manage a group's members (inherited down the tree).
    Members {
        #[command(subcommand)]
        cmd: GroupMembersCmd,
    },
}

#[derive(Subcommand)]
enum GroupMembersCmd {
    /// List a group's members.
    Ls { group: String },
    /// Grant a member a role on the group.
    Add {
        group: String,
        user_id: String,
        #[arg(long, default_value = "viewer")]
        role: String,
    },
    /// Change a member's role.
    Set {
        group: String,
        user_id: String,
        #[arg(long)]
        role: String,
    },
    /// Remove a member from the group.
    Rm { group: String, user_id: String },
}

#[derive(Subcommand)]
enum DomainsCmd {
    /// List the app's domain claims.
    Ls { app: String },

    /// Claim a domain for the app. Patterns: exact (`app.example.com`),
    /// wildcard (`*.example.com`), or parameterized (`{tenant}.example.com`
    /// — `{name}` syntax, never `:name`).
    Add { app: String, pattern: String },

    /// Release a domain claim by its id.
    Rm { app: String, domain_id: String },
}

#[derive(Subcommand)]
enum ExtensionPointsCmd {
    /// List a node's extension points. `--app` shows every EP visible on the
    /// app's chain with its resolved provider; `--group` shows the group's own.
    Ls {
        #[arg(long)]
        app: Option<String>,
        #[arg(long, conflicts_with = "app")]
        group: Option<String>,
    },
}

#[derive(Subcommand)]
enum CollectionsCmd {
    /// List the shared collections a group declares (§11.6). Group-only —
    /// shared collections are owned by groups, not apps.
    Ls {
        #[arg(long)]
        group: String,
    },
}

#[derive(Subcommand)]
enum SuppressCmd {
    /// List the inherited templates an owner declines (§11 tail). Exactly one
    /// of `--app` (declines for itself) or `--group` (declines for its whole
    /// subtree, §11 tail M1).
    Ls {
        #[arg(long, conflicts_with = "group")]
        app: Option<String>,
        #[arg(long)]
        group: Option<String>,
    },
}

#[derive(Subcommand)]
enum ScriptsCmd {
    /// List scripts. With `--app`, scoped to one app; with `--group`, a
    /// group's own scripts (Phase 4); without either, one
    /// `GET /admin/scripts` for everything the caller can see.
    Ls {
        #[arg(long)]
        app: Option<String>,
        /// Phase 4: list a group's own (non-inherited) scripts.
        #[arg(long, conflicts_with = "app")]
        group: Option<String>,
    },

    /// Upload a `.rhai` file. Patches the existing script with the
    /// matching name in `--app`/`--group` if one exists, otherwise creates it.
    Deploy(DeployArgs),

    /// POST to `/api/v1/execute/{id}`. Body via `--body @path`,
    /// `--body @-` for stdin, or inline JSON.
    Invoke(InvokeArgs),

    /// Delete a script. Requires AppAdmin on the owning app.
    Delete { id: String },
}

#[derive(Args)]
struct DeployArgs {
    file: PathBuf,
    /// Owning app (slug or id). Exactly one of `--app` / `--group`.
    #[arg(long)]
    app: Option<String>,
    /// Phase 4: deploy a group-owned script (template inherited by
    /// descendant apps) instead of an app-owned one. Exactly one of
    /// `--app` / `--group`.
    #[arg(long, conflicts_with = "app")]
    group: Option<String>,
    #[arg(long)]
    name: Option<String>,
    #[arg(long)]
    description: Option<String>,
    /// Per-script wall-clock timeout in seconds (overrides the instance
    /// default; still clamped by `PICLOUD_SANDBOX_MAX_*` ceilings).
    #[arg(long)]
    timeout: Option<i32>,
    /// Per-script memory ceiling in MB.
    #[arg(long)]
    memory: Option<i32>,
    /// Script kind: `endpoint` (default) or `module` (importable, no route).
    #[arg(long, value_enum)]
    kind: Option<ScriptKindArg>,
    /// Sandbox override as `key=value`, repeatable. Keys: `max_operations`,
    /// `max_string_size`, `max_array_size`, `max_map_size`,
    /// `max_call_levels`, `max_expr_depth`. E.g. `--sandbox max_operations=500000`.
    #[arg(long = "sandbox", value_name = "KEY=VALUE")]
    sandbox: Vec<String>,
}

#[derive(Copy, Clone, clap::ValueEnum)]
enum ScriptKindArg {
    Endpoint,
    Module,
}

impl From<ScriptKindArg> for picloud_shared::ScriptKind {
    fn from(v: ScriptKindArg) -> Self {
        match v {
            ScriptKindArg::Endpoint => Self::Endpoint,
            ScriptKindArg::Module => Self::Module,
        }
    }
}

impl DeployArgs {
    /// Fold the runtime-config flags into a `ScriptConfig`, parsing and
    /// validating the `--sandbox key=value` pairs.
    fn script_config(&self) -> anyhow::Result<client::ScriptConfig> {
        let sandbox = parse_sandbox_overrides(&self.sandbox)?;
        Ok(client::ScriptConfig {
            timeout_seconds: self.timeout,
            memory_limit_mb: self.memory,
            kind: self.kind.map(Into::into),
            sandbox,
        })
    }
}

/// Parse repeatable `--sandbox key=value` flags into a `ScriptSandbox`.
/// Unknown keys and non-integer values are hard errors so a typo doesn't
/// silently deploy an unrestricted script.
fn parse_sandbox_overrides(
    pairs: &[String],
) -> anyhow::Result<Option<picloud_shared::ScriptSandbox>> {
    use anyhow::{anyhow, Context};
    if pairs.is_empty() {
        return Ok(None);
    }
    let mut sb = picloud_shared::ScriptSandbox::default();
    for raw in pairs {
        let (key, val) = raw
            .split_once('=')
            .ok_or_else(|| anyhow!("--sandbox expects key=value, got {raw:?}"))?;
        let n: u64 = val
            .trim()
            .parse()
            .with_context(|| format!("--sandbox {key}: {val:?} is not a non-negative integer"))?;
        match key.trim() {
            "max_operations" => sb.max_operations = Some(n),
            "max_string_size" => sb.max_string_size = Some(n),
            "max_array_size" => sb.max_array_size = Some(n),
            "max_map_size" => sb.max_map_size = Some(n),
            "max_call_levels" => sb.max_call_levels = Some(n),
            "max_expr_depth" => sb.max_expr_depth = Some(n),
            other => return Err(anyhow!("unknown --sandbox key: {other:?}")),
        }
    }
    Ok(Some(sb))
}

#[derive(Args)]
struct InvokeArgs {
    id: String,
    #[arg(long)]
    body: Option<String>,
    #[arg(short = 'H', long = "header", value_parser = client::parse_kv_header)]
    headers: Vec<(String, String)>,
}

#[derive(Subcommand)]
enum ApiKeysCmd {
    /// Mint a new long-lived bearer key. Token printed exactly once.
    Mint {
        name: String,
        /// Repeat for multiple scopes: `--scope script:read --scope log:read`.
        #[arg(long = "scope", required = true)]
        scopes: Vec<String>,
        /// Bind the key to a single app (slug or id). Rejects
        /// `instance:*` scopes when set.
        #[arg(long)]
        app: Option<String>,
        /// Absolute RFC 3339 (`2026-12-31T23:59:59Z`) or shorthand
        /// `<N>d`/`<N>h`/`<N>m`.
        #[arg(long)]
        expires: Option<String>,
    },

    /// List the caller's keys (no `raw_token` after mint).
    Ls,

    /// Revoke a key by id.
    Rm { id: String },
}

#[derive(Args)]
struct LogsArgs {
    script_id: String,
    #[arg(long, default_value_t = 50)]
    limit: u32,
    /// Filter by execution origin: `http`, `kv`, `cron`, `queue`,
    /// `invoke`, `dead_letter`, `docs`, `files`, `pubsub`, `email`.
    /// Omit (or `all`) to show every source.
    #[arg(long)]
    source: Option<String>,
}

#[derive(Clone, Copy, ValueEnum)]
enum DispatchModeArg {
    Sync,
    Async,
}

impl From<DispatchModeArg> for picloud_shared::DispatchMode {
    fn from(v: DispatchModeArg) -> Self {
        match v {
            DispatchModeArg::Sync => Self::Sync,
            DispatchModeArg::Async => Self::Async,
        }
    }
}

/// Wire form for trigger dispatch — the trigger handlers accept
/// `"sync"` / `"async"` as JSON strings, so we send that directly.
const fn dispatch_wire(d: DispatchModeArg) -> &'static str {
    match d {
        DispatchModeArg::Sync => "sync",
        DispatchModeArg::Async => "async",
    }
}

#[derive(Clone, Copy, ValueEnum)]
enum HostKindArg {
    /// Any host (default).
    Any,
    /// Exact `host` only.
    Strict,
    /// `*.<host>` — wildcard subdomain.
    Wildcard,
}

impl From<HostKindArg> for picloud_shared::HostKind {
    fn from(v: HostKindArg) -> Self {
        match v {
            HostKindArg::Any => Self::Any,
            HostKindArg::Strict => Self::Strict,
            HostKindArg::Wildcard => Self::Wildcard,
        }
    }
}

#[derive(Clone, Copy, ValueEnum)]
enum PathKindArg {
    Exact,
    Prefix,
    Param,
}

impl From<PathKindArg> for picloud_shared::PathKind {
    fn from(v: PathKindArg) -> Self {
        match v {
            PathKindArg::Exact => Self::Exact,
            PathKindArg::Prefix => Self::Prefix,
            PathKindArg::Param => Self::Param,
        }
    }
}

#[derive(Clone, Copy, ValueEnum)]
enum InstanceRoleArg {
    Owner,
    Admin,
    Member,
}

impl From<InstanceRoleArg> for picloud_shared::InstanceRole {
    fn from(v: InstanceRoleArg) -> Self {
        match v {
            InstanceRoleArg::Owner => Self::Owner,
            InstanceRoleArg::Admin => Self::Admin,
            InstanceRoleArg::Member => Self::Member,
        }
    }
}

#[derive(Subcommand)]
enum RoutesCmd {
    /// List routes bound to a script, or a group's route TEMPLATES (`--group`).
    Ls {
        #[arg(conflicts_with = "group", required_unless_present = "group")]
        script_id: Option<String>,
        /// List the group's §11 tail route templates instead.
        #[arg(long)]
        group: Option<String>,
    },

    /// Create a new route. Host defaults to `*` (any). Path-kind
    /// defaults to `exact`. Dispatch defaults to `sync`.
    Create {
        /// The script to bind. Routes inherit the script's `app_id`.
        #[arg(long)]
        script: String,
        /// HTTP path (e.g. `/webhook`, `/users/:id`, `/blobs/*`).
        #[arg(long)]
        path: String,
        /// HTTP method to match. Omit for ANY.
        #[arg(long)]
        method: Option<String>,
        /// `exact`, `prefix`, or `param`.
        #[arg(long = "path-kind", value_enum, default_value_t = PathKindArg::Exact)]
        path_kind: PathKindArg,
        /// Host pattern (e.g. `*`, `api.example.com`, `*.example.com`).
        /// Pair with `--host-kind` when ambiguous; the CLI does not
        /// auto-detect.
        #[arg(long, default_value = "")]
        host: String,
        /// `any` (default), `strict`, or `wildcard`.
        #[arg(long = "host-kind", value_enum, default_value_t = HostKindArg::Any)]
        host_kind: HostKindArg,
        /// Name to bind the host-wildcard segment to (for
        /// `host-kind=wildcard` routes that want to read the subdomain).
        #[arg(long = "host-param-name")]
        host_param_name: Option<String>,
        /// `sync` (default) or `async`. Async returns 202 immediately;
        /// the dispatcher fires the script in the background.
        #[arg(long, value_enum, default_value_t = DispatchModeArg::Sync)]
        dispatch: DispatchModeArg,
    },

    /// Delete a route by id.
    Rm { route_id: String },

    /// Dry-run conflict check for a hypothetical route.
    Check {
        /// App slug or UUID (conflict checks are intra-app).
        #[arg(long)]
        app: String,
        #[arg(long)]
        path: String,
        #[arg(long)]
        method: Option<String>,
        #[arg(long = "path-kind", value_enum, default_value_t = PathKindArg::Exact)]
        path_kind: PathKindArg,
        #[arg(long, default_value = "")]
        host: String,
        #[arg(long = "host-kind", value_enum, default_value_t = HostKindArg::Any)]
        host_kind: HostKindArg,
    },

    /// What route would the given URL+method match, if any?
    Match {
        /// App slug or UUID.
        #[arg(long)]
        app: String,
        /// Full URL — host+path must be present.
        url: String,
        #[arg(long, default_value = "GET")]
        method: String,
    },
}

#[derive(Subcommand)]
enum WorkflowsCmd {
    /// List an app's workflow definitions.
    Ls {
        #[arg(long)]
        app: String,
    },
    /// Start a run of a workflow.
    Run {
        #[arg(long)]
        app: String,
        /// Workflow name.
        name: String,
        /// Run input as a JSON object (e.g. '{"date":"2026-07-12"}').
        #[arg(long)]
        input: Option<String>,
    },
    /// List recent runs of a workflow.
    Runs {
        #[arg(long)]
        app: String,
        /// Workflow name.
        name: String,
    },
    /// Show one run's status + its per-step detail.
    #[command(name = "run-status")]
    RunStatus {
        #[arg(long)]
        app: String,
        /// Run id (from `pic workflows run` / `runs`).
        run_id: String,
    },
}

#[derive(Subcommand)]
enum TriggersCmd {
    /// List an app's triggers, or a group's trigger TEMPLATES (`--group`).
    Ls {
        #[arg(long, conflicts_with = "group", required_unless_present = "group")]
        app: Option<String>,
        /// List the group's §11 tail trigger templates instead.
        #[arg(long)]
        group: Option<String>,
    },

    /// Delete a trigger by id.
    Rm {
        #[arg(long)]
        app: String,
        trigger_id: String,
    },

    /// Create a KV trigger.
    #[command(name = "create-kv")]
    CreateKv {
        #[arg(long)]
        app: String,
        #[arg(long)]
        script: String,
        /// Glob over collection names (`*`, `users`, `events_*`, …).
        #[arg(long)]
        collection: String,
        /// Repeat to filter ops: `--op insert --op delete`. Empty
        /// (the default) means any op fires the trigger.
        #[arg(long = "op")]
        ops: Vec<String>,
        #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)]
        dispatch: DispatchModeArg,
    },

    /// Create a cron trigger.
    #[command(name = "create-cron")]
    CreateCron {
        #[arg(long)]
        app: String,
        #[arg(long)]
        script: String,
        /// 6-field cron expression (with seconds).
        #[arg(long)]
        schedule: String,
        /// IANA timezone name. Defaults to UTC.
        #[arg(long)]
        timezone: Option<String>,
        #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)]
        dispatch: DispatchModeArg,
    },

    /// Create a dead-letter trigger — fires when ANOTHER trigger's
    /// retries exhaust. Filters narrow the match: omit to fire on
    /// every DL in the app.
    #[command(name = "create-dead-letter")]
    CreateDeadLetter {
        #[arg(long)]
        app: String,
        #[arg(long)]
        script: String,
        /// Source filter — match only DL rows that originated from
        /// `kv` / `docs` / `queue` / etc.
        #[arg(long = "source-filter")]
        source_filter: Option<String>,
        /// Trigger id filter — match only DL rows from one trigger.
        #[arg(long = "trigger-id-filter")]
        trigger_id_filter: Option<String>,
        /// Script id filter — match only DL rows from one script.
        #[arg(long = "script-id-filter")]
        script_id_filter: Option<String>,
        #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)]
        dispatch: DispatchModeArg,
    },

    /// Create a docs trigger — fires on document mutations in matching
    /// collections.
    #[command(name = "create-docs")]
    CreateDocs {
        #[arg(long)]
        app: String,
        #[arg(long)]
        script: String,
        /// Glob over collection names (`*`, `posts`, `events_*`, …).
        #[arg(long)]
        collection: String,
        /// Repeat to filter ops: `--op insert --op delete`. Empty fires on any.
        #[arg(long = "op")]
        ops: Vec<String>,
        #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)]
        dispatch: DispatchModeArg,
    },

    /// Create a files trigger — fires on blob create/update/delete in
    /// matching collections.
    #[command(name = "create-files")]
    CreateFiles {
        #[arg(long)]
        app: String,
        #[arg(long)]
        script: String,
        #[arg(long)]
        collection: String,
        #[arg(long = "op")]
        ops: Vec<String>,
        #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)]
        dispatch: DispatchModeArg,
    },

    /// Create a pub/sub trigger — fires on messages published to topics
    /// matching the pattern.
    #[command(name = "create-pubsub")]
    CreatePubsub {
        #[arg(long)]
        app: String,
        #[arg(long)]
        script: String,
        /// Topic glob (`*`, `orders.*`, `user.signup`, …).
        #[arg(long = "topic")]
        topic_pattern: String,
        #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)]
        dispatch: DispatchModeArg,
    },

    /// Create a queue consumer trigger — fires per message claimed off
    /// the named queue.
    #[command(name = "create-queue")]
    CreateQueue {
        #[arg(long)]
        app: String,
        #[arg(long)]
        script: String,
        #[arg(long = "queue")]
        queue_name: String,
        /// Per-message visibility timeout in seconds (claim lease).
        #[arg(long = "visibility-timeout")]
        visibility_timeout_secs: Option<u32>,
        #[arg(long, value_enum, default_value_t = DispatchModeArg::Async)]
        dispatch: DispatchModeArg,
    },

    /// Create an email trigger — fires on inbound mail POSTed to the
    /// webhook receiver. No dispatch mode (inbound webhook only).
    #[command(name = "create-email")]
    CreateEmail {
        #[arg(long)]
        app: String,
        #[arg(long)]
        script: String,
        /// Shared HMAC secret the provider signs inbound POSTs with.
        /// Omit to accept unsigned POSTs.
        #[arg(long = "inbound-secret")]
        inbound_secret: Option<String>,
    },

    /// Create a trigger of any kind from a raw JSON body — escape
    /// hatch for advanced retry/dispatch settings beyond the per-kind
    /// wrappers' defaults.
    #[command(name = "create-from-json")]
    CreateFromJson {
        #[arg(long)]
        app: String,
        /// Trigger kind: `kv`, `docs`, `cron`, `files`, `pubsub`,
        /// `dead_letter`, `email`, or `queue`.
        #[arg(long)]
        kind: String,
        /// Body JSON. Pass inline, `@<file>`, or `-` for stdin.
        #[arg(long)]
        body: String,
    },
}

#[derive(Clone, Copy, ValueEnum)]
enum TopicAuthModeArg {
    /// No auth required to subscribe.
    Public,
    /// Subscriber bearer token required.
    Token,
    /// Per-app user session required.
    Session,
}

/// Wire form — the topics API accepts the lowercase strings directly.
const fn topic_auth_wire(m: TopicAuthModeArg) -> &'static str {
    match m {
        TopicAuthModeArg::Public => "public",
        TopicAuthModeArg::Token => "token",
        TopicAuthModeArg::Session => "session",
    }
}

#[derive(Subcommand)]
enum TopicsCmd {
    /// List registered topics for an app.
    Ls {
        #[arg(long)]
        app: String,
    },

    /// Register a topic. Defaults to not externally subscribable; pass
    /// `--external` to open it to outside SSE subscribers.
    Create {
        #[arg(long)]
        app: String,
        /// Concrete topic name (no `*` wildcards).
        name: String,
        /// Allow external clients to subscribe over SSE.
        #[arg(long)]
        external: bool,
        /// Auth required of external subscribers. Defaults to `public`.
        #[arg(long = "auth-mode", value_enum, default_value_t = TopicAuthModeArg::Public)]
        auth_mode: TopicAuthModeArg,
    },

    /// Update a topic's external/auth settings. Only the flags you pass
    /// change; the rest are left as-is.
    Update {
        #[arg(long)]
        app: String,
        name: String,
        /// `true` to open external SSE subscription, `false` to close it.
        #[arg(long)]
        external: Option<bool>,
        #[arg(long = "auth-mode", value_enum)]
        auth_mode: Option<TopicAuthModeArg>,
    },

    /// Unregister a topic. Live SSE subscribers are disconnected.
    Rm {
        #[arg(long)]
        app: String,
        name: String,
    },
}

#[derive(Subcommand)]
enum UsersCmd {
    /// List an app's registered end-users.
    Ls {
        #[arg(long)]
        app: String,
        #[arg(long, default_value_t = 50)]
        limit: u32,
    },

    /// Show a single end-user by id.
    Show {
        #[arg(long)]
        app: String,
        user_id: String,
    },

    /// Mint a one-shot password-reset token for an end-user. Printed
    /// exactly once — paste it into a manual reset link.
    #[command(name = "reset-password")]
    ResetPassword {
        #[arg(long)]
        app: String,
        user_id: String,
    },

    /// Revoke all of an end-user's active sessions.
    #[command(name = "revoke-sessions")]
    RevokeSessions {
        #[arg(long)]
        app: String,
        user_id: String,
    },
}

#[derive(Subcommand)]
enum DeadLettersCmd {
    /// Print the unresolved DL count. Cheap probe for alerting /
    /// dashboards — no row payloads, just the bare integer.
    Count {
        #[arg(long)]
        app: String,
    },

    /// List dead-letter rows. Exactly one of `--app` (per-app) or `--group`
    /// (a group's §11.6 shared queues; list-only).
    Ls {
        #[arg(long, conflicts_with = "group")]
        app: Option<String>,
        #[arg(long)]
        group: Option<String>,
        /// Show only unresolved rows.
        #[arg(long)]
        unresolved: bool,
        #[arg(long, default_value_t = 50)]
        limit: u32,
    },

    /// Show a single dead-letter row's full payload.
    Show {
        #[arg(long)]
        app: String,
        dl_id: String,
    },

    /// Re-enqueue the original event so the trigger fires again. The
    /// DL row is marked resolved with reason `replayed`.
    Replay {
        #[arg(long)]
        app: String,
        dl_id: String,
    },

    /// Mark a DL row resolved without replaying. Useful for permanent
    /// failures the operator has already addressed manually.
    Resolve {
        #[arg(long)]
        app: String,
        dl_id: String,
        /// Free-form resolution reason. Required by the server.
        #[arg(long)]
        reason: String,
    },
}

#[derive(Subcommand)]
enum SecretsCmd {
    /// List secret names + last-modified for the owner. Values never
    /// leave the server. `--env` filters group secrets (app secrets are
    /// env-agnostic; the `env` column shows the scope for groups).
    Ls {
        /// Owning group (slug or id). Mutually exclusive with `--app`.
        #[arg(long)]
        group: Option<String>,
        /// Owning app (slug or id). Mutually exclusive with `--group`.
        #[arg(long)]
        app: Option<String>,
        /// Environment scope (group owners only).
        #[arg(long)]
        env: Option<String>,
    },

    /// Set a secret. Reads the value from stdin (the only safe
    /// channel — inline values would land in shell history). Pipe
    /// the value in: `echo -n "mysecret" | pic secrets set --app foo my_key`.
    /// For group owners, `--env` scopes the secret.
    Set {
        /// Owning group (slug or id). Mutually exclusive with `--app`.
        #[arg(long)]
        group: Option<String>,
        /// Owning app (slug or id). Mutually exclusive with `--group`.
        #[arg(long)]
        app: Option<String>,
        name: String,
        /// Environment scope (group owners only).
        #[arg(long)]
        env: Option<String>,
        /// Treat stdin as raw JSON (numbers, maps, …) instead of a
        /// string literal.
        #[arg(long)]
        json: bool,
    },

    /// Delete a secret by name (optionally env-scoped for groups).
    Rm {
        /// Owning group (slug or id). Mutually exclusive with `--app`.
        #[arg(long)]
        group: Option<String>,
        /// Owning app (slug or id). Mutually exclusive with `--group`.
        #[arg(long)]
        app: Option<String>,
        name: String,
        /// Environment scope (group owners only).
        #[arg(long)]
        env: Option<String>,
    },

    /// Fetch and print a secret's PLAINTEXT value. This is the ONLY
    /// command that reveals a secret value, and it is gated server-side
    /// at the owning group — hence `--group` only, no `--app`.
    Read {
        /// Owning group (slug or id).
        #[arg(long)]
        group: String,
        name: String,
        /// Environment scope.
        #[arg(long)]
        env: Option<String>,
    },
}

#[derive(Subcommand)]
enum VarsCmd {
    /// List the owner's OWN vars (not the resolved/inherited view).
    Ls {
        /// Owning group (slug or id). Mutually exclusive with `--app`.
        #[arg(long)]
        group: Option<String>,
        /// Owning app (slug or id). Mutually exclusive with `--group`.
        #[arg(long)]
        app: Option<String>,
    },

    /// Set a var. The value is stored as a JSON string by default; pass
    /// `--json` to parse it as raw JSON. `--tombstone` writes a deletion
    /// marker that suppresses an inherited key.
    Set {
        key: String,
        /// Ignored (but accepted) when `--tombstone` is set.
        #[arg(default_value = "")]
        value: String,
        #[arg(long)]
        group: Option<String>,
        #[arg(long)]
        app: Option<String>,
        /// Environment scope (`*` = env-agnostic, the default).
        #[arg(long)]
        env: Option<String>,
        /// Parse `value` as raw JSON instead of a string literal.
        #[arg(long)]
        json: bool,
        /// Write a tombstone (suppress an inherited key) instead of a value.
        #[arg(long)]
        tombstone: bool,
    },

    /// Delete a var by key (optionally scoped to one environment).
    Rm {
        key: String,
        #[arg(long)]
        group: Option<String>,
        #[arg(long)]
        app: Option<String>,
        #[arg(long)]
        env: Option<String>,
    },
}

#[derive(Subcommand)]
enum AdminsCmd {
    /// List admin accounts.
    Ls,

    /// Create a new admin. Password via `--password -` reads from
    /// stdin (the recommended path for non-interactive use). Without
    /// `--instance-role`, defaults to `admin`.
    Create {
        username: String,
        /// Password. Use `--password -` to read from stdin (interactive
        /// no-echo prompt, or piped). An inline value is rejected — it
        /// would leak into shell history, `ps`, and /proc.
        #[arg(long)]
        password: Option<String>,
        /// `owner`, `admin`, or `member`. Defaults to `admin`.
        #[arg(long = "instance-role", value_enum, default_value_t = InstanceRoleArg::Admin)]
        instance_role: InstanceRoleArg,
        #[arg(long)]
        email: Option<String>,
    },

    /// Show a single admin by id.
    Show { id: String },

    /// Patch an admin. Pass only the fields you want to change.
    Set {
        id: String,
        #[arg(long)]
        username: Option<String>,
        /// Use `--password -` to read from stdin. An inline value is
        /// rejected (it leaks into shell history, `ps`, and /proc).
        #[arg(long)]
        password: Option<String>,
        /// `true` to (re)activate, `false` to deactivate (deactivation
        /// also expires every API key owned by the user).
        #[arg(long)]
        active: Option<bool>,
        #[arg(long = "instance-role", value_enum)]
        instance_role: Option<InstanceRoleArg>,
    },

    /// Delete an admin by id.
    Rm { id: String },
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> ExitCode {
    let cli = Cli::parse();
    let mode = cli.output;
    let result = match cli.cmd {
        Cmd::Login(args) => {
            cmds::login::run(
                args.url.as_deref(),
                args.token.as_deref(),
                args.username.as_deref(),
                args.password_stdin,
            )
            .await
        }
        Cmd::Logout => cmds::logout::run().await,
        Cmd::Whoami => cmds::whoami::run(mode).await,
        Cmd::Apply(args) => match &args.dir {
            Some(dir) => {
                let structure_mode = if args.force_local_structure {
                    "force-local"
                } else if args.adopt_server_structure {
                    "adopt-server"
                } else {
                    "refuse"
                };
                cmds::apply::run_tree(
                    dir,
                    args.prune,
                    args.yes,
                    args.force,
                    args.takeover,
                    structure_mode,
                    &args.approve,
                    args.env.as_deref(),
                    mode,
                )
                .await
            }
            None => {
                cmds::apply::run(
                    &args.file,
                    args.env.as_deref(),
                    args.prune,
                    args.yes,
                    args.force,
                    args.takeover,
                    &args.approve,
                    mode,
                )
                .await
            }
        },
        Cmd::Plan(args) => match &args.dir {
            Some(dir) => cmds::plan::run_tree(dir, args.env.as_deref(), mode).await,
            None => cmds::plan::run(&args.file, args.env.as_deref(), mode).await,
        },
        Cmd::Pull(args) => cmds::pull::run(&args.app, &args.dir, args.force, mode).await,
        Cmd::Config(args) => {
            cmds::config::run(
                &args.file,
                args.effective,
                args.explain,
                args.env.as_deref(),
                mode,
            )
            .await
        }
        Cmd::Init(args) => cmds::init::run(
            &args.dir,
            args.slug.as_deref(),
            args.name.as_deref(),
            args.force,
            mode,
        ),
        Cmd::Apps { cmd: AppsCmd::Ls } => cmds::apps::ls(mode).await,
        Cmd::Apps {
            cmd:
                AppsCmd::Create {
                    slug,
                    name,
                    description,
                    group,
                },
        } => {
            cmds::apps::create(
                &slug,
                name.as_deref(),
                description.as_deref(),
                group.as_deref(),
                mode,
            )
            .await
        }
        Cmd::Apps {
            cmd: AppsCmd::Show { ident },
        } => cmds::apps::show(&ident, mode).await,
        Cmd::Apps {
            cmd: AppsCmd::Delete { ident, force },
        } => cmds::apps::delete(&ident, force).await,
        Cmd::Apps {
            cmd: AppsCmd::Domains {
                cmd: DomainsCmd::Ls { app },
            },
        } => cmds::apps_domains::ls(&app, mode).await,
        Cmd::Apps {
            cmd:
                AppsCmd::Domains {
                    cmd: DomainsCmd::Add { app, pattern },
                },
        } => cmds::apps_domains::add(&app, &pattern, mode).await,
        Cmd::Apps {
            cmd:
                AppsCmd::Domains {
                    cmd: DomainsCmd::Rm { app, domain_id },
                },
        } => cmds::apps_domains::rm(&app, &domain_id).await,
        Cmd::Projects {
            cmd: ProjectsCmd::Ls,
        } => cmds::projects::ls(mode).await,
        Cmd::Groups { cmd: GroupsCmd::Ls } => cmds::groups::ls(mode).await,
        Cmd::Groups {
            cmd: GroupsCmd::Tree,
        } => cmds::groups::tree(mode).await,
        Cmd::Groups {
            cmd:
                GroupsCmd::Create {
                    slug,
                    name,
                    description,
                    parent,
                },
        } => {
            cmds::groups::create(
                &slug,
                name.as_deref(),
                description.as_deref(),
                parent.as_deref(),
                mode,
            )
            .await
        }
        Cmd::Groups {
            cmd: GroupsCmd::Show { ident },
        } => cmds::groups::show(&ident, mode).await,
        Cmd::Groups {
            cmd:
                GroupsCmd::Rename {
                    ident,
                    name,
                    description,
                },
        } => cmds::groups::rename(&ident, name.as_deref(), description.as_deref(), mode).await,
        Cmd::Groups {
            cmd: GroupsCmd::Reparent { ident, to },
        } => cmds::groups::reparent(&ident, to.as_deref(), mode).await,
        Cmd::Groups {
            cmd: GroupsCmd::Rm { ident, recursive },
        } => cmds::groups::rm(&ident, recursive).await,
        Cmd::Groups {
            cmd:
                GroupsCmd::Members {
                    cmd: GroupMembersCmd::Ls { group },
                },
        } => cmds::groups::members_ls(&group, mode).await,
        Cmd::Groups {
            cmd:
                GroupsCmd::Members {
                    cmd:
                        GroupMembersCmd::Add {
                            group,
                            user_id,
                            role,
                        },
                },
        } => cmds::groups::members_add(&group, &user_id, &role, mode).await,
        Cmd::Groups {
            cmd:
                GroupsCmd::Members {
                    cmd:
                        GroupMembersCmd::Set {
                            group,
                            user_id,
                            role,
                        },
                },
        } => cmds::groups::members_set(&group, &user_id, &role, mode).await,
        Cmd::Groups {
            cmd:
                GroupsCmd::Members {
                    cmd: GroupMembersCmd::Rm { group, user_id },
                },
        } => cmds::groups::members_rm(&group, &user_id).await,
        Cmd::Scripts {
            cmd: ScriptsCmd::Ls { app, group },
        } => cmds::scripts::ls(app.as_deref(), group.as_deref(), mode).await,
        Cmd::Scripts {
            cmd: ScriptsCmd::Deploy(args),
        } => match args.script_config() {
            Ok(cfg) => {
                cmds::scripts::deploy(
                    &args.file,
                    args.app.as_deref(),
                    args.group.as_deref(),
                    args.name.as_deref(),
                    args.description.as_deref(),
                    &cfg,
                    mode,
                )
                .await
            }
            Err(e) => Err(e),
        },
        Cmd::Scripts {
            cmd: ScriptsCmd::Invoke(args),
        } => cmds::scripts::invoke(&args.id, args.body.as_deref(), &args.headers).await,
        Cmd::Scripts {
            cmd: ScriptsCmd::Delete { id },
        } => cmds::scripts::delete(&id).await,
        Cmd::ApiKeys {
            cmd:
                ApiKeysCmd::Mint {
                    name,
                    scopes,
                    app,
                    expires,
                },
        } => cmds::api_keys::mint(&name, &scopes, app.as_deref(), expires.as_deref(), mode).await,
        Cmd::ApiKeys {
            cmd: ApiKeysCmd::Ls,
        } => cmds::api_keys::ls(mode).await,
        Cmd::ApiKeys {
            cmd: ApiKeysCmd::Rm { id },
        } => cmds::api_keys::rm(&id).await,
        Cmd::Logs(LogsArgs {
            script_id,
            limit,
            source,
        }) => cmds::logs::run(&script_id, limit, source.as_deref(), mode).await,
        Cmd::Invoke(args) => {
            cmds::scripts::invoke(&args.id, args.body.as_deref(), &args.headers).await
        }
        Cmd::Deploy(args) => match args.script_config() {
            Ok(cfg) => {
                cmds::scripts::deploy(
                    &args.file,
                    args.app.as_deref(),
                    args.group.as_deref(),
                    args.name.as_deref(),
                    args.description.as_deref(),
                    &cfg,
                    mode,
                )
                .await
            }
            Err(e) => Err(e),
        },
        Cmd::Routes {
            cmd: RoutesCmd::Ls { script_id, group },
        } => match (script_id, group) {
            (_, Some(group)) => cmds::routes::ls_group(&group, mode).await,
            (Some(script_id), None) => cmds::routes::ls(&script_id, mode).await,
            (None, None) => Err(anyhow::anyhow!("provide a script id or --group")),
        },
        Cmd::Routes {
            cmd:
                RoutesCmd::Create {
                    script,
                    path,
                    method,
                    path_kind,
                    host,
                    host_kind,
                    host_param_name,
                    dispatch,
                },
        } => {
            cmds::routes::create(
                &script,
                method.as_deref(),
                &path,
                path_kind.into(),
                &host,
                host_kind.into(),
                host_param_name.as_deref(),
                dispatch.into(),
            )
            .await
        }
        Cmd::Routes {
            cmd: RoutesCmd::Rm { route_id },
        } => cmds::routes::rm(&route_id).await,
        Cmd::Routes {
            cmd:
                RoutesCmd::Check {
                    app,
                    path,
                    method,
                    path_kind,
                    host,
                    host_kind,
                },
        } => {
            cmds::routes::check(
                &app,
                method.as_deref(),
                &path,
                path_kind.into(),
                &host,
                host_kind.into(),
                mode,
            )
            .await
        }
        Cmd::Routes {
            cmd: RoutesCmd::Match { app, url, method },
        } => cmds::routes::match_route(&app, &url, &method, mode).await,
        Cmd::Admins { cmd: AdminsCmd::Ls } => cmds::admins::ls(mode).await,
        Cmd::Admins {
            cmd:
                AdminsCmd::Create {
                    username,
                    password,
                    instance_role,
                    email,
                },
        } => {
            cmds::admins::create(
                &username,
                password.as_deref(),
                instance_role.into(),
                email.as_deref(),
                mode,
            )
            .await
        }
        Cmd::Admins {
            cmd: AdminsCmd::Show { id },
        } => cmds::admins::show(&id, mode).await,
        Cmd::Admins {
            cmd:
                AdminsCmd::Set {
                    id,
                    username,
                    password,
                    active,
                    instance_role,
                },
        } => {
            cmds::admins::set(
                &id,
                username.as_deref(),
                password.as_deref(),
                active,
                instance_role.map(Into::into),
                mode,
            )
            .await
        }
        Cmd::Admins {
            cmd: AdminsCmd::Rm { id },
        } => cmds::admins::rm(&id).await,
        Cmd::Workflows {
            cmd: WorkflowsCmd::Ls { app },
        } => cmds::workflows::ls(&app, mode).await,
        Cmd::Workflows {
            cmd: WorkflowsCmd::Run { app, name, input },
        } => cmds::workflows::run(&app, &name, input.as_deref(), mode).await,
        Cmd::Workflows {
            cmd: WorkflowsCmd::Runs { app, name },
        } => cmds::workflows::runs(&app, &name, mode).await,
        Cmd::Workflows {
            cmd: WorkflowsCmd::RunStatus { app, run_id },
        } => cmds::workflows::run_status(&app, &run_id, mode).await,
        Cmd::Triggers {
            cmd: TriggersCmd::Ls { app, group },
        } => match cmds::require_one_owner(app.as_deref(), group.as_deref()) {
            Ok(cmds::OwnerRef::App(a)) => cmds::triggers::ls(a, mode).await,
            Ok(cmds::OwnerRef::Group(g)) => cmds::triggers::ls_group(g, mode).await,
            Err(e) => Err(e),
        },
        Cmd::Triggers {
            cmd: TriggersCmd::Rm { app, trigger_id },
        } => cmds::triggers::rm(&app, &trigger_id).await,
        Cmd::Triggers {
            cmd:
                TriggersCmd::CreateKv {
                    app,
                    script,
                    collection,
                    ops,
                    dispatch,
                },
        } => {
            cmds::triggers::create_kv(
                &app,
                &script,
                &collection,
                &ops,
                dispatch_wire(dispatch),
                mode,
            )
            .await
        }
        Cmd::Triggers {
            cmd:
                TriggersCmd::CreateCron {
                    app,
                    script,
                    schedule,
                    timezone,
                    dispatch,
                },
        } => {
            cmds::triggers::create_cron(
                &app,
                &script,
                &schedule,
                timezone.as_deref(),
                dispatch_wire(dispatch),
                mode,
            )
            .await
        }
        Cmd::Triggers {
            cmd:
                TriggersCmd::CreateDeadLetter {
                    app,
                    script,
                    source_filter,
                    trigger_id_filter,
                    script_id_filter,
                    dispatch,
                },
        } => {
            cmds::triggers::create_dead_letter(
                &app,
                &script,
                source_filter.as_deref(),
                trigger_id_filter.as_deref(),
                script_id_filter.as_deref(),
                dispatch_wire(dispatch),
                mode,
            )
            .await
        }
        Cmd::Triggers {
            cmd:
                TriggersCmd::CreateDocs {
                    app,
                    script,
                    collection,
                    ops,
                    dispatch,
                },
        } => {
            cmds::triggers::create_docs(
                &app,
                &script,
                &collection,
                &ops,
                dispatch_wire(dispatch),
                mode,
            )
            .await
        }
        Cmd::Triggers {
            cmd:
                TriggersCmd::CreateFiles {
                    app,
                    script,
                    collection,
                    ops,
                    dispatch,
                },
        } => {
            cmds::triggers::create_files(
                &app,
                &script,
                &collection,
                &ops,
                dispatch_wire(dispatch),
                mode,
            )
            .await
        }
        Cmd::Triggers {
            cmd:
                TriggersCmd::CreatePubsub {
                    app,
                    script,
                    topic_pattern,
                    dispatch,
                },
        } => {
            cmds::triggers::create_pubsub(
                &app,
                &script,
                &topic_pattern,
                dispatch_wire(dispatch),
                mode,
            )
            .await
        }
        Cmd::Triggers {
            cmd:
                TriggersCmd::CreateQueue {
                    app,
                    script,
                    queue_name,
                    visibility_timeout_secs,
                    dispatch,
                },
        } => {
            cmds::triggers::create_queue(
                &app,
                &script,
                &queue_name,
                visibility_timeout_secs,
                dispatch_wire(dispatch),
                mode,
            )
            .await
        }
        Cmd::Triggers {
            cmd:
                TriggersCmd::CreateEmail {
                    app,
                    script,
                    inbound_secret,
                },
        } => cmds::triggers::create_email(&app, &script, inbound_secret.as_deref(), mode).await,
        Cmd::Triggers {
            cmd: TriggersCmd::CreateFromJson { app, kind, body },
        } => cmds::triggers::create_from_json(&app, &kind, &body, mode).await,
        Cmd::Topics {
            cmd: TopicsCmd::Ls { app },
        } => cmds::topics::ls(&app, mode).await,
        Cmd::Topics {
            cmd:
                TopicsCmd::Create {
                    app,
                    name,
                    external,
                    auth_mode,
                },
        } => cmds::topics::create(&app, &name, external, topic_auth_wire(auth_mode), mode).await,
        Cmd::Topics {
            cmd:
                TopicsCmd::Update {
                    app,
                    name,
                    external,
                    auth_mode,
                },
        } => {
            cmds::topics::update(&app, &name, external, auth_mode.map(topic_auth_wire), mode).await
        }
        Cmd::Topics {
            cmd: TopicsCmd::Rm { app, name },
        } => cmds::topics::rm(&app, &name).await,
        Cmd::Users {
            cmd: UsersCmd::Ls { app, limit },
        } => cmds::users::ls(&app, limit, mode).await,
        Cmd::Users {
            cmd: UsersCmd::Show { app, user_id },
        } => cmds::users::show(&app, &user_id, mode).await,
        Cmd::Users {
            cmd: UsersCmd::ResetPassword { app, user_id },
        } => cmds::users::reset_password(&app, &user_id, mode).await,
        Cmd::Users {
            cmd: UsersCmd::RevokeSessions { app, user_id },
        } => cmds::users::revoke_sessions(&app, &user_id, mode).await,
        Cmd::DeadLetters {
            cmd: DeadLettersCmd::Count { app },
        } => cmds::dead_letters::count(&app, mode).await,
        Cmd::DeadLetters {
            cmd:
                DeadLettersCmd::Ls {
                    app,
                    group,
                    unresolved,
                    limit,
                },
        } => {
            cmds::dead_letters::ls(app.as_deref(), group.as_deref(), unresolved, limit, mode).await
        }
        Cmd::DeadLetters {
            cmd: DeadLettersCmd::Show { app, dl_id },
        } => cmds::dead_letters::show(&app, &dl_id, mode).await,
        Cmd::DeadLetters {
            cmd: DeadLettersCmd::Replay { app, dl_id },
        } => cmds::dead_letters::replay(&app, &dl_id).await,
        Cmd::DeadLetters {
            cmd: DeadLettersCmd::Resolve { app, dl_id, reason },
        } => cmds::dead_letters::resolve(&app, &dl_id, &reason).await,
        Cmd::Secrets {
            cmd: SecretsCmd::Ls { group, app, env },
        } => cmds::secrets::ls(group.as_deref(), app.as_deref(), env.as_deref(), mode).await,
        Cmd::Secrets {
            cmd:
                SecretsCmd::Set {
                    group,
                    app,
                    name,
                    env,
                    json,
                },
        } => {
            cmds::secrets::set(
                group.as_deref(),
                app.as_deref(),
                &name,
                env.as_deref(),
                json,
            )
            .await
        }
        Cmd::Secrets {
            cmd:
                SecretsCmd::Rm {
                    group,
                    app,
                    name,
                    env,
                },
        } => cmds::secrets::rm(group.as_deref(), app.as_deref(), &name, env.as_deref()).await,
        Cmd::Secrets {
            cmd: SecretsCmd::Read { group, name, env },
        } => cmds::secrets::read(&group, &name, env.as_deref()).await,
        Cmd::Members {
            cmd: MembersCmd::Ls { app },
        } => cmds::members::ls(&app, mode).await,
        Cmd::Members {
            cmd: MembersCmd::Add { app, user_id, role },
        } => cmds::members::add(&app, &user_id, &role, mode).await,
        Cmd::Members {
            cmd: MembersCmd::Set { app, user_id, role },
        } => cmds::members::set(&app, &user_id, &role, mode).await,
        Cmd::Members {
            cmd: MembersCmd::Rm { app, user_id },
        } => cmds::members::rm(&app, &user_id).await,
        Cmd::Vars {
            cmd: VarsCmd::Ls { group, app },
        } => cmds::vars::ls(group.as_deref(), app.as_deref(), mode).await,
        Cmd::Vars {
            cmd:
                VarsCmd::Set {
                    key,
                    value,
                    group,
                    app,
                    env,
                    json,
                    tombstone,
                },
        } => {
            cmds::vars::set(
                group.as_deref(),
                app.as_deref(),
                &key,
                &value,
                env.as_deref(),
                json,
                tombstone,
            )
            .await
        }
        Cmd::Vars {
            cmd:
                VarsCmd::Rm {
                    key,
                    group,
                    app,
                    env,
                },
        } => cmds::vars::rm(group.as_deref(), app.as_deref(), &key, env.as_deref()).await,
        Cmd::ExtensionPoints {
            cmd: ExtensionPointsCmd::Ls { app, group },
        } => cmds::extension_points::ls(app.as_deref(), group.as_deref(), mode).await,
        Cmd::Collections {
            cmd: CollectionsCmd::Ls { group },
        } => cmds::collections::ls(&group, mode).await,
        Cmd::Suppress {
            cmd: SuppressCmd::Ls { app, group },
        } => cmds::suppress::ls(app.as_deref(), group.as_deref(), mode).await,
        Cmd::Files {
            cmd:
                FilesCmd::Ls {
                    app,
                    group,
                    collection,
                    limit,
                },
        } => cmds::files::ls(app.as_deref(), group.as_deref(), &collection, limit, mode).await,
        Cmd::Files {
            cmd:
                FilesCmd::Get {
                    app,
                    group,
                    collection,
                    file_id,
                    out,
                },
        } => {
            cmds::files::get(
                app.as_deref(),
                group.as_deref(),
                &collection,
                &file_id,
                out.as_deref(),
            )
            .await
        }
        Cmd::Files {
            cmd:
                FilesCmd::Rm {
                    app,
                    collection,
                    file_id,
                },
        } => cmds::files::rm(&app, &collection, &file_id).await,
        Cmd::Queues {
            cmd: QueuesCmd::Ls { app },
        } => cmds::queues::ls(&app, mode).await,
        Cmd::Queues {
            cmd: QueuesCmd::Show { app, queue_name },
        } => cmds::queues::show(&app, &queue_name, mode).await,
        Cmd::Kv {
            cmd:
                KvCmd::Ls {
                    app,
                    group,
                    collection,
                    limit,
                },
        } => cmds::kv::ls(app.as_deref(), group.as_deref(), &collection, limit, mode).await,
        Cmd::Kv {
            cmd:
                KvCmd::Get {
                    app,
                    group,
                    collection,
                    key,
                },
        } => cmds::kv::get(app.as_deref(), group.as_deref(), &collection, &key).await,
        Cmd::Docs {
            cmd:
                DocsCmd::Ls {
                    group,
                    collection,
                    limit,
                },
        } => cmds::docs::ls(&group, &collection, limit, mode).await,
        Cmd::Docs {
            cmd:
                DocsCmd::Get {
                    group,
                    collection,
                    id,
                },
        } => cmds::docs::get(&group, &collection, &id).await,
    };

    match result {
        Ok(()) => ExitCode::SUCCESS,
        Err(err) => {
            output::print_error(&err);
            ExitCode::FAILURE
        }
    }
}
