#!/bin/sh

set -eu
export LC_ALL=C

SELF=${0##*/}
LOG_DIR=${ERASE_LOG_DIR:-/var/log/erase-disk}
ATA_PASSWORD=erase-disk
DISK=
MODEL=
SERIAL=
SIZE=
SIZE_BYTES=
TRANSPORT=
ROTA=
METHOD=
METHOD_DESCRIPTION=
NVME_CONTROLLER=
ATA_IDENTIFY=
ATA_UNAVAILABLE_REASON=
ATA_SANITIZE_STATUS=
ATA_SANITIZE_STATUS_REASON=
LOG_FILE=
SCOPE_DISKS=

usage() {
    cat <<-EOF
	Utilisation :
	  sudo $SELF DISQUE

	Efface intégralement un disque en choisissant une méthode adaptée :
	  - NVMe Sanitize Block Erase, Crypto Erase ou Overwrite ;
	  - ATA Sanitize ou ATA Secure Erase pour les disques SATA ;
	  - réécriture et vérification pour un disque dur magnétique seulement.

	Les SSD et mémoires flash sans commande contrôleur fiable sont refusés.
	Les journaux sont écrits dans $LOG_DIR, configurable avec ERASE_LOG_DIR.

	ATTENTION : toutes les données du disque sont détruites irréversiblement.
	EOF
}

die() {
    printf 'Erreur : %s\n' "$*" >&2
    if [ -n "$LOG_FILE" ]; then
        printf '%s ERREUR : %s\n' "$(date -Is)" "$*" >>"$LOG_FILE"
    fi
    exit 1
}

log() {
    printf '%s\n' "$*"
    printf '%s\n' "$*" >>"$LOG_FILE"
}

run_logged() {
    rl_status_file=$(mktemp /tmp/erase-disk-status.XXXXXX)
    (
        set +e
        "$@"
        printf '%s\n' "$?" >"$rl_status_file"
        exit 0
    ) 2>&1 | tee -a "$LOG_FILE"
    rl_status=$(cat "$rl_status_file")
    rm -f "$rl_status_file"
    return "$rl_status"
}

trim() {
    printf '%s\n' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
}

check_requirements() {
    for cr_command in \
        awk blockdev cat cmp date findmnt grep id lsblk mkdir mktemp \
        readlink rm sed shred swapon sync tee tr
    do
        command -v "$cr_command" >/dev/null 2>&1 || \
            die "commande requise absente : $cr_command"
    done
}

check_unused() {
    for cu_disk in $SCOPE_DISKS; do
        for cu_node in $(lsblk -nrpo NAME "$cu_disk"); do
            if findmnt -rn -S "$cu_node" >/dev/null 2>&1; then
                die "$cu_node est monté"
            fi
            if swapon --noheadings --show=NAME 2>/dev/null | \
                awk -v node="$cu_node" '$1 == node { found = 1 } END { exit !found }'
            then
                die "$cu_node est utilisé comme swap"
            fi

            cu_name=${cu_node##*/}
            for cu_holder in "/sys/class/block/$cu_name/holders/"*; do
                [ -e "$cu_holder" ] || continue
                die "$cu_node est utilisé par ${cu_holder##*/}"
            done
        done
    done
}

read_identity() {
    MODEL=$(trim "$(lsblk -dnro MODEL "$DISK")")
    SERIAL=$(trim "$(lsblk -dnro SERIAL "$DISK")")
    SIZE=$(trim "$(lsblk -dnro SIZE "$DISK")")
    SIZE_BYTES=$(blockdev --getsize64 "$DISK")
    TRANSPORT=$(trim "$(lsblk -dnro TRAN "$DISK")")
    ROTA=$(trim "$(lsblk -dnro ROTA "$DISK")")
}

check_identity_unchanged() {
    ci_model=$(trim "$(lsblk -dnro MODEL "$DISK")")
    ci_serial=$(trim "$(lsblk -dnro SERIAL "$DISK")")
    ci_size=$(blockdev --getsize64 "$DISK")

    [ "$ci_model" = "$MODEL" ] && \
        [ "$ci_serial" = "$SERIAL" ] && \
        [ "$ci_size" = "$SIZE_BYTES" ] || \
        die "l'identité du périphérique a changé depuis la confirmation"
}

json_number() {
    jn_key=$1
    tr ',' '\n' | sed -n \
        "s/.*\"$jn_key\"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p" | \
        sed -n '1p'
}

detect_nvme_method() {
    command -v nvme >/dev/null 2>&1 || die "commande requise absente : nvme"

    dnm_name=${DISK##*/}
    NVME_CONTROLLER=/dev/${dnm_name%n*}
    [ -c "$NVME_CONTROLLER" ] || \
        die "contrôleur NVMe introuvable pour $DISK"

    dnm_controller_name=${NVME_CONTROLLER##*/}
    dnm_subsysnqn=$(cat "/sys/class/nvme/$dnm_controller_name/subsysnqn" 2>/dev/null) || \
        die "impossible d'identifier le sous-système de $NVME_CONTROLLER"
    dnm_controller_count=0
    for dnm_subsys_file in /sys/class/nvme/nvme*/subsysnqn; do
        [ -f "$dnm_subsys_file" ] || continue
        if [ "$(cat "$dnm_subsys_file")" = "$dnm_subsysnqn" ]; then
            dnm_controller_count=$((dnm_controller_count + 1))
        fi
    done
    [ "$dnm_controller_count" -eq 1 ] || \
        die "le sous-système NVMe utilise plusieurs contrôleurs ; effacement automatique refusé"

    SCOPE_DISKS=
    for dnm_sys_block in /sys/class/block/${dnm_controller_name}n*; do
        [ -e "$dnm_sys_block" ] || continue
        dnm_node=/dev/${dnm_sys_block##*/}
        [ "$(lsblk -dnro TYPE "$dnm_node" 2>/dev/null)" = disk ] || continue
        SCOPE_DISKS="$SCOPE_DISKS $dnm_node"
    done
    [ -n "$SCOPE_DISKS" ] || die "aucun espace de noms trouvé pour $NVME_CONTROLLER"

    dnm_json=$(nvme id-ctrl "$NVME_CONTROLLER" -o json 2>/dev/null) || \
        die "impossible d'interroger $NVME_CONTROLLER"
    dnm_sanicap=$(printf '%s\n' "$dnm_json" | json_number sanicap)
    [ -n "$dnm_sanicap" ] || \
        die "impossible de lire les capacités Sanitize de $NVME_CONTROLLER"

    if [ $((dnm_sanicap & 2)) -ne 0 ]; then
        METHOD=nvme-block-erase
        METHOD_DESCRIPTION="NVMe Sanitize Block Erase"
    elif [ $((dnm_sanicap & 1)) -ne 0 ]; then
        METHOD=nvme-crypto-erase
        METHOD_DESCRIPTION="NVMe Sanitize Crypto Erase"
    elif [ $((dnm_sanicap & 4)) -ne 0 ]; then
        METHOD=nvme-overwrite
        METHOD_DESCRIPTION="NVMe Sanitize Overwrite"
    else
        die "ce NVMe ne fournit aucune méthode Sanitize prise en charge"
    fi

    nvme sanitize --help 2>&1 | grep -q -- '--wait' || \
        die "la version de nvme-cli ne permet pas d'attendre la fin du Sanitize"
}

ata_security_section() {
    awk '
        /^[[:space:]]*Security:/ { in_security = 1 }
        in_security { print }
        in_security && /^[^[:space:]][^:]*:/ && $0 !~ /^Security:/ { exit }
    '
}

# Some USB bridges return malformed ATA sense data while hdparm still exits 0
# and prints an apparent state. Do not trust registers decoded from that reply.
read_ata_sanitize_status() {
    ATA_SANITIZE_STATUS_REASON=
    if ! ATA_SANITIZE_STATUS=$(hdparm --sanitize-status "$DISK" 2>&1); then
        ATA_SANITIZE_STATUS_REASON="échec de la commande de lecture de l'état ATA Sanitize"
        return 1
    fi
    if printf '%s\n' "$ATA_SANITIZE_STATUS" | grep -q 'SG_IO:'; then
        ATA_SANITIZE_STATUS_REASON="retour SG_IO non fiable ; l'état Sanitize affiché ne permet pas de vérifier l'effacement via cet adaptateur"
        return 1
    fi
    if ! printf '%s\n' "$ATA_SANITIZE_STATUS" | \
        grep -Eq '^[[:space:]]*State:[[:space:]]*SD[012][[:space:]]+Sanitize '
    then
        ATA_SANITIZE_STATUS_REASON="état ATA Sanitize absent ou non reconnu"
        return 1
    fi
}

detect_ata_method() {
    ATA_UNAVAILABLE_REASON="aucune méthode ATA compatible détectée"
    if ! command -v hdparm >/dev/null 2>&1; then
        ATA_UNAVAILABLE_REASON="hdparm est absent ; les capacités ATA du disque n'ont pas pu être interrogées"
        return 1
    fi
    if ! ATA_IDENTIFY=$(hdparm -I "$DISK" 2>/dev/null); then
        ATA_UNAVAILABLE_REASON="impossible de lire les capacités ATA du disque ; l'adaptateur peut bloquer ces commandes"
        return 1
    fi

    if printf '%s\n' "$ATA_IDENTIFY" | grep -q 'BLOCK_ERASE_EXT command'; then
        read_ata_sanitize_status || {
            ATA_UNAVAILABLE_REASON="le disque annonce ATA Sanitize, mais $ATA_SANITIZE_STATUS_REASON"
            return 1
        }
        METHOD=ata-sanitize-block
        METHOD_DESCRIPTION="ATA Sanitize Block Erase"
        return 0
    fi
    if printf '%s\n' "$ATA_IDENTIFY" | grep -q 'CRYPTO_SCRAMBLE_EXT command'; then
        read_ata_sanitize_status || {
            ATA_UNAVAILABLE_REASON="le disque annonce ATA Sanitize, mais $ATA_SANITIZE_STATUS_REASON"
            return 1
        }
        METHOD=ata-sanitize-crypto
        METHOD_DESCRIPTION="ATA Sanitize Crypto Scramble"
        return 0
    fi
    if printf '%s\n' "$ATA_IDENTIFY" | grep -q 'OVERWRITE_EXT command'; then
        read_ata_sanitize_status || {
            ATA_UNAVAILABLE_REASON="le disque annonce ATA Sanitize, mais $ATA_SANITIZE_STATUS_REASON"
            return 1
        }
        METHOD=ata-sanitize-overwrite
        METHOD_DESCRIPTION="ATA Sanitize Overwrite"
        return 0
    fi

    if [ "$TRANSPORT" = usb ]; then
        ATA_UNAVAILABLE_REASON="aucune commande ATA Sanitize compatible ; ATA Secure Erase est désactivé via USB dans ce script"
        return 1
    fi
    das_security=$(printf '%s\n' "$ATA_IDENTIFY" | ata_security_section)
    printf '%s\n' "$das_security" | \
        grep -Eq '^[[:space:]]*supported[[:space:]]*$' || return 1
    printf '%s\n' "$das_security" | grep -q 'not[[:space:]]*frozen' || return 1
    printf '%s\n' "$das_security" | grep -q 'not[[:space:]]*enabled' || \
        die "la sécurité ATA est déjà activée ; intervention manuelle requise"
    printf '%s\n' "$das_security" | grep -q 'not[[:space:]]*locked' || \
        die "le disque ATA est verrouillé ; intervention manuelle requise"

    if printf '%s\n' "$das_security" | grep -q 'supported: enhanced erase'; then
        METHOD=ata-security-enhanced
        METHOD_DESCRIPTION="ATA Enhanced Security Erase"
    else
        METHOD=ata-security-erase
        METHOD_DESCRIPTION="ATA Security Erase"
    fi
    return 0
}

detect_method() {
    case "${DISK##*/}" in
        nvme*n*)
            detect_nvme_method
            return
            ;;
    esac

    if detect_ata_method; then
        return
    fi

    if [ "$ROTA" = 1 ]; then
        METHOD=overwrite-hdd
        METHOD_DESCRIPTION="réécriture complète avec des zéros et vérification"
        return
    fi

    if [ "$TRANSPORT" = usb ]; then
        die "effacement sécurisé du SSD USB refusé : $ATA_UNAVAILABLE_REASON. M.2 désigne le format du disque, pas son interface ; pour un effacement contrôleur, utiliser une connexion directe adaptée (SATA ou NVMe)."
    fi
    die "SSD ou mémoire flash sans commande contrôleur fiable ; effacement refusé : $ATA_UNAVAILABLE_REASON"
}

confirm_erasure() {
    printf '\nATTENTION : toutes les données de %s seront détruites.\n' "$DISK" >/dev/tty
    if [ -n "$SERIAL" ]; then
        printf 'Recopiez le numéro de série « %s » : ' "$SERIAL" >/dev/tty
        IFS= read -r ce_answer </dev/tty || die "impossible de lire la confirmation"
        [ "$ce_answer" = "$SERIAL" ] || die "confirmation incorrecte"
    else
        ce_expected="EFFACER $DISK"
        printf 'Recopiez exactement « %s » : ' "$ce_expected" >/dev/tty
        IFS= read -r ce_answer </dev/tty || die "impossible de lire la confirmation"
        [ "$ce_answer" = "$ce_expected" ] || die "confirmation incorrecte"
    fi
}

wait_ata_sanitize() {
    while :; do
        if ! read_ata_sanitize_status; then
            printf '%s\n' "$ATA_SANITIZE_STATUS" | tee -a "$LOG_FILE"
            die "$ATA_SANITIZE_STATUS_REASON ; la réussite de l'effacement n'est pas confirmée"
        fi
        was_output=$ATA_SANITIZE_STATUS
        printf '%s\n' "$was_output" | tee -a "$LOG_FILE"

        if printf '%s\n' "$was_output" | \
            grep -q 'Last Sanitize Operation Completed Without Error'
        then
            return
        fi
        if printf '%s\n' "$was_output" | grep -Eqi 'failed|failure'; then
            die "ATA Sanitize a signalé un échec"
        fi
        sleep 15
    done
}

erase_nvme() {
    case "$METHOD" in
        nvme-block-erase) en_action=start-block-erase ;;
        nvme-crypto-erase) en_action=start-crypto-erase ;;
        nvme-overwrite) en_action=start-overwrite ;;
        *) die "méthode NVMe interne invalide : $METHOD" ;;
    esac

    run_logged nvme sanitize "$NVME_CONTROLLER" \
        --sanact="$en_action" --force --wait || \
        die "échec de $METHOD_DESCRIPTION"
    en_log=$(nvme sanitize-log "$NVME_CONTROLLER" 2>&1) || {
        printf '%s\n' "$en_log" | tee -a "$LOG_FILE"
        die "impossible de vérifier l'état final NVMe Sanitize"
    }
    printf '%s\n' "$en_log" | tee -a "$LOG_FILE"
    en_sstat=$(printf '%s\n' "$en_log" | awk '
        /Sanitize Status.*\(SSTAT\)/ {
            for (i = 1; i <= NF; i++) {
                if ($i ~ /^0[xX][[:xdigit:]]+$/ || $i ~ /^[0-9]+$/) {
                    print $i
                    exit
                }
            }
        }
    ')
    printf '%s\n' "$en_sstat" | \
        grep -Eq '^(0[xX][0-9A-Fa-f]+|[0-9]+)$' || \
        die "état final NVMe Sanitize illisible"
    en_sstat=$(printf '%s\n' "$en_sstat" | sed 's/^0X/0x/')
    [ $((en_sstat & 7)) -eq 1 ] || \
        die "le contrôleur NVMe ne confirme pas la réussite du Sanitize"
}

erase_ata_sanitize() {
    if ! read_ata_sanitize_status; then
        printf '%s\n' "$ATA_SANITIZE_STATUS" | tee -a "$LOG_FILE"
        die "$ATA_SANITIZE_STATUS_REASON"
    fi
    eas_status=$ATA_SANITIZE_STATUS
    printf '%s\n' "$eas_status" | grep -qi 'frozen' && \
        die "ATA Sanitize est gelé ; effectuer un arrêt électrique complet"

    case "$METHOD" in
        ata-sanitize-block)
            eas_option=--sanitize-block-erase
            ;;
        ata-sanitize-crypto)
            eas_option=--sanitize-crypto-scramble
            ;;
        ata-sanitize-overwrite)
            eas_option=--sanitize-overwrite
            ;;
        *) die "méthode ATA Sanitize interne invalide : $METHOD" ;;
    esac

    if [ "$METHOD" = ata-sanitize-overwrite ]; then
        run_logged hdparm --yes-i-know-what-i-am-doing \
            "$eas_option" 0x00000000 "$DISK" || \
            die "échec du démarrage de $METHOD_DESCRIPTION"
    else
        run_logged hdparm --yes-i-know-what-i-am-doing \
            "$eas_option" "$DISK" || \
            die "échec du démarrage de $METHOD_DESCRIPTION"
    fi
    wait_ata_sanitize
}

