#!/usr/bin/env bash

_LIBENV_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
LIMRISTEM_MAIL_CONFIG_DIR=${LIMRISTEM_MAIL_CONFIG_DIR:-"$(cd "$_LIBENV_DIR/.." && pwd)/config"}
# Runtime state written by the unprivileged service account. Kept out of the config
# directory on purpose: config/ holds the env files, the encryption key and the backup
# records the root helpers read back as trusted input, so it must not be group-writable.
# A writable directory lets that account unlink and replace root-owned files inside it,
# whatever mode the files themselves carry.
LIMRISTEM_MAIL_STATE_DIR=${LIMRISTEM_MAIL_STATE_DIR:-/var/lib/limristem-mail/state}

limristem_mail_managed_group() {
  if getent group limristem-mail >/dev/null 2>&1; then
    printf 'limristem-mail\n'
  else
    printf 'root\n'
  fi
}

limristem_mail_prepare_managed_dir() {
  local dir=$1
  local group mode
  group=$(limristem_mail_managed_group)
  case "$dir" in
    # Runtime state: the service account has to create files here, so it is
    # setgid + group-writable.
    "$LIMRISTEM_MAIL_STATE_DIR"|"$LIMRISTEM_MAIL_STATE_DIR"/*)
      mode=2770
      ;;
    # Configuration and secrets: readable by the service account, written only by the
    # root helpers. Deliberately NOT group-writable — see the note at the top of this
    # file: directory write permission is what allows an unlink+replace of the
    # root-owned env files, and those are inputs to every privileged helper.
    "$LIMRISTEM_MAIL_CONFIG_DIR"|"$LIMRISTEM_MAIL_CONFIG_DIR"/*|/etc/limristem-mail.d|/etc/limristem-mail.d/*)
      mode=0750
      ;;
    *)
      mkdir -p -- "$dir"
      return 0
      ;;
  esac
  # In a git development repository workspace, never take over ownership as root or lock directory permissions
  if [[ -e "$_LIBENV_DIR/../.git" || -e "$_LIBENV_DIR/../../.git" ]]; then
    mkdir -p -- "$dir"
    return 0
  fi
  [[ "$group" == "limristem-mail" ]] || mode=0750
  # Runtime descendants can be swapped by the app. Pin every component before
  # root changes its ownership/mode; a pathname check followed by chown races.
  /usr/bin/python3 -I - "$dir" "$mode" "$group" <<'PY'
import grp
import os
from pathlib import PurePosixPath
import sys

path, mode, group = sys.argv[1:]
parts = PurePosixPath(path).parts
if not path.startswith("/") or ".." in parts:
    raise SystemExit("Managed directory must be an absolute, traversal-free path")
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
fd = os.open("/", flags)
try:
    for part in parts[1:]:
        try:
            os.mkdir(part, 0o750, dir_fd=fd)
        except FileExistsError:
            pass
        child = os.open(part, flags, dir_fd=fd)
        os.close(fd)
        fd = child
    os.fchown(fd, 0, grp.getgrnam(group).gr_gid)
    os.fchmod(fd, int(mode, 8))
finally:
    os.close(fd)
PY
}

limristem_mail_prepare_state_dir() {
  limristem_mail_prepare_managed_dir "$LIMRISTEM_MAIL_STATE_DIR"
}

# Preserve a shared map's contents while repairing its permissions. Plain touch,
# chown and chmod follow a symlink planted in the group-writable parent directory.
limristem_mail_prepare_shared_file() {
  /usr/bin/python3 -I - "$1" "$2" "$3" <<'PY'
import grp
import os
from pathlib import PurePosixPath
import stat
import sys

path, mode, group = sys.argv[1:]
parts = PurePosixPath(path).parts
if not path.startswith("/") or len(parts) < 2 or ".." in parts:
    raise SystemExit("Shared file must have an absolute, traversal-free path")
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
parent = os.open("/", flags)
try:
    for part in parts[1:-1]:
        child = os.open(part, flags, dir_fd=parent)
        os.close(parent)
        parent = child
    fd = os.open(parts[-1], os.O_RDONLY | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC, int(mode, 8), dir_fd=parent)
    try:
        info = os.fstat(fd)
        if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
            raise SystemExit("Shared file must be a regular file without hard links")
        os.fchown(fd, 0, grp.getgrnam(group).gr_gid)
        os.fchmod(fd, int(mode, 8))
    finally:
        os.close(fd)
finally:
    os.close(parent)
PY
}

# The API site config includes this snippet unconditionally, so manage-webmail.sh can
# own the webmail routes without editing the site file — which manage-ssl.sh re-renders
# from template on every TLS apply, silently dropping anything patched into it.
# nginx treats a missing non-wildcard include as a fatal config error, so the file has
# to exist even when no webmail is installed.
LIMRISTEM_MAIL_WEBMAIL_NGINX_SNIPPET=${LIMRISTEM_MAIL_WEBMAIL_NGINX_SNIPPET:-/etc/nginx/snippets/limristem-mail-webmail.conf}

limristem_mail_ensure_webmail_nginx_snippet() {
  local snippet=${1:-$LIMRISTEM_MAIL_WEBMAIL_NGINX_SNIPPET}
  [[ -d /etc/nginx ]] || return 0
  mkdir -p "$(dirname "$snippet")" 2>/dev/null || return 0
  if [[ ! -f "$snippet" ]]; then
    {
      printf '# Roundcube webmail locations — generated by manage-webmail.sh.\n'
      printf '# Intentionally empty while no webmail is installed.\n'
    } > "$snippet" 2>/dev/null || return 0
    chmod 0644 "$snippet" 2>/dev/null || true
  fi
}

limristem_mail_install_managed_file() {
  local source_file=$1
  local target_file=$2
  local mode=${3:-0600}
  local group
  group=$(limristem_mail_managed_group)
  install -m "$mode" "$source_file" "$target_file"
  if [[ -e "$_LIBENV_DIR/../.git" || -e "$_LIBENV_DIR/../../.git" ]]; then
    return 0
  fi
  chown root:"$group" "$target_file" 2>/dev/null || true
}

limristem_mail_path_is_writable() {
  local path=$1
  local parent

  if [[ -e "$path" ]]; then
    [[ -w "$path" ]] || return 1
    parent=$(dirname "$path")
  else
    parent=$(dirname "$path")
  fi

  [[ -d "$parent" ]] || return 1

  local probe_file
  if ! probe_file=$(mktemp "$parent/.limristem-mail-write-test.XXXXXX" 2>/dev/null); then
    return 1
  fi
  rm -f "$probe_file"
}

limristem_mail_default_main_env_file() {
  printf '%s/limristem-mail.env\n' "$LIMRISTEM_MAIL_CONFIG_DIR"
}

limristem_mail_default_backup_env_file() {
  printf '%s/limristem-mail-backup.env\n' "$LIMRISTEM_MAIL_CONFIG_DIR"
}

limristem_mail_resolve_managed_config_dir() {
  local default_dir=${LIMRISTEM_MAIL_MANAGED_CONFIG_DIR:-$LIMRISTEM_MAIL_CONFIG_DIR}
  if limristem_mail_path_is_writable "$default_dir/.limristem-mail-managed-config-probe"; then
    printf '%s\n' "$default_dir"
  elif limristem_mail_path_is_writable /etc/limristem-mail.d/.limristem-mail-managed-config-probe; then
    printf '/etc/limristem-mail.d\n'
  else
    printf '%s\n' "$default_dir"
  fi
}

limristem_mail_resolve_main_env_file() {
  local default_file
  default_file=$(limristem_mail_default_main_env_file)

  if [[ -n "${LIMRISTEM_MAIL_ENV_FILE:-}" ]]; then
    printf '%s\n' "$LIMRISTEM_MAIL_ENV_FILE"
  elif limristem_mail_path_is_writable "$default_file"; then
    printf '%s\n' "$default_file"
  elif limristem_mail_path_is_writable /etc/limristem-mail.env; then
    printf '/etc/limristem-mail.env\n'
  else
    printf '%s\n' "$default_file"
  fi
}

limristem_mail_resolve_backup_env_file() {
  local default_file
  default_file=$(limristem_mail_default_backup_env_file)

  if [[ -n "${LIMRISTEM_MAIL_BACKUP_ENV_FILE:-}" ]]; then
    printf '%s\n' "$LIMRISTEM_MAIL_BACKUP_ENV_FILE"
  elif [[ "$(limristem_mail_resolve_main_env_file)" == "/etc/limristem-mail.env" ]] && limristem_mail_path_is_writable /etc/limristem-mail-backup.env; then
    printf '/etc/limristem-mail-backup.env\n'
  elif limristem_mail_path_is_writable "$default_file"; then
    printf '%s\n' "$default_file"
  elif limristem_mail_path_is_writable /etc/limristem-mail-backup.env; then
    printf '/etc/limristem-mail-backup.env\n'
  else
    printf '%s\n' "$default_file"
  fi
}

limristem_mail_resolve_rclone_config_file() {
  local main_env
  main_env=$(limristem_mail_resolve_main_env_file)
  if [[ "$main_env" == "/etc/limristem-mail.env" ]]; then
    printf '/etc/limristem-mail-rclone.conf\n'
  else
    printf '%s/rclone-backup.conf\n' "$LIMRISTEM_MAIL_CONFIG_DIR"
  fi
}

limristem_mail_env_quote() {
  local value=${1-}
  local backtick='`'
  value=${value//\\/\\\\}
  value=${value//\"/\\\"}
  value=${value//\$/\\$}
  value=${value//"$backtick"/\\$backtick}
  value=${value//$'\n'/\\n}
  value=${value//$'\r'/\\r}
  printf '"%s"' "$value"
}

limristem_mail_env_decode() {
  local value=${1-}
  local decoded='' character next index
  if [[ ${#value} -ge 2 && ${value:0:1} == '"' && ${value: -1} == '"' ]]; then
    value=${value:1:${#value}-2}
    # Consume each escape once: sequential replacements corrupt literal \\n
    # and placeholder-like strings in passwords, tokens and remote paths.
    for ((index=0; index<${#value}; index++)); do
      character=${value:index:1}
      if [[ "$character" == '\' && $((index + 1)) -lt ${#value} ]]; then
        next=${value:index+1:1}
        case "$next" in
          n) decoded+=$'\n'; index=$((index + 1)); continue ;;
          r) decoded+=$'\r'; index=$((index + 1)); continue ;;
          '\'|'"'|'$'|'`') decoded+=$next; index=$((index + 1)); continue ;;
        esac
      fi
      decoded+=$character
    done
    value=$decoded
  elif [[ ${#value} -ge 2 && ${value:0:1} == "'" && ${value: -1} == "'" ]]; then
    value=${value:1:${#value}-2}
  elif [[ ${value:0:1} != '"' && ${value:0:1} != "'" ]]; then
    # The shipped examples use whitespace followed by # for inline comments.
    # A hash inside an unquoted secret (without preceding space) remains literal.
    if [[ $value =~ ^(.*)[[:space:]]#.*$ ]]; then
      value=${BASH_REMATCH[1]}
    fi
    while [[ -n "$value" && ${value: -1} =~ [[:space:]] ]]; do value=${value:0:${#value}-1}; done
  fi
  printf '%s' "$value"
}

limristem_mail_env_write_var() {
  local key=$1
  local value=${2-}
  printf '%s=' "$key"
  limristem_mail_env_quote "$value"
  printf '\n'
}

limristem_mail_load_env_file() {
  local file=$1
  local line key value

  [[ -f "$file" ]] || return 0

  while IFS= read -r line || [[ -n "$line" ]]; do
    line=${line%$'\r'}
    [[ $line =~ ^[[:space:]]*# ]] && continue
    [[ $line =~ ^[[:space:]]*$ ]] && continue
    [[ $line =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || continue
    key=${BASH_REMATCH[1]}
    value=${BASH_REMATCH[2]}
    # Command substitution strips trailing newlines unless a marker is appended.
    value=$(limristem_mail_env_decode "$value"; printf '.')
    value=${value%.}
    printf -v "$key" '%s' "$value"
    # shellcheck disable=SC2163  # key intentionally contains the variable name to export.
    export "$key"
  done < "$file"
}

limristem_mail_get_env_value() {
  local file=$1
  local key=$2
  local line value
  if [[ -f "$file" ]]; then
    while IFS= read -r line || [[ -n "$line" ]]; do
      line=${line%$'\r'}
      if [[ $line == "$key="* ]]; then
        value=${line#"$key="}
        value=$(limristem_mail_env_decode "$value"; printf '.')
        value=${value%.}
        printf '%s\n' "$value"
        return 0
      fi
    done < "$file"
  fi
  return 1
}

limristem_mail_upsert_env_value() {
  local file=$1
  local key=$2
  local value=${3-}
  local tmp_file found=no line

  limristem_mail_prepare_managed_dir "$(dirname "$file")"
  tmp_file=$(mktemp)
  if [[ -f "$file" ]]; then
    while IFS= read -r line || [[ -n "$line" ]]; do
      line=${line%$'\r'}
      if [[ $line == "$key="* ]]; then
        limristem_mail_env_write_var "$key" "$value" >> "$tmp_file"
        found=yes
      else
        printf '%s\n' "$line" >> "$tmp_file"
      fi
    done < "$file"
  fi
  if [[ "$found" != "yes" ]]; then
    limristem_mail_env_write_var "$key" "$value" >> "$tmp_file"
  fi
  # 0640: env files hold DB/admin/DNS secrets and feed root helpers (MAIL_HOME,
  # update host allowlist, …). The service account reads them but must not be able
  # to rewrite them; every writer here already runs as root via sudo.
  limristem_mail_install_managed_file "$tmp_file" "$file" 0640
  rm -f "$tmp_file"
}

# Re-run a privileged write command in a transient systemd unit with a fresh,
# fully writable mount namespace. The app service runs with ProtectSystem=strict,
# so helper scripts invoked via sudo inherit a read-only view of /etc and other
# paths; escaping into a transient unit restores normal root write access.
# Usage: limristem_mail_escape_write_namespace <worker-env-flag> <unit-prefix> <original-args...>
# When the escape happens this function never returns (the script exits with the
# transient unit status); otherwise it returns 0 and the caller proceeds normally.
limristem_mail_escape_write_namespace() {
  local worker_flag=$1 unit_prefix=$2
  shift 2
  [[ $EUID -eq 0 ]] || return 0
  if [[ "$(printenv "$worker_flag" 2>/dev/null || true)" == "yes" ]]; then
    return 0
  fi
  [[ -d /run/systemd/system ]] || return 0
  command -v systemd-run >/dev/null 2>&1 || return 0
  local unit_name="${unit_prefix}-$(date +%s)-$$"
  # Propagate the env-resolution variables so the transient unit loads the exact
  # same configuration as this invocation (systemd-run starts with a clean env).
  local env_args=(--setenv="${worker_flag}=yes")
  local var
  for var in LIMRISTEM_MAIL_ENV_FILE LIMRISTEM_MAIL_BACKUP_ENV_FILE LIMRISTEM_MAIL_CONFIG_DIR LIMRISTEM_MAIL_MANAGED_CONFIG_DIR LIMRISTEM_MAIL_STATE_DIR; do
    if [[ -n "${!var:-}" ]]; then
      env_args+=(--setenv="$var=${!var}")
    fi
  done
  local rc
  set +e
  systemd-run --quiet --wait --pipe --collect \
    --unit="$unit_name" \
    "${env_args[@]}" \
    bash "$0" "$@"
  rc=$?
  set -e
  exit "$rc"
}

limristem_mail_normalize_env_file() {
  local file=$1
  local tmp_file line key value

  [[ -f "$file" ]] || return 0
  limristem_mail_path_is_writable "$file" || return 0

  tmp_file=$(mktemp)
  while IFS= read -r line || [[ -n "$line" ]]; do
    line=${line%$'\r'}
    if [[ $line =~ ^[[:space:]]*# ]] || [[ $line =~ ^[[:space:]]*$ ]]; then
      printf '%s\n' "$line" >> "$tmp_file"
      continue
    fi
    if [[ $line =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
      key=${BASH_REMATCH[1]}
      value=${BASH_REMATCH[2]}
      value=$(limristem_mail_env_decode "$value"; printf '.')
      value=${value%.}
      limristem_mail_env_write_var "$key" "$value" >> "$tmp_file"
    else
      printf '%s\n' "$line" >> "$tmp_file"
    fi
  done < "$file"
  # 0640: env files hold DB/admin/DNS secrets and feed root helpers (MAIL_HOME,
  # update host allowlist, …). The service account reads them but must not be able
  # to rewrite them; every writer here already runs as root via sudo.
  limristem_mail_install_managed_file "$tmp_file" "$file" 0640
  rm -f "$tmp_file"
}
