///|
/// Discord REST CLI for coding agents, built on `gaato/discord`.
///
/// Run: moonx gaato/discord-cli  [args] with DISCORD_TOKEN set.
/// Every command prints one JSON document on stdout and exits 0. Failures
/// print `error: ...` on stderr and exit 1; usage errors exit 2.
const DEFAULT_MESSAGE_LIMIT : Int = 50

///|
const MAX_PAGE_SIZE : Int = 100

///|
suberror UsageError {
  UsageError(String)
}

///|
fn describe_error(err : Error) -> String {
  match err {
    @dhttp.DiscordHttpError::Api(status~, error~) =>
      "Discord API responded \{status} (code \{error.code}): \{error.message}"
    @dhttp.DiscordHttpError::RateLimited(retry_after_ms~, global~) =>
      "rate limited (global=\{global}); retry after \{retry_after_ms} ms"
    @dhttp.DiscordHttpError::Deserialize(message~) =>
      "could not decode the response: \{message}"
    @dhttp.DiscordHttpError::Transport(message~) =>
      "transport failure: \{message}"
    @dhttp.DiscordHttpError::Timeout(timeout_ms~) =>
      "request timed out after \{timeout_ms} ms"
    @dhttp.DiscordHttpError::Validation(message~) =>
      "invalid request: \{message}"
    _ => err.to_string()
  }
}

///|
fn[M] parse_id(raw : String, what : String) -> @model.Id[M] raise UsageError {
  guard !raw.is_empty() && raw.iter().all(c => c >= '0' && c <= '9') else {
    raise UsageError("\{what} must be a Discord snowflake, got \{raw}")
  }
  @model.Id::parse(raw) catch {
    _ => raise UsageError("\{what} must be a Discord snowflake, got \{raw}")
  }
}

///|
fn[M] optional_id(
  matches : @argparse.Matches,
  name : String,
) -> @model.Id[M]? raise UsageError {
  match matches.values.get(name) {
    Some(values) if values.length() > 0 => Some(parse_id(values[0], name))
    _ => None
  }
}

///|
fn first_value(
  matches : @argparse.Matches,
  name : String,
) -> String raise UsageError {
  match matches.values.get(name) {
    Some(values) if values.length() > 0 => values[0]
    _ => raise UsageError("missing required argument: \{name}")
  }
}

///|
fn parse_method(raw : String) -> @ahttp.RequestMethod raise UsageError {
  match raw.to_upper() {
    "GET" => Get
    "POST" => Post
    "PUT" => Put
    "PATCH" => Patch
    "DELETE" => Delete
    "HEAD" => Head
    "OPTIONS" => Options
    other => raise UsageError("unsupported HTTP method: \{other}")
  }
}

///|
fn parse_command(matches : @argparse.Matches) -> Command raise UsageError {
  match matches.subcommand {
    Some(("me", _)) => Me
    Some(("guilds", _)) => Guilds
    Some(("channels", child)) => {
      let guild_id : @model.GuildId = parse_id(
        first_value(child, "guild_id"),
        "guild_id",
      )
      Channels(guild_id)
    }
    Some(("messages", child)) => {
      let channel_id : @model.ChannelId = parse_id(
        first_value(child, "channel_id"),
        "channel_id",
      )
      let limit = @string.parse_int(first_value(child, "limit")) catch {
        _ => raise UsageError("--limit must be an integer")
      }
      if limit <= 0 {
        raise UsageError("--limit must be positive")
      }
      let before = optional_id(child, "before")
      let after = optional_id(child, "after")
      if before is Some(_) && after is Some(_) {
        raise UsageError("--before and --after cannot be used together")
      }
      Messages(channel_id, limit~, before~, after~)
    }
    Some(("send", child)) => {
      let channel_id : @model.ChannelId = parse_id(
        first_value(child, "channel_id"),
        "channel_id",
      )
      let content = if child.flags.get("stdin") == Some(true) {
        None
      } else {
        match child.values.get("text") {
          Some(values) if values.length() > 0 =>
            Some(validate_content(values.join(" ")))
          _ => raise UsageError("send needs message text or --stdin")
        }
      }
      Send(channel_id, content~, reply_to=optional_id(child, "reply_to"))
    }
    Some(("api", child)) => {
      let request_method = parse_method(first_value(child, "method"))
      let path = first_value(child, "path")
      let body : Json? = match child.values.get("body") {
        Some(values) if values.length() > 0 =>
          Some(
            @json.parse(values[0]) catch {
              _ => raise UsageError("--body must be valid JSON")
            },
          )
        _ => None
      }
      let route = @dhttp.Route::custom(request_method~, path~) catch {
        _ =>
          raise UsageError(
            "invalid route: use a path beginning with / under /api/v10, without // or #",
          )
      }
      Api(route, body)
    }
    _ => raise UsageError("unknown command")
  }
}

