#!/usr/bin/env bash
# Copyright (c) The mldsa-native project authors
# Copyright (c) The mlkem-native project authors
# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT

set -o errexit
set -o errtrace
set -o nounset
set -o pipefail

# consts
ROOT="$(realpath "$(dirname "$0")"/../)"
GITHUB_STEP_SUMMARY=${GITHUB_STEP_SUMMARY:-/dev/stdout}

# Check if we're in GitHub context
IN_GITHUB_CONTEXT=false
if [[ $GITHUB_STEP_SUMMARY != "/dev/stdout" ]]; then
  IN_GITHUB_CONTEXT=true
fi

# Standard color definitions
GREEN="\033[32m"
RED="\033[31m"
NORMAL="\033[0m"

# Global error info
ERROR_LOG=""

info()
{
  printf "%b %b\n" "${GREEN}✓" "${NORMAL}${*}"
}

error()
{
  printf "%b %b\n" "${RED}✗" "${NORMAL}${*}"
}

checkerr()
{
  local code=$?
  local title="$1"
  local out="$2"
  local success=true
  if [[ $code == 127 ]]; then
    success=false
  fi

  if [[ ${#out} != 0 ]]; then
    if $IN_GITHUB_CONTEXT; then
      echo "$out" | while read -r file line; do
        echo "::error file=$file,line=${line:-1},title=Format error::$file require to be formatted"
      done
    fi
    success=false
  fi

  if $success; then
    info "$title"
    gh_summary_success "$title"
  else
    error "$title"
    SUCCESS=false
    gh_summary_failure "$title"
  fi
}

gh_group_start()
{
  if $IN_GITHUB_CONTEXT; then
    echo "::group::$1"
  fi
}

gh_group_end()
{
  if $IN_GITHUB_CONTEXT; then
    echo "::endgroup::"
  fi
}

gh_summary_success()
{
  if $IN_GITHUB_CONTEXT; then
    echo ":white_check_mark: $1" >>"$GITHUB_STEP_SUMMARY"
  fi
}

gh_summary_failure()
{
  if $IN_GITHUB_CONTEXT; then
    echo ":x: $1" >>"$GITHUB_STEP_SUMMARY"
    ERROR_LOG+=" - $1"$'\n'
  fi
}

gh_error()
{
  error "$4"
  if $IN_GITHUB_CONTEXT; then
    echo "::error file=$1,line=${2:-1},title=$3::$4"
  fi
}

gh_error_simple()
{
  if $IN_GITHUB_CONTEXT; then
    echo "::error title=$1::$2"
  fi
}

run-shellcheck()
{
  if ! command -v shellcheck >/dev/null; then
    gh_error_simple "Shellcheck missing" "shellcheck is not installed"
    error "Lint shellcheck"
    SUCCESS=false
    gh_summary_failure "Lint shellcheck"
    return 0
  fi

  checkerr "Lint shellcheck" "$(echo $SHELL_SCRIPTS | xargs shellcheck --severity=warning)"
}

# Formatting
SUCCESS=true

# Get list of shell scripts for linting
SHELL_SCRIPTS=$(git grep -l '' :/ | xargs printf "'%s' " | xargs -L 1 shfmt -f)

gh_group_start "Linting nix files with nixpkgs-fmt"
checkerr "Lint nix" "$(nixpkgs-fmt --check "$ROOT")"
gh_group_end

gh_group_start "Linting shell scripts with shfmt"
checkerr "Lint shell" "$(echo $SHELL_SCRIPTS | xargs -L 1 shfmt -s -l -i 2 -ci -fn)"
gh_group_end

gh_group_start "Linting shell scripts with shellcheck"
run-shellcheck
gh_group_end

gh_group_start "Linting GitHub Actions workflows with actionlint"
if ! command -v actionlint >/dev/null; then
  gh_error_simple "actionlint missing" "actionlint is not installed"
  error "Lint GitHub Actions"
  SUCCESS=false
  gh_summary_failure "Lint GitHub Actions"
elif ! actionlint; then
  error "Lint GitHub Actions"
  SUCCESS=false
  gh_summary_failure "Lint GitHub Actions"
else
  info "Lint GitHub Actions"
  gh_summary_success "Lint GitHub Actions"
fi
gh_group_end

gh_group_start "Linting python scripts with ruff (format)"
if ! diff=$(ruff format --check --diff "$ROOT" 2>&1); then
  echo "$diff"
  gh_error_simple "Format error" "$diff"
  error "Lint Python (ruff format)"
  SUCCESS=false
  gh_summary_failure "Lint Python (ruff format)"
else
  info "Lint Python (ruff format)"
  gh_summary_success "Lint Python (ruff format)"
fi
gh_group_end

gh_group_start "Linting python scripts with ruff (check)"
if ! out=$(ruff check "$ROOT" 2>&1); then
  echo "$out"
  gh_error_simple "Lint error" "$out"
  error "Lint Python (ruff check)"
  SUCCESS=false
  gh_summary_failure "Lint Python (ruff check)"
else
  info "Lint Python (ruff check)"
  gh_summary_success "Lint Python (ruff check)"
fi
gh_group_end

gh_group_start "Linting c files with clang-format"
lint-c-files()
{
  for file in $(git ls-files -- ":/*.c" ":/*.h"); do
    # Ignore symlinks
    if [[ ! -L $file ]]; then
      clang-format --Werror --dry-run "$file" 2>&1 | grep "error:" | cut -d ':' -f 1,2 | tr ':' ' '
    fi
  done
}
checkerr "Lint C" "$(lint-c-files)"
gh_group_end

lint-clang-tidy()
{
  if ! command -v clang-tidy >/dev/null; then
    gh_error_simple "clang-tidy missing" "clang-tidy is not installed"
    error "Lint clang-tidy"
    SUCCESS=false
    gh_summary_failure "Lint clang-tidy"
    return 0
  fi

  # Files to analyse, by language. Add directories here to widen coverage;
  # native backends are deliberately excluded (they need per-arch flags).
  # Pathspecs use :(glob) so '*' does not cross '/' (top-level of each dir).
  local c_files=(":(glob)mldsa/src/*.c" ":(glob)mldsa/src/fips202/*.c")
  local h_files=(":(glob)mldsa/src/*.h" ":(glob)mldsa/src/fips202/*.h")

  local nproc success=true
  nproc=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)

  # Run clang-tidy over a set of files. $1 is a label for diagnostics, $2 is a
  # space-separated list of git pathspecs, and the rest are passed verbatim to
  # clang-tidy's compiler invocation (after `--`). Note: not invoked via a pipe
  # so that `success=false` propagates to the enclosing function.
  run-clang-tidy()
  {
    local label="$1" pathspecs="$2" out
    shift 2
    if ! out=$(git ls-files -- $pathspecs |
      xargs -P "$nproc" -I {} \
        clang-tidy --quiet {} -- -I"$ROOT"/mldsa -std=c90 "$@" 2>&1); then
      echo "$out"
      gh_error_simple "clang-tidy error" "clang-tidy reported findings ($label)"
      success=false
    fi
  }

  for params in 44 65 87; do
    # Headers are passed with `-x c` so clang-tidy treats them as C, not C++.
    run-clang-tidy "ML-DSA-$params .c" "${c_files[*]}" \
      -DMLD_CONFIG_PARAMETER_SET="$params"
    run-clang-tidy "ML-DSA-$params .h" "${h_files[*]}" \
      -x c -DMLD_CONFIG_PARAMETER_SET="$params"
  done

  if $success; then
    info "Lint clang-tidy"
    gh_summary_success "Lint clang-tidy"
  else
    error "Lint clang-tidy"
    SUCCESS=false
    gh_summary_failure "Lint clang-tidy"
  fi
}

gh_group_start "Static analysis with clang-tidy"
lint-clang-tidy
gh_group_end

check-eol-dry-run()
{
  for file in $(git ls-files -- ":/" ":/!:*.png"); do
    # Ignore symlinks
    if [[ ! -L $file && $(tail -c1 "$file" | wc -l) == 0 ]]; then
      l=$(wc -l <"$file")
      echo "$file $l"
    fi
  done
}
gh_group_start "Checking eol"
checkerr "Check eol" "$(check-eol-dry-run)"
gh_group_end

check-spdx()
{
  local success=true
  for file in $(git ls-files -- ":/" ":/!:*.json" ":/!:*.png" ":/!:*LICENSE*" ":/!:.git*" ":/!:flake.lock"); do
    # Ignore symlinks
    if [[ ! -L $file && $(grep "SPDX-License-Identifier:" "$file" | wc -l) == 0 ]]; then
      gh_error "$file" "${line:-1}" "Missing license header error" "$file is missing SPDX License header"
      success=false
    fi
  done
  for file in $(git ls-files -- "*.[chsS]" "*.py" "*.mk" "*.yml" "**/Makefile*" ":/!proofs/cbmc/*.py" ":/!examples/bring_your_own_fips202/custom_fips202/tiny_sha3/*" ":/!examples/custom_backend/mldsa_native/src/fips202/native/custom/src/*"); do
    # Ignore symlinks
    if [[ ! -L $file && $(grep "Copyright (c) The mldsa-native project authors" "$file" | wc -l) == 0 ]]; then
      gh_error "$file" "${line:-1}" "Missing copyright header error" "$file is missing copyright header"
      success=false
    fi
  done
  # For source files in dev/* and mldsa/*, we enforce `Apache-2.0 OR ISC OR MIT`
  for file in $(git ls-files -- "*.[chsSi]" | grep "^dev/\|^mldsa/"); do
    # Ignore symlinks
    if [[ ! -L $file && $(grep "SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT" "$file" | wc -l) == 0 ]]; then
      gh_error "$file" "${line:-1}" "Missing license header error" "$file is not licensed under 'Apache-2.0 OR ISC OR MIT'"
      success=false
    fi
  done

  if $success; then
    info "Check SPDX + Copyright"
    gh_summary_success "Check SPDX + Copyright"
  else
    error "Check SPDX + Copyright"
    SUCCESS=false
    gh_summary_failure "Check SPDX + Copyright"

  fi
}
gh_group_start "Checking SPDX + Copyright headers"
check-spdx
gh_group_end

check-autogenerated-files()
{
  if python3 "$ROOT"/scripts/autogen --dry-run; then
    info "Check native auto-generated files"
    gh_summary_success "Check native auto-generated files"
  else
    error "Check native auto-generated files"
    gh_summary_failure "Check native auto-generated files"
    SUCCESS=false
  fi
}

gh_group_start "Check native auto-generated files"
check-autogenerated-files
gh_group_end

check-magic()
{
  if python3 "$ROOT"/scripts/check-magic >/dev/null; then
    info "Check magic constants"
    gh_summary_success "Check magic constants"
  else
    error "Check magic constants"
    gh_summary_failure "Check magic constants"
    SUCCESS=false
  fi
}

gh_group_start "Check magic constants"
check-magic
gh_group_end

check-contracts()
{
  if python3 "$ROOT"/scripts/check-contracts >/dev/null; then
    info "Check contracts"
    gh_summary_success "Check contracts"
  else
    error "Check contracts"
    gh_summary_failure "Check contracts"
    SUCCESS=false
  fi
}

gh_group_start "Check contracts"
check-contracts
gh_group_end

check-doxygen()
{
  if ! command -v doxygen >/dev/null; then
    gh_error_simple "doxygen missing" "doxygen is not installed"
    error "Lint Doxygen comments"
    SUCCESS=false
    gh_summary_failure "Lint Doxygen comments"
    return 0
  fi

  local out
  if out=$(cd "$ROOT" && doxygen scripts/Doxyfile.lint 2>&1) && [[ -z $out ]]; then
    info "Lint Doxygen comments"
    gh_summary_success "Lint Doxygen comments"
  else
    echo "$out"
    gh_error_simple "Doxygen lint error" "$out"
    error "Lint Doxygen comments"
    SUCCESS=false
    gh_summary_failure "Lint Doxygen comments"
  fi
  rm -rf "$ROOT"/.doxylint-xml
}

gh_group_start "Linting Doxygen comments"
check-doxygen
gh_group_end

check-hol-light-imports()
{
  if python3 "$ROOT"/scripts/check-hol-light-imports; then
    info "Check HOL-Light imports"
    gh_summary_success "Check HOL-Light imports"
  else
    error "Check HOL-Light imports"
    gh_summary_failure "Check HOL-Light imports"
    SUCCESS=false
  fi
}

gh_group_start "Check HOL-Light imports"
check-hol-light-imports
gh_group_end

if ! $SUCCESS; then
  if $IN_GITHUB_CONTEXT; then
    printf "%b%s%b\n" "${RED}" "The following checks failed, expand each for more details." "${NORMAL}"
    echo "$ERROR_LOG"
  fi
  exit 1
fi
