#!/usr/bin/env bash
set -euo pipefail

LOG_TAG="harresi-logrotate"
STATE_DIR="${HARRESI_LOG_STATE_DIR:-/var/lib/harresi/logrotate}"
LOCK_FILE="${HARRESI_LOG_LOCK_FILE:-/run/harresi-logrotate.lock}"
PERSISTENT_AUDIT_MOUNT="${HARRESI_AUDIT_MOUNT:-/var/log}"
VOLATILE_LOG_MOUNT="${HARRESI_LOG_MOUNT:-/var/log/volatile}"
MIN_FREE_PERCENT="${HARRESI_LOG_MIN_FREE_PERCENT:-20}"
MIN_FREE_BYTES="${HARRESI_LOG_MIN_FREE_BYTES:-33554432}"
MIN_CHECK_INTERVAL_SEC="${HARRESI_LOG_MIN_CHECK_INTERVAL_SEC:-30}"
VOLATILE_THRESHOLD_PERCENT="${HARRESI_VOLATILE_THRESHOLD_PERCENT:-50}"

GROUP10_CONFIGS=(
  "/etc/harresi/logrotate.d/harresi-logs-10percent"
)
GROUP50_CONFIGS=(
  "/etc/harresi/logrotate.d/harresi-logs-50percent"
  "/etc/logrotate.d/harresi-aide"
)
VOLATILE_CONFIGS=(
  "/etc/harresi/logrotate.d/harresi-logs-volatile"
)

GROUP10_TOTAL_GLOBS=(
  "/var/log/auth.log"
  "/var/log/auth.log.*"
  "/var/log/wtmp"
  "/var/log/wtmp.*"
  "/var/log/btmp"
  "/var/log/btmp.*"
  "/var/log/lastlog"
  "/var/log/lastlog.*"
  "/var/log/harresi/audit/security/harresi_admin.log"
  "/var/log/harresi/audit/security/harresi_admin.log.*"
  "/var/log/harresi/audit/security/harresi_operador.log"
  "/var/log/harresi/audit/security/harresi_operador.log.*"
  "/var/log/harresi/audit/security/harresi_auditor.log"
  "/var/log/harresi/audit/security/harresi_auditor.log.*"
  "/var/log/harresi/audit/security/audit-security.log"
  "/var/log/harresi/audit/security/audit-security.log.*"
  "/var/log/harresi/audit/security/tmux-status/*.log"
  "/var/log/harresi/audit/security/tmux-status/*.log.*"
)
GROUP10_ROTATED_GLOBS=(
  "/var/log/auth.log.*"
  "/var/log/wtmp.*"
  "/var/log/btmp.*"
  "/var/log/lastlog.*"
  "/var/log/harresi/audit/security/harresi_admin.log.*"
  "/var/log/harresi/audit/security/harresi_operador.log.*"
  "/var/log/harresi/audit/security/harresi_auditor.log.*"
  "/var/log/harresi/audit/security/audit-security.log.*"
  "/var/log/harresi/audit/security/tmux-status/*.log.*"
)

GROUP50_TOTAL_GLOBS=(
  "/var/log/harresi/audit/auditd/audit.log"
  "/var/log/harresi/audit/auditd/audit.log.*"
  "/var/log/harresi/audit/product/audit-product.log"
  "/var/log/harresi/audit/product/audit-product.log.*"
  "/var/log/harresi/audit/product/ids-ips-events.log"
  "/var/log/harresi/audit/product/ids-ips-events.log.*"
  "/var/log/harresi/audit/aide/aide.log"
  "/var/log/harresi/audit/aide/aide_history.log"
  "/var/log/harresi/audit/aide/aide.log.*"
  "/var/log/harresi/audit/aide/aide_history.log.*"
)
GROUP50_ROTATED_GLOBS=(
  "/var/log/harresi/audit/auditd/audit.log.*"
  "/var/log/harresi/audit/product/audit-product.log.*"
  "/var/log/harresi/audit/product/ids-ips-events.log.*"
  "/var/log/harresi/audit/aide/aide.log.*"
  "/var/log/harresi/audit/aide/aide_history.log.*"
)

