///|
priv struct Opts {
  urls : Array[String]
  raw : Bool
  links : Bool
  read_stdin : Bool
  max_bytes : Int
  text_limit : Int
  user_agent : String
}

///|
priv enum ParseResult {
  Parsed(Opts)
  PrintHelp
  UsageError(String)
}

///|
fn usage() -> String {
  let text =
    #|usage: moonx tonyfettes/skills-web-fetch [options]  [...]
    #|
    #|Fetch web pages and print their content. HTML pages are reduced to their
    #|title and main text; other responses are printed raw. Body size is capped
    #|with --max so a huge download cannot flood the output.
    #|
    #|options:
    #|  --raw          print the raw response body instead of extracting text
    #|  --links        also list absolute links found on HTML pages
    #|  --stdin        additionally read one URL per line from stdin
    #|  --max   cap the response body size (default 2000000)
    #|  --text  cap extracted or printed text (default 400000)
    #|  --ua   set the User-Agent header
    #|  -h, --help     show this help
  text
}

///|
fn parse_int_arg(v : String) -> Int? {
  let n : Int = @string.from_str(v) catch { _ => return None }
  Some(n)
}

///|
fn is_flag_arg(s : String) -> Bool {
  s.has_prefix("-")
}

///|
fn is_url_arg(s : String) -> Bool {
  s.contains("://")
}

///|
fn looks_like_program_path(s : String) -> Bool {
  s.contains("/") ||
  s.contains("\\") ||
  s.contains(".exe") ||
  s.contains(".wasm")
}

///|
/// Some runners pass argv[0] (the program path) as the first argument, others
/// do not. Drop a leading non-option, non-URL token that looks like a path so
/// both invocation styles work.
fn args_without_program(args : Array[String]) -> Array[String] {
  if args.is_empty() {
    return args
  }
  let first = args[0]
  if is_flag_arg(first) || is_url_arg(first) || !looks_like_program_path(first) {
    return args
  }
  let rest : Array[String] = []
  for i in 1.. ParseResult {
  let args = args_without_program(raw_args)
  let urls : Array[String] = []
  let mut raw = false
  let mut links = false
  let mut read_stdin = false
  let mut max_bytes = 2_000_000
  let mut text_limit = 400_000
  let mut user_agent = "tonyfettes/skills-web-fetch/0.1"
  let mut i = 0
  while i < args.length() {
    let arg = args[i]
    if arg == "-h" || arg == "--help" {
      return PrintHelp
    }
    if arg == "--raw" {
      raw = true
    } else if arg == "--links" {
      links = true
    } else if arg == "--stdin" {
      read_stdin = true
    } else if arg == "--max" || arg == "--text" || arg == "--ua" {
      if i + 1 >= args.length() {
        return UsageError("missing value for \{arg}")
      }
      i = i + 1
      let value = args[i]
      if arg == "--max" {
        match parse_int_arg(value) {
          Some(n) =>
            if n > 0 {
              max_bytes = n
            } else {
              return UsageError("--max must be positive")
            }
          None => return UsageError("invalid --max value: \{value}")
        }
      } else if arg == "--text" {
        match parse_int_arg(value) {
          Some(n) =>
            if n > 0 {
              text_limit = n
            } else {
              return UsageError("--text must be positive")
            }
          None => return UsageError("invalid --text value: \{value}")
        }
      } else {
        user_agent = value
      }
    } else if arg.has_prefix("--") {
      return UsageError("unknown option: \{arg}")
    } else {
      urls.push(arg)
    }
    i = i + 1
  }
  if urls.is_empty() && !read_stdin {
    return UsageError("no URLs given")
  }
  Parsed({ urls, raw, links, read_stdin, max_bytes, text_limit, user_agent, })
}

///|
fn print_bounded(s : String, limit : Int) -> Unit {
  if s.length() > limit {
    println(s[:limit].to_owned() + "\n[...]")
  } else {
    println(s)
  }
}

///|
fn print_page(opts : Opts, page : @fetch.Page, multiple : Bool) -> Unit {
  if opts.raw {
    if multiple {
      println("== \{page.url} ==")
    }
    println(page.body)
    return
  }
  if multiple {
    println("== \{page.url} ==")
  }
  if @fetch.looks_like_html(page.content_type, page.body) {
    let article = @extract.extract_article(page.body, page.url)
    if !article.title.is_empty() {
      println("Title: \{article.title}")
    }
    if !article.text.is_empty() {
      println("")
      print_bounded(article.text, opts.text_limit)
    } else {
      println("")
      println("(no readable text found)")
    }
    if opts.links && !article.links.is_empty() {
      println("")
      println("Links:")
      for pair in article.links {
        println("- \{pair.0}: \{pair.1}")
      }
    }
  } else {
    print_bounded(page.body, opts.text_limit)
  }
}

///|
async fn run_one(opts : Opts, url : String, multiple : Bool) -> Bool {
  let page = @fetch.fetch_page(
    url,
    max_bytes=opts.max_bytes,
    user_agent=opts.user_agent,
  ) catch {
    @fetch.FetchError::HttpStatus(code, final_url, reason) => {
      println("error: HTTP \{code} \{reason} for \{final_url}")
      return true
    }
    @fetch.FetchError::TooManyRedirects(final_url) => {
      println("error: too many redirects for \{final_url}")
      return true
    }
    @fetch.FetchError::RequestFailed(final_url, detail) => {
      println("error: \{detail} for \{final_url}")
      return true
    }
    @fetch.FetchError::NotText(final_url) => {
      println("error: response body is not valid UTF-8 text for \{final_url}")
      return true
    }
    _ => {
      println("error: unexpected failure while fetching \{url}")
      return true
    }
  }
  print_page(opts, page, multiple)
  false
}

///|
async fn collect_urls(opts : Opts) -> Array[String] {
  if !opts.read_stdin {
    return opts.urls
  }
  let all = opts.urls
  let content = @stdio.stdin.read_all().text() catch { _ => "" }
  for line in content.split("\n") {
    let url = line.trim().to_owned()
    if !url.is_empty() {
      all.push(url)
    }
  }
  all
}

///|
async fn main {
  match parse_args(@env.args()) {
    PrintHelp => println(usage())
    UsageError(message) => {
      println("skills-web-fetch: error: \{message}")
      println("")
      println(usage())
      @sys.exit(2)
    }
    Parsed(opts) => {
      let urls = collect_urls(opts)
      let multiple = urls.length() > 1
      let mut failed = false
      for url in urls {
        if run_one(opts, url, multiple) {
          failed = true
        }
      }
      if failed {
        @sys.exit(1)
      }
    }
  }
}