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

###############################################################################
# FileWave Smart Disk Expansion (filewave-expand-disk.sh)
# Features:
# - LVM byte outputs forced to integers (no scientific notation)
# - PV device list deduplicated
# - Clean terminal output by default; --verbose shows command output
# - Smart tool detection and installation
###############################################################################

DRY_RUN=0
REBOOT=0
VERBOSE=0

usage() {
  cat <<'EOF'
Usage: sudo ./filewave-expand-disk.sh [options]

Options:
  --dry-run      Print planned actions; do not modify anything
  --reboot       Reboot at the end
  --verbose      Show command output in terminal (still logs everything)
  -h, --help     Show help

Notes:
- Must be run as root.
- Log file is written to /var/log and made world-readable.
- Required tools will be installed automatically if missing.
EOF
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --dry-run)  DRY_RUN=1; shift ;;
    --reboot)   REBOOT=1; shift ;;
    --verbose)  VERBOSE=1; shift ;;
    -h|--help)  usage; exit 0 ;;
    *)
      echo "ERROR: Unknown argument: $1" >&2
      usage >&2
      exit 2
      ;;
  esac
done

if [[ "${EUID}" -ne 0 ]]; then
  cat >&2 <<'EOF'
ERROR: This script must be run as root.

Please re-run using sudo:
  sudo ./filewave-expand-disk.sh [options]
EOF
  exit 1
fi

ts="$(date -u +%Y%m%d-%H%M%S)"
LOG_FILE="/var/log/filewave-expand-disk-${ts}.log"
touch "$LOG_FILE"
chmod 0644 "$LOG_FILE"

echo "Log file: $LOG_FILE"
echo "If opening a support case, attach: $LOG_FILE"
echo

log() {
  printf '[%s] %s\n' "$(date -u +%FT%TZ)" "$*" | tee -a "$LOG_FILE" >/dev/null
}

run() {
  log "+ $*"
  if [[ "$DRY_RUN" -eq 1 ]]; then
    log "DRY_RUN: command not executed"
    return 0
  fi

  if [[ "$VERBOSE" -eq 1 ]]; then
    "$@" 2>&1 | tee -a "$LOG_FILE"
  else
    "$@" >>"$LOG_FILE" 2>&1
  fi
}

section() {
  echo "== $1 =="
  echo
  log "== $1 =="
}

human_bytes() {
  local b="${1:-0}"
  if command -v numfmt >/dev/null 2>&1; then
    numfmt --to=iec --suffix=B "$b" 2>/dev/null || echo "${b}B"
  else
    echo "${b}B"
  fi
}

# Force LVM to give integer bytes (no suffix, no sci-notation)
lvm_bytes() {
  # Usage: lvm_bytes "vgs args..." OR "lvs args..."
  # Must return an integer or 0.
  local out
  out="$("$@" 2>/dev/null | tr -d ' ' | head -n1 || true)"
  # empty -> 0
  [[ -z "$out" ]] && { echo 0; return; }
  # defensive: strip anything non-digit (should already be digits)
  echo "$out" | tr -cd '0-9'
}

section "Checking and installing prerequisites"
log "dry_run=$DRY_RUN reboot=$REBOOT verbose=$VERBOSE log_file=$LOG_FILE"

