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

SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=/dev/null
source "$SCRIPT_DIR/libenv.sh"

STATE_DIR=$(limristem_mail_resolve_managed_config_dir)
MANUAL_BAN_FILE=$STATE_DIR/fail2ban-manual-bans.tsv

usage() {
  cat <<'EOF'
Usage:
  manage-bans.sh list [--json]
  manage-bans.sh ban <ip> [reason]
  manage-bans.sh unban <ip>
  manage-bans.sh apply
EOF
}

require_root() {
  if [[ $EUID -ne 0 ]]; then
    echo "Run as root." >&2
    exit 1
  fi
}

ensure_files() {
  limristem_mail_prepare_managed_dir "$STATE_DIR"
  touch "$MANUAL_BAN_FILE"
  chown root:"$(limristem_mail_managed_group)" "$MANUAL_BAN_FILE" 2>/dev/null || true
  chmod 0660 "$MANUAL_BAN_FILE" 2>/dev/null || true
}

validate_ip() {
  python3 - "$1" <<'PY'
import ipaddress
import sys

ipaddress.ip_address(sys.argv[1])
PY
}

fail2ban_available() {
  command -v fail2ban-client >/dev/null 2>&1 || return 1
  fail2ban-client ping >/dev/null 2>&1
}

require_fail2ban() {
  if ! fail2ban_available; then
    echo "Fail2ban is unavailable." >&2
    exit 1
  fi
}

list_fail2ban_jails() {
  fail2ban_available || return 0
  fail2ban-client status 2>/dev/null \
    | sed -n 's/^.*Jail list:[[:space:]]*//p' \
    | tail -n 1 \
    | tr ',' '\n' \
    | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
    | sed '/^$/d'
}

jail_banned_ips() {
  local jail=$1
  fail2ban_available || return 0
  fail2ban-client status "$jail" 2>/dev/null \
    | sed -n 's/^.*Banned IP list:[[:space:]]*//p' \
    | tail -n 1
}

upsert_manual_ban() {
  local ip=$1
  local reason=$2
  local tmp_file
  tmp_file=$(mktemp)
  trap 'rm -f "$tmp_file"' EXIT
  awk -F'\t' -v ip="$ip" '$2 != ip { print $0 }' "$MANUAL_BAN_FILE" > "$tmp_file" || true
  printf '%s\t%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$ip" "$reason" >> "$tmp_file"
  sort -u -k2,2 "$tmp_file" > "$MANUAL_BAN_FILE"
  rm -f "$tmp_file"
  trap - EXIT
}

remove_manual_ban() {
  local ip=$1
  local tmp_file
  tmp_file=$(mktemp)
  trap 'rm -f "$tmp_file"' EXIT
  awk -F'\t' -v ip="$ip" '$2 != ip { print $0 }' "$MANUAL_BAN_FILE" > "$tmp_file" || true
  cat "$tmp_file" > "$MANUAL_BAN_FILE"
  rm -f "$tmp_file"
  trap - EXIT
}

list_bans_json() {
  local tmp_file jail ip
  tmp_file=$(mktemp)
  trap 'rm -f "$tmp_file"' EXIT
  while IFS= read -r jail; do
    while IFS= read -r ip; do
      [[ -n "${ip:-}" ]] || continue
      printf '%s\t%s\n' "$ip" "$jail" >> "$tmp_file"
    done < <(printf '%s\n' "$(jail_banned_ips "$jail")" | tr ' ' '\n' | sed '/^$/d')
  done < <(list_fail2ban_jails)
  python3 - "$MANUAL_BAN_FILE" "$tmp_file" <<'PY'
import csv
import json
import sys

manual = {}
with open(sys.argv[1], newline="", encoding="utf-8") as handle:
    reader = csv.reader(handle, delimiter="\t")
    for row in reader:
        if len(row) < 2:
            continue
        manual[row[1]] = {
            "created_at": row[0],
            "reason": row[2] if len(row) > 2 else "manual",
        }

items = []
items_by_ip = {}
with open(sys.argv[2], newline="", encoding="utf-8") as handle:
    reader = csv.reader(handle, delimiter="\t")
    for row in reader:
        if len(row) < 2:
            continue
        ip, jail = row[0], row[1]
        if ip not in items_by_ip:
            metadata = manual.get(ip, {})
            items_by_ip[ip] = {
                "created_at": metadata.get("created_at", ""),
                "ip": ip,
                "reason": metadata.get("reason", "fail2ban"),
                "source": "manual, fail2ban" if ip in manual else "fail2ban",
                "jail": "",
                "services": [],
            }
            items.append(items_by_ip[ip])
        if jail and jail not in items_by_ip[ip]["services"]:
            items_by_ip[ip]["services"].append(jail)

for item in items:
    services = sorted(item.get("services", []))
    if item.get("source") == "manual, fail2ban" and not services:
        item["source"] = "manual"
    item["services"] = ", ".join(services) if services else "manual"
    item["jail"] = item["services"]

print(json.dumps(items))
PY
  rm -f "$tmp_file"
  trap - EXIT
}

