#!/usr/bin/env bash
#
# Run the matched experiments required after the all-future first-onset update.
#
# Shared by every experiment:
#   - corrected first-onset likelihood and outcome-specific risk exposure;
#   - patient/interval/time-uniform query sampling in train/valid/test;
#   - timed disease history and smoking/alcohol/BMI extra information.
#
# Per seed, the script trains:
#   1. TrajMixer + relative Weibull + fitted risk baseline (primary model);
#   2. the primary model without the fitted risk baseline (baseline ablation);
#   3. TrajMixer + relative exponential (time-distribution control);
#   4. FFN + relative Weibull (architecture control).
#
# After training, AUC and calibration/point-process-NLL evaluation run
# automatically unless --train-only is supplied.

set -uo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

GPU_CSV=""
SEED_CSV="42,43,44"
NUM_WORKERS=4
PYTHON_BIN="${PYTHON_BIN:-python}"
CAMPAIGN_NAME="all_future_first_onset_v2_multiseed"
DRY_RUN=0
TRAIN_ONLY=0

BATCH_SIZE=256
EXTRA_INFO_TYPES_FILE="$SCRIPT_DIR/extra_info_types_smoking_alcohol_bmi.txt"
TRAIN_EID_FILE="$SCRIPT_DIR/ukb_train_eid.csv"
VAL_EID_FILE="$SCRIPT_DIR/ukb_val_eid.csv"
TEST_EID_FILE="$SCRIPT_DIR/ukb_test_eid.csv"

usage() {
    cat <<'EOF'
Usage:
  bash run_all_future_first_onset_experiments_linux.sh --gpus LIST [options]

Required:
  --gpus LIST            Comma-separated GPU ids, for example 0 or 0,1,2,3.

Options:
  --seeds LIST           Comma-separated seeds (default: 42,43,44).
  --num-workers N        DataLoader workers per active GPU (default: 4).
  --python PATH          Python executable (default: $PYTHON_BIN or python).
  --campaign NAME        Output campaign directory name.
  --train-only           Skip AUC and calibration/NLL evaluation.
  --dry-run              Print commands without running them.
  -h, --help             Show this help message.

Fixed settings:
  batch_size             256
  extra information     smoking + alcohol + BMI
  patient split         fixed train/validation/test EID files
  disease history       timed
  experiments per seed  4

Outputs:
  runs/<campaign>/seed_<seed>/<architecture>/...
  batch_logs/<campaign>/seed_<seed>/<experiment>.log
EOF
}

while (($# > 0)); do
    case "$1" in
        --gpus)
            [[ $# -ge 2 ]] || {
                echo "ERROR: --gpus requires a value." >&2
                exit 2
            }
            GPU_CSV="$2"
            shift 2
            ;;
        --seeds)
            [[ $# -ge 2 ]] || {
                echo "ERROR: --seeds requires a value." >&2
                exit 2
            }
            SEED_CSV="$2"
            shift 2
            ;;
        --num-workers)
            [[ $# -ge 2 ]] || {
                echo "ERROR: --num-workers requires a value." >&2
                exit 2
            }
            NUM_WORKERS="$2"
            shift 2
            ;;
        --python)
            [[ $# -ge 2 ]] || {
                echo "ERROR: --python requires a value." >&2
                exit 2
            }
            PYTHON_BIN="$2"
            shift 2
            ;;
        --campaign)
            [[ $# -ge 2 ]] || {
                echo "ERROR: --campaign requires a value." >&2
                exit 2
            }
            CAMPAIGN_NAME="$2"
            shift 2
            ;;
        --train-only)
            TRAIN_ONLY=1
            shift
            ;;
        --dry-run)
            DRY_RUN=1
            shift
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            echo "ERROR: unknown argument: $1" >&2
            usage >&2
            exit 2
            ;;
    esac
done

[[ -n "$GPU_CSV" ]] || {
    echo "ERROR: --gpus is required." >&2
    usage >&2
    exit 2
}
[[ -n "$SEED_CSV" ]] || {
    echo "ERROR: --seeds must not be empty." >&2
    exit 2
}
[[ "$NUM_WORKERS" =~ ^[0-9]+$ ]] || {
    echo "ERROR: --num-workers must be a non-negative integer." >&2
    exit 2
}
[[ "$CAMPAIGN_NAME" =~ ^[A-Za-z0-9._-]+$ ]] || {
    echo "ERROR: --campaign may contain only letters, numbers, ., _, and -." >&2
    exit 2
}

