#!/usr/bin/env bash
#
# migrate_mail_domain.sh
#
# Migrate ispmanager mailboxes from one domain to another,
# including message content (Maildir) and passwords.
#
# TESTED AND WORKING on ispmanager 6 with Dovecot
#
# Usage: ./migrate_mail_domain.sh <step> <old_domain> <new_domain>
#
set -u

exec < /dev/null

export PAGER=cat
export SYSTEMD_PAGER=cat

if [[ $# -lt 3 ]]; then
  cat <<'EOF'
Usage: ./migrate_mail_domain.sh <step> <old_domain> <new_domain>

STEPS:
  all        - backup + run all steps below (full migration)
  backup     - back up the panel database (copy ispmgr.db / mysqldump)
  discover   - collect a CSV list of mailboxes
  create     - create mailboxes on the new domain
  aliases    - transfer mailbox aliases (via mgrctl email.edit)
  responders - transfer autoresponders (via mgrctl email.responder)
  maildata   - transfer Maildir via rsync
  creds      - transfer passwords
  verify     - compare sizes

EXAMPLES:
  ./migrate_mail_domain.sh all test.ru test1.ru
  ./migrate_mail_domain.sh backup test.ru test1.ru
  ./migrate_mail_domain.sh discover test.ru test1.ru
  ./migrate_mail_domain.sh create test.ru test1.ru
  ./migrate_mail_domain.sh aliases test.ru test1.ru
  ./migrate_mail_domain.sh responders test.ru test1.ru
  ./migrate_mail_domain.sh maildata test.ru test1.ru
  ./migrate_mail_domain.sh creds test.ru test1.ru
  ./migrate_mail_domain.sh verify test.ru test1.ru

A full log is written to:
  /root/mail-migration-<old_domain>-<new_domain>/migrate.log

EOF
  exit 1
fi

STEP="$1"
OLD_DOMAIN="$2"
NEW_DOMAIN="$3"

MGRCTL="/usr/local/mgr5/sbin/mgrctl -m ispmgr"
ISPMGR_DB="/usr/local/mgr5/etc/ispmgr.db"
PASSWD_FILE="/etc/dovecot/dovecot.passwd"
WORKDIR="/root/mail-migration-${OLD_DOMAIN}-${NEW_DOMAIN}"
CSV="${WORKDIR}/mailboxes.csv"
LOG="${WORKDIR}/migrate.log"

mkdir -p "$WORKDIR"

# ─── Database detection ───────────────────────────────────────────────

DB_CONF="/usr/local/mgr5/etc/ispmgr.conf.d/db.conf"
DB_TYPE="sqlite"
DB_HOST=""
DB_PORT=""
DB_NAME=""
DB_USER=""
DB_PASS=""

if [[ -f "$DB_CONF" ]] && [[ -s "$DB_CONF" ]]; then
  DB_TYPE="mysql"
  while IFS='= ' read -r key val; do
    [[ -z "$key" || "$key" == \#* ]] && continue
    key="${key^^}"
    case "$key" in
      DBHOST|HOST)  DB_HOST="$val" ;;
      DBPORT|PORT)  DB_PORT="$val" ;;
      DBNAME|DATABASE|DB) DB_NAME="$val" ;;
      DBUSER|USER)  DB_USER="$val" ;;
      DBPASS|PASSWORD|PASS) DB_PASS="$val" ;;
    esac
  done < "$DB_CONF"
  if ! command -v mysql &>/dev/null; then
    echo "ERROR: panel uses MySQL but the mysql client is not installed" >&2
    exit 1
  fi
  if mysql -u root "$DB_NAME" -N -B -e "SELECT 1;" &>/dev/null; then
    MYSQL_AUTH="-u root"
  else
    MYSQL_AUTH="-h $DB_HOST -P ${DB_PORT:-3306} -u $DB_USER -p$DB_PASS"
  fi
fi

sql() {
  local query="$1"
  if [[ "$DB_TYPE" == "mysql" ]]; then
    mysql $MYSQL_AUTH "$DB_NAME" -N -B -e "$query" < /dev/null 2>/dev/null | sed 's/\t/,/g' || true
  else
    sqlite3 -cmd '.pager off' -cmd '.timeout 5000' -csv "$ISPMGR_DB" "$query" < /dev/null 2>/dev/null || true
  fi
}

