///|
let crawl_url_option : @admiral.OptionDef[String] = @admiral.string(
  "url",
  description="Target URL",
  env="BW_URL",
  config="url",
)

///|
let crawl_html_option : @admiral.OptionDef[String] = @admiral.string(
  "html",
  description="Path to local HTML file",
  env="BW_HTML",
  config="html",
)

///|
let crawl_wait_until_option : @admiral.OptionDef[String] = @admiral.string(
  "wait-until",
  description="Page load strategy",
  env="BW_WAIT_UNTIL",
  config="wait_until",
)

///|
let crawl_output_option : @admiral.OptionDef[String] = @admiral.string(
  "output",
  short='o',
  description="Output file path",
  env="BW_OUTPUT",
  config="output",
)

///|
let crawl_limit_option : @admiral.OptionDef[Int] = @admiral.int(
  "limit",
  description="Max pages to crawl",
  env="BW_CRAWL_LIMIT",
  config="limit",
)

///|
let crawl_depth_option : @admiral.OptionDef[Int] = @admiral.int(
  "depth",
  description="Max link depth",
  env="BW_CRAWL_DEPTH",
  config="depth",
)

///|
let crawl_format_option : @admiral.OptionDef[Array[String]] = @admiral.strings(
  "format",
  description="Output formats: html,markdown,json",
  env="BW_CRAWL_FORMATS",
  config="formats",
)

///|
let crawl_id_option : @admiral.OptionDef[String] = @admiral.string(
  "id",
  description="Crawl job ID",
  env="BW_CRAWL_ID",
  config="id",
  required=true,
)

///|
struct CrawlStartOptionsInput {
  account_id : String
  api_token : String
  url : String?
  html : String?
  wait_until : String?
  output : String?
  limit : Int?
  depth : Int?
  formats : Array[String]?
}

///|
struct CrawlGetOptionsInput {
  account_id : String
  api_token : String
  id : String
  output : String?
}

///|
struct CrawlGotoOptions {
  waitUntil : String
}

///|
struct CrawlStartBody {
  url : String?
  html : String?
  gotoOptions : CrawlGotoOptions?
  limit : Int?
  depth : Int?
  formats : Array[String]?
}

///|
let crawl_goto_wait_until_lens : @lens.Lens[String] = @lens.root().string(
  "waitUntil",
)

///|
let crawl_body_url_lens : @lens.PresenceLens[String] = @lens.root()
  .string("url")
  .optional()

///|
let crawl_body_html_lens : @lens.PresenceLens[String] = @lens.root()
  .string("html")
  .optional()

///|
let crawl_body_goto_options_lens : @lens.PresenceLens[Json] = @lens.root()
  .json("gotoOptions")
  .optional()

///|
let crawl_body_limit_lens : @lens.PresenceLens[Int] = @lens.root()
  .int("limit")
  .optional()

///|
let crawl_body_depth_lens : @lens.PresenceLens[Int] = @lens.root()
  .int("depth")
  .optional()

///|
let crawl_body_formats_lens : @lens.PresenceLens[Array[String]] = @lens.root()
  .string("formats")
  .array()
  .optional()

///|
impl ToJson for CrawlGotoOptions with fn to_json(self) -> Json {
  let builder = @lens.JsonBuilder::JsonBuilder()
  crawl_goto_wait_until_lens.set_or_abort(builder, self.waitUntil)
  builder.to_json()
}

///|
extend CrawlGotoOptions with ToJson::{to_json}

///|
impl ToJson for CrawlStartBody with fn to_json(self) -> Json {
  let builder = @lens.JsonBuilder::JsonBuilder()
  crawl_body_url_lens.set_or_abort(builder, self.url)
  crawl_body_html_lens.set_or_abort(builder, self.html)
  crawl_body_goto_options_lens.set_or_abort(
    builder,
    self.gotoOptions.map(fn(value) { value.to_json() }),
  )
  crawl_body_limit_lens.set_or_abort(builder, self.limit)
  crawl_body_depth_lens.set_or_abort(builder, self.depth)
  crawl_body_formats_lens.set_or_abort(builder, self.formats)
  builder.to_json()
}