VOLATILE_TOTAL_GLOBS=(
  "/var/log/volatile/nginx/*.log"
  "/var/log/volatile/nginx/*.log.*"
  "/var/log/volatile/harresi_webui.log"
  "/var/log/volatile/harresi_webui.log.*"
  "/var/log/volatile/suricata-monitor.log"
  "/var/log/volatile/suricata-monitor.log.*"
  "/var/log/volatile/suricata/*.log"
  "/var/log/volatile/suricata/*.log.*"
  "/var/log/volatile/suricata/*.json"
  "/var/log/volatile/suricata/*.json.*"
  "/var/log/volatile/suricata/*.pcap"
  "/var/log/volatile/suricata/*.pcap.*"
  "/var/log/volatile/suricata-mitm/*.log"
  "/var/log/volatile/suricata-mitm/*.log.*"
  "/var/log/volatile/suricata-mitm/*.json"
  "/var/log/volatile/suricata-mitm/*.json.*"
  "/var/log/volatile/suricata-mitm/*.pcap"
  "/var/log/volatile/suricata-mitm/*.pcap.*"
  "/var/log/volatile/sslproxy/*.log"
  "/var/log/volatile/sslproxy/*.log.*"
  "/var/log/volatile/suricata-aggregator.log"
  "/var/log/volatile/suricata-aggregator.log.*"
  "/var/log/volatile/suricata-aggregator.service.log"
  "/var/log/volatile/suricata-aggregator.service.log.*"
)
VOLATILE_ROTATED_GLOBS=(
  "/var/log/volatile/nginx/*.log.*"
  "/var/log/volatile/harresi_webui.log.*"
  "/var/log/volatile/suricata-monitor.log.*"
  "/var/log/volatile/suricata/*.log.*"
  "/var/log/volatile/suricata/*.json.*"
  "/var/log/volatile/suricata/*.pcap.*"
  "/var/log/volatile/suricata-mitm/*.log.*"
  "/var/log/volatile/suricata-mitm/*.json.*"
  "/var/log/volatile/suricata-mitm/*.pcap.*"
  "/var/log/volatile/sslproxy/*.log.*"
  "/var/log/volatile/suricata-aggregator.log.*"
  "/var/log/volatile/suricata-aggregator.service.log.*"
)

log_msg() {
  logger -t "$LOG_TAG" -- "$*" 2>/dev/null || true
}

filesystem_stats() {
  local mount_path="$1"
  LC_ALL=C df -P -B1 -- "$mount_path" 2>/dev/null |
    awk 'NR == 2 { print $2, $4; exit }'
}

filesystem_capacity_bytes() {
  local mount_path="$1"
  local capacity="" available=""
  read -r capacity available < <(filesystem_stats "$mount_path") || return 1
  [[ "$capacity" =~ ^[0-9]+$ ]] && printf '%s\n' "$capacity"
}

filesystem_available_bytes() {
  local mount_path="$1"
  local capacity="" available=""
  read -r capacity available < <(filesystem_stats "$mount_path") || return 1
  [[ "$available" =~ ^[0-9]+$ ]] && printf '%s\n' "$available"
}

minimum_free_bytes() {
  local mount_path="$1"
  local capacity percent_min absolute_min
  capacity=$(filesystem_capacity_bytes "$mount_path" || true)
  [[ "$capacity" =~ ^[0-9]+$ ]] || return 1
  [[ "$MIN_FREE_PERCENT" =~ ^[0-9]+$ ]] || MIN_FREE_PERCENT=20
  [[ "$MIN_FREE_BYTES" =~ ^[0-9]+$ ]] || MIN_FREE_BYTES=33554432
  percent_min=$(( capacity * MIN_FREE_PERCENT / 100 ))
  absolute_min="$MIN_FREE_BYTES"
  if (( percent_min > absolute_min )); then
    printf '%s\n' "$percent_min"
  else
    printf '%s\n' "$absolute_min"
  fi
}

threshold_bytes() {
  local percent="$1"
  local mount_path="$2"
  local capacity

  capacity=$(filesystem_capacity_bytes "$mount_path" || true)
  if [[ ! "$capacity" =~ ^[0-9]+$ ]] || (( capacity <= 0 )); then
    log_msg "unable to determine filesystem capacity for $mount_path"
    return 1
  fi

  printf '%s\n' $(( capacity * percent / 100 ))
}

expand_globs() {
  local pattern file

  shopt -s nullglob
  for pattern in "$@"; do
    for file in $pattern; do
      [[ -f "$file" ]] && printf '%s\0' "$file"
    done
  done
  shopt -u nullglob
}