sql_raw() {
  local query="$1"
  if [[ "$DB_TYPE" == "mysql" ]]; then
    mysql $MYSQL_AUTH "$DB_NAME" -N -B -e "$query" < /dev/null 2>/dev/null || true
  else
    sqlite3 -cmd '.pager off' -cmd '.timeout 5000' "$ISPMGR_DB" "$query" < /dev/null 2>/dev/null || true
  fi
}

# ─── Panel database backup ────────────────────────────────────────────

step_backup() {
  log_start "backup"

  local backup_path
  if [[ "$DB_TYPE" == "mysql" ]]; then
    backup_path="${WORKDIR}/ispmgr_db_backup_$(date +%s).sql"
    log "  MySQL: ${DB_HOST:-localhost}:${DB_PORT:-3306}, db=${DB_NAME}"
    if mysqldump $MYSQL_AUTH "$DB_NAME" < /dev/null > "$backup_path" 2>> "$LOG"; then
      log "  OK backup created: $backup_path ($(stat -c%s "$backup_path" 2>/dev/null) bytes)"
    else
      log "  ERROR mysqldump failed (exit $?)"
      log_end "backup"
      return 1
    fi
  else
    if [[ ! -f "$ISPMGR_DB" ]]; then
      log "  ERROR: database file $ISPMGR_DB not found"
      log_end "backup"
      return 1
    fi
    backup_path="${ISPMGR_DB}.bak.$(date +%s)"
    sqlite3 "$ISPMGR_DB" ".backup '$backup_path'" < /dev/null 2>> "$LOG"
    if [[ -f "$backup_path" ]]; then
      log "  OK backup created: $backup_path ($(stat -c%s "$backup_path" 2>/dev/null) bytes)"
    else
      log "  ERROR: failed to copy the database"
      log_end "backup"
      return 1
    fi
  fi

  log "  IMPORTANT: keep this backup until the migration is complete - it is the rollback for creds."
  log_end "backup"
}

# ─── Logging ──────────────────────────────────────────────────────────

log() {
  local ts
  ts=$(date '+%F %T')
  echo "[${ts}] $*" >> "$LOG"
  echo "[${ts}] $*"
}

log_raw() {
  echo "$*" >> "$LOG"
}

log_start() {
  log ">>> ================================================================"
  log ">>> STARTING STEP: $*"
  log ">>> ================================================================"
}

log_end() {
  log "<<< ================================================================"
  log "<<< FINISHED STEP: $*"
  log "<<< ================================================================"
}

duration() {
  local diff=$(( $(date +%s) - $1 ))
  local min=$(( diff / 60 ))
  local sec=$(( diff % 60 ))
  printf "%dm %02ds" "$min" "$sec"
}

check_csv() {
  if [[ ! -f "$CSV" ]]; then
    log "ERROR: file $CSV not found. Run 'discover' first."
    return 1
  fi
}

csv_count() {
  wc -l < "$CSV" | tr -d ' '
}

get_email_id() {
  local login="$1" domain="$2"
  sql "SELECT e.id FROM email e JOIN emaildomain d ON e.domain=d.id WHERE e.name='$login' AND d.name='$domain' LIMIT 1;"
}

get_domain_id() {
  sql "SELECT id FROM emaildomain WHERE name='$1' LIMIT 1;"
}

get_maildir_path() {
  local login="$1" domain="$2"
  sql "SELECT e.path FROM email e JOIN emaildomain d ON e.domain=d.id WHERE e.name='$login' AND d.name='$domain' LIMIT 1;"
}

get_mail_count() {
  local path="$1"
  if [[ -d "$path" ]]; then
    find "$path" -type f ! -name ".responder.*" 2>/dev/null | wc -l | tr -d ' '
  else
    echo "0"
  fi
}

get_size_bytes() {
  local path="$1"
  if [[ -d "$path" ]]; then
    du -sb --exclude=.responder.* "$path" 2>/dev/null | cut -f1
  else
    echo "0"
  fi
}

format_bytes() {
  numfmt --to=iec "$1" 2>/dev/null || echo "$1"
}

# ─── Migration steps ──────────────────────────────────────────────────