required_files=(
    "$SCRIPT_DIR/train_all_future.py"
    "$SCRIPT_DIR/evaluate_all_runs_linux.sh"
    "$SCRIPT_DIR/evaluate_calibration_all_runs_linux.sh"
    "$EXTRA_INFO_TYPES_FILE"
    "$TRAIN_EID_FILE"
    "$VAL_EID_FILE"
    "$TEST_EID_FILE"
)
for required_file in "${required_files[@]}"; do
    [[ -f "$required_file" ]] || {
        echo "ERROR: missing required file: $required_file" >&2
        exit 2
    }
done
command -v "$PYTHON_BIN" >/dev/null 2>&1 || {
    echo "ERROR: Python executable not found: $PYTHON_BIN" >&2
    exit 2
}

IFS=',' read -r -a GPU_IDS <<< "$GPU_CSV"
declare -A SEEN_GPUS=()
for gpu in "${GPU_IDS[@]}"; do
    [[ -n "$gpu" && "$gpu" =~ ^[A-Za-z0-9._:-]+$ ]] || {
        echo "ERROR: invalid GPU id: $gpu" >&2
        exit 2
    }
    [[ -z "${SEEN_GPUS[$gpu]+x}" ]] || {
        echo "ERROR: duplicate GPU id: $gpu" >&2
        exit 2
    }
    SEEN_GPUS["$gpu"]=1
done

IFS=',' read -r -a SEEDS <<< "$SEED_CSV"
declare -A SEEN_SEEDS=()
for seed in "${SEEDS[@]}"; do
    [[ "$seed" =~ ^[0-9]+$ ]] || {
        echo "ERROR: invalid seed: $seed" >&2
        exit 2
    }
    [[ -z "${SEEN_SEEDS[$seed]+x}" ]] || {
        echo "ERROR: duplicate seed: $seed" >&2
        exit 2
    }
    SEEN_SEEDS["$seed"]=1
done

RUNS_ROOT="$SCRIPT_DIR/runs/$CAMPAIGN_NAME"
LOG_ROOT="$SCRIPT_DIR/batch_logs/$CAMPAIGN_NAME"
if ((DRY_RUN == 0)); then
    mkdir -p "$RUNS_ROOT" "$LOG_ROOT"
fi

declare -a JOB_NAMES=()
declare -a JOB_SEEDS=()
declare -a JOB_ARCHITECTURES=()
declare -a JOB_DIST_MODES=()
declare -a JOB_BASELINES=()

add_job() {
    JOB_NAMES+=("$1")
    JOB_SEEDS+=("$2")
    JOB_ARCHITECTURES+=("$3")
    JOB_DIST_MODES+=("$4")
    JOB_BASELINES+=("$5")
}

for seed in "${SEEDS[@]}"; do
    add_job \
        "traj_mixer_relative_weibull_first_onset" \
        "$seed" \
        "traj_mixer_v5" \
        "weibull" \
        "on"
    add_job \
        "traj_mixer_relative_weibull_no_rate_baseline" \
        "$seed" \
        "traj_mixer_v5" \
        "weibull" \
        "off"
    add_job \
        "traj_mixer_relative_exponential_first_onset" \
        "$seed" \
        "traj_mixer_v5" \
        "exponential" \
        "on"
    add_job \
        "ffn_relative_weibull_first_onset" \
        "$seed" \
        "transformer_ffn_v1" \
        "weibull" \
        "on"
done

print_command() {
    printf '%q ' "$@"
    printf '\n'
}

