#!/bin/bash

## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## AI-Assisted

## See text-safety-scan(1) for the full description, options,
## examples, and security model.

set -o errexit
set -o nounset
set -o errtrace
set -o pipefail
shopt -s inherit_errexit
shopt -s shift_verbose

# shellcheck source=../libexec/helper-scripts/log_run_die.sh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/log_run_die.sh
# shellcheck source=../libexec/helper-scripts/strings.bsh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/strings.bsh
# shellcheck source=../libexec/helper-scripts/has.bsh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/has.bsh

log_level=info

PLUGIN_LIST=( 'unicode-show' 'modeline-show' )

declare -i plugins_run=0
declare -i plugins_findings=0
declare -i plugins_errors=0
declare -i internal_errors=0
declare -i files_scanned=0
declare -i files_with_findings=0
declare -i files_with_errors=0
declare -i overall_exit=0

captured_stdin_file=""
declare -a remaining_args=()

usage() {
  cat <<'EOF'
Usage: text-safety-scan [PATH...]
       text-safety-scan < input

Scan one or more files (or stdin) using all known plugins.
All plugins run for every input even if an earlier plugin reports
a finding.

Options:
  -h, --help     Show this help and exit.
  --             End of options; remaining arguments are paths.
                 Paths whose first character is '-' are supported via
                 this marker, e.g.
                     text-safety-scan -- --weird-name

Exit codes:
  0  clean
  1  at least one finding
  2  at least one internal error (unreadable file, plugin not found,
     target is a directory, etc.)

To scan directories, use text-safety-scan-find.
EOF
}

# shellcheck disable=SC2317
cleanup() {
  local _exit_code="${1:-$?}"
  trap - EXIT ERR
  if [ -n "${captured_stdin_file}" ]; then
    safe-rm --force -- "${captured_stdin_file}" || true
  fi
  if [ "${_exit_code}" = "0" ]; then
    log notice "OK (${files_scanned} file(s), ${plugins_run} plugin run(s))"
  else
    log error "FAIL (${plugins_findings} finding(s), ${plugins_errors} plugin error(s), ${internal_errors} internal error(s); ${files_with_findings} of ${files_scanned} scanned file(s) flagged, ${files_with_errors} file(s) with error(s))"
  fi
  exit "${_exit_code}"
}

# shellcheck disable=SC2317
on_err() {
  local rc=$? line="${BASH_LINENO[0]}" cmd="${BASH_COMMAND}"
  printf '%s\n' "ERROR: ${BASH_SOURCE[0]##*/}: line ${line}: command failed (rc=${rc}): ${cmd}" >&2
}
trap on_err ERR
trap 'cleanup' EXIT

## Bump 'overall_exit' to the highest severity seen so far.
bump_exit() {
  local new="$1"
  if [ "${new}" -gt "${overall_exit}" ]; then
    overall_exit="${new}"
  fi
}

## Internal-error helper: log + bump counter + bump overall_exit.
internal_error() {
  log error "$*"
  internal_errors=$(( internal_errors + 1 ))
  bump_exit 2
}

## Check for required external tools once at startup.
check_deps() {
  local cmd
  for cmd in safe-rm sponge "${PLUGIN_LIST[@]}"; do
    if ! has "${cmd}" >/dev/null 2>&1; then
      internal_error "required command not found on PATH: ${cmd}"
    fi
  done
  if [ "${overall_exit}" -ge 2 ]; then
    exit "${overall_exit}"
  fi
}

## Run every plugin in PLUGIN_LIST against a single target.
run_plugins_for_target() {
  local mode="$1"
  local file_name="${2:-}"
  local plugin plugin_exit target_label
  local had_finding='false' had_error='false'

  if [ "${mode}" = "stdin" ]; then
    target_label="stdin"
  else
    target_label="$(string_quote_safe "${file_name}")"
  fi

  files_scanned=$(( files_scanned + 1 ))

  for plugin in "${PLUGIN_LIST[@]}"; do
    plugins_run=$(( plugins_run + 1 ))
    plugin_exit=0
    if [ "${mode}" = "stdin" ]; then
      "${plugin}" < "${captured_stdin_file}" || plugin_exit=$?
    else
      "${plugin}" "${file_name}" || plugin_exit=$?
    fi

    case "${plugin_exit}" in
      0)
        log info "${plugin}: ${target_label} OK"
        ;;
      1)
        plugins_findings=$(( plugins_findings + 1 ))
        had_finding='true'
        log warn "${plugin}: ${target_label} finding (exit 1)"
        bump_exit 1
        ;;
      *)
        plugins_errors=$(( plugins_errors + 1 ))
        had_error='true'
        log error "${plugin}: ${target_label} error (exit ${plugin_exit})"
        bump_exit 2
        ;;
    esac
  done

  if [ "${had_finding}" = 'true' ]; then
    files_with_findings=$(( files_with_findings + 1 ))
  fi
  if [ "${had_error}" = 'true' ]; then
    files_with_errors=$(( files_with_errors + 1 ))
  fi
}

parse_options() {
  while [ "$#" -gt 0 ]; do
    case "$1" in
      -h|--help)
        usage
        trap - EXIT
        exit 0
        ;;
      --)
        shift
        break
        ;;
      -*)
        usage >&2
        die 2 "unknown option: '$1'"
        ;;
      *)
        break
        ;;
    esac
  done
  remaining_args=( "$@" )
}

main() {
  check_deps
  parse_options "$@"
  set -- "${remaining_args[@]}"

  if [ "$#" -eq 0 ]; then
    if ! [ -e '/proc/self/fd/0' ]; then
      die 2 'stdin is not open!'
    fi
    captured_stdin_file="$(mktemp)" || die 2 'mktemp failed for stdin capture!'
    if ! sponge -- "${captured_stdin_file}"; then
      die 2 'failed to capture stdin'
    fi
    run_plugins_for_target stdin
  else
    local target
    for target in "$@"; do
      if [ ! -e "${target}" ]; then
        internal_error "target '$(string_quote_safe "${target}")' does not exist"
        continue
      fi
      if [ -d "${target}" ]; then
        internal_error "target '$(string_quote_safe "${target}")' is a directory; use text-safety-scan-find"
        continue
      fi
      run_plugins_for_target file "${target}"
    done
  fi
}

main "$@"

exit "${overall_exit}"
