#!/bin/bash

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

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

# shellcheck source=../share/mediawiki-shell/common
source /usr/share/mediawiki-shell/common

log info "START"

usage() {
  printf '%s\n' "Usage: ${0##*/} [OPTIONS] WIKI BACKUP_DIR
Pushes git-modified .mw files from BACKUP_DIR to the wiki using mw-edit.
Uses 'git diff' to detect which files have changed.

Options:
  --edit-msg=MSG             Edit summary for wiki edits (default: ${default_edit_msg})
  --git-diff-arg=ARG         Argument passed to 'git diff' to select commits
                             (default: HEAD~1, i.e. changes in the last commit)
  --continue-from=N|TITLE    Continue from file index (N) or page title (case-insensitive)
  --keep-going               Continue past per-item failures (record them, exit non-zero)
  --retry                    Skip items already recorded done; retry the rest
  --reset                    Clear any prior state before starting
  --state-file=PATH          Override the state-file location (default: under TMPFOLDER)
  --dry-run                  Preview only; skip the actual write on the server.
Example:
  ${0##*/} 'https://www.kicksecure.com/w' ~/derivative-backup/kicksecure-wiki-backup
  ${0##*/} --git-diff-arg=HEAD~1 'https://www.kicksecure.com/w' ~/derivative-backup/kicksecure-wiki-backup
  ${0##*/} --git-diff-arg=HEAD~1 'https://www.whonix.org/w' ~/derivative-backup/whonix-wiki-backup" >&2
  exit 1
}

default_edit_msg="mediawiki-shell-bot-default-edit-message"
git_diff_arg="HEAD~1"
edit_msg="${default_edit_msg}"
continue_from=""

# shellcheck disable=SC2034  # KEEP_GOING/RETRY/STATE_RESET/STATE_FILE set here, read by common's state_* helpers
while true; do
  case "${1-}" in
    --edit-msg=*)
      edit_msg="${1#--edit-msg=}"
      shift
      ;;
    --git-diff-arg=*)
      git_diff_arg="${1#--git-diff-arg=}"
      shift
      ;;
    --continue-from=*)
      continue_from="${1#--continue-from=}"
      shift
      ;;
    --keep-going)
      KEEP_GOING="true"
      shift
      ;;
    --retry)
      RETRY="true"
      shift
      ;;
    --reset)
      STATE_RESET="true"
      shift
      ;;
    --state-file=*)
      STATE_FILE="${1#--state-file=}"
      shift
      ;;
    --dry-run)
      export DRY_RUN="true"
      shift
      ;;
    -h|--help)
      usage
      ;;
    --)
      shift
      break
      ;;
    -*)
      die 2 "Invalid option: '$1'"
      ;;
    *)
      break
      ;;
  esac
done

if [ -z "${2-}" ]; then
  usage
fi

wiki_url="$1"
backup_dir="$2"

check_vars_exist wiki_url backup_dir

if [ ! -d "${backup_dir}" ]; then
  die 1 "backup_dir '${backup_dir}' does not exist!"
fi

## backup_dir must be inside a git repository.
if ! git -C "${backup_dir}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  die 1 "backup_dir '${backup_dir}' is not inside a git repository!"
fi

log info "wiki_url      : ${wiki_url}"
log info "backup_dir    : ${backup_dir}"
log info "git_diff_arg  : ${git_diff_arg}"
log info "edit_msg      : ${edit_msg}"
log info "continue_from : ${continue_from-}"

state_setup "${wiki_url}"

mw-login-test "${wiki_url}"

## Get list of modified/added .mw files from git diff.
## --diff-filter=AM: only Added or Modified files (not Deleted).
## --name-only: output file names only.
## --relative: paths relative to backup_dir.
git_diff_rc=0
modified_files="$(git -C "${backup_dir}" diff --diff-filter=AM --name-only --relative "${git_diff_arg}" -- '*.mw')" || git_diff_rc=$?
if [ "${git_diff_rc}" != "0" ]; then
  die 1 "git diff failed in '${backup_dir}' (rc ${git_diff_rc}); is --git-diff-arg '${git_diff_arg}' valid?"
fi

if [ -z "${modified_files}" ]; then
  log info "No modified .mw files found. Nothing to push."
  exit 0
fi

## Defense-in-depth: check filenames from git for malicious unicode
## (bidi overrides, homoglyphs, etc.) before decoding them to page titles.
printf '%s\n' "${modified_files}" | unicode-show

counter_total="$(printf '%s\n' "${modified_files}" | wc -l)"
counter_currently=0

log info "Found ${counter_total} modified .mw file(s) to push."

# shellcheck disable=SC2034  # mutated by should_start_processing via nameref
continue_state="no"
while IFS= read -r mw_file; do
  (( counter_currently++ )) || true

  ## Convert filename back to page title.
  ## Uses decode_backup_filename_item() which strips .mw and runs full
  ## percent-decoding (urllib.parse.unquote), reversing set_backup_page_item().
  page_title="$(decode_backup_filename_item "${mw_file}")"

  if ! should_start_processing "${counter_currently}" "${page_title}" "${continue_from}" continue_state; then
    log info "skip ${counter_currently} / ${counter_total} | ${page_title}"
    continue
  fi

  if state_should_skip "${page_title}"; then
    log info "retry-skip ${counter_currently} / ${counter_total} | already done | ${page_title}"
    continue
  fi

  file_path="${backup_dir}/${mw_file}"
  assert_path_within_dir "${backup_dir}" "${file_path}"

  if [ ! -r "${file_path}" ]; then
    log warn "SKIP ${counter_currently} / ${counter_total} | file not readable: ${file_path}"
    continue
  fi

  log info "push ${counter_currently} / ${counter_total} | ${page_title} | ${file_path}"

  push_rc=0
  mw-edit "${wiki_url}" "${file_path}" "${page_title}" "${edit_msg}" || push_rc="$?"
  if [ "${push_rc}" = "0" ]; then
    state_record_done "${page_title}"
  else
    state_handle_failure "${page_title}" "mw-edit rc=${push_rc}"
  fi

done <<< "${modified_files}"

state_finish
if [ "${state_fail_count}" -gt 0 ]; then
  exit 2
fi

## TODO: Should we print some sort of progress message in the failure
## path above too?
log info "Done. Pushed ${counter_currently} / ${counter_total} page(s)."
