#!/bin/bash

# ============================================================
#  FRU Write Utility Script
#
#  Description: 
#    This script writes FRU information to BMC using "ipmitool fru edit" and raw IPMI commands.
#
#  IMPORTANT NOTICE:
#    ❗ All FRU fields(Besides MfgDate) in this script are written using fixed 24 bytes length strings to
#      avoid issues when using "ipmitool fru edit".
#
#    Known issues with ipmitool (especially ≤ v1.8.18):
#      1. Editing a field with a new string length ≠ original length may cause incorrect section relocation.
#      2. This can result in out-of-bounds FRU offsets, checksum errors, or corrupted FRU areas.
#    Even in v1.8.19 versions, "fru edit" is not fully reliable:
#      - Editing strings of different lengths may still trigger FRU Area offset incorrect.
#    To ensure safe and repeatable FRU updates while using "fru edit":
#      This script writes each field using a fixed 24 bytes length string.
#
#  Usage: ./write_fru.sh <BMC_IP> <BMC_Username> <BMC_Password> <FRU_Write_Password> <FRU_Conf_File> [-v]
#         ./write_fru.sh -h
#
#  FRU_Conf_File:
#   - fru_info_factory.conf:
#       1. Defines ALL FRU fields.  
#       2. Intended for factory use where all board/chassis/system information—including serial numbers—is programmed.
#
#   - fru_info_customer.conf:
#       1. Defines all FRU fields EXCEPT Board Serial Number and Chassis Serial Number.  
#       2. Intended for customer use to update general system information without modifying factory-programmed SN fields.
#
#  Behavior:
#   - With -h: Show usage help
#   - With -v: Show verbose field-by-field write log
#   - Parse <FRU_CONF_FILE> and write each valid field to BMC.
#   - Special handling for Board Manufacturer Date Time (human-readable -> 3-byte FRU)
#   - Exit immediately on failure
#   - Final summary: PASS or FAIL
# ============================================================

# === Arguments ===
BMC_IP="$1"
BMC_USER="$2"
BMC_PASSWORD="$3"
FRU_WRITE_PASSWORD="$4"
FRU_FILE="$5"
VERBOSE=0

# === Parse optional verbose flag ===
if [[ "$6" == "-v" || "$6" == "--verbose" ]]; then
    VERBOSE=1
fi

# === Help/usage ===
show_usage() {
    cat <<EOF
Usage: $0 <BMC_IP> <BMC_Username> <BMC_Password> <FRU_Write_Password> <FRU_INFO.CONF> [-v]
       $0 -h

Options:
  -h, --help      Show this help message
  -v, --verbose   Show per-field write log

Examples:
  Basic usage:
    $0 192.168.1.100 bmc_user bmc_pw fru_pw fru_info_customer.conf

  Verbose:
    $0 192.168.1.100 bmc_user bmc_pw fru_pw fru_info_customer.conf -v
EOF
    exit 0
}

# === Validate args ===
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
    show_usage
fi