erase_ata_security() {
    log "Mot de passe ATA temporaire : $ATA_PASSWORD"
    log "En cas d'interruption, ne pas jeter le mot de passe et consulter hdparm -I."

    run_logged hdparm --user-master u --security-set-pass \
        "$ATA_PASSWORD" "$DISK" || \
        die "impossible d'activer temporairement la sécurité ATA"

    if [ "$METHOD" = ata-security-enhanced ]; then
        eas_erase_option=--security-erase-enhanced
    else
        eas_erase_option=--security-erase
    fi

    if ! run_logged hdparm --user-master u "$eas_erase_option" \
        "$ATA_PASSWORD" "$DISK"
    then
        log "Le disque peut rester protégé par le mot de passe : $ATA_PASSWORD"
        die "échec de $METHOD_DESCRIPTION"
    fi

    eas_after=$(hdparm -I "$DISK" 2>&1) || \
        die "impossible de vérifier l'état ATA après effacement"
    printf '%s\n' "$eas_after" | tee -a "$LOG_FILE" >/dev/null
    printf '%s\n' "$eas_after" | ata_security_section | \
        grep -q 'not[[:space:]]*enabled' || \
        die "la sécurité ATA paraît toujours activée"
}

erase_hdd_by_overwrite() {
    run_logged shred --verbose --iterations=0 --zero "$DISK" || \
        die "échec de la réécriture complète"
    log "Vérification complète de la réécriture..."
    run_logged cmp --bytes="$SIZE_BYTES" "$DISK" /dev/zero || \
        die "la vérification a trouvé des données différentes de zéro"
}