step_discover() {
  log_start "discover"

  log "  Panel database: $DB_TYPE"

  if [[ "$DB_TYPE" == "sqlite" ]]; then
    log "  Database file: $ISPMGR_DB"
    if [[ ! -f "$ISPMGR_DB" ]]; then
      log "  ERROR: database file $ISPMGR_DB not found"
      return 1
    fi
    local db_size
    db_size=$(stat -c%s "$ISPMGR_DB" 2>/dev/null)
    log "  Database size: $(format_bytes "$db_size")"
  else
    log "  MySQL: ${DB_HOST:-localhost}:${DB_PORT:-3306}, db=${DB_NAME}, user=${DB_USER}"
    if ! sql "SELECT 1;" | grep -q '1'; then
      log "  ERROR: cannot connect to MySQL"
      return 1
    fi
    log "  MySQL connection OK"
  fi

  log "  Looking for domain ${OLD_DOMAIN} in emaildomain..."
  local domain_id
  domain_id=$(get_domain_id "$OLD_DOMAIN")

  if [[ -z "$domain_id" ]]; then
    log "  ERROR: domain '${OLD_DOMAIN}' not found in ispmanager"
    log "  Available domains:"
    sql "SELECT id, name FROM emaildomain ORDER BY name;" | while IFS=',' read -r id name; do
      log "    [id=$id] $name"
    done
    return 1
  fi

  log "  Found: id=$domain_id, name=${OLD_DOMAIN}"

  local total_old
  total_old=$(sql "SELECT COUNT(*) FROM email e JOIN emaildomain d ON e.domain=d.id WHERE d.name='${OLD_DOMAIN}';")
  log "  Total email records for the domain: $total_old"

  echo "login,old_domain,quota" > "$CSV"
  sql "SELECT e.name, d.name, COALESCE(e.maxsize, '') FROM email e JOIN emaildomain d ON e.domain=d.id WHERE d.name='${OLD_DOMAIN}' ORDER BY e.name;" >> "$CSV"

  local total_found
  total_found=$(csv_count)

  log "  Exported $total_found CSV rows (header included)"
  log "  Mailboxes: $((total_found - 1))"

  log "  First 5 CSV records:"
  head -5 "$CSV" | while IFS=',' read -r login domain quota; do
    log "    login=$login domain=$domain quota=${quota:-unlimited}"
  done

  log "  CSV file created: $CSV"
  log_end "discover"
}

step_create() {
  log_start "create"
  check_csv || return 1

  local total_lines
  total_lines=$(csv_count)
  local total_mailboxes=$((total_lines - 1))
  log "  Mailboxes to create: $total_mailboxes"
  log "  Panel database: $DB_TYPE"

  local new_domain_id
  new_domain_id=$(get_domain_id "$NEW_DOMAIN")
  if [[ -z "$new_domain_id" ]]; then
    log "  ERROR: target domain '${NEW_DOMAIN}' not found in ispmanager"
    return 1
  fi
  log "  Target domain: ${NEW_DOMAIN} (id=$new_domain_id)"
  log ""

  echo "login,new_elid" > "${WORKDIR}/created.csv"

  local count=0 ok=0 err=0 skip=0
  local step_start
  step_start=$(date +%s)

  while IFS=, read -r login old_domain quota; do
    [[ "$login" == "login" ]] && continue
    [[ -z "$login" ]] && continue

    ((count++))

    quota="${quota//\"/}"

    local existing_id
    existing_id=$(get_email_id "$login" "$NEW_DOMAIN")
    if [[ -n "$existing_id" ]]; then
      log "  [$count/$total_mailboxes] SKIP: ${login}@${NEW_DOMAIN} already exists (id=$existing_id)"
      echo "${login},${login}@${NEW_DOMAIN}" >> "${WORKDIR}/created.csv"
      ((skip++))
      continue
    fi

    local tmp_pass
    tmp_pass="TempPass$(openssl rand -base64 8 | tr -dc 'A-Za-z0-9' | head -c 12)"

    log "  [$count/$total_mailboxes] Creating ${login}@${NEW_DOMAIN}... (quota=${quota:-unlimited})"

    local output
    if [[ -n "$quota" ]]; then
      output=$($MGRCTL email.edit sok=ok name="$login" domainname="$NEW_DOMAIN" passwd="$tmp_pass" confirm="$tmp_pass" maxsize="$quota" < /dev/null 2>&1)
    else
      output=$($MGRCTL email.edit sok=ok name="$login" domainname="$NEW_DOMAIN" passwd="$tmp_pass" confirm="$tmp_pass" < /dev/null 2>&1)
    fi
    local exit_code=$?

    log_raw "    mgrctl exit=$exit_code, output: $output"

    if echo "$output" | grep -q "^OK" && [[ $exit_code -eq 0 ]]; then
      echo "${login},${login}@${NEW_DOMAIN}" >> "${WORKDIR}/created.csv"
      ((ok++))
      log "    OK - temporary password set"
    else
      ((err++))
      log "    ERROR: $output"
    fi
  done < "$CSV"

  local elapsed
  elapsed=$(duration $step_start)
  log ""
  log "  TOTALS for create:"
  log "    Processed:        $count"
  log "    Created:          $ok"
  log "    Skipped (existed):$skip"
  log "    Errors:           $err"
  log "    Time:             $elapsed"
  log "    File:             ${WORKDIR}/created.csv"
  log_end "create"
}