///|
extend CrawlStartBody with ToJson::{to_json}

///|
fn crawl_command_def(
  run_start : async (@admiral.Context) -> Unit,
  run_status : async (@admiral.Context) -> Unit,
  run_results : async (@admiral.Context) -> Unit,
) -> @admiral.CommandDef {
  @admiral.CommandDef::CommandDef(
    name="crawl",
    description="Manage async crawl jobs",
    subcommands=[
      @admiral.CommandDef::CommandDef(
        name="start",
        description="Start a crawl job",
        options=[
          account_id_option, api_token_option, crawl_url_option, crawl_html_option,
          crawl_wait_until_option, crawl_output_option, crawl_limit_option, crawl_depth_option,
          crawl_format_option,
        ],
        run=Some(run_start),
      ),
      @admiral.CommandDef::CommandDef(
        name="status",
        description="Check crawl job status",
        options=[account_id_option, api_token_option, crawl_id_option],
        run=Some(run_status),
      ),
      @admiral.CommandDef::CommandDef(
        name="results",
        description="Retrieve crawl results",
        options=[
          account_id_option, api_token_option, crawl_id_option, crawl_output_option,
        ],
        run=Some(run_results),
      ),
    ],
  )
}

///|
fn split_csv(value : String) -> Array[String] {
  value.split(",").map(fn(item) { item.to_owned() }).collect()
}

///|
async fn crawl_start_options(
  context : @admiral.Context,
) -> CrawlStartOptionsInput {
  let format_values = context.get_strings(crawl_format_option)
  let formats : Array[String] = format_values
    .iter()
    .flat_map(fn(value) { split_csv(value).iter() })
    .collect()
  {
    account_id: context.get_string_required(account_id_option),
    api_token: context.get_string_required(api_token_option),
    url: context.get_string(crawl_url_option),
    html: read_optional_file(context.get_string(crawl_html_option)),
    wait_until: context.get_string(crawl_wait_until_option),
    output: context.get_string(crawl_output_option),
    limit: context.get_int(crawl_limit_option),
    depth: context.get_int(crawl_depth_option),
    formats: if formats.is_empty() {
      None
    } else {
      Some(formats)
    },
  }
}

///|
fn CrawlStartBody::from_options(
  options : CrawlStartOptionsInput,
) -> CrawlStartBody raise {
  required_body_source(options.url, options.html)
  {
    url: options.url,
    html: options.html,
    gotoOptions: match options.wait_until {
      Some(value) => Some({ waitUntil: value })
      None => None
    },
    limit: options.limit,
    depth: options.depth,
    formats: options.formats,
  }
}

///|
async fn run_crawl_start(context : @admiral.Context) -> Unit {
  let options = crawl_start_options(context)
  let body = CrawlStartBody::from_options(options)
  let (code, reason, data) = post_json(
    options.account_id,
    options.api_token,
    "/crawl",
    body.to_json(),
  )
  ensure_success(code, reason, data)
  write_text_or_stdout(options.output, data.text())
}

///|
fn crawl_get_options(context : @admiral.Context) -> CrawlGetOptionsInput raise {
  {
    account_id: context.get_string_required(account_id_option),
    api_token: context.get_string_required(api_token_option),
    id: context.get_string_required(crawl_id_option),
    output: context.get_string(crawl_output_option),
  }
}

///|
async fn run_crawl_get(
  context : @admiral.Context,
  suffix : String,
  output : Bool,
) -> Unit {
  let options = crawl_get_options(context)
  let (code, reason, data) = get_json(
    options.account_id,
    options.api_token,
    "/crawl/\{options.id}\{suffix}",
  )
  ensure_success(code, reason, data)
  if output {
    write_text_or_stdout(options.output, data.text())
  } else {
    println(data.text())
  }
}

///|
async fn run_crawl_status(context : @admiral.Context) -> Unit {
  run_crawl_get(context, "", false)
}

///|
async fn run_crawl_results(context : @admiral.Context) -> Unit {
  run_crawl_get(context, "/results", true)
}