///|
priv struct PreparedPack {
  document : Document
  display : @output.Display
  counter : @metrics.CountSession
  files : Array[FileStats]
  skipped : Array[Skipped]
  findings : Array[Finding]
  security : SecurityStatus
}

///|
/// Build and count a complete document. Remote inputs are fetched and cleaned here.
/// The caller decides where to write the result.
pub async fn pack(options : Options, runtime : Runtime) -> PackResult {
  let prepared = prepare_pack(options, runtime)
  let text = render_document(
    prepared.document,
    options.style,
    options.full_metadata,
    prepared.display,
  )
  @async.sleep(0)
  let tokens = prepared.counter.count(text)
  let budget = match options.token_budget {
    None => NotSet
    Some(limit) if tokens > limit => Exceeded(limit)
    Some(limit) => Within(limit)
  }
  {
    document: prepared.document,
    text,
    bytes: @utf8.encode(text).length(),
    tokens,
    files: prepared.files,
    skipped: prepared.skipped,
    findings: prepared.findings,
    security: prepared.security,
    budget,
  }
}

///|
async fn prepare_pack(options : Options, runtime : Runtime) -> PreparedPack {
  validate_source_limits(options)
  if options.max_git_bytes <= 0 ||
    options.include_logs_count <= 0 ||
    options.include_logs_count > 10000 ||
    options.encoding != "o200k_base" ||
    (options.token_budget is Some(n) && n <= 0) ||
    (options.remote is None && options.remote_branch is Some(_)) {
    raise ConfigurationError
  }
  let keep_full = @selection.file_matcher(options.keep_full_patterns) catch {
    _ => raise ConfigurationError
  }
  let context = prepare_context_texts(options)
  match options.remote {
    None => pack_directory(options, runtime, Json::null(), keep_full, context)
    Some(url) => {
      if options.directory != "." {
        raise ConfigurationError
      }
      let history = if options.include_logs {
        options.include_logs_count
      } else {
        0
      }
      let checkout = try {
        match options.remote_branch {
          None => @git.checkout(url, history~)
          Some(revision) => @git.checkout(url, revision~, history~)
        }
      } catch {
        err => {
          if @async.is_cancellation_error(err) {
            raise err
          }
          raise RemoteError
        }
      }
      defer @git.cleanup(checkout)
      let local_options = {
        ..options,
        directory: checkout.directory,
        remote: None,
        remote_branch: None,
      }
      pack_directory(
        local_options,
        runtime,
        { "kind": "remote", "url": checkout.url, "revision": checkout.revision },
        keep_full,
        context,
      )
    }
  }
}

///|
async fn pack_directory(
  options : Options,
  runtime : Runtime,
  remote_source : Json,
  keep_full : @selection.FileMatcher,
  context : ContextTexts,
) -> PreparedPack {
  let cwd = @env.current_dir().unwrap_or(".")
  let root = @platform.absolute(options.directory, cwd)
  let destination = if options.stdout {
    None
  } else {
    Some(
      @platform.output_path(options.output, cwd) catch {
        err => {
          if @async.is_cancellation_error(err) {
            raise err
          }
          raise ConfigurationError
        }
      },
    )
  }
  let selection_options : @selection.SelectionOptions = {
    include_patterns: options.include_patterns,
    ignore: options.ignore,
    use_gitignore: options.use_gitignore,
    use_dot_ignore: options.use_dot_ignore,
    use_default_patterns: options.use_default_patterns,
    output: destination,
  }
  let selected = @selection.select(
    root,
    selection_options,
    file_paths=options.file_paths,
    max_files=options.max_files,
  ) catch {
    @selection.SelectionError::FileLimitExceeded(limit~) =>
      raise InputLimitExceeded(kind="maxFiles", limit=limit.to_int64())
    err => {
      if @async.is_cancellation_error(err) {
        raise err
      }
      raise SelectionError
    }
  }
  let accepted : Array[@document.File] = []
  let files : Array[FileStats] = []
  let transforms : Array[Json] = []
  let excluded : Set[String] = Set([])
  let skipped = selected.skipped.copy()
  let findings : Array[Finding] = []
  let counter = runtime.encoder.session()
  let pipeline = FilePipeline::new(
    counter,
    options.compress,
    accepted,
    files,
    transforms,
  )
  defer pipeline.close()
  let mut since_timer_poll = 0
  let mut read_bytes = 0L
  for candidate in selected.files {
    // pause keeps the task cancellable without creating an OS timer per file.
    // Periodically poll timers as well, including when filesystem calls finish
    // synchronously, so a busy ready queue cannot starve timeout cancellation.
    if since_timer_poll >= 32 {
      @async.sleep(0)
      since_timer_poll = 0
    } else {
      @async.pause()
      since_timer_poll += 1
    }
    let screened = read_screened(
      candidate,
      options,
      options.max_total_size - read_bytes,
    )
    read_bytes += screened.read_bytes
    findings.append(screened.findings)
    match screened.result {
      Skipped(reason) => {
        skipped.push({ path: candidate.relative, reason, })
        excluded.add(context_path_key(candidate.relative))
      }
      Loaded(snapshot) =>
        pipeline.push(
          candidate.relative,
          snapshot,
          options.compress && keep_full.matches(candidate.relative),
        )
    }
  }
  pipeline.flush()
  pipeline.close()
  let git = git_context(
    selected.root,
    options,
    selection_options,
    excluded,
    skipped,
    findings,
  )
  let metadata : Json = if options.compress ||
    options.include_diffs ||
    options.include_logs ||
    remote_source != Json::null() {
    let source : Json = if remote_source != Json::null() {
      remote_source
    } else {
      let revision : Json = match git.revision {
        None => Json::null()
        Some(id) => Json(id)
      }
      { "kind": "local", "revision": revision }
    }
    { "source": source, "compression": transforms, "git": git.metadata }
  } else {
    Json::null()
  }
  if options.style is Xml &&
    metadata != Json::null() &&
    !@output.xml_representable(metadata.stringify()) {
    raise ConfigurationError
  }
  let document = @document.create(
    accepted,
    sections=git.sections,
    metadata~,
    header_text=context.header,
    instruction_text=context.instructions,
  )
  let display = display_for(options, selected.root, cwd)
  {
    document,
    display,
    counter,
    files,
    skipped,
    findings,
    security: if !options.security_check {
      Disabled
    } else if findings.is_empty() {
      Passed
    } else {
      Excluded
    },
  }
}