# Check for required tools and install if missing
check_and_install_tools() {
  local missing=()
  
  # Check each required command
  for cmd in growpart pvresize lvextend resize2fs xfs_growfs sgdisk partx blockdev udevadm lsblk lvs vgs df findmnt numfmt; do
    if ! command -v "$cmd" >/dev/null 2>&1; then
      missing+=("$cmd")
    fi
  done
  
  if [[ ${#missing[@]} -gt 0 ]]; then
    echo "Missing required tools: ${missing[*]}"
    log "Missing tools: ${missing[*]}. Installing prerequisite packages..."
    
    run apt-get update -y
    # cloud-guest-utils provides growpart
    # lvm2 provides lvs, vgs, lvextend, pvresize
    # util-linux provides blockdev, lsblk, findmnt
    # gdisk provides sgdisk
    # e2fsprogs provides resize2fs
    # xfsprogs provides xfs_growfs
    run apt-get install -y cloud-guest-utils lvm2 util-linux gdisk e2fsprogs xfsprogs
    
    log "Tool installation complete. Verifying..."
    local still_missing=()
    for cmd in growpart pvresize lvextend; do
      if ! command -v "$cmd" >/dev/null 2>&1; then
        still_missing+=("$cmd")
      fi
    done
    
    if [[ ${#still_missing[@]} -gt 0 ]]; then
      echo "ERROR: Critical tools still missing after installation: ${still_missing[*]}" >&2
      log "ERROR: Critical tools still missing: ${still_missing[*]}"
      exit 1
    fi
  else
    log "All required tools are present; skipping installation."
  fi
}

check_and_install_tools
echo

section "Detecting root LVM layout"

ROOT_SRC="$(findmnt -n -o SOURCE / || true)"
if [[ -z "$ROOT_SRC" ]]; then
  echo "ERROR: Unable to determine root filesystem source." >&2
  log "ERROR: Unable to determine root SOURCE via findmnt."
  exit 1
fi

# Resolve root LV
ROOT_LV_PATH=""
VG_NAME=""
LV_NAME=""

if lvs --noheadings "$ROOT_SRC" >/dev/null 2>&1; then
  read -r VG_NAME LV_NAME < <(lvs --noheadings -o vg_name,lv_name "$ROOT_SRC" | awk '{print $1, $2}')
  ROOT_LV_PATH="/dev/${VG_NAME}/${LV_NAME}"
else
  echo "ERROR: Root filesystem ($ROOT_SRC) is not an LVM logical volume; this script targets LVM appliances." >&2
  log "ERROR: Root filesystem ($ROOT_SRC) not recognized as LVM LV."
  exit 1
fi

FSTYPE="$(findmnt -n -o FSTYPE / || true)"

echo "Root LV: $ROOT_LV_PATH"
echo "VG:      $VG_NAME"
echo "FS type: ${FSTYPE:-unknown}"
echo
log "root_source=$ROOT_SRC root_lvpath=$ROOT_LV_PATH vg_name=$VG_NAME lv_name=$LV_NAME root_fstype=$FSTYPE"

section "Finding PV backing the root LV"

# Pull devices backing LV; strip "(...)" and commas; dedupe
LV_DEVS="$(
  lvs --noheadings -o devices "$ROOT_LV_PATH" \
  | awk '{print $1}' \
  | sed 's/([^)]*)//g' \
  | tr -d ',' \
  | awk 'NF' \
  | sort -u \
  | tr '\n' ' '
)"
LV_DEVS="$(echo "$LV_DEVS" | xargs || true)"

if [[ -z "$LV_DEVS" ]]; then
  echo "ERROR: Unable to determine PV device(s) behind $ROOT_LV_PATH." >&2
  log "ERROR: Unable to determine LV devices."
  exit 1
fi

echo "PV(s):   $LV_DEVS"
echo
log "lv_devices=$LV_DEVS"

vg_free_bytes() {
  lvm_bytes vgs --noheadings --nosuffix --units b -o vg_free "$VG_NAME"
}
lv_size_bytes() {
  lvm_bytes lvs --noheadings --nosuffix --units b -o lv_size "$ROOT_LV_PATH"
}
fs_size_bytes() { df --output=size -B1 / | tail -n1 | tr -d ' '; }
fs_avail_bytes(){ df --output=avail -B1 / | tail -n1 | tr -d ' '; }

section "BEFORE state"
BEFORE_VG_FREE="$(vg_free_bytes)"
BEFORE_LV="$(lv_size_bytes)"
BEFORE_FS="$(fs_size_bytes)"
BEFORE_AVAIL="$(fs_avail_bytes)"

echo "VG free:      $(human_bytes "$BEFORE_VG_FREE")"
echo "LV size:      $(human_bytes "$BEFORE_LV")"
echo "FS size (/):  $(human_bytes "$BEFORE_FS")"
echo "FS avail (/): $(human_bytes "$BEFORE_AVAIL")"
echo

log "before_vg_free_bytes=$BEFORE_VG_FREE"
log "before_lv_bytes=$BEFORE_LV"
log "before_fs_bytes=$BEFORE_FS"
log "before_fs_avail_bytes=$BEFORE_AVAIL"

detect_parent_sysfs() {
  local dev="$1"
  local real
  real="$(readlink -f "$dev" 2>/dev/null || echo "$dev")"

  DET_KIND="unknown"
  DET_PARENT="$real"
  DET_PARTNUM=""

  local type pkname
  type="$(lsblk -n -o TYPE "$real" 2>/dev/null | head -n1 || true)"
  pkname="$(lsblk -n -o PKNAME "$real" 2>/dev/null | head -n1 || true)"

  if [[ "$type" == "part" && -n "$pkname" ]]; then
    DET_KIND="part"
    DET_PARENT="/dev/$pkname"
    DET_PARTNUM="$(echo "$real" | sed -E 's@.*/[^0-9]*([0-9]+)$@\1@' || true)"
  elif [[ "$type" == "disk" ]]; then
    DET_KIND="disk"
    DET_PARENT="$real"
  fi

  log "detect_parent_sysfs: dev=$dev real=$real kind=$DET_KIND parent=$DET_PARENT partnum=${DET_PARTNUM:-none}"
}

STRONGLY_RECOMMEND_REBOOT=0
reboot_recommend_reason=""

try_gpt_fix_and_rescan() {
  local disk="$1"
  log "Rescanning disk (non-interactive, best-effort): $disk"

  local pttype
  pttype="$(lsblk -n -o PTTYPE "$disk" 2>/dev/null | head -n1 || true)"
  if [[ "$pttype" == "gpt" ]]; then
    local out
    out="$({ sgdisk -e "$disk"; } 2>&1)" || true
    log "+ sgdisk -e $disk"
    printf "%s\n" "$out" >>"$LOG_FILE"

    if echo "$out" | grep -qi "kernel is still using the old partition table"; then
      STRONGLY_RECOMMEND_REBOOT=1
      reboot_recommend_reason="Kernel is still using the old partition table after GPT repair (sgdisk -e)."
      log "WARN: kernel still using old partition table; partx/udevadm may refresh; otherwise reboot."
    fi
  else
    log "PTTYPE for $disk is '$pttype' (not gpt); skipping sgdisk -e."
  fi

  run partx -u "$disk" || true

  local bd_out bd_rc
  bd_out="$({ blockdev --rereadpt "$disk"; } 2>&1)" && bd_rc=0 || bd_rc=$?
  if [[ "$bd_rc" -ne 0 ]]; then
    if echo "$bd_out" | grep -qi "Device or resource busy"; then
      log "WARN: blockdev --rereadpt reported busy (expected on mounted/in-use disks). Continuing."
      log "blockdev_output=$bd_out"
    else
      log "WARN: blockdev --rereadpt failed: $bd_out"
    fi
  else
    log "+ blockdev --rereadpt $disk"
    printf "%s\n" "$bd_out" >>"$LOG_FILE"
  fi

  run udevadm settle || true
}

section "Expanding PV(s) and partition(s) (if needed)"

declare -A GROW_STATUS
ANY_GROWPART_CHANGED=0

for pv in $LV_DEVS; do
  echo "Processing: $pv"
  log "Processing PV backing device: $pv"

  detect_parent_sysfs "$pv"
  local_parent="$DET_PARENT"
  local_partnum="${DET_PARTNUM:-}"
  local_kind="$DET_KIND"

  if [[ "$local_kind" == "part" && -n "$local_partnum" ]]; then
    try_gpt_fix_and_rescan "$local_parent"

    if [[ "$DRY_RUN" -eq 1 ]]; then
      echo "  - would run: growpart $local_parent $local_partnum"
      log "DRY_RUN: would run growpart $local_parent $local_partnum"
      GROW_STATUS["$pv"]="SKIPPED"
    else
      gp_out="$({ growpart "$local_parent" "$local_partnum"; } 2>&1)" || true
      printf "%s\n" "$gp_out" >>"$LOG_FILE"

      if echo "$gp_out" | grep -q "^CHANGED:"; then
        echo "  - action: grew partition ${local_parent} ${local_partnum} (growpart)"
        log "growpart_status__${pv}=CHANGED"
        GROW_STATUS["$pv"]="CHANGED"
        ANY_GROWPART_CHANGED=1
      elif echo "$gp_out" | grep -q "^NOCHANGE:"; then
        log "growpart_status__${pv}=NOCHANGE"
        GROW_STATUS["$pv"]="NOCHANGE"
      else
        log "growpart_status__${pv}=UNKNOWN"
        GROW_STATUS["$pv"]="UNKNOWN"
      fi
    fi

    echo "  - pvresize $pv"
    run pvresize "$pv"

  elif [[ "$local_kind" == "disk" ]]; then
    GROW_STATUS["$pv"]="WHOLEDISK"
    echo "  - pvresize $pv"
    run pvresize "$pv"
  else
    GROW_STATUS["$pv"]="UNKNOWN"
    echo "  - pvresize $pv"
    run pvresize "$pv"
  fi
done

VG_FREE_AFTER_PV="$(vg_free_bytes)"
log "vg_free_bytes_after_pvresize=$VG_FREE_AFTER_PV"
echo
echo "VG free after pvresize: $(human_bytes "$VG_FREE_AFTER_PV")"
echo

if [[ "$VG_FREE_AFTER_PV" -gt 0 ]]; then
  echo "Action: detected free space in VG after pvresize ($(human_bytes "$VG_FREE_AFTER_PV"))."
  echo
fi

if [[ "$STRONGLY_RECOMMEND_REBOOT" -eq 1 ]]; then
  echo "RECOMMENDATION: Reboot strongly recommended."
  echo "Reason: $reboot_recommend_reason"
  echo "Tip: You can re-run this script after reboot to ensure all space is consumed."
  echo
  log "RECOMMEND_REBOOT_STRONG=1 reason=$reboot_recommend_reason"
fi

ALL_NONEXPANDING=1
for pv in $LV_DEVS; do
  st="${GROW_STATUS[$pv]:-UNKNOWN}"
  [[ "$st" == "CHANGED" ]] && ALL_NONEXPANDING=0
done

NOOP_SHORT_CIRCUIT=0
if [[ "$VG_FREE_AFTER_PV" -le 0 && "$ALL_NONEXPANDING" -eq 1 ]]; then
  NOOP_SHORT_CIRCUIT=1
fi

if [[ "$NOOP_SHORT_CIRCUIT" -eq 1 ]]; then
  section "No expansion needed"
  echo "Disk/partition appears fully expanded and VG has no free space."
  echo
  log "No-op short-circuit: no growpart CHANGED and VG has 0 free bytes; skipping lvextend + filesystem grow."
fi

section "Extending root LV and filesystem"

if [[ "$NOOP_SHORT_CIRCUIT" -eq 1 ]]; then
  log "Skipping LV/filesystem extension due to no-op short-circuit."
else
  echo "Action: extending root LV to consume free VG space..."
  run lvextend -l +100%FREE "$ROOT_LV_PATH"

  case "${FSTYPE}" in
    ext4|ext3|ext2)
      echo "Action: growing ext filesystem online (resize2fs)..."
      run resize2fs "$ROOT_LV_PATH"
      ;;
    xfs)
      echo "Action: growing XFS filesystem online (xfs_growfs /)..."
      run xfs_growfs /
      ;;
    *)
      echo "WARN: Unsupported/unknown filesystem type '$FSTYPE'; skipping filesystem grow."
      log "WARN: Unsupported/unknown filesystem type '$FSTYPE'; skipping filesystem grow."
      ;;
  esac
  echo
fi

section "AFTER state"

AFTER_VG_FREE="$(vg_free_bytes)"
AFTER_LV="$(lv_size_bytes)"
AFTER_FS="$(fs_size_bytes)"
AFTER_AVAIL="$(fs_avail_bytes)"

echo "VG free:      $(human_bytes "$AFTER_VG_FREE")"
echo "LV size:      $(human_bytes "$AFTER_LV")"
echo "FS size (/):  $(human_bytes "$AFTER_FS")"
echo "FS avail (/): $(human_bytes "$AFTER_AVAIL")"
echo

log "after_vg_free_bytes=$AFTER_VG_FREE"
log "after_lv_bytes=$AFTER_LV"
log "after_fs_bytes=$AFTER_FS"
log "after_fs_avail_bytes=$AFTER_AVAIL"

DELTA_LV=$(( AFTER_LV - BEFORE_LV ))
DELTA_FS=$(( AFTER_FS - BEFORE_FS ))

if [[ "$DELTA_LV" -gt 0 || "$DELTA_FS" -gt 0 ]]; then
  msg="SUMMARY: Expansion performed. LV grew by $(human_bytes "$DELTA_LV") and filesystem grew by $(human_bytes "$DELTA_FS")."
  echo "$msg"
  log "$msg"
else
  msg="SUMMARY: No measurable growth detected. This is normal if the disk was already fully expanded."
  echo "$msg"
  log "WARN: $msg"
fi
echo

section "Post-operation state (for support)"
run lsblk
run pvs
run vgs
run lvs
run df -h /

echo
echo "Support log file: $LOG_FILE"
echo
log "Support log file: $LOG_FILE"

if [[ "$REBOOT" -eq 1 ]]; then
  echo "Reboot requested (--reboot). Rebooting now..."
  log "Reboot requested. Rebooting now..."
  run reboot
else
  echo "Reboot not requested (default). No reboot will be performed."
  if [[ "$STRONGLY_RECOMMEND_REBOOT" -eq 1 ]]; then
    echo "NOTE: A reboot is strongly recommended due to partition table refresh warnings."
  fi
  log "Reboot not requested (default). No reboot will be performed."
fi