///|
fn build_cli() -> @argparse.Command {
  let snowflake_positional = (name : String, about : String) => {
    @argparse.PositionArg(name, about~, num_args=@argparse.ValueRange::single())
  }
  @argparse.Command(
    "discord-cli",
    about="Discord REST CLI for coding agents. Reads the bot token from DISCORD_TOKEN or --token.",
    version="0.1.3",
    subcommand_required=true,
    options=[
      @argparse.OptionArg(
        "token",
        long="token",
        about="Bot token. Defaults to the DISCORD_TOKEN environment variable.",
        env="DISCORD_TOKEN",
        global=true,
        hidden=false,
      ),
    ],
    subcommands=[
      @argparse.Command("me", about="Show the bot user behind the token."),
      @argparse.Command(
        "guilds",
        about="List the guilds the bot belongs to (with member counts).",
      ),
      @argparse.Command("channels", about="List every channel in a guild.", positionals=[
        snowflake_positional("guild_id", "Guild snowflake."),
      ]),
      @argparse.Command(
        "messages",
        about="Fetch recent messages from a channel, newest first.",
        options=[
          @argparse.OptionArg(
            "limit",
            short='n',
            long="limit",
            about="How many messages to fetch.",
            default_values=[DEFAULT_MESSAGE_LIMIT.to_string()],
          ),
          @argparse.OptionArg(
            "before",
            long="before",
            about="Only messages older than this message snowflake.",
          ),
          @argparse.OptionArg(
            "after",
            long="after",
            about="Only messages newer than this message snowflake.",
          ),
        ],
        positionals=[snowflake_positional("channel_id", "Channel snowflake.")],
      ),
      @argparse.Command(
        "send",
        about="Send a message to a channel.",
        flags=[
          @argparse.FlagArg(
            "stdin",
            long="stdin",
            about="Read the message body from stdin instead of the arguments.",
          ),
        ],
        options=[
          @argparse.OptionArg(
            "reply_to",
            long="reply-to",
            about="Reply to this message snowflake.",
          ),
        ],
        positionals=[
          snowflake_positional("channel_id", "Channel snowflake."),
          @argparse.PositionArg(
            "text",
            about="Message text (joined with spaces).",
            num_args=@argparse.ValueRange(lower=0),
            allow_hyphen_values=true,
          ),
        ],
      ),
      @argparse.Command(
        "api",
        about="Call any REST route with the bot token; prints the JSON response.",
        options=[
          @argparse.OptionArg(
            "body",
            short='d',
            long="body",
            about="JSON request body.",
          ),
        ],
        positionals=[
          @argparse.PositionArg(
            "method",
            about="HTTP method (GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS).",
            num_args=@argparse.ValueRange::single(),
          ),
          @argparse.PositionArg(
            "path",
            about="Route path under /api/v10, for example /users/@me/guilds.",
            num_args=@argparse.ValueRange::single(),
          ),
        ],
      ),
    ],
  )
}