#!/usr/bin/env bash
# avi2appletv_pi4.sh
#
# Optimized for Raspberry Pi 4 (aarch64, 2GB RAM):
# - Limits memory/threads, uses faster preset + moderate CRF
# - Writes temp output then atomically renames (prevents partial/corrupt .mp4)
# - Extensive logging + per-step timing + end-of-run report
# - Verifies existing outputs (optional) and skips if OK
# - Retries with safer settings on failure
#
# Apple TV compatible output:
#   Container: MP4
#   Video: H.264 (yuv420p), High profile, level 4.1 (or 4.0 in fallback), faststart
#   Audio: AAC
#
# Requirements: bash 4+, ffmpeg, ffprobe
#
# Usage:
#   chmod +x avi2appletv_pi4.sh
#   ./avi2appletv_pi4.sh [folder]
#
# Env vars (optional):
#   OUT_DIR="converted"          # default: same folder
#   OVERWRITE=0|1                # default: 0
#   VERIFY_EXISTING=0|1          # default: 1
#   SCALE_1080=0|1               # default: 1 (downscale >1080p to <=1080p)
#   CRF=23                       # default: 23 (good balance on Pi4)
#   PRESET="veryfast"            # default: veryfast
#   AUDIO_BR="160k"              # default: 160k
#   THREADS=3                    # default: auto for Pi4 (keeps RAM in check)
#   NICE=10                      # default: 10 (be nicer to the system)
#   IONICE_CLASS=2 IONICE_LEVEL=7# default: best-effort / low priority
#   RETRY_ON_FAIL=1              # default: 1
#   DRY_RUN=0|1                  # default: 0

set -uo pipefail

IN_DIR="${1:-.}"
OUT_DIR="${OUT_DIR:-}"
OVERWRITE="${OVERWRITE:-0}"
VERIFY_EXISTING="${VERIFY_EXISTING:-1}"
SCALE_1080="${SCALE_1080:-1}"

CRF="${CRF:-23}"
PRESET="${PRESET:-veryfast}"
AUDIO_BR="${AUDIO_BR:-160k}"
RETRY_ON_FAIL="${RETRY_ON_FAIL:-1}"
DRY_RUN="${DRY_RUN:-0}"

# Pi4-friendly defaults
THREADS="${THREADS:-3}"
NICE="${NICE:-10}"
IONICE_CLASS="${IONICE_CLASS:-2}"
IONICE_LEVEL="${IONICE_LEVEL:-7}"

ts() { date "+%Y-%m-%d %H:%M:%S"; }
ns_now() { date +%s%N; }
ms_since() { local s="$1"; local e; e="$(ns_now)"; echo $(( (e - s)/1000000 )); }
human_ms() {
  local ms="$1"; local s=$((ms/1000)); local rem_ms=$((ms%1000))
  local m=$((s/60)); local rem_s=$((s%60))
  local h=$((m/60)); local rem_m=$((m%60))
  if (( h>0 )); then printf "%dh %dm %ds.%03d" "$h" "$rem_m" "$rem_s" "$rem_ms"
  elif (( m>0 )); then printf "%dm %ds.%03d" "$m" "$rem_s" "$rem_ms"
  else printf "%ds.%03d" "$rem_s" "$rem_ms"; fi
}

require_bin() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: missing $1" >&2; exit 127; }; }
safe_mkdir() { [[ -z "$1" ]] && return 0; mkdir -p "$1"; }

RUN_ID="$(date "+%Y%m%d_%H%M%S")"
if [[ ! -d "$IN_DIR" ]]; then echo "ERROR: not a directory: $IN_DIR" >&2; exit 2; fi
IN_DIR="$(cd "$IN_DIR" && pwd -P)"

LOG_DIR="${IN_DIR%/}/.avi2appletv_logs"
safe_mkdir "$LOG_DIR"
MAIN_LOG="$LOG_DIR/run_${RUN_ID}.log"
CSV_REPORT="$LOG_DIR/report_${RUN_ID}.csv"
TXT_REPORT="$LOG_DIR/report_${RUN_ID}.txt"