step_aliases() {
  log_start "aliases"
  check_csv || return 1

  local total_lines
  total_lines=$(csv_count)
  local total_mailboxes=$((total_lines - 1))
  log "  Mailboxes in CSV: $total_mailboxes"
  log "  Reading aliases from DB (email_alias) for ${OLD_DOMAIN} and applying to ${NEW_DOMAIN}"
  log ""

  local count=0 ok=0 err=0 none=0

  while IFS=, read -r login old_domain quota; do
    [[ "$login" == "login" ]] && continue
    [[ -z "$login" ]] && continue

    ((count++))

    local aliases
    aliases=$(sql "SELECT ea.name FROM email_alias ea JOIN email e ON ea.email=e.id JOIN emaildomain d ON e.domain=d.id WHERE e.name='$login' AND d.name='$OLD_DOMAIN' ORDER BY ea.name;")
    aliases=$(echo "$aliases" | tr '\n' ' ' | sed 's/ *$//')

    log "  [$count/$total_mailboxes] ${login}@${NEW_DOMAIN}: aliases='${aliases}'"

    if [[ -z "$aliases" ]]; then
      log "    No aliases - skip"
      ((none++))
      continue
    fi

    local output
    output=$($MGRCTL email.edit sok=ok elid="${login}@${NEW_DOMAIN}" aliases="$aliases" < /dev/null 2>&1)
    local exit_code=$?
    log_raw "    mgrctl exit=$exit_code, output: $output"

    if echo "$output" | grep -q "^OK" && [[ $exit_code -eq 0 ]]; then
      ((ok++))
      log "    OK"
    else
      ((err++))
      log "    ERROR: $output"
    fi
  done < "$CSV"

  log ""
  log "  TOTALS for aliases:"
  log "    Processed:    $count"
  log "    With aliases: $ok"
  log "    No aliases:   $none"
  log "    Errors:       $err"
  log_end "aliases"
}