if [[ $# -lt 5 || $# -gt 6 ]]; then
    echo "Error: Invalid number of arguments" >&2
    show_usage
fi

if [[ ! -f "$FRU_FILE" ]]; then
    echo "❌ Error: FRU config file '$FRU_FILE' not found" >&2
    echo "❌ FRU Write Result: FAIL"
    exit 1
fi

# === Constants ===
MAX_LEN=24
BASE_DATE="1996-01-01 00:00:00"
# maximum minutes representable in 3 bytes
MAX_MINUTES=$((0xFFFFFF))

# === Convert human-readable date to 3-byte FRU time (little endian) ===
date_to_fru_time_hex() {
    local human="$1"
    local target_epoch base_epoch delta_seconds total_minutes

    target_epoch=$(date -u -d "$human" +%s 2>/dev/null) || return 1
    base_epoch=$(date -u -d "$BASE_DATE" +%s)
    delta_seconds=$(( target_epoch - base_epoch ))
    total_minutes=$(( delta_seconds / 60 ))

    # If total_minutes < 0 OR > MAX_MINUTES → return 3 bytes of zero
    if (( total_minutes < 0 || total_minutes > MAX_MINUTES )); then
        echo "0x00 0x00 0x00"
        return 0
    fi

    local b0=$(( total_minutes & 0xFF ))
    local b1=$(( (total_minutes >> 8) & 0xFF ))
    local b2=$(( (total_minutes >> 16) & 0xFF ))
    printf "0x%02X 0x%02X 0x%02X" "$b0" "$b1" "$b2"
}

# === Run ipmitool ===
run_ipmitool() {
    "$@" > /dev/null 2>&1
    if [[ $? -ne 0 ]]; then
        echo "❌ Error: ipmitool command failed: $*" >&2
        return 1
    fi
    return 0
}

# Safe write wrapper (fixed MAX_LEN bytes + retry once)
# Writes a FRU field with fixed MAX_LEN bytes: values > MAX_LEN bytes → ERROR, values < MAX_LEN bytes are padded with spaces. 
# Includes one retry to avoid ipmitool’s first-edit length/checksum failure.
write_fru_field_safe() {
    local fru_id="$1"
    local section="$2"
    local idx="$3"
    local value="$4"

    # Check maximum length
    if [[ ${#value} -gt $MAX_LEN ]]; then
        echo "❌ Error: Field value length (${#value}) exceeds maximum ${MAX_LEN} bytes for $section $idx" >&2
        return 1
    fi

    # Pad the value with spaces to ensure it is MAX_LEN bytes long
    while [[ ${#value} -lt $MAX_LEN ]]; do
        value="$value "
    done

    # Write FRU.bin by ipmitool fru edit with a retry mechanism
    for attempt in 1 2; do
        if ipmitool -H "$BMC_IP" -I lanplus -U "$BMC_USER" -P "$BMC_PASSWORD" fru edit "$fru_id" field "$section" "$idx" "$value" > /dev/null 2>&1; then
            # Successfully written
            return 0
        else
            # First failure，retry
            if [[ $attempt -eq 1 ]]; then
                [[ $VERBOSE -eq 1 ]] && echo "⚠️ Retry writing field: $section $idx => '$value'"
            else
                # The second attempt also failed, returning an error.
                echo "❌ Failed to write field safely after retry: $section $idx => '$value'" >&2
                return 1
            fi
        fi
    done
}

# === Parse FRU_ID ===
FRU_ID=$(grep -E '^FRU ID:' "$FRU_FILE" | awk -F'"' '{print $2}')
if [[ -z "$FRU_ID" ]]; then
    echo "❌ Error: FRU ID not found in $FRU_FILE" >&2
    exit 1
fi

# === Parse FRU file ===
declare -A fru_data
ordered_keys=()
current_section=""

while IFS= read -r line || [ -n "$line" ]; do
    # Update current_section
    [[ "$line" =~ ^\*([A-Za-z_]+)\* ]] && current_section="${BASH_REMATCH[1]}" && continue

    # Exclude FRU ID lines
    [[ "$line" =~ ^FRU\ ID: ]] && continue

    # Matching field key: "value"
    [[ "$line" =~ ^([A-Za-z ]+):\ \"(.*)\" ]] || continue

    key="${current_section}.${BASH_REMATCH[1]}"
    value="${BASH_REMATCH[2]}"
    fru_data["$key"]="$value"
    ordered_keys+=("$key")
done < "$FRU_FILE"

# === Mapping table: section + index ===
declare -A FRU_SECTION_MAP=(
    ["Chassis.Part Number"]="c 0"
    ["Chassis.Serial Number"]="c 1"
    ["Board.Manufacturer"]="b 0"
    ["Board.Product Name"]="b 1"
    ["Board.Part Number"]="b 2"
    ["Board.Serial Number"]="b 3"
    ["Product.Manufacturer"]="p 0"
    ["Product.Product Name"]="p 1"
    ["Product.Part Number"]="p 2"
    ["Product.Version"]="p 3"
    ["Product.Serial Number"]="p 4"
    ["Product.AssetTag"]="p 5"
)

# === Start writing ===
overall_status=0
echo "Start writing FRU ID: $FRU_ID"

for field in "${ordered_keys[@]}"; do
    value="${fru_data[$field]}"
    map="${FRU_SECTION_MAP[$field]}"

    # special handling for Manufacturer Date Time
    if [[ "$field" == "Board.Manufacturer Date Time" ]]; then
        hex_value_str=$(date_to_fru_time_hex "$value")
        [[ $VERBOSE -eq 1 ]] && echo "Writing [$field] => \"$hex_value_str\" via raw 0x3c 0xf0"
        read -r -a hex_value <<< "$hex_value_str"
        if ! run_ipmitool ipmitool -H "$BMC_IP" -I lanplus -U "$BMC_USER" -P "$BMC_PASSWORD" raw 0x3A 0xF0 "$FRU_ID" "${hex_value[@]}"; then
            overall_status=1
            break
        fi
        continue
    fi

    # skip unknown field
    if [[ -z "$map" ]]; then
        [[ $VERBOSE -eq 1 ]] && echo "⚠️  Skipping unknown field [$field]"
        continue
    fi

    read -r section idx <<< "$map"
    [[ $VERBOSE -eq 1 ]] && echo "Writing [$field] => \"$value\" ($section $idx)"
    if ! write_fru_field_safe "$FRU_ID" "$section" "$idx" "$value"; then
        echo "❌ Failed to write field: [$field]" >&2
        overall_status=1
        break
    fi
done

# === Use the correct password to allow FRU.bin to be written to EEPROM ===
if [[ $overall_status -eq 0 ]]; then
    [[ $VERBOSE -eq 1 ]] && echo "Committing FRU.bin to EEPROM..."

    # Convert password string to hex bytes array
    password_bytes=()
    for ((i=0; i<${#FRU_WRITE_PASSWORD}; i++)); do
        c="${FRU_WRITE_PASSWORD:i:1}"
        printf -v hex "0x%02X" "'$c"
        password_bytes+=("$hex")
    done

    if ! run_ipmitool ipmitool -H "$BMC_IP" -I lanplus -U "$BMC_USER" -P "$BMC_PASSWORD" raw 0x3A 0xF1 "$FRU_ID" "${password_bytes[@]}"; then
        echo "❌ Failed to write FRU.bin to EEPROM. Your password may be incorrect." >&2
        overall_status=1
    fi
fi

# === Summary ===
if [[ $overall_status -eq 0 ]]; then
    echo -e "✅ FRU Write Result: PASS\n"
    exit 0
else
    echo -e "❌ FRU Write Result: FAIL\n"
    exit 1
fi