run_job() {
    local job_index="$1"
    local gpu="$2"
    local job_name="${JOB_NAMES[$job_index]}"
    local seed="${JOB_SEEDS[$job_index]}"
    local architecture="${JOB_ARCHITECTURES[$job_index]}"
    local dist_mode="${JOB_DIST_MODES[$job_index]}"
    local baseline="${JOB_BASELINES[$job_index]}"
    local seed_runs_root="$RUNS_ROOT/seed_$seed"
    local seed_log_root="$LOG_ROOT/seed_$seed"
    local log_file="$seed_log_root/$job_name.log"
    local -a command=(
        "$PYTHON_BIN"
        -u
        "$SCRIPT_DIR/train_all_future.py"
        --runs_root "$seed_runs_root"
        --seed "$seed"
        --batch_size "$BATCH_SIZE"
        --num_workers "$NUM_WORKERS"
        --device cuda
        --model_architecture "$architecture"
        --time_mode relative
        --dist_mode "$dist_mode"
        --disease_history_mode timed
        --extra_info_types_file "$EXTRA_INFO_TYPES_FILE"
        --train_eid_file "$TRAIN_EID_FILE"
        --val_eid_file "$VAL_EID_FILE"
        --test_eid_file "$TEST_EID_FILE"
    )

    if [[ "$baseline" == "on" ]]; then
        command+=(--risk_head_bias)
    else
        command+=(--no-risk_head_bias)
    fi

    if ((DRY_RUN == 0)); then
        mkdir -p "$seed_runs_root" "$seed_log_root"
    fi

    echo "[$(date '+%F %T')] START seed=$seed job=$job_name gpu=$gpu"
    echo "  log=$log_file"
    if ((DRY_RUN)); then
        printf '  CUDA_VISIBLE_DEVICES=%q ' "$gpu"
        print_command "${command[@]}"
        return 0
    fi

    if CUDA_VISIBLE_DEVICES="$gpu" PYTHONUNBUFFERED=1 \
        "${command[@]}" >"$log_file" 2>&1; then
        echo "[$(date '+%F %T')] DONE  seed=$seed job=$job_name gpu=$gpu"
        return 0
    else
        local exit_code=$?
        echo "[$(date '+%F %T')] FAIL  seed=$seed job=$job_name gpu=$gpu exit=$exit_code" >&2
        echo "  See: $log_file" >&2
        return "$exit_code"
    fi
}

worker() {
    local slot="$1"
    local gpu="${GPU_IDS[$slot]}"
    local job_index
    local failed=0

    for ((job_index = slot; job_index < ${#JOB_NAMES[@]}; job_index += ${#GPU_IDS[@]})); do
        run_job "$job_index" "$gpu" || failed=1
    done
    return "$failed"
}

echo "Campaign: $CAMPAIGN_NAME"
echo "Seeds: ${SEEDS[*]}"
echo "GPUs: ${GPU_IDS[*]}"
echo "Experiments per seed: 4"
echo "Total training tasks: ${#JOB_NAMES[@]}"
echo "Runs root: $RUNS_ROOT"
echo "Log root: $LOG_ROOT"
echo

declare -a WORKER_PIDS=()
for ((slot = 0; slot < ${#GPU_IDS[@]}; slot++)); do
    worker "$slot" &
    WORKER_PIDS+=("$!")
done

overall_status=0
for pid in "${WORKER_PIDS[@]}"; do
    wait "$pid" || overall_status=1
done

if ((overall_status != 0)); then
    echo "One or more training tasks failed. Inspect: $LOG_ROOT" >&2
    exit 1
fi

if ((TRAIN_ONLY == 0)); then
    evaluation_status=0
    auc_command=(
        bash
        "$SCRIPT_DIR/evaluate_all_runs_linux.sh"
        --gpus "$GPU_CSV"
        --runs-root "$RUNS_ROOT"
        --log-root "$LOG_ROOT/evaluate_auc"
        --python "$PYTHON_BIN"
        --num-workers "$NUM_WORKERS"
        --num-workers-auc "$NUM_WORKERS"
    )
    calibration_command=(
        bash
        "$SCRIPT_DIR/evaluate_calibration_all_runs_linux.sh"
        --gpus "$GPU_CSV"
        --runs-root "$RUNS_ROOT"
        --log-root "$LOG_ROOT/evaluate_calibration"
        --python "$PYTHON_BIN"
        --num-workers "$NUM_WORKERS"
        --num-workers-calibration "$NUM_WORKERS"
    )

    echo ">> AUC evaluation"
    print_command "${auc_command[@]}"
    if ((DRY_RUN == 0)); then
        "${auc_command[@]}" || evaluation_status=1
    fi

    echo ">> Calibration and point-process NLL evaluation"
    print_command "${calibration_command[@]}"
    if ((DRY_RUN == 0)); then
        "${calibration_command[@]}" || evaluation_status=1
    fi

    if ((evaluation_status != 0)); then
        echo "One or more evaluation workflows failed. Inspect: $LOG_ROOT" >&2
        exit 1
    fi
fi

if ((DRY_RUN)); then
    echo "Dry run completed successfully."
else
    echo "All required all-future experiments completed successfully."
fi