perform_erasure() {
    sync
    blockdev --flushbufs "$DISK" || :

    case "$METHOD" in
        nvme-*) erase_nvme ;;
        ata-sanitize-*) erase_ata_sanitize ;;
        ata-security-*) erase_ata_security ;;
        overwrite-hdd) erase_hdd_by_overwrite ;;
        *) die "méthode interne inconnue : $METHOD" ;;
    esac

    sync
    blockdev --flushbufs "$DISK" || :
    blockdev --rereadpt "$DISK" >/dev/null 2>&1 || :
}

case "${1:-}" in
    -h|--help|help)
        usage
        exit 0
        ;;
esac

[ "$#" -eq 1 ] || {
    usage >&2
    exit 2
}

[ "$(id -u)" -eq 0 ] || die "ce script doit être lancé avec sudo ou en root"
check_requirements

DISK=$(readlink -f -- "$1")
[ -b "$DISK" ] || die "$1 ne désigne pas un périphérique bloc"
[ "$(lsblk -dnro TYPE "$DISK")" = disk ] || \
    die "$DISK n'est pas un disque entier"
[ "$(blockdev --getro "$DISK")" -eq 0 ] || die "$DISK est en lecture seule"

SCOPE_DISKS=$DISK
check_unused
read_identity

mkdir -p "$LOG_DIR"
ed_safe_serial=$(printf '%s' "${SERIAL:-sans-serie}" | tr -c 'A-Za-z0-9._-' '_')
LOG_FILE=$LOG_DIR/$(date '+%Y%m%d-%H%M%S')-$ed_safe_serial.log
: >"$LOG_FILE"
chmod 600 "$LOG_FILE"

detect_method

log "Début             : $(date -Is)"
log "Disque            : $DISK"
log "Modèle            : ${MODEL:-inconnu}"
log "Numéro de série   : ${SERIAL:-inconnu}"
log "Capacité          : $SIZE"
log "Transport         : ${TRANSPORT:-inconnu}"
log "Rotation          : ${ROTA:-inconnue}"
log "Méthode retenue   : $METHOD_DESCRIPTION"
log "Journal           : $LOG_FILE"

case "$DISK" in
    /dev/nvme*n*)
        log "Portée NVMe       :$SCOPE_DISKS"
        ;;
esac
if [ "$TRANSPORT" = usb ]; then
    log "Avertissement     : le pont USB doit transmettre correctement la commande."
fi

confirm_erasure
check_identity_unchanged
check_unused

log "Effacement commencé : $(date -Is)"
perform_erasure
log "Effacement terminé  : $(date -Is)"
log "Résultat             : SUCCÈS"