total_size() {
  local total=0
  local file size

  while IFS= read -r -d '' file; do
    size=$(stat -c '%s' -- "$file" 2>/dev/null || printf '0')
    [[ "$size" =~ ^[0-9]+$ ]] || size=0
    total=$(( total + size ))
  done < <(expand_globs "$@")

  printf '%s\n' "$total"
}

oldest_rotated_file() {
  local file mtime
  local oldest_file=""
  local oldest_mtime=""

  while IFS= read -r -d '' file; do
    mtime=$(stat -c '%Y' -- "$file" 2>/dev/null || printf '0')
    [[ "$mtime" =~ ^[0-9]+$ ]] || mtime=0
    if [[ -z "$oldest_mtime" ]] || (( mtime < oldest_mtime )); then
      oldest_mtime="$mtime"
      oldest_file="$file"
    fi
  done < <(expand_globs "$@")

  [[ -n "$oldest_file" ]] && printf '%s\n' "$oldest_file"
}

run_logrotate() {
  local group_name="$1"
  shift

  local config status_file

  for config in "$@"; do
    if [[ ! -f "$config" ]]; then
      log_msg "missing logrotate config for $group_name: $config"
      continue
    fi

    status_file="$STATE_DIR/$(basename "$config").status"
    /usr/sbin/logrotate -s "$status_file" -f "$config" >/dev/null 2>&1 ||
      log_msg "logrotate failed for $group_name using $config"
  done
}

prune_rotated_until_below_threshold() {
  local group_name="$1"
  local threshold="$2"
  local total_globs_name="$3"
  local rotated_globs_name="$4"
  local -n total_globs="$total_globs_name"
  local -n rotated_globs="$rotated_globs_name"
  local total oldest

  while :; do
    total=$(total_size "${total_globs[@]}")
    (( total <= threshold )) && break

    oldest=$(oldest_rotated_file "${rotated_globs[@]}" || true)
    [[ -n "$oldest" ]] || break

    rm -f -- "$oldest" 2>/dev/null || {
      log_msg "unable to remove rotated log $oldest"
      break
    }
    log_msg "$group_name over threshold; removed oldest rotated log $oldest"
  done
}

prune_rotated_until_free() {
  local mount_path="$1"
  local minimum_free="$2"
  local available oldest
  shift 2
  local -a rotated_globs=("$@")

  while :; do
    available=$(filesystem_available_bytes "$mount_path" || true)
    [[ "$available" =~ ^[0-9]+$ ]] || return 0
    (( available >= minimum_free )) && return 0

    oldest=$(oldest_rotated_file "${rotated_globs[@]}" || true)
    [[ -n "$oldest" ]] || return 0

    rm -f -- "$oldest" 2>/dev/null || {
      log_msg "unable to remove rotated log $oldest during free-space recovery"
      return 0
    }
    log_msg "filesystem $mount_path below free-space reserve; removed oldest rotated log $oldest"
  done
}

emergency_free_space_check() {
  local mount_path="$1"
  local group_name="$2"
  local configs_name="$3"
  local rotated_globs_name="$4"
  local -n configs="$configs_name"
  local -n rotated_globs="$rotated_globs_name"
  local minimum_free available

  minimum_free=$(minimum_free_bytes "$mount_path" || true)
  available=$(filesystem_available_bytes "$mount_path" || true)
  [[ "$minimum_free" =~ ^[0-9]+$ && "$available" =~ ^[0-9]+$ ]] || return 0
  (( available >= minimum_free )) && return 0

  log_msg "filesystem $mount_path below free-space reserve (${available}/${minimum_free} bytes); rotating $group_name logs"
  run_logrotate "emergency-$group_name" "${configs[@]}"
  prune_rotated_until_free "$mount_path" "$minimum_free" "${rotated_globs[@]}"

  available=$(filesystem_available_bytes "$mount_path" || true)
  if [[ "$available" =~ ^[0-9]+$ ]] && (( available < minimum_free )); then
    log_msg "filesystem $mount_path remains below free-space reserve after log rotation"
  fi
}