step_responders() {
  log_start "responders"
  check_csv || return 1

  local total_lines
  total_lines=$(csv_count)
  local total_mailboxes=$((total_lines - 1))
  log "  Mailboxes in CSV: $total_mailboxes"
  log "  Reading autoresponders from DB (email_responder) for ${OLD_DOMAIN} and applying to ${NEW_DOMAIN}"
  log ""

  local count=0 ok=0 err=0 none=0

  while IFS=, read -r login old_domain quota; do
    [[ "$login" == "login" ]] && continue
    [[ -z "$login" ]] && continue

    ((count++))

    local old_has
    old_has=$(sql_raw "SELECT COUNT(*) FROM email_responder er JOIN email e ON er.email=e.id JOIN emaildomain d ON e.domain=d.id WHERE e.name='$login' AND d.name='$OLD_DOMAIN';")
    if [[ -z "$old_has" ]] || [[ "$old_has" == "0" ]]; then
      log "  [$count/$total_mailboxes] ${login}@${OLD_DOMAIN}: no autoresponder - skip"
      ((none++))
      continue
    fi

    local arfrom arsubj arbody arattach arenable
    arfrom=$(sql_raw "SELECT er.fromaddr FROM email_responder er JOIN email e ON er.email=e.id JOIN emaildomain d ON e.domain=d.id WHERE e.name='$login' AND d.name='$OLD_DOMAIN' LIMIT 1;")
    arsubj=$(sql_raw "SELECT er.subject FROM email_responder er JOIN email e ON er.email=e.id JOIN emaildomain d ON e.domain=d.id WHERE e.name='$login' AND d.name='$OLD_DOMAIN' LIMIT 1;")
    arbody=$(sql_raw "SELECT er.body FROM email_responder er JOIN email e ON er.email=e.id JOIN emaildomain d ON e.domain=d.id WHERE e.name='$login' AND d.name='$OLD_DOMAIN' LIMIT 1;")
    arattach=$(sql_raw "SELECT er.attach FROM email_responder er JOIN email e ON er.email=e.id JOIN emaildomain d ON e.domain=d.id WHERE e.name='$login' AND d.name='$OLD_DOMAIN' LIMIT 1;")
    arenable=$(sql_raw "SELECT er.active FROM email_responder er JOIN email e ON er.email=e.id JOIN emaildomain d ON e.domain=d.id WHERE e.name='$login' AND d.name='$OLD_DOMAIN' LIMIT 1;")

    arfrom=${arfrom//@${OLD_DOMAIN}/@${NEW_DOMAIN}}
    arsubj=${arsubj//@${OLD_DOMAIN}/@${NEW_DOMAIN}}
    arbody=${arbody//@${OLD_DOMAIN}/@${NEW_DOMAIN}}

    log "  [$count/$total_mailboxes] ${login}@${NEW_DOMAIN}: setting autoresponder"
    log "    arfrom='${arfrom}' arsubj='${arsubj}'"
    log "    active=${arenable:-off}"

    local output
    output=$($MGRCTL email.responder sok=ok elid="${login}@${NEW_DOMAIN}" arfrom="$arfrom" arsubj="$arsubj" arbody="$arbody" ${arattach:+arattach="$arattach"} arenable="${arenable:-off}" < /dev/null 2>&1)
    local exit_code=$?
    log_raw "    mgrctl exit=$exit_code, output: $output"

    if echo "$output" | grep -q "^OK" && [[ $exit_code -eq 0 ]]; then
      ((ok++))
      log "    OK"
    else
      ((err++))
      log "    ERROR: $output"
    fi
  done < "$CSV"

  log ""
  log "  TOTALS for responders:"
  log "    Processed:      $count"
  log "    Autoresponders: $ok"
  log "    No responder:   $none"
  log "    Errors:         $err"
  log_end "responders"
}

step_maildata() {
  log_start "maildata"
  check_csv || return 1

  local total_lines
  total_lines=$(csv_count)
  log "  Processing mailboxes from CSV: $((total_lines - 1))"

  local paths="${WORKDIR}/mailpaths.txt"
  : > "$paths"

  local count=0 not_found=0
  while IFS=, read -r login old_domain quota; do
    [[ "$login" == "login" ]] && continue
    [[ -z "$login" ]] && continue

    ((count++))

    local old_path new_path
    old_path=$(get_maildir_path "$login" "$OLD_DOMAIN")
    new_path=$(get_maildir_path "$login" "$NEW_DOMAIN")

    if [[ -z "$old_path" ]]; then
      log "  [$count] SKIP: source path not found for ${login}@${OLD_DOMAIN}"
      ((not_found++))
      continue
    fi

    if [[ -z "$new_path" ]]; then
      log "  [$count] SKIP: target path not found for ${login}@${NEW_DOMAIN}"
      ((not_found++))
      continue
    fi

    echo "$login|$old_path|$new_path" >> "$paths"

    local old_exists="no"
    [[ -d "$old_path" ]] && old_exists="yes"
    log "  [$count] ${login}: old=$old_path (exists=$old_exists) new=$new_path"
  done < "$CSV"

  local total_for_rsync
  total_for_rsync=$(wc -l < "$paths" | tr -d ' ')
  log ""
  log "  Mailboxes for rsync: $total_for_rsync"
  log "  Skipped (no path): $not_found"
  log ""

  if [[ $total_for_rsync -eq 0 ]]; then
    log "  No mailboxes to transfer. Done."
    log_end "maildata"
    return 0
  fi

  local rsync_start
  rsync_start=$(date +%s)
  log "  Starting rsync: $(date '+%F %T')"

  local total_bytes_old=0 total_bytes_new=0 total_files_old=0 total_files_new=0
  local count=0 ok_rsync=0 ok_chown=0 ok_sieve=0 err_rsync=0 err_chown=0 err_sieve=0

  while IFS='|' read -r login old_path new_path; do
    [[ -z "$login" ]] && continue
    ((count++))

    if [[ ! -d "$old_path" ]]; then
      log "  [$count/$total_for_rsync] ${login}: SKIP - source folder ${old_path} does not exist"
      continue
    fi

    local maildir_parent
    maildir_parent=$(dirname "$new_path")
    mkdir -p "$maildir_parent"

    local old_size old_files
    old_size=$(du -sh "$old_path" 2>/dev/null | cut -f1)
    old_files=$(get_mail_count "$old_path")
    old_bytes=$(get_size_bytes "$old_path")
    total_bytes_old=$((total_bytes_old + old_bytes))
    total_files_old=$((total_files_old + old_files))

    local new_size_before
    new_size_before=$(du -sh "$new_path" 2>/dev/null | cut -f1 || echo "-")

    log "  [$count/$total_for_rsync] ${login}:"
    log "    old: ${old_path} (${old_size}, ${old_files} files)"
    log "    new: ${new_path} (before rsync: ${new_size_before})"

    local ts_start
    ts_start=$(date +%s)

    log "    rsync..."
    rsync -a --delete --exclude=.responder.msg --exclude=.responder.db "${old_path}/" "${new_path}/" >> "$LOG" 2>&1
    local rsync_exit=$?

    if [[ $rsync_exit -ne 0 ]]; then
      log "    ERROR rsync exited with code $rsync_exit"
      ((err_rsync++))
      log "      LOG: $(tail -3 "$LOG" | tr '\n' ' ')"
      continue
    fi
    ((ok_rsync++))

    local new_size_after new_files
    new_size_after=$(du -sh "$new_path" 2>/dev/null | cut -f1)
    new_files=$(get_mail_count "$new_path")
    new_bytes=$(get_size_bytes "$new_path")
    total_bytes_new=$((total_bytes_new + new_bytes))
    total_files_new=$((total_files_new + new_files))

    local rsync_duration
    rsync_duration=$(duration $ts_start)
    log "    rsync OK (${rsync_duration})"

    log "    Setting owner (chown)..."
    local new_uid new_gid
    new_uid=$(grep "^${login}@${NEW_DOMAIN}:" "$PASSWD_FILE" 2>/dev/null | cut -d: -f3)
    new_gid=$(grep "^${login}@${NEW_DOMAIN}:" "$PASSWD_FILE" 2>/dev/null | cut -d: -f4)

    if [[ -n "$new_uid" ]] && [[ -n "$new_gid" ]]; then
      chown -R "${new_uid}:${new_gid}" "${new_path}" 2>> "$LOG"
      local chown_exit=$?
      if [[ $chown_exit -eq 0 ]]; then
        log "    OK chown ${new_uid}:${new_gid}"
        ((ok_chown++))
      else
        log "    ERROR chown ${new_uid}:${new_gid} (exit $chown_exit)"
        ((err_chown++))
      fi
    else
      log "    ERROR chown SKIPPED: cannot read uid/gid from ${PASSWD_FILE}"
      ((err_chown++))
    fi

    log "    Restoring Sieve filters..."
    local email_id
    email_id=$(get_email_id "$login" "$NEW_DOMAIN")
    if [[ -n "$email_id" ]]; then
      $MGRCTL recache.email.filter.one id="$email_id" < /dev/null >> "$LOG" 2>&1
      local sieve_exit=$?
      if [[ $sieve_exit -eq 0 ]]; then
        log "    OK sieve recovery (id=$email_id)"
        ((ok_sieve++))
      else
        log "    ERROR sieve recovery (id=$email_id) (exit $sieve_exit)"
        ((err_sieve++))
      fi
    else
      log "    ERROR sieve recovery SKIPPED: id for ${login}@${NEW_DOMAIN} not found in DB"
      ((err_sieve++))
    fi

    local total_duration
    total_duration=$(duration $ts_start)
    log "    Mailbox done: ${old_size}->${new_size_after}, ${old_files}->${new_files} files, time ${total_duration}"
  done < "$paths"

  local total_elapsed
  total_elapsed=$(duration $rsync_start)

  log ""
  log "  ===== MAILDATA TOTALS ====="
  log "  Mailboxes:        $count"
  log "  rsync OK:         $ok_rsync"
  log "  rsync errors:     $err_rsync"
  log "  chown OK:         $ok_chown"
  log "  chown errors:     $err_chown"
  log "  sieve OK:         $ok_sieve"
  log "  sieve errors:     $err_sieve"
  log "  Total size old:   $(format_bytes $total_bytes_old)"
  log "  Total size new:   $(format_bytes $total_bytes_new)"
  log "  Total files old:  $total_files_old"
  log "  Total files new:  $total_files_new"
  log "  Total time:       $total_elapsed"
  log ""

  log "  First rsync pass complete."
  log "  Before the final cutover, run this step again to catch the delta."
  log ""
  log "  Paths file: $paths"
  log_end "maildata"
}

step_creds() {
  log_start "creds"
  check_csv || return 1

  if [[ ! -f "$PASSWD_FILE" ]]; then
    log "  ERROR: file $PASSWD_FILE not found"
    return 1
  fi

  log "  Dovecot passwd-file: $PASSWD_FILE"

  local backup_path="${PASSWD_FILE}.bak.$(date +%s)"
  cp -a "$PASSWD_FILE" "$backup_path"
  log "  Backup created: $backup_path"

  local total_lines
  total_lines=$(csv_count)
  local total_mailboxes=$((total_lines - 1))
  log "  Records in CSV: $total_mailboxes"

  local count=0 ok=0 err=0 warn=0

  while IFS=, read -r login old_domain quota; do
    [[ "$login" == "login" ]] && continue
    [[ -z "$login" ]] && continue

    ((count++))

    log "  [$count/$total_mailboxes] ${login}@${OLD_DOMAIN} -> ${login}@${NEW_DOMAIN}"

    local old_entry
    old_entry=$(grep "^${login}@${OLD_DOMAIN}:" "$PASSWD_FILE" 2>/dev/null) || true
    if [[ -z "$old_entry" ]]; then
      log "    WARNING: no entry in dovecot.passwd for ${login}@${OLD_DOMAIN}"
      ((warn++))
      continue
    fi
    log "    Old entry found: $(echo "$old_entry" | cut -d: -f1,2,3,4)"

    local old_hash
    old_hash=$(echo "$old_entry" | cut -d: -f2)
    log "    Password hash (CRAM-MD5): ${old_hash:0:20}..."

    local old_db_password
    old_db_password=$(sql "SELECT password FROM email e JOIN emaildomain d ON e.domain=d.id WHERE e.name='$login' AND d.name='$OLD_DOMAIN' LIMIT 1;")
    if [[ -n "$old_db_password" ]]; then
      log "    DB password column: ${#old_db_password} bytes"
    else
      log "    DB password column: empty or not found"
    fi

    local new_domain_id
    new_domain_id=$(get_domain_id "$NEW_DOMAIN")

    log "    Replacing hash in dovecot.passwd..."

    local before_line
    before_line=$(grep "^${login}@${NEW_DOMAIN}:" "$PASSWD_FILE" 2>/dev/null || echo "-")
    log "    Before: $before_line"

    sed -i "s|^\(${login}@${NEW_DOMAIN}\):[^:]*|\1:${old_hash}|" "$PASSWD_FILE"

    local after_line
    after_line=$(grep "^${login}@${NEW_DOMAIN}:" "$PASSWD_FILE" 2>/dev/null || echo "-")
    log "    After: $after_line"

    local new_hash
    new_hash=$(echo "$after_line" | cut -d: -f2)

    if [[ "$new_hash" == "$old_hash" ]]; then
      if [[ -n "$old_db_password" ]] && [[ -n "$new_domain_id" ]]; then
        sql "UPDATE email SET password='$old_db_password' WHERE name='$login' AND domain=$new_domain_id;" > /dev/null 2>&1 || true
        log "    DB password copied"
      fi
      log "    OK"
      ((ok++))
    else
      log "    ERROR: hash does not match after replacement"
      log "      Expected: ${old_hash:0:20}..."
      log "      Got:      ${new_hash:0:20}..."
      ((err++))
    fi
  done < "$CSV"

  log ""
  log "  TOTALS for creds:"
  log "    Processed:  $count"
  log "    OK:         $ok"
  log "    Skipped:    $warn"
  log "    Errors:     $err"

  if [[ $err -eq 0 ]]; then
    log ""
    log "  Dovecot passwd-file updated. To apply the changes run:"
    log "    doveadm reload"
    log "    pkill core"
  fi

  log_end "creds"
}

step_verify() {
  log_start "verify"

  local paths="${WORKDIR}/mailpaths.txt"
  if [[ ! -f "$paths" ]]; then
    log "  File $paths not found. Run maildata at least once."
    log_end "verify"
    return 0
  fi

  local total_paths
  total_paths=$(wc -l < "$paths" | tr -d ' ')
  log "  Records in mailpaths.txt: $total_paths"
  log ""

  local total_old_bytes=0 total_new_bytes=0
  local total_old_files=0 total_new_files=0
  local diff_count=0 match_count=0 missing_count=0

  local verify_input="${WORKDIR}/verify_input.txt"
  local verify_table="${WORKDIR}/verify_table.txt"
  grep -v '^$' "$paths" > "$verify_input"
  printf "%-25s | %-12s | %-12s | %-7s | %-7s | %s\n" "Login" "Old" "New" "Files O" "Files N" "Status" > "$verify_table"

  while IFS='|' read -r login old_path new_path; do
    [[ -z "$login" ]] && continue

    local old_size_h="-" new_size_h="-"
    local old_files="-" new_files="-"
    local status="OK"

    if [[ -d "$old_path" ]]; then
      old_size_h=$(format_bytes "$(get_size_bytes "$old_path")")
      old_size_b=$(get_size_bytes "$old_path")
      old_files=$(get_mail_count "$old_path")
      total_old_bytes=$((total_old_bytes + old_size_b))
      total_old_files=$((total_old_files + old_files))
    else
      status="OLD MISSING"
      ((missing_count++))
    fi

    if [[ -d "$new_path" ]]; then
      new_size_h=$(format_bytes "$(get_size_bytes "$new_path")")
      new_size_b=$(get_size_bytes "$new_path")
      new_files=$(get_mail_count "$new_path")
      total_new_bytes=$((total_new_bytes + new_size_b))
      total_new_files=$((total_new_files + new_files))
    else
      [[ "$status" == "OLD MISSING" ]] && status="BOTH MISSING" || status="NEW MISSING"
      ((missing_count++))
    fi

    if [[ "$old_size_h" != "-" ]] && [[ "$new_size_h" != "-" ]]; then
      if [[ "$old_size_h" != "$new_size_h" ]]; then
        status="DIFFERENT"
        ((diff_count++))
      else
        ((match_count++))
      fi
    fi

    local fmt_old_files="-" fmt_new_files="-"
    [[ "$old_files" != "-" ]] && printf -v fmt_old_files "%'d" "$old_files"
    [[ "$new_files" != "-" ]] && printf -v fmt_new_files "%'d" "$new_files"

    printf "%-25s | %-12s | %-12s | %7s | %7s | %s\n" "$login" "$old_size_h" "$new_size_h" "$fmt_old_files" "$fmt_new_files" "$status" >> "$verify_table"
  done < "$verify_input"

  cat "$verify_table" | tee -a "$LOG"

  local total_old_h total_new_h
  total_old_h=$(format_bytes $total_old_bytes)
  total_new_h=$(format_bytes $total_new_bytes)

  log ""
  log "  ===== TOTALS ====="
  log "  Matched:          $match_count"
  log "  Different:        $diff_count"
  log "  Missing:          $missing_count"
  log ""
  log "  Total old:        $total_old_h ($(printf "%'d" $total_old_files) files)"
  log "  Total new:        $total_new_h ($(printf "%'d" $total_new_files) files)"
  log ""

  if [[ $diff_count -eq 0 ]] && [[ $missing_count -eq 0 ]]; then
    log "  All mailboxes match."
  elif [[ $diff_count -gt 0 ]]; then
    log "  WARNING: $diff_count mailbox(es) differ in size - run a catch-up sync."
  fi

  log_end "verify"
}

cleanup_workdir() {
  # Remove temporary files created by the steps. Keep only the log.
  rm -f "$CSV" \
        "${WORKDIR}/created.csv" \
        "${WORKDIR}/mailpaths.txt" \
        "${WORKDIR}/verify_input.txt" \
        "${WORKDIR}/verify_table.txt"
}

case "$STEP" in
  backup) step_backup ;;
  discover) step_discover ;;
  create) step_create ;;
  aliases) step_aliases ;;
  responders) step_responders ;;
  maildata) step_maildata ;;
  creds) step_creds ;;
  verify) step_verify ;;
  all)
    start_all=$(date +%s)
    log ""
    log "████████████████████████████████████████████████████████████████"
    log "█ STARTING FULL MIGRATION: ${OLD_DOMAIN} -> ${NEW_DOMAIN}"
    log "████████████████████████████████████████████████████████████████"
    log ""
    step_backup; step_discover; step_create; step_aliases; step_maildata; step_responders; step_creds; step_verify
    end_all=$(date +%s)
    elapsed=$(duration $start_all)
    log ""
    log "████████████████████████████████████████████████████████████████"
    log "█ MIGRATION COMPLETE: ${OLD_DOMAIN} -> ${NEW_DOMAIN}"
    log "█ Total time: ${elapsed}"
    log "████████████████████████████████████████████████████████████████"
    log ""
    log "Full log: $LOG"
    cleanup_workdir
    ;;
  *)
    echo "ERROR: unknown step '$STEP'"
    echo ""
    echo "Usage: ./migrate_mail_domain.sh <step> <old_domain> <new_domain>"
    echo ""
    echo "Available steps: all, backup, discover, create, aliases, responders, maildata, creds, verify"
    exit 1
    ;;
esac

log "Done."
