#!/bin/bash

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

## AI-Assisted

set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose
export LC_ALL=C

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

error() {
   printf '%s\n' "ERROR: $*" >&2
   exit 2
}

print_usage() {
  cat <<EOF
Emit a reproducibility buildinfo file for a built image. This is a small,
signed Deb822 file modeled on Debian '.buildinfo' conventions. It records the
parameters needed to reproduce the image so a verifier can rebuild it
trivially. The build metadata placed in the file is read from the environment.

This script intentionally does not record checksums or sign files;
'dm-prepare-release' does both already.

Usage:
  dm-reproducible-buildinfo --target TARGET --image IMAGE_PATH [--output FILE]

Exit codes: 0 for success, 2 for a usage/environment error.
EOF
}

target=""
image=""
output=""
do_force="false"

while [ "$#" -gt 0 ]; do
   case "${1:-}" in
      --target)
         [ "$#" -ge 2 ] || error "--target requires a value."
         target="$2"
         shift 2
         ;;
      --image)
         [ "$#" -ge 2 ] || error "--image requires a value."
         image="$2"
         shift 2
         ;;
      --output)
         [ "$#" -ge 2 ] || error "--output requires a value."
         output="$2"
         shift 2
         ;;
      --force)
         do_force='true'
         shift
         ;;
      -h|--help)
         print_usage
         exit 0
         ;;
      *)
         error "unknown argument: '$1'."
         ;;
   esac
done

[ -n "${target}" ] || error "--target is required."
[ -n "${image}" ]  || error "--image is required."
if [ "${do_force}" = 'true' ]; then
   [ -f "${image}" ] || printf '%s\n' "INFO: --force: '${image}' does not exist, but emitting the record anyway." >&2
else
   [ -f "${image}" ] || error "image does not exist: '${image}'"
fi
[ -n "${output}" ] || output="${image}.dm-buildinfo"

val() {
   if ! check_variable_name "${1:-}"; then
      printf '%s' "unknown"
      return 0
   fi

   local v="${!1:-}"
   printf '%s' "${v:-unknown}"
}

## In CI and AI agent builds, sign-and-tag is sometimes run, which amends repo
## HEAD commits. We record the pre-amend state in dm_source_state_file.
source_state_file="${dm_source_state_file:-}"
if [ -z "${source_state_file}" ] && [ -n "${binary_build_folder_dist:-}" ]; then
   source_state_file="${binary_build_folder_dist}/dm-source-state"
fi

state_unrecorded() {
   printf '%s\n' "Source-Commit: unrecorded ($1)" "Submodule-State: unrecorded"
}

state_from_head() {
   local repo toplevel head submodules
   repo="${source_code_folder_dist:-}"
   [ -n "${repo}" ] || return 1

   ## Make sure the repo dir is itself a Git repo.
   toplevel="$(git -C "${repo}" rev-parse --show-toplevel 2>/dev/null)" || return 1
   [ -n "${toplevel}" ] || return 1
   [ "${repo}" -ef "${toplevel}" ] || return 1

   head="$(git -C "${repo}" rev-parse --verify HEAD 2>/dev/null)" || return 1
   [ -n "${head}" ] || return 1

   submodules="$(git -C "${repo}" submodule status --recursive 2>/dev/null)" || return 1
   if [ -n "${submodules}" ]; then
      ## Leading space per line keeps each gitlink a Deb822 continuation of
      ## Submodule-State.
      submodules="$(printf '%s\n' "${submodules}" | sed -e 's/^[[:space:]]*/ /')"
   fi
   printf '%s\n' "Source-Commit: ${head}"
   printf '%s\n' "Submodule-State:"
   [ -z "${submodules}" ] || printf '%s\n' "${submodules}"
}

source_state_block=""
head_block=""
if [ -z "${source_state_file}" ] || [ ! -f "${source_state_file}" ] || [ ! -r "${source_state_file}" ]; then
   if head_block="$(state_from_head)" && [ -n "${head_block}" ]; then
      source_state_block="${head_block}"
   elif [ -z "${source_state_file}" ]; then
      source_state_block="$(state_unrecorded "neither dm_source_state_file nor binary_build_folder_dist is set, and no source_code_folder_dist git tree to read HEAD from")"
   else
      source_state_block="$(state_unrecorded "help-steps/sign-and-tag did not run, and no source_code_folder_dist git tree to read HEAD from")"
   fi
else
   source_state_candidate="$(cat -- "${source_state_file}")"
   state_reject=""
   case "${source_state_candidate}" in
      "")
         state_reject="${source_state_file} is empty"
         ;;
      *$'\n\n'*)
         state_reject="${source_state_file} contains a blank line, which would terminate this Deb822 record"
         ;;
   esac
   if [ -z "${state_reject}" ] \
      && [[ "${source_state_candidate}" =~ ^$'\n' ]]; then
      ## A blank line at the beginning of the file would cause record
      ## termination too.
      state_reject="${source_state_file} contains a blank line, which would terminate this Deb822 record"
   fi
   if [ -z "${state_reject}" ]; then
      case "${source_state_candidate}" in
         "Source-Commit: "*)
            true
            ;;
         *)
            state_reject="${source_state_file} does not start with a Source-Commit field"
            ;;
      esac
   fi
   if [ -z "${state_reject}" ]; then
      case "${source_state_candidate}" in
         *$'\n'"Submodule-State:"*)
            true
            ;;
         *)
            state_reject="${source_state_file} carries no Submodule-State field"
            ;;
      esac
   fi
   if [ -n "${state_reject}" ]; then
      source_state_block="$(state_unrecorded "${state_reject}")"
   else
      source_state_block="${source_state_candidate}"
   fi
fi

## Source-Version and Source-Commit name different commits when sign-and-tag
## is used as part of a build. Do not assert they agree.

## Record if --reproducible-dist-build-version was used, so no one confuses a
## commit-built image with a release image.
version_normalized_field=""
if [ "${dist_build_version_reproducible:-}" = "true" ]; then
   version_normalized_field="
Version-Normalized: true"
   if [ -n "${dist_build_version_unnormalized:-}" ]; then
      version_normalized_field="${version_normalized_field}
Source-Version-Unnormalized: ${dist_build_version_unnormalized}"
   fi
fi

printf '%s\n' "\
Format: 1.0
Buildinfo-Type: derivative-image
Source-Repo: $(val project_clearnet)
${source_state_block}
Source-Version: $(val dist_build_version)${version_normalized_field}
Flavor: $(val dist_build_flavor)
Target: ${target}
Build-Type: $(val dist_build_type)
Architecture: $(val dist_build_target_arch)
Freedom: $(val build_freedom_only)
Debian-Suite: $(val dist_build_apt_stable_release)
APT-Snapshot: $(val dist_build_apt_sources_mirror)
Source-Date-Epoch: $(val SOURCE_DATE_EPOCH)
Image-File: $(basename -- "${image}")
" > "${output}"

printf '%s\n' "INFO: wrote buildinfo -> ${output}" >&2
printf '%s\n' "INFO: register '${output}' with dm-prepare-release so it is signed alongside the image." >&2