check_group() {
  local group_name="$1"
  local percent="$2"
  local mount_path="$3"
  local configs_name="$4"
  local total_globs_name="$5"
  local rotated_globs_name="$6"
  local -n configs="$configs_name"
  local -n total_globs="$total_globs_name"
  local threshold total

  threshold=$(threshold_bytes "$percent" "$mount_path") || return 0
  total=$(total_size "${total_globs[@]}")

  if (( total >= threshold )); then
    prune_rotated_until_below_threshold "$group_name" "$threshold" "$total_globs_name" "$rotated_globs_name"
    total=$(total_size "${total_globs[@]}")
  fi

  if (( total >= threshold )); then
    log_msg "$group_name reached ${percent}% threshold (${total}/${threshold} bytes); rotating"
    run_logrotate "$group_name" "${configs[@]}"
    prune_rotated_until_below_threshold "$group_name" "$threshold" "$total_globs_name" "$rotated_globs_name"
  fi
}

check_10percent() {
  check_group "harresi-logs-10percent" 10 "$PERSISTENT_AUDIT_MOUNT" GROUP10_CONFIGS GROUP10_TOTAL_GLOBS GROUP10_ROTATED_GLOBS
}

check_50percent() {
  check_group "harresi-logs-50percent" 50 "$PERSISTENT_AUDIT_MOUNT" GROUP50_CONFIGS GROUP50_TOTAL_GLOBS GROUP50_ROTATED_GLOBS
}

check_volatile() {
  check_group "harresi-logs-volatile" "$VOLATILE_THRESHOLD_PERCENT" "$VOLATILE_LOG_MOUNT" VOLATILE_CONFIGS VOLATILE_TOTAL_GLOBS VOLATILE_ROTATED_GLOBS
}

should_run_check() {
  local check_name="$1"
  local now last age check_stamp

  [[ "$MIN_CHECK_INTERVAL_SEC" =~ ^[0-9]+$ ]] || MIN_CHECK_INTERVAL_SEC=30
  check_stamp="$STATE_DIR/last-check-$check_name"
  now=$(printf '%(%s)T' -1)
  last=$(stat -c '%Y' "$check_stamp" 2>/dev/null || printf '0')
  if [[ "$last" =~ ^[0-9]+$ ]]; then
    age=$(( now - last ))
    (( age >= 0 && age < MIN_CHECK_INTERVAL_SEC )) && return 1
  fi

  touch "$check_stamp" 2>/dev/null || {
    log_msg "unable to update check state $check_stamp; skipping this run"
    return 1
  }
  return 0
}

with_lock() {
  local group="$1"

  (
    flock -n 9 || exit 0
    mkdir -p "$STATE_DIR" 2>/dev/null || exit 0
    should_run_check "$group" || exit 0
    case "$group" in
      10)
        check_10percent
        emergency_free_space_check "$PERSISTENT_AUDIT_MOUNT" persistent GROUP10_CONFIGS GROUP10_ROTATED_GLOBS
        ;;
      50)
        check_50percent
        emergency_free_space_check "$PERSISTENT_AUDIT_MOUNT" persistent GROUP50_CONFIGS GROUP50_ROTATED_GLOBS
        ;;
      volatile)
        check_volatile
        emergency_free_space_check "$VOLATILE_LOG_MOUNT" volatile VOLATILE_CONFIGS VOLATILE_ROTATED_GLOBS
        ;;
      all)
        check_10percent
        check_50percent
        check_volatile
        emergency_free_space_check "$PERSISTENT_AUDIT_MOUNT" persistent GROUP10_CONFIGS GROUP10_ROTATED_GLOBS
        emergency_free_space_check "$PERSISTENT_AUDIT_MOUNT" persistent GROUP50_CONFIGS GROUP50_ROTATED_GLOBS
        emergency_free_space_check "$VOLATILE_LOG_MOUNT" volatile VOLATILE_CONFIGS VOLATILE_ROTATED_GLOBS
        ;;
    esac
  ) 9>"$LOCK_FILE"
}

main() {
  local line

  case "${1:-}" in
    --check-10|10) with_lock 10; return 0 ;;
    --check-50|50) with_lock 50; return 0 ;;
    --check-volatile|volatile) with_lock volatile; return 0 ;;
    --check-all|all) with_lock all; return 0 ;;
  esac

  while IFS= read -r line; do
    case "$line" in
      *harresi_logrotate_10percent*) with_lock 10 ;;
      *harresi_logrotate_50percent*) with_lock 50 ;;
      *harresi_logrotate_volatile*) with_lock volatile ;;
    esac
  done
}

main "$@"
