///|
priv suberror CliError {
  CliError(String)
}

///|
/// `Last(n)` selects the final n units; `FromStart(k)` selects from the k-th
/// unit (1-based) to the end, mirroring tail's `-n +K` syntax.
priv enum CountSpec {
  Last(Int)
  FromStart(Int)
}

///|
fn tail_command() -> @argparse.Command {
  Command(
    "tail",
    about="Print the last lines (or bytes) of each input.",
    flags=[
      FlagArg("quiet", short='q', about="Never print file name headers."),
      FlagArg("verbose", short='v', about="Always print file name headers."),
    ],
    options=[
      OptionArg(
        "lines",
        short='n',
        about="Print the last N lines, or from line +N (default 10).",
        allow_hyphen_values=true,
      ),
      OptionArg(
        "bytes",
        short='c',
        about="Print the last N bytes, or from byte +N.",
        conflicts_with=["lines"],
      ),
    ],
    positionals=[
      PositionArg(
        "files",
        about="[file...] ('-' or no file reads stdin)",
        num_args=ValueRange(lower=0),
      ),
    ],
    disable_help_subcommand=true,
  )
}

///|
fn option_value(matches : @argparse.Matches, name : String) -> String? {
  match matches.values.get(name) {
    Some(vals) =>
      if vals.is_empty() {
        None
      } else {
        Some(vals[vals.length() - 1])
      }
    None => None
  }
}

///|
fn parse_spec(text : String) -> CountSpec raise CliError {
  let (from_start, digits) = if text is ['+', .. rest] {
    (true, rest)
  } else {
    (false, text)
  }
  let n = @string.parse_int(digits) catch {
    _ => raise CliError("tail: invalid count: '\{text}'")
  }
  if n < 0 {
    raise CliError("tail: invalid count: '\{text}'")
  }
  if from_start {
    FromStart(if n < 1 { 1 } else { n })
  } else {
    Last(n)
  }
}

///|
/// Select lines as raw bytes so binary or invalid-UTF-8 data passes through
/// unchanged. A trailing newline terminates the final line rather than
/// starting an empty one.
fn select_lines(data : Bytes, spec : CountSpec) -> BytesView {
  match spec {
    Last(n) => {
      if n <= 0 || data.length() == 0 {
        return data[0:0]
      }
      // Scan backward; the trailing newline (if any) does not count as a
      // line boundary of its own.
      let mut i = if data[data.length() - 1] is b'\n' {
        data.length() - 2
      } else {
        data.length() - 1
      }
      let mut newlines = 0
      while i >= 0 {
        if data[i] is b'\n' {
          newlines += 1
          if newlines == n {
            return data[i + 1:]
          }
        }
        i -= 1
      }
      data
    }
    FromStart(k) => {
      if k <= 1 {
        return data
      }
      let mut seen = 0
      for i in 0.. BytesView {
  match spec {
    Last(n) => {
      let start = if n >= data.length() { 0 } else { data.length() - n }
      data[start:]
    }
    FromStart(k) => {
      let start = if k - 1 > data.length() { data.length() } else { k - 1 }
      data[start:]
    }
  }
}

///|
async fn read_source_bytes(path : String) -> Bytes {
  if path == "-" {
    @stdio.stdin.read_all().binary()
  } else {
    @fs.read_file_to_bytes(path)
  }
}

///|
async fn main {
  let args = @env.args()[1:]
  let command = tail_command()
  let matches = command.parse(argv=args, env=Map([])) catch {
    err => {
      @stdio.stderr.write("\{err}\n")
      @sys.exit(2)
      return
    }
  }
  let quiet = matches.flags.get_or_default("quiet", false)
  let verbose = matches.flags.get_or_default("verbose", false)
  let (line_spec, byte_spec) = try {
    let byte_spec : CountSpec? = match option_value(matches, "bytes") {
      Some(text) => Some(parse_spec(text))
      None => None
    }
    let line_spec = match option_value(matches, "lines") {
      Some(text) => parse_spec(text)
      None => Last(10)
    }
    (line_spec, byte_spec)
  } catch {
    CliError(msg) => {
      @stdio.stderr.write("\{msg}\n")
      @sys.exit(2)
      return
    }
  }
  let files = matches.values.get("files").unwrap_or([])
  let sources = if files.is_empty() { ["-"] } else { files }
  let show_headers = (sources.length() > 1 || verbose) && !quiet
  let mut failed = false
  for index, path in sources {
    if show_headers {
      let name = if path == "-" { "standard input" } else { path }
      let prefix = if index > 0 { "\n" } else { "" }
      @stdio.stdout.write("\{prefix}==> \{name} <==\n")
    }
    try {
      let data = read_source_bytes(path)
      match byte_spec {
        Some(spec) => @stdio.stdout.write(select_bytes(data, spec))
        None => @stdio.stdout.write(select_lines(data, line_spec))
      }
    } catch {
      err => {
        @stdio.stderr.write("tail: \{err}\n")
        failed = true
      }
    }
  }
  if failed {
    @sys.exit(1)
  }
}