log() { echo "[$(ts)] $*" | tee -a "$MAIN_LOG"; }
log_file() { local flog="$1"; shift; echo "[$(ts)] $*" | tee -a "$flog" >>"$MAIN_LOG"; }

# --- ffprobe helpers (best-effort) ---
probe_duration_s() { ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$1" 2>/dev/null || echo ""; }
probe_codec() { # $1=file $2=selector (v:0 or a:0)
  ffprobe -v error -select_streams "$2" -show_entries stream=codec_name -of default=nw=1:nk=1 "$1" 2>/dev/null || echo ""
}
probe_frames_best_effort() {
  ffprobe -v error -count_frames -select_streams v:0 \
    -show_entries stream=nb_read_frames -of default=nw=1:nk=1 "$1" 2>/dev/null | head -n 1 || echo ""
}
probe_height() {
  ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=nw=1:nk=1 "$1" 2>/dev/null || echo ""
}
verify_output() {
  ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=nw=1:nk=1 "$1" >/dev/null 2>&1
}

# --- run wrapper for lower priority on Pi ---
run_cmd() {
  # Uses nice + ionice if available
  if command -v ionice >/dev/null 2>&1; then
    nice -n "$NICE" ionice -c "$IONICE_CLASS" -n "$IONICE_LEVEL" "$@"
  else
    nice -n "$NICE" "$@"
  fi
}

# --- build vf (scale down to <=1080p optionally) ---
build_vf() {
  local in_h="$1"
  if [[ "$SCALE_1080" != "1" ]]; then
    echo ""
    return
  fi
  # If we can’t detect height, still apply a safe downscale clause.
  if [[ -z "$in_h" ]]; then
    echo "scale=w=1920:h=1080:force_original_aspect_ratio=decrease"
    return
  fi
  if [[ "$in_h" =~ ^[0-9]+$ ]] && (( in_h > 1080 )); then
    echo "scale=w=1920:h=1080:force_original_aspect_ratio=decrease"
  else
    echo ""
  fi
}

encode_primary() {
  # $1=in $2=out_tmp $3=filelog $4=vf
  local in="$1" out_tmp="$2" flog="$3" vf="$4"
  local start_ns; start_ns="$(ns_now)"
  log_file "$flog" "FFmpeg(primary) start threads=$THREADS preset=$PRESET crf=$CRF vf=${vf:-<none>}"

  local args=(ffmpeg -hide_banner -nostdin -y
    -i "$in"
    -map 0:v:0 -map 0:a:0?
    -c:v libx264 -threads "$THREADS" -preset "$PRESET" -crf "$CRF"
    -profile:v high -level 4.1 -pix_fmt yuv420p
    -c:a aac -b:a "$AUDIO_BR"
    -movflags +faststart
    "$out_tmp"
  )
  if [[ -n "$vf" ]]; then
    # insert -vf before video codec options: easiest is to rebuild the args with vf after maps
    args=(ffmpeg -hide_banner -nostdin -y
      -i "$in"
      -map 0:v:0 -map 0:a:0?
      -vf "$vf"
      -c:v libx264 -threads "$THREADS" -preset "$PRESET" -crf "$CRF"
      -profile:v high -level 4.1 -pix_fmt yuv420p
      -c:a aac -b:a "$AUDIO_BR"
      -movflags +faststart
      "$out_tmp"
    )
  fi

  if [[ "$DRY_RUN" == "1" ]]; then
    log_file "$flog" "DRY_RUN: ${args[*]}"
    echo "0,$(ms_since "$start_ns")"
    return
  fi

  run_cmd "${args[@]}" >>"$flog" 2>&1
  local rc=$?
  local ms; ms="$(ms_since "$start_ns")"
  log_file "$flog" "FFmpeg(primary) done rc=$rc time=$(human_ms "$ms")"
  echo "$rc,$ms"
}

encode_fallback() {
  # $1=in $2=out_tmp $3=filelog $4=vf
  local in="$1" out_tmp="$2" flog="$3" vf="$4"
  local start_ns; start_ns="$(ns_now)"
  log_file "$flog" "FFmpeg(fallback) start: force fps=30, safer timestamps, lower level, faster preset"

  local args=(ffmpeg -hide_banner -nostdin -y
    -fflags +genpts
    -i "$in"
    -map 0:v:0 -map 0:a:0?
    -vf "fps=30${vf:+,$vf}"
    -c:v libx264 -threads "$THREADS" -preset veryfast -crf 24
    -profile:v high -level 4.0 -pix_fmt yuv420p
    -c:a aac -b:a 160k -ar 48000
    -movflags +faststart
    "$out_tmp"
  )

  if [[ "$DRY_RUN" == "1" ]]; then
    log_file "$flog" "DRY_RUN: ${args[*]}"
    echo "0,$(ms_since "$start_ns")"
    return
  fi

  run_cmd "${args[@]}" >>"$flog" 2>&1
  local rc=$?
  local ms; ms="$(ms_since "$start_ns")"
  log_file "$flog" "FFmpeg(fallback) done rc=$rc time=$(human_ms "$ms")"
  echo "$rc,$ms"
}

# ---- start ----
require_bin ffmpeg
require_bin ffprobe

if [[ -z "$OUT_DIR" ]]; then OUT_DIR="$IN_DIR"; else
  [[ "$OUT_DIR" != /* ]] && OUT_DIR="$IN_DIR/$OUT_DIR"
fi
safe_mkdir "$OUT_DIR"

log "Run ID        : $RUN_ID"
log "Host          : $(hostname) | $(uname -a)"
log "Input folder  : $IN_DIR"
log "Output folder : $OUT_DIR"
log "Pi4 tuning    : threads=$THREADS nice=$NICE ionice=${IONICE_CLASS}:${IONICE_LEVEL}"
log "Options       : OVERWRITE=$OVERWRITE VERIFY_EXISTING=$VERIFY_EXISTING SCALE_1080=$SCALE_1080 RETRY_ON_FAIL=$RETRY_ON_FAIL DRY_RUN=$DRY_RUN"
log "Encode        : preset=$PRESET crf=$CRF audio=$AUDIO_BR"
log "Logs          : $MAIN_LOG"

echo "file,input_path,output_path,status,reason,input_frames,output_frames,input_duration_s,output_duration_s,probe_ms,convert_ms,verify_ms,total_ms,video_in,audio_in,video_out,audio_out,scaled_to_1080" >"$CSV_REPORT"

run_start_ns="$(ns_now)"
total_files=0; ok_files=0; skip_files=0; fail_files=0
total_probe_ms=0; total_convert_ms=0; total_verify_ms=0; total_total_ms=0

while IFS= read -r -d '' in_file; do
  ((total_files++)) || true
  base="$(basename "$in_file")"
  name="${base%.*}"
  out_file="$OUT_DIR/${name}.mp4"

  file_log="$LOG_DIR/${RUN_ID}_${name}.log"
  : >"$file_log"

  log "----------------------------------------"
  log "Processing: $base"
  log_file "$file_log" "Input : $in_file"
  log_file "$file_log" "Output: $out_file"

  file_start_ns="$(ns_now)"

  # skip if exists (and optionally verifies)
  if [[ -f "$out_file" && "$OVERWRITE" != "1" ]]; then
    if [[ "$VERIFY_EXISTING" == "1" ]]; then
      vstart="$(ns_now)"
      if verify_output "$out_file"; then
        vms="$(ms_since "$vstart")"
        log_file "$file_log" "Output exists + verifies OK -> SKIP (verify_time=$(human_ms "$vms"))"
        ((skip_files++)) || true

        pstart="$(ns_now)"
        in_dur="$(probe_duration_s "$in_file")"
        in_frames="$(probe_frames_best_effort "$in_file")"
        vin="$(probe_codec "$in_file" "v:0")"
        ain="$(probe_codec "$in_file" "a:0")"
        out_dur="$(probe_duration_s "$out_file")"
        out_frames="$(probe_frames_best_effort "$out_file")"
        vout="$(probe_codec "$out_file" "v:0")"
        aout="$(probe_codec "$out_file" "a:0")"
        pms="$(ms_since "$pstart")"
        ftotal_ms="$(ms_since "$file_start_ns")"

        echo "\"$base\",\"$in_file\",\"$out_file\",\"SKIP\",\"exists+verified\",\"${in_frames}\",\"${out_frames}\",\"${in_dur}\",\"${out_dur}\",\"$pms\",\"0\",\"$vms\",\"$ftotal_ms\",\"$vin\",\"$ain\",\"$vout\",\"$aout\",\"\"" >>"$CSV_REPORT"
        total_probe_ms=$((total_probe_ms+pms))
        total_verify_ms=$((total_verify_ms+vms))
        total_total_ms=$((total_total_ms+ftotal_ms))
        continue
      else
        vms="$(ms_since "$vstart")"
        log_file "$file_log" "Output exists but verify FAILED -> will re-encode (verify_time=$(human_ms "$vms"))"
        out_file="$OUT_DIR/${name}.reencoded_${RUN_ID}.mp4"
        log_file "$file_log" "New output: $out_file"
      fi
    else
      log_file "$file_log" "Output exists and OVERWRITE=0 VERIFY_EXISTING=0 -> SKIP"
      ((skip_files++)) || true
      pstart="$(ns_now)"
      in_dur="$(probe_duration_s "$in_file")"
      in_frames="$(probe_frames_best_effort "$in_file")"
      vin="$(probe_codec "$in_file" "v:0")"
      ain="$(probe_codec "$in_file" "a:0")"
      pms="$(ms_since "$pstart")"
      ftotal_ms="$(ms_since "$file_start_ns")"
      echo "\"$base\",\"$in_file\",\"$out_file\",\"SKIP\",\"exists\",\"${in_frames}\",\"\",\"${in_dur}\",\"\",\"$pms\",\"0\",\"0\",\"$ftotal_ms\",\"$vin\",\"$ain\",\"\",\"\",\"\"" >>"$CSV_REPORT"
      total_probe_ms=$((total_probe_ms+pms))
      total_total_ms=$((total_total_ms+ftotal_ms))
      continue
    fi
  fi

  # probe input
  pstart="$(ns_now)"
  in_dur="$(probe_duration_s "$in_file")"
  in_frames="$(probe_frames_best_effort "$in_file")"
  in_h="$(probe_height "$in_file")"
  vin="$(probe_codec "$in_file" "v:0")"
  ain="$(probe_codec "$in_file" "a:0")"
  vf="$(build_vf "$in_h")"
  scaled_flag=""
  [[ -n "$vf" ]] && scaled_flag="yes"
  pms="$(ms_since "$pstart")"
  total_probe_ms=$((total_probe_ms+pms))
  log_file "$file_log" "Probe: dur_s=${in_dur:-?} frames=${in_frames:-?} height=${in_h:-?} v=${vin:-?} a=${ain:-?} vf=${vf:-<none>} time=$(human_ms "$pms")"

  # temp file then rename
  out_tmp="$OUT_DIR/.${name}.${RUN_ID}.tmp.mp4"
  rm -f "$out_tmp" 2>/dev/null || true
  [[ "$OVERWRITE" == "1" ]] && rm -f "$out_file" 2>/dev/null || true

  # encode primary
  IFS=',' read -r c_rc c_ms < <(encode_primary "$in_file" "$out_tmp" "$file_log" "$vf") || true
  total_convert_ms=$((total_convert_ms+c_ms))

  # fallback if needed
  if [[ "$c_rc" != "0" ]]; then
    log_file "$file_log" "Primary failed rc=$c_rc"
    if [[ "$RETRY_ON_FAIL" == "1" ]]; then
      rm -f "$out_tmp" 2>/dev/null || true
      IFS=',' read -r fb_rc fb_ms < <(encode_fallback "$in_file" "$out_tmp" "$file_log" "$vf") || true
      total_convert_ms=$((total_convert_ms+fb_ms))
      if [[ "$fb_rc" == "0" ]]; then
        c_rc=0
        c_ms=$((c_ms+fb_ms))
        log_file "$file_log" "Fallback succeeded"
      else
        log_file "$file_log" "Fallback failed rc=$fb_rc"
      fi
    fi
  fi

  # move temp to final if encode succeeded
  if [[ "$c_rc" == "0" && -f "$out_tmp" ]]; then
    mv -f "$out_tmp" "$out_file" 2>>"$file_log" || c_rc=3
  else
    rm -f "$out_tmp" 2>/dev/null || true
  fi

  # verify + probe output
  vstart="$(ns_now)"
  out_ok=0
  out_dur=""; out_frames=""; vout=""; aout=""
  if [[ "$c_rc" == "0" && -f "$out_file" ]]; then
    if verify_output "$out_file"; then out_ok=1; fi
    out_dur="$(probe_duration_s "$out_file")"
    out_frames="$(probe_frames_best_effort "$out_file")"
    vout="$(probe_codec "$out_file" "v:0")"
    aout="$(probe_codec "$out_file" "a:0")"
  fi
  vms="$(ms_since "$vstart")"
  total_verify_ms=$((total_verify_ms+vms))

  ftotal_ms="$(ms_since "$file_start_ns")"
  total_total_ms=$((total_total_ms+ftotal_ms))

  if [[ "$c_rc" == "0" && "$out_ok" == "1" ]]; then
    status="OK"; reason=""
    ((ok_files++)) || true
    log_file "$file_log" "SUCCESS verify OK (verify=$(human_ms "$vms") total=$(human_ms "$ftotal_ms"))"
  else
    status="FAIL"
    reason=$([[ "$c_rc" != "0" ]] && echo "encode_failed_rc_$c_rc" || echo "verify_failed")
    ((fail_files++)) || true
    log_file "$file_log" "FAILED $reason (verify=$(human_ms "$vms") total=$(human_ms "$ftotal_ms"))"
  fi

  echo "\"$base\",\"$in_file\",\"$out_file\",\"$status\",\"$reason\",\"${in_frames}\",\"${out_frames}\",\"${in_dur}\",\"${out_dur}\",\"$pms\",\"$c_ms\",\"$vms\",\"$ftotal_ms\",\"$vin\",\"$ain\",\"$vout\",\"$aout\",\"${scaled_flag}\"" >>"$CSV_REPORT"

done < <(find "$IN_DIR" -maxdepth 1 -type f \( -iname "*.avi" \) -print0)

run_total_ms="$(ms_since "$run_start_ns")"

{
  echo "AVI -> Apple TV MP4 Conversion Report (Pi4 optimized)"
  echo "Run ID        : $RUN_ID"
  echo "When          : $(ts)"
  echo "Host          : $(hostname)"
  echo "Kernel        : $(uname -r)"
  echo "Input folder  : $IN_DIR"
  echo "Output folder : $OUT_DIR"
  echo "Log folder    : $LOG_DIR"
  echo "Main log      : $MAIN_LOG"
  echo "CSV report    : $CSV_REPORT"
  echo ""
  echo "Summary"
  echo "  Total .avi found : $total_files"
  echo "  Converted OK     : $ok_files"
  echo "  Skipped          : $skip_files"
  echo "  Failed           : $fail_files"
  echo ""
  echo "Timing totals"
  echo "  Probe total   : $(human_ms "$total_probe_ms")"
  echo "  Convert total : $(human_ms "$total_convert_ms")"
  echo "  Verify total  : $(human_ms "$total_verify_ms")"
  echo "  Run total     : $(human_ms "$run_total_ms")"
  echo ""
  echo "Pi4 notes"
  echo "  - Using threads=$THREADS and preset=$PRESET to keep RAM/CPU reasonable on 2GB."
  echo "  - Output is written to a temp file then renamed to avoid partial .mp4 if interrupted."
  echo "  - If videos are >1080p and SCALE_1080=1, they will be downscaled to <=1080p."
  echo "  - Frame counts are best-effort (ffprobe -count_frames)."
} | tee "$TXT_REPORT" | tee -a "$MAIN_LOG"

log "Done."
log "Report: $TXT_REPORT"
log "CSV   : $CSV_REPORT"