list_bans() {
  if [[ ${1:-} == "--json" ]]; then
    list_bans_json
    return 0
  fi
  local json_output
  json_output=$(list_bans_json)
  python3 - "$json_output" <<'PY'
import json
import sys

items = json.loads(sys.argv[1])
rows = [["created_at", "ip", "reason", "source", "jail"]]
for item in items:
    rows.append([
        item.get("created_at", ""),
        item.get("ip", ""),
        item.get("reason", ""),
        item.get("source", ""),
        item.get("jail", ""),
    ])
widths = [max(len(row[index]) for row in rows) for index in range(len(rows[0]))]
for row in rows:
    print("  ".join(value.ljust(widths[index]) for index, value in enumerate(row)))
PY
}

ban_ip() {
  local ip=$1
  local reason=${2:-manual}
  local jail_count=0
  local applied_count=0
  validate_ip "$ip"
  require_fail2ban
  reason=${reason//$'\t'/ }
  while IFS= read -r jail; do
    [[ -n "${jail:-}" ]] || continue
    jail_count=$((jail_count + 1))
    if fail2ban-client set "$jail" banip "$ip" >/dev/null 2>&1; then
      applied_count=$((applied_count + 1))
    fi
  done < <(list_fail2ban_jails)
  if [[ $jail_count -eq 0 ]]; then
    echo "No active fail2ban jails found." >&2
    exit 1
  fi
  if [[ $applied_count -eq 0 ]]; then
    echo "Unable to apply the ban to any active fail2ban jail." >&2
    exit 1
  fi
  upsert_manual_ban "$ip" "$reason"
}

unban_ip() {
  local ip=$1
  local jail jail_ip
  validate_ip "$ip"
  remove_manual_ban "$ip"
  if ! fail2ban_available; then
    return 0
  fi
  while IFS= read -r jail; do
    while IFS= read -r jail_ip; do
      [[ -n "${jail_ip:-}" ]] || continue
      if [[ "$jail_ip" != "$ip" ]]; then
        continue
      fi
      fail2ban-client set "$jail" unbanip "$ip" >/dev/null 2>&1 || true
      break
    done < <(printf '%s\n' "$(jail_banned_ips "$jail")" | tr ' ' '\n' | sed '/^$/d')
  done < <(list_fail2ban_jails)
}

apply_manual_bans() {
  require_fail2ban
  local jail ip reason _created
  while IFS=$'\t' read -r _created ip reason; do
    [[ -n "${ip:-}" ]] || continue
    while IFS= read -r jail; do
      [[ -n "${jail:-}" ]] || continue
      fail2ban-client set "$jail" banip "$ip" >/dev/null 2>&1 || true
    done < <(list_fail2ban_jails)
  done < "$MANUAL_BAN_FILE"
}

require_root
ensure_files

command=${1:-}
case "$command" in
  list)
    shift
    list_bans "${1:-}"
    ;;
  ban)
    ban_ip "${2:?ip required}" "${3:-manual}"
    ;;
  unban)
    unban_ip "${2:?ip required}"
    ;;
  apply)
    apply_manual_bans
    ;;
  *)
    usage >&2
    exit 1
    ;;
esac
