#!/usr/bin/env bash
#clear
#export PS4='\e[90m+${LINENO} in ${#BASH_SOURCE[@]}>${FUNCNAME[0]}:${BASH_SOURCE[@]##*/} \e[0m'
#set -x

#echo "starting: $0 <LOG_LEVEL=$1>"

# Tracking directory for all backup configs - must be at top level before any functions
BACKUP_CONFIGS_DIR="$HOME/config/backup.configs"

### new.method

backup.usage()
{
  local this=${0##*/}
  echo "You started 
$0

  Usage:
  $this: command   description and Parameter

      usage     prints this dialog while it will print the status when there are no parameters          
      v         print version information
      
      list
      list.config
      config.list      lists the configuration
      config.discover  walks from current dir upward, shows first .backup.env found
      config.which     shows which config file is currently active
      config.save      <?scope:local|global> saves config (default: local .backup.env in current dir)
      config.create    <?target> creates config for pwd with auto-generated target path
      config.list.all  lists all registered .backup.env files with their paths and targets
      config.register  <path> registers an existing .backup.env in the tracking directory
      config.unregister <?path> removes a config from the tracking directory
      config.register.existing  scans and registers all existing .backup.env files
      config.repair    <?configPath> scan registered configs for double-path bug and fix them
      config.disable   <configPath> disable a registered config (renames to disabled.backup.env)
      config.enable    <configPath> enable a disabled config (renames back to .backup.env)

      to               <directory> sets the backup target directory                 and saves the configuration
      from             <directory> sets the root source directroy to be backuped    and saves the configuration
      strategy         stes the startegy.... use Tab Completion to find out values  and saves the configuration

      diff             <?directory> compares SOURCE/<?directory> to TARGET/<?directory> ${RED}not recursively...only flat${NORMAL}
      full.diff        same but recursively  ${yellow}can tike some time${NORMAL}

      run              runs the rsync backup for the given <dir>
      run.mv           moves files to backup target (copy + remove source + clean empty dirs)
      here             syncs current directory (auto-detects relative path from backup source)
      status           shows if a backup (rsync) is currently running
      stop             gracefully stops any running backup
      
      normalize.scan   <?dir:.> scans for files with Linux-invalid characters (dry-run)
      normalize.all    <?dir:.> normalizes all files: creates safe names, originals become symlinks
      normalize.file   <file> normalizes a single file
      normalize.revert <file> reverts a normalized file back to original name
      
      verify.sync      <?dir:.> verifies if local directory is fully synced with backup target
      sync.and.remove  <?dir:.> syncs, verifies, and interactively prompts to remove local folder

      inspect          inspects the current backup process log file so that yo can follow a long lasting process

      list.last.diff.result     all following command work on the last result 
      list.last.result.raw

      list.notBackuped
      list.onlyInSource

      list.onlyInBackup
      list.onlyInTarget

      list.same         user Tab Completion to prevent the warning
  
  Hierarchical Config Discovery:
    The script looks for .backup.env starting from the current directory
    and walking upward until it finds one. This allows:
    - Per-project backup destinations (project/.backup.env)
    - Per-user backup destinations (~/.backup.env)
    - Global fallback ($CONFIG_PATH/backup.env)
    
    Use 'backup config.which' to see which config is currently active.
    Use 'backup config.save' to create a local .backup.env in the current directory.
    Use 'backup config.save global' to save to the global config.
  
  Examples
    $this v
    $this init

    $this list
    $this from /Users/Shared/TimeMachine/Sicherung
    $this to /Volumes/myData/Devices/McDonges.native/TimeMachine/Sicherung/
    $this strategy full
    $this capture.log.mode full
    $this config.save           # saves local .backup.env
    $this config.which          # shows active config
    $this list
  "
}

backup.from()  # <fromPath="/"> # sets the root for bakups (normally "/")
{
  export BACKUP_SOURCE="$1"
  shift
  backup.config.save
  RETURN="$1"
}


backup.to()  # <toPath="/media/disk1/backups"> # sets the bakups location on a share or an external disk
{
  export BACKUP_TARGET="$1"
  shift
  backup.config.save
  RETURN="$1"
}

backup.strategy()  # <strategy> # sets the backup strategy as one of: incremental, full, secureMuve, replaceFoldersByLinks
{
  if [ -z "$1" ]; then
    important.log "current BACKUP_STRATEGY: $BACKUP_STRATEGY"
    return 0
  fi
  export BACKUP_STRATEGY="$1"
  shift
  backup.config.save
  RETURN="$1"
}

backup.strategy.help() # # shows available backup strategies and their behavior
{
  console.log ""
  console.log "${CYAN}  BACKUP STRATEGIES${NORMAL}"
  console.log "${CYAN}═══════════════════════════════════════════════════════════════════${NORMAL}"
  console.log ""
  console.log "  ${WHITE}Strategy${NORMAL}                ${WHITE}Behavior${NORMAL}                     ${WHITE}--delete${NORMAL}"
  console.log "  ${CYAN}───────────────────────────────────────────────────────────────${NORMAL}"
  console.log "  ${GREEN}full${NORMAL}                    Exact mirror                     Yes — removes target files not in source"
  console.log "  ${GREEN}incremental${NORMAL}             Additive only                    No — never deletes from target"
  console.log "  ${GREEN}secureMove${NORMAL}              Move files                       Removes from source after copy"
  console.log "  ${GREEN}replaceByFolderLinks${NORMAL}    Replace source with symlink      Source dir becomes link to target"
  console.log ""
  console.log "  Current: ${WHITE}${BACKUP_STRATEGY:-full}${NORMAL}"
  console.log ""
}

backup.strategy.completion()
{
  echo -e "full \\nincremental \\nsecureMove \\nreplaceByFolderLinks " | grep "^$1"
}

backup.capture.log.mode() # <logMode> # set the log mode as one of: slient, full, screen, none
{
  if [ -z "$1" ]; then
    important.log "current BACKUP_CAPTURE_LOG_MODE: $BACKUP_CAPTURE_LOG_MODE    and command: $BACKUP_CAPTURE_LOG_COMMAND"
    return 0
  fi
  export BACKUP_CAPTURE_LOG_MODE="$1"
  shift
  private.check.log.mode

  backup.config.save
  RETURN="$1"
}

private.check.log.mode() 
{
  case "$BACKUP_CAPTURE_LOG_MODE" in
    "full")
      export BACKUP_CAPTURE_LOG_COMMAND="capture.log"
      ;;
    "silent")
      export BACKUP_CAPTURE_LOG_COMMAND="capture.log.silent"
      ;;
    "none"|"screen")
      export BACKUP_CAPTURE_LOG_COMMAND=""
      ;;
  esac
}

backup.parameter.completion.logMode() 
{
  echo -e "full \\nsilent \\nnone \\nscreen " | grep "^$1" 
}




backup.from.completion() 
{
  compgen -o nospace -o dirnames  "$1" | grep "^$1" 
}

backup.to.completion() 
{
  private.complete.folders "$1"
}


private.complete.folders()
{
  #https://unix.stackexchange.com/questions/151118/understand-compgen-builtin-command
  compgen -o dirnames "$1" | grep "^$1"
}

backup.source() # <fromPath> # alias for backup.from — set backup source directory
{ backup.from "$@"; }
backup.source.completion() { backup.from.completion "$@"; }

backup.target() # <toPath> # alias for backup.to — set backup target directory
{ backup.to "$@"; }
backup.target.completion() { backup.to.completion "$@"; }

backup.config.list() 
{
  config list backup 
}

backup.config.discover() # # walks from pwd upward, returns path to first .backup.env found
{
  local dir="$(pwd)"
  while [ "$dir" != "/" ]; do
    if [ -f "$dir/.backup.env" ]; then
      RESULT="$dir/.backup.env"
      echo "$RESULT"
      return 0
    fi
    dir="$(dirname "$dir")"
  done
  # Fallback to global
  RESULT="$CONFIG_PATH/backup.env"
  echo "$RESULT"
  return 0
}

backup.config.which() # # shows which config file is currently active
{
  local config_file
  config_file=$(backup.config.discover)
  console.log "Active config: ${CYAN}$config_file${NORMAL}"
  
  if [ -f "$config_file" ]; then
    console.log "\nContents:"
    cat "$config_file"
  else
    warn.log "Config file does not exist yet: $config_file"
  fi
}

backup.config.save() # <?scope:local|global> # saves config to local .backup.env or global
{
  local scope="${1:-local}"
  
  if [ "$scope" = "global" ]; then
    config save backup BACKUP_
    info.log "Saved to global config: $CONFIG_PATH/backup.env"
  else
    # Save to current directory's .backup.env
    local config_file=".backup.env"
    info.log "Saving backup config to: $(pwd)/$config_file"
    cat > "$config_file" << EOF
# Backup config for $(pwd)
# Created: $(date -u +%Y-%m-%dT%H:%M:%SZ)
export BACKUP_SOURCE="$BACKUP_SOURCE"
export BACKUP_TARGET="$BACKUP_TARGET"
export BACKUP_STRATEGY="$BACKUP_STRATEGY"
export BACKUP_CAPTURE_LOG_MODE="$BACKUP_CAPTURE_LOG_MODE"
EOF
    success.log "Local config saved: $(pwd)/$config_file"
    
    # Register in tracking directory
    backup.config.register "$(pwd)/$config_file"
  fi
}

backup.config.save.completion.scope() 
{
  echo -e "local\nglobal" | grep "^$1"
}

backup.config.create() # <?targetBase:pi@pi400:/media/pi/myData/Devices/MacStudio.native> # creates config for current directory
{
  local current="$(pwd)"
  local targetBase="${1:-pi@pi400:/media/pi/myData/Devices/MacStudio.native}"

  # Generate target path: remote = mirror local path on remote host, local = use as-is
  local targetPath
  if [[ "$targetBase" == *"@"* ]]; then
    targetPath="$targetBase$current"
  else
    targetPath="$targetBase"
  fi
  
  console.log "Creating backup config for: ${CYAN}$current${NORMAL}"
  console.log "Target: ${CYAN}$targetPath${NORMAL}"

  export BACKUP_SOURCE="$current"
  export BACKUP_TARGET="$targetPath"
  export BACKUP_STRATEGY="${BACKUP_STRATEGY:-full}"
  export BACKUP_CAPTURE_LOG_MODE="${BACKUP_CAPTURE_LOG_MODE:-full}"

  backup.config.save local

  # Create remote directory
  if [[ "$targetPath" == *"@"* ]]; then
    local remoteHost="${targetPath%%:*}"
    local remotePath="${targetPath#*:}"
    console.log "Creating remote directory..."
    ssh "$remoteHost" "mkdir -p '$remotePath'" 2>/dev/null && \
      success.log "Remote directory created" || \
      warn.log "Could not create remote directory (may already exist)"
  fi
  
  backup.list
}

backup.config.create.completion.targetBase()
{
  compgen -o dirnames "$1" | grep "^$1"
}

# Naming Convention for Registered Configs:
# ─────────────────────────────────────────
# Symlink names use: <parent-dir-name>.backup.env (e.g., "home.donges.it.backup.env")
# The actual path is always available via: readlink <symlink>
# If duplicate names exist, a numeric suffix is added (e.g., "myproject.2.backup.env")

backup.config.register() # <config_path> # registers a .backup.env in the tracking directory
{
  local config_path="$1"
  
  # Ensure tracking directory exists
  if [ ! -d "$BACKUP_CONFIGS_DIR" ]; then
    mkdir -p "$BACKUP_CONFIGS_DIR"
    debug.log "Created tracking directory: $BACKUP_CONFIGS_DIR"
  fi
  
  # Use parent directory name + .backup.env as the symlink name
  local dir_path=$(dirname "$config_path")
  local base_name=$(basename "$dir_path")
  local link_name="${base_name}.backup.env"
  local link_path="$BACKUP_CONFIGS_DIR/$link_name"
  
  # Handle duplicates by adding numeric suffix
  if [ -L "$link_path" ]; then
    local existing_target=$(readlink "$link_path")
    if [ "$existing_target" = "$config_path" ]; then
      debug.log "Config already registered: $link_name"
      return 0
    fi
    # Find next available suffix
    local i=2
    while [ -L "$BACKUP_CONFIGS_DIR/${base_name}.${i}.backup.env" ]; do
      i=$((i + 1))
    done
    link_name="${base_name}.${i}.backup.env"
    link_path="$BACKUP_CONFIGS_DIR/$link_name"
  fi
  
  ln -s "$config_path" "$link_path"
  debug.log "Registered config: $link_name -> $config_path"
}

backup.config.unregister() # <?config_path:./.backup.env> # removes a config from the tracking directory
{
  local config_path="${1:-$(pwd)/.backup.env}"
  
  # Make absolute if relative
  if [[ "$config_path" != /* ]]; then
    config_path="$(pwd)/$config_path"
  fi
  
  # Find the symlink pointing to this config
  for link in "$BACKUP_CONFIGS_DIR"/*; do
    if [ -L "$link" ]; then
      local target=$(readlink "$link")
      if [ "$target" = "$config_path" ]; then
        rm "$link"
        success.log "Unregistered: $(basename "$link") -> $config_path"
        return 0
      fi
    fi
  done
  
  warn.log "Config not registered: $config_path"
}

backup.config.disable() # <?configPath:here> # disable a registered config by renaming .backup.env to disabled.backup.env
{
  local configPath="$1"

  if [ -z "$configPath" ] || [ "$configPath" = "here" ]; then
    configPath=$(backup.config.discover)
  fi

  local dir=$(dirname "$configPath")
  local disabledPath="$dir/disabled.backup.env"

  if [ ! -f "$configPath" ]; then
    if [ -f "$disabledPath" ]; then
      info.log "Already disabled: $configPath"
      return 0
    fi
    error.log "Config not found: $configPath"
    return 1
  fi

  mv "$configPath" "$disabledPath"
  success.log "Disabled: $configPath"
}

backup.config.disable.completion.configPath()
{
  echo "here"
  if [ -d "$BACKUP_CONFIGS_DIR" ]; then
    for link in "$BACKUP_CONFIGS_DIR"/*; do
      if [ -L "$link" ]; then
        local target=$(readlink "$link")
        if [ -f "$target" ]; then
          echo "$target"
        fi
      fi
    done
  fi
}

backup.config.enable() # <?configPath:here> # enable a disabled config by renaming disabled.backup.env back to .backup.env
{
  local configPath="$1"

  if [ -z "$configPath" ] || [ "$configPath" = "here" ]; then
    # Walk up looking for .backup.env (enabled) or disabled.backup.env
    local dir="$(pwd)"
    configPath=""
    while [ "$dir" != "/" ]; do
      if [ -f "$dir/.backup.env" ] || [ -f "$dir/disabled.backup.env" ]; then
        configPath="$dir/.backup.env"
        break
      fi
      dir="$(dirname "$dir")"
    done
    if [ -z "$configPath" ]; then
      error.log "No config found walking up from $(pwd)"
      return 1
    fi
  fi

  # Already enabled — no-op
  if [ -f "$configPath" ]; then
    info.log "Already enabled: $configPath"
    return 0
  fi

  local dir=$(dirname "$configPath")
  local disabledPath="$dir/disabled.backup.env"

  if [ ! -f "$disabledPath" ]; then
    error.log "No disabled config found at: $disabledPath"
    return 1
  fi

  mv "$disabledPath" "$configPath"
  success.log "Enabled: $configPath"
}

backup.config.enable.completion.configPath()
{
  echo "here"
  if [ -d "$BACKUP_CONFIGS_DIR" ]; then
    for link in "$BACKUP_CONFIGS_DIR"/*; do
      if [ -L "$link" ]; then
        echo "$(readlink "$link")"
      fi
    done
  fi
}

backup.config.list.all() # # lists all registered .backup.env files with their paths and targets
{
  if [ ! -d "$BACKUP_CONFIGS_DIR" ]; then
    console.log "No backup configs registered yet."
    console.log "Use 'backup config.save' in a directory to create and register a config."
    return 0
  fi
  
  local count=0

  console.log "${CYAN}═══════════════════════════════════════════════════════════${NORMAL}"
  console.log "${CYAN}  REGISTERED BACKUP CONFIGURATIONS${NORMAL}"
  console.log "${CYAN}═══════════════════════════════════════════════════════════${NORMAL}"
  console.log ""
  
  for link in "$BACKUP_CONFIGS_DIR"/*; do
    if [ -L "$link" ]; then
      local target=$(readlink "$link")
      count=$((count + 1))
      
      local dir=$(dirname "$target")
      if [ -f "$target" ]; then
        local backup_target=$(grep "BACKUP_TARGET=" "$target" | cut -d'"' -f2)
        console.log "${GREEN}[$count] [enabled]${NORMAL} ${YELLOW}$dir${NORMAL}"
        console.log "    → ${CYAN}$backup_target${NORMAL}"
      elif [ -f "$dir/disabled.backup.env" ]; then
        local backup_target=$(grep "BACKUP_TARGET=" "$dir/disabled.backup.env" | cut -d'"' -f2)
        console.log "${YELLOW}[$count] [disabled]${NORMAL} ${YELLOW}$dir${NORMAL}"
        console.log "    → ${CYAN}$backup_target${NORMAL}"
      else
        console.log "${RED}[$count] [missing]${NORMAL} ${YELLOW}$target${NORMAL}"
      fi
      console.log ""
    fi
  done
  
  if [ $count -eq 0 ]; then
    console.log "No backup configs registered."
  else
    console.log "${CYAN}───────────────────────────────────────────────────────────${NORMAL}"
    console.log "Total: ${GREEN}$count${NORMAL} registered backup configuration(s)"
  fi
}

backup.config.register.existing() # # scans common locations and registers any existing .backup.env files
{
  console.log "Scanning for existing .backup.env files..."
  
  local found=0
  
  # Scan /Users/Shared and home directory
  while IFS= read -r -d '' config_file; do
    backup.config.register "$config_file"
    found=$((found + 1))
    info.log "Found and registered: $config_file"
  done < <(find /Users/Shared "$HOME" -name ".backup.env" -type f -print0 2>/dev/null)
  
  if [ $found -eq 0 ]; then
    info.log "No existing .backup.env files found."
  else
    success.log "Registered $found existing config(s)."
  fi
}

backup.config.repair() # <?configPath> # scan registered configs for double-path bug and fix them
{
  local configPath="$1"
  local fixed=0
  local skipped=0

  if [ -n "$configPath" ]; then
    # Fix a specific config
    private.backup.repair.config "$configPath"
    return $?
  fi

  # Scan all registered configs
  if [ ! -d "$BACKUP_CONFIGS_DIR" ]; then
    info.log "No backup configs registered."
    return 0
  fi

  console.log "${CYAN}Scanning registered configs for double-path bug...${NORMAL}"
  console.log ""

  for link in "$BACKUP_CONFIGS_DIR"/*; do
    if [ -L "$link" ]; then
      local target=$(readlink "$link")
      if [ -f "$target" ]; then
        if private.backup.repair.config "$target"; then
          fixed=$((fixed + 1))
        else
          skipped=$((skipped + 1))
        fi
      fi
    fi
  done

  console.log ""
  console.log "${CYAN}───────────────────────────────────────────────────────────${NORMAL}"
  if [ $fixed -gt 0 ]; then
    success.log "Repaired: $fixed config(s)"
  fi
  if [ $skipped -gt 0 ]; then
    info.log "Already correct: $skipped config(s)"
  fi
}

backup.config.repair.completion.configPath()
{
  # Complete with registered config paths
  if [ -d "$BACKUP_CONFIGS_DIR" ]; then
    for link in "$BACKUP_CONFIGS_DIR"/*; do
      if [ -L "$link" ]; then
        readlink "$link"
      fi
    done
  fi
}

private.backup.repair.config() # <configPath> # detect and fix double-path in a single config
{
  local configPath="$1"

  if [ ! -f "$configPath" ]; then
    error.log "Config not found: $configPath"
    return 1
  fi

  local backupSource=$(grep "BACKUP_SOURCE=" "$configPath" | cut -d'"' -f2)
  local backupTarget=$(grep "BACKUP_TARGET=" "$configPath" | cut -d'"' -f2)

  if [ -z "$backupSource" ] || [ -z "$backupTarget" ]; then
    warn.log "Incomplete config (missing SOURCE or TARGET): $configPath"
    return 1
  fi

  # Detect double-path: target contains source path appended after the base
  local repairedTarget=""

  if [[ "$backupTarget" == *"@"* ]]; then
    # Remote target — extract host:path, check if path contains source duplicated
    local remoteHost="${backupTarget%%:*}"
    local remotePath="${backupTarget#*:}"

    # Double-path pattern: remotePath ends with backupSource appended to a base that already contains it
    # e.g., /Users/Shared/Workspaces/Users/Shared/Workspaces when source is /Users/Shared/Workspaces
    if [[ "$remotePath" == *"$backupSource$backupSource"* ]]; then
      repairedTarget="$remoteHost:${remotePath/$backupSource$backupSource/$backupSource}"
    elif [[ "$remotePath" != *"$backupSource" ]] && [[ "$remotePath" == *"$backupSource"* ]]; then
      # Check if source appears as a suffix after already being part of the base path
      local withoutSuffix="${remotePath%$backupSource}"
      if [[ "$withoutSuffix" == *"$backupSource" ]]; then
        repairedTarget="$remoteHost:$withoutSuffix"
      fi
    fi
  else
    # Local target — check if target has source path appended
    if [[ "$backupTarget" == *"$backupSource" ]] && [ "$backupTarget" != "$backupSource" ]; then
      local base="${backupTarget%$backupSource}"
      # Only repair if the base is a reasonable path (not empty)
      if [ -n "$base" ]; then
        repairedTarget="${base%/}"
      fi
    fi
  fi

  if [ -z "$repairedTarget" ]; then
    debug.log "Config OK: $configPath"
    return 1
  fi

  console.log "${YELLOW}Double-path detected:${NORMAL} $configPath"
  console.log "  Before: ${RED}$backupTarget${NORMAL}"
  console.log "  After:  ${GREEN}$repairedTarget${NORMAL}"

  # Write the corrected target (delimiter is |, so escape & and | but not /)
  local escapedOld=$(printf '%s\n' "$backupTarget" | sed 's/[&|]/\\&/g')
  local escapedNew=$(printf '%s\n' "$repairedTarget" | sed 's/[&|]/\\&/g')
  sed -i '' "s|BACKUP_TARGET=\"$escapedOld\"|BACKUP_TARGET=\"$escapedNew\"|" "$configPath"

  success.log "Repaired: $configPath"
  return 0
}

backup.status() # # shows if a backup (rsync) is currently running
{
  local rsync_procs=$(pgrep -f "rsync.*-e ssh" 2>/dev/null)
  
  if [ -n "$rsync_procs" ]; then
    console.log "${GREEN}Backup is RUNNING${NORMAL}"
    console.log ""
    console.log "Active rsync processes:"
    ps aux | grep "[r]sync.*-e ssh" | while read line; do
      console.log "  $line"
    done
    console.log ""
    console.log "To stop gracefully: ${YELLOW}pkill -INT -f 'rsync.*-e ssh'${NORMAL}"
  else
    console.log "${CYAN}No backup currently running${NORMAL}"
  fi
  
  # Show last run info if available
  if [ -f "$CONFIG_PATH/result.txt" ] && [ -s "$CONFIG_PATH/result.txt" ]; then
    local last_line=$(tail -1 "$CONFIG_PATH/result.txt" 2>/dev/null)
    local file_time=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$CONFIG_PATH/result.txt" 2>/dev/null)
    console.log ""
    console.log "Last log update: ${YELLOW}$file_time${NORMAL}"
    console.log "Last log line: $last_line"
  fi
}

backup.stop() # # gracefully stops any running backup
{
  local rsync_procs=$(pgrep -f "rsync.*-e ssh" 2>/dev/null)
  
  if [ -n "$rsync_procs" ]; then
    console.log "Stopping backup gracefully (sending SIGINT)..."
    pkill -INT -f "rsync.*-e ssh"
    sleep 2
    
    # Check if still running
    if pgrep -f "rsync.*-e ssh" >/dev/null 2>&1; then
      warn.log "Backup still running, sending SIGTERM..."
      pkill -TERM -f "rsync.*-e ssh"
    else
      success.log "Backup stopped"
    fi
  else
    console.log "No backup currently running"
  fi
}

# Characters that are invalid on Linux/ext4 filesystems or problematic
BACKUP_BAD_CHARS=':|\?*"<> '

backup.normalize.filename() # <filename> # returns a normalized filename safe for Linux
{
  local filename="$1"
  # Replace bad characters: colons, pipes, etc. with underscore, spaces with dot
  local normalized=$(echo "$filename" | tr ':|\?*"<>' '________' | tr ' ' '.')
  RESULT="$normalized"
  echo "$normalized"
}

backup.normalize.file() # <filepath> # normalizes a file: creates safe-named copy, replaces original with symlink
{
  local filepath="$1"
  
  if [ ! -e "$filepath" ]; then
    error.log "File not found: $filepath"
    return 1
  fi
  
  local dir=$(dirname "$filepath")
  local filename=$(basename "$filepath")
  local normalized=$(backup.normalize.filename "$filename")
  
  # Check if already normalized
  if [ "$filename" = "$normalized" ]; then
    info.log "Already normalized: $filename"
    return 0
  fi
  
  local normalized_path="$dir/$normalized"
  
  # Check if normalized version already exists
  if [ -e "$normalized_path" ] && [ ! -L "$filepath" ]; then
    warn.log "Normalized version already exists: $normalized_path"
    return 1
  fi
  
  # If original is already a symlink, skip
  if [ -L "$filepath" ]; then
    info.log "Already a symlink: $filepath"
    return 0
  fi
  
  console.log "Normalizing: $filename"
  console.log "         -> $normalized"
  
  # Move original to normalized name
  mv "$filepath" "$normalized_path"
  
  # Create symlink from original name to normalized name
  ln -s "$normalized" "$filepath"
  
  success.log "Normalized: $filename -> $normalized"
  RESULT="$normalized_path"
}

backup.normalize.scan() # <?dir:.> # scans for files with bad characters (dry-run)
{
  local dir="${1:-.}"
  local count=0
  
  console.log "Scanning for files with characters invalid on Linux: $BACKUP_BAD_CHARS"
  console.log "Directory: $dir"
  console.log ""
  
  while IFS= read -r -d '' file; do
    local filename=$(basename "$file")
    local normalized=$(backup.normalize.filename "$filename")
    
    if [ "$filename" != "$normalized" ]; then
      count=$((count + 1))
      if [ -L "$file" ]; then
        console.log "${YELLOW}[symlink]${NORMAL} $file"
      else
        console.log "${RED}[needs fix]${NORMAL} $file"
        console.log "         -> $normalized"
      fi
    fi
  done < <(find "$dir" -name "*[${BACKUP_BAD_CHARS}]*" -print0 2>/dev/null)
  
  console.log ""
  console.log "Found: $count files with bad characters"
  
  if [ $count -gt 0 ]; then
    console.log "Run ${GREEN}backup normalize.all${NORMAL} to fix them"
  fi
  
  RESULT=$count
}

backup.normalize.all() # <?dir:.> # normalizes all files with bad characters in directory
{
  local dir="${1:-.}"
  local count=0
  local errors=0
  
  console.log "Normalizing files with characters invalid on Linux: $BACKUP_BAD_CHARS"
  console.log "Directory: $dir"
  console.log ""
  
  # Process files (not directories) first, deepest first to handle nested paths
  while IFS= read -r -d '' file; do
    if [ -f "$file" ] && [ ! -L "$file" ]; then
      local filename=$(basename "$file")
      local normalized=$(backup.normalize.filename "$filename")
      
      if [ "$filename" != "$normalized" ]; then
        if backup.normalize.file "$file"; then
          count=$((count + 1))
        else
          errors=$((errors + 1))
        fi
      fi
    fi
  done < <(find "$dir" -name "*[${BACKUP_BAD_CHARS}]*" -type f -print0 2>/dev/null)
  
  # Then process directories, deepest first
  while IFS= read -r -d '' file; do
    if [ -d "$file" ] && [ ! -L "$file" ]; then
      local dirname=$(basename "$file")
      local normalized=$(backup.normalize.filename "$dirname")
      
      if [ "$dirname" != "$normalized" ]; then
        if backup.normalize.file "$file"; then
          count=$((count + 1))
        else
          errors=$((errors + 1))
        fi
      fi
    fi
  done < <(find "$dir" -name "*[${BACKUP_BAD_CHARS}]*" -type d -depth -print0 2>/dev/null)
  
  console.log ""
  success.log "Normalized: $count items"
  if [ $errors -gt 0 ]; then
    warn.log "Errors: $errors items"
  fi
  
  RESULT=$count
}

backup.normalize.revert() # <filepath> # reverts a normalized file back to original (removes symlink, renames back)
{
  local filepath="$1"
  
  if [ ! -L "$filepath" ]; then
    error.log "Not a symlink: $filepath"
    return 1
  fi
  
  local dir=$(dirname "$filepath")
  local target=$(readlink "$filepath")
  local target_path="$dir/$target"
  
  if [ ! -e "$target_path" ]; then
    error.log "Target not found: $target_path"
    return 1
  fi
  
  console.log "Reverting: $filepath"
  
  # Remove the symlink
  rm "$filepath"
  
  # Move normalized back to original name
  mv "$target_path" "$filepath"
  
  success.log "Reverted: $filepath"
}

backup.get.relative.path() # # returns relative path from BACKUP_SOURCE to pwd (or empty if not inside)
{
  local current="$(pwd)"
  local source="$BACKUP_SOURCE"
  
  # Normalize paths (remove trailing slashes)
  source="${source%/}"
  current="${current%/}"
  
  # Check if current is inside source
  if [[ "$current" == "$source" ]]; then
    RESULT=""
    return 0
  elif [[ "$current" == "$source/"* ]]; then
    # Strip the source prefix to get relative path
    RESULT="${current#$source/}"
    return 0
  else
    RESULT=""
    return 1
  fi
}

backup.here() # # syncs current directory to backup target (auto-detects relative path)
{
  # First discover config if not loaded
  if [ -z "$BACKUP_SOURCE" ]; then
    backup.config.discover > /dev/null
    if [ -n "$RESULT" ]; then
      source "$RESULT"
    fi
  fi
  
  if [ -z "$BACKUP_SOURCE" ]; then
    error.log "No backup config found. Use 'backup from <path>' to set source."
    return 1
  fi
  
  backup.get.relative.path
  local rel_path="$RESULT"
  
  if [ $? -ne 0 ] && [ -z "$rel_path" ]; then
    # Not inside backup source - check if pwd matches a registered config
    local current="$(pwd)"
    warn.log "Current directory is not inside BACKUP_SOURCE ($BACKUP_SOURCE)"
    console.log "Checking registered configs..."
    
    # Look for a config that matches current directory
    for config in "$BACKUP_CONFIGS_DIR"/*; do
      if [ -L "$config" ]; then
        local config_path=$(readlink "$config")
        local config_dir=$(dirname "$config_path")
        if [[ "$current" == "$config_dir"* ]]; then
          console.log "Found matching config: $config_path"
          source "$config_path"
          backup.get.relative.path
          rel_path="$RESULT"
          break
        fi
      fi
    done
    
    if [ -z "$rel_path" ] && [[ "$(pwd)" != "$BACKUP_SOURCE"* ]]; then
      error.log "Not inside any registered backup source"
      console.log "Run 'backup from $(pwd)' to register this directory as a backup source"
      return 1
    fi
  fi
  
  if [ -n "$rel_path" ]; then
    console.log "Detected relative path: ${CYAN}$rel_path${NORMAL}"
    backup.run "$rel_path"
  else
    console.log "At backup source root"
    backup.run
  fi
}

private.backup.rsync() # <sourcePath> <targetPath> <?extraFlags> # run rsync with logging and optional extra flags
{
  local sourcePath="$1"
  local targetPath="$2"
  local extraFlags="$3"

  console.log "Source: $sourcePath"
  console.log "Target: $targetPath"

  # Check if target is remote (contains @) - need to use ssh
  local rsyncCmd="rsync -avh --progress --exclude .backup.env"
  if [ -n "$extraFlags" ]; then
    rsyncCmd="$rsyncCmd $extraFlags"
  fi
  if [[ "$BACKUP_TARGET" == *"@"* ]]; then
    rsyncCmd="$rsyncCmd -e ssh"
    console.log "Using SSH for remote target"
    # Create remote target directory (first-time backups need this)
    local remoteHost="${BACKUP_TARGET%%:*}"
    local remotePath="${targetPath#*:}"
    console.log "Ensuring remote directory exists: $remotePath"
    ssh "$remoteHost" "mkdir -p '$remotePath'" 2>/dev/null
  else
    if ! [ -d "$targetPath" ]; then
      console.log "Creating local target directory"
      mkdir -p "$targetPath"
    fi
  fi

  console.log "Running: $rsyncCmd \"$sourcePath/\" \"$targetPath/\""
  console.log "Logging to: $CONFIG_PATH/result.txt and $CONFIG_PATH/error.txt"

  # Clear previous logs
  > "$CONFIG_PATH/result.txt"
  > "$CONFIG_PATH/error.txt"

  # Run rsync with output captured to log files
  # stdout+stderr to result.txt, stderr also to error.txt
  $rsyncCmd "$sourcePath/" "$targetPath/" > >(tee -a "$CONFIG_PATH/result.txt") 2> >(tee -a "$CONFIG_PATH/result.txt" "$CONFIG_PATH/error.txt" >&2)

  local exitCode=$?

  if [ $exitCode -eq 0 ]; then
    success.log "Rsync completed successfully"
  else
    warn.log "Rsync completed with errors (exit code: $exitCode)"
    warn.log "Check errors with: backup list.errors"
  fi

  return $exitCode
}

private.backup.resolve.paths() # <dir> # sets RESOLVED_SOURCE and RESOLVED_TARGET from BACKUP_SOURCE/TARGET + optional subdir
{
  RESOLVED_SOURCE="$BACKUP_SOURCE"
  RESOLVED_TARGET="$BACKUP_TARGET"

  local subdir="$1"

  # Auto-detect relative path from CWD when no subdir given
  if [ -z "$subdir" ] || [ "$subdir" = "." ]; then
    local cwd="$(pwd)"
    local src="${BACKUP_SOURCE%/}"
    if [ "$cwd" != "$src" ] && [[ "$cwd" == "$src/"* ]]; then
      subdir="${cwd#$src/}"
    fi
  fi

  if [ -n "$subdir" ] && [ "$subdir" != "." ]; then
    RESOLVED_SOURCE="$BACKUP_SOURCE/$subdir"
    RESOLVED_TARGET="$BACKUP_TARGET/$subdir"
  fi
}

backup.run()  # <?dir> # backups the directroy (which you are in) to the backup location
{
  local strategy="${BACKUP_STRATEGY:-full}"

  private.backup.resolve.paths "$1"
  local sourcePath="$RESOLVED_SOURCE"
  local targetPath="$RESOLVED_TARGET"

  console.log "Strategy: ${WHITE}$strategy${NORMAL}"

  case "$strategy" in
    full)
      private.backup.rsync "$sourcePath" "$targetPath" "--delete"
      ;;
    incremental)
      private.backup.rsync "$sourcePath" "$targetPath"
      ;;
    secureMove)
      private.backup.strategy.secureMove "$sourcePath" "$targetPath"
      ;;
    replaceByFolderLinks)
      private.backup.strategy.replaceByFolderLinks "$sourcePath" "$targetPath"
      ;;
    *)
      error.log "Unknown strategy: $strategy"
      return 1
      ;;
  esac
}

private.backup.strategy.secureMove() # <sourcePath> <targetPath> # rsync + remove source files + clean empty dirs
{
  local sourcePath="$1"
  local targetPath="$2"

  private.backup.rsync "$sourcePath" "$targetPath" "--remove-source-files"
  local exitCode=$?

  if [ $exitCode -eq 0 ]; then
    # Clean up empty directories left behind (rsync only removes files)
    find "$sourcePath" -mindepth 1 -type d -empty -delete 2>/dev/null
    success.log "secureMove completed — source files removed, empty dirs cleaned"
  fi

  return $exitCode
}

private.backup.strategy.replaceByFolderLinks() # <sourcePath> <targetPath> # secure move to target, then replace source with symlink
{
  local sourcePath="$1"
  local targetPath="$2"

  # Secure move: rsync with --remove-source-files (files only removed after successful copy)
  private.backup.rsync "$sourcePath" "$targetPath" "--remove-source-files"
  local exitCode=$?

  if [ $exitCode -ne 0 ]; then
    error.log "Rsync failed — will NOT replace source with symlink"
    return $exitCode
  fi

  # Clean up empty directories left behind by rsync
  find "$sourcePath" -mindepth 1 -type d -empty -delete 2>/dev/null

  # Replace source directory with symlink to target
  console.log "Replacing source with symlink to target..."
  rm -rf "$sourcePath"
  ln -s "$targetPath" "$sourcePath"

  if [ -L "$sourcePath" ]; then
    success.log "replaceByFolderLinks completed — source is now symlink to target"
  else
    error.log "Failed to create symlink: $sourcePath -> $targetPath"
    return 1
  fi

  return 0
}

backup.run.mv() # <?dir> # moves files to backup target (shortcut for secureMove strategy)
{
  private.backup.resolve.paths "$1"
  private.backup.strategy.secureMove "$RESOLVED_SOURCE" "$RESOLVED_TARGET"
}

backup.parameter.completion.dir() 
{
  info.log "backup.parameter.completion.dir $*"
  c2 folders.completion "$1"
}

backup.diff()   # <dir> # logs the brief diff btween the current deirectory and the backup location
{
  ## ${ENV/serchPattern/replaceValue}  replace first only
  ## ${ENV//serchPattern/replaceValue} replace all
  local backup_source="${BACKUP_SOURCE/\//}"
  debug.log "backup_source: $backup_source"
  
  backup.get.backupPath "$1"  >/dev/null
  local path="$RESULT"

  debug.log "path: $path"

  if [ "$backup_source/" = "//" ]; then
    backup_source=""
  fi
  debug.log "backup_source after : $backup_source"
  
  $BACKUP_CAPTURE_LOG_COMMAND diff -q --brief "$backup_source/$path" "$BACKUP_TARGET/$path"
  #diff -q --brief "$backup_source/$path" "$BACKUP_TARGET/$path" > >(tee -a $CONFIG_PATH/result.txt) 2> >(tee -a $CONFIG_PATH/result.txt $CONFIG_PATH/error.txt >&2)
  #vimdiff "$backup_source/$path" "$BACKUP_TARGET/$path" 
  
  backup.list.last.diff.result
}

backup.full.diff()   # <dir> # logs the full diff btween the current deirectory and the backup location
{
  ## ${ENV/serchPattern/replaceValue}  replace first only
  ## ${ENV//serchPattern/replaceValue} replace all
  local backup_source="${BACKUP_SOURCE/\//}"
  debug.log "backup_source: $backup_source"
  
  local path="${1/\//}"
  debug.log "path: $path"

  if [ "$backup_source/" = "//" ]; then
    backup_source=""
  fi
  debug.log "backup_source after : $backup_source"               # write to console AND result.text AND wite errors to errot.txt AND result.txt
  $BACKUP_CAPTURE_LOG_COMMAND diff -qr --brief "$backup_source/$path" "$BACKUP_TARGET/$path" > >(tee -a $CONFIG_PATH/result.txt) 2> >(tee -a $CONFIG_PATH/result.txt $CONFIG_PATH/error.txt >&2)
  backup.list.last.diff.result no-warn
}

backup.inspect() 
{
  tail -f $CONFIG_PATH/result.txt
}

backup.list.errors()  # # logs the latest errors
{
  cat $CONFIG_PATH/error.txt | xargs -I {} printf "$RED%s\n" {}
  console.log "$NORMAL"
}

backup.list.last.result.raw()  # # logs the latest run
{
  cat $CONFIG_PATH/result.txt
}

backup.list.last.diff.result()  # # logs the latest diff
{
  backup.list.onlyInSource
  backup.list.onlyInTarget
  backup.list.same "$1"
}

backup.list.onlyInSource() 
{
  cat $CONFIG_PATH/result.txt | grep "Only in $backup_source/$path" | xargs -I {} printf "$YELLOW%s\n" {}
  console.log "$NORMAL"
}
backup.list.onlyInTarget() 
{
  cat $CONFIG_PATH/result.txt | grep "Only in $BACKUP_TARGET/$path" | xargs -I {} printf "$CYAN%s\n" {}
  console.log "$NORMAL"
}

backup.list.onlyInBackup() 
{
  backup.list.onlyInTarget
}

backup.list.notBackuped() 
{
  backup.list.onlyInSource
}

backup.list.same() 
{
  cat $CONFIG_PATH/result.txt | grep "Common " | xargs -I {} printf "$GREEN%s\n" {}

  if [ -z "$1" ]; then
    console.log "
    ${YELLOW}The ${GREEN}green files are common, ${YELLOW}but this does ${RED}not${YELLOW} guarantee that they are the ${RED}same${NORMAL} RECURSIVELY
    fot that use 
    
    ${GREEN}backup full.diff
    "
  fi
  console.log "$NORMAL"
}

backup.replace.same.with.folderLinks() 
{
  backup.list.same no-warn | sed 's/\(Common subdirectories: \)\(.*\)\( and \)\(.*\)/private.replace.by.folderLink \"\4\" \"\2\"\n/' | xargs
}

backup.replace.by.folderLink() 
{
  backup.get.backupPath "$1" >/dev/null

  local backup_source="${BACKUP_SOURCE/\//}"
  info.log "Path absolute local: $backup_source/$RESULT"
  info.log "Path      in Backup: $backup_source/$RESULT"
  private.replace.by.folderLink $BACKUP_TARGET/$RESULT $backup_source/$RESULT

}

backup.replace.by.folderLink.completion() 
{
    compgen -o dirnames "$1" | grep "^$1" 
}

private.replace.by.folderLink() 
{
  mv "${2}" "${2}.isBackuped"
  ln -s "$1" "${2}.lnk"
  ln -s "$1" "${2}"
  important.log "Please check result:"
  ls -al "$2/.."
}

backup.get.backupPath()  # <dir="."> # prints the backupPath
{
  local path="$1"
  if [ -z "$path" ]; then
    path="."
  fi

  this.absolutePath "$path"
  local path=$RESULT

  info.log "$1 system absolute path: $path"

  export RESULT=${path/$BACKUP_SOURCE/}
  info.log "$1 backup relative path: $RESULT"
  echo $RESULT
}

backup.get.backupPath.completion() 
{
    compgen -o dirnames "$1" | grep "^$1" 
}

backup.verify.sync() # <?dir:.> # verifies if local directory is fully synced with backup target
{
  local dir="${1:-.}"
  
  console.log "${CYAN}Verifying sync status...${NORMAL}"
  console.log "Source: ${YELLOW}$BACKUP_SOURCE/$dir${NORMAL}"
  console.log "Target: ${CYAN}$BACKUP_TARGET/$dir${NORMAL}"
  
  # Build rsync options - add SSH for remote targets
  local rsync_opts="-avhn --delete"
  if [[ "$BACKUP_TARGET" == *"@"* ]]; then
    rsync_opts="$rsync_opts -e ssh"
  fi
  
  # Use rsync dry-run to check differences
  local diff_output
  diff_output=$(rsync $rsync_opts "$BACKUP_SOURCE/$dir/" "$BACKUP_TARGET/$dir/" 2>&1)
  
  local changes=$(echo "$diff_output" | grep -v "^$" | grep -v "sending incremental" | grep -v "total size" | grep -v "^\./$" | wc -l)
  
  if [ "$changes" -le 2 ]; then
    success.log "✓ Directory is FULLY SYNCED with backup target"
    RESULT="synced"
    return 0
  else
    warn.log "⚠ Directory has ${changes} differences with backup target:"
    echo "$diff_output" | head -20
    if [ "$changes" -gt 20 ]; then
      console.log "... and $((changes - 20)) more differences"
    fi
    RESULT="not_synced"
    return 1
  fi
}

backup.sync.and.remove() # <?dir:.> # syncs directory to backup, verifies, then prompts to remove local
{
  local dir="${1:-.}"
  local full_path
  
  if [ "$dir" = "." ]; then
    full_path="$(pwd)"
  else
    full_path="$BACKUP_SOURCE/$dir"
  fi
  
  console.log "${YELLOW}═══════════════════════════════════════════════════════════${NORMAL}"
  console.log "${YELLOW}  SYNC AND REMOVE WORKFLOW${NORMAL}"
  console.log "${YELLOW}═══════════════════════════════════════════════════════════${NORMAL}"
  console.log ""
  console.log "Source: ${YELLOW}$full_path${NORMAL}"
  console.log "Target: ${CYAN}$BACKUP_TARGET${NORMAL}"
  console.log ""
  
  # Step 1: Show current size
  local size=$(du -sh "$full_path" 2>/dev/null | cut -f1)
  console.log "📦 Local size: ${GREEN}$size${NORMAL}"
  console.log ""
  
  # Step 2: Run sync
  console.log "${CYAN}Step 1: Syncing to backup target...${NORMAL}"
  
  # Build rsync options - add SSH for remote targets
  local rsync_opts="-avh --progress"
  if [[ "$BACKUP_TARGET" == *"@"* ]]; then
    rsync_opts="$rsync_opts -e ssh"
  fi
  
  rsync $rsync_opts "$full_path/" "$BACKUP_TARGET/" 2>&1
  local sync_exit=$?
  
  if [ $sync_exit -ne 0 ]; then
    error.log "❌ Sync failed with exit code $sync_exit"
    return 1
  fi
  success.log "✓ Sync completed"
  console.log ""
  
  # Step 3: Verify sync
  console.log "${CYAN}Step 2: Verifying sync...${NORMAL}"
  backup.verify.sync "$dir"
  local verify_result=$RESULT
  
  if [ "$verify_result" != "synced" ]; then
    error.log "❌ Verification failed - local and remote are NOT identical"
    console.log "${RED}Will NOT remove local folder - sync incomplete${NORMAL}"
    return 1
  fi
  console.log ""
  
  # Step 4: Interactive confirmation
  console.log "${YELLOW}═══════════════════════════════════════════════════════════${NORMAL}"
  console.log "${RED}  ⚠️  DANGER ZONE - LOCAL DELETION${NORMAL}"
  console.log "${YELLOW}═══════════════════════════════════════════════════════════${NORMAL}"
  console.log ""
  console.log "The following directory is FULLY SYNCED and can be removed:"
  console.log "  ${YELLOW}$full_path${NORMAL} (${GREEN}$size${NORMAL})"
  console.log ""
  console.log "Backup location:"
  console.log "  ${CYAN}$BACKUP_TARGET${NORMAL}"
  console.log ""
  
  read -p "🗑️  Remove local folder? [yes/NO]: " confirm
  
  if [ "$confirm" = "yes" ]; then
    console.log ""
    console.log "${RED}Removing local folder...${NORMAL}"
    rm -rf "$full_path"
    if [ $? -eq 0 ]; then
      success.log "✓ Local folder removed. Freed ${GREEN}$size${NORMAL}"
      console.log ""
      console.log "To access your files, use:"
      console.log "  ${CYAN}ssh pi@pi400${NORMAL}"
      console.log "  ${CYAN}cd /media/pi/myData/Devices/MacStudio.native${NORMAL}"
    else
      error.log "❌ Failed to remove folder"
      return 1
    fi
  else
    console.log ""
    info.log "Keeping local folder. No changes made."
  fi
  
  return 0
}

backup.list.same.completion() 
{
  echo -e "no-warn-for-pipe" | grep "^$1" 
}

backup.diff.completion() 
{
  local backup_source="${BACKUP_SOURCE/\//}"
  local path="${1/\//}"
  #echo path: $path
  if [ "$backup_source" = "/" ]; then
    backup_source=""
  fi
  compgen -o dirnames "$backup_source/$path" | grep "^$backup_source/$path" 
}

backup.list() 
{
  backup.list.config
}


backup.list.config()
{
  local activeConfig=$(backup.config.discover)
  console.log "Active config: ${GREEN}$activeConfig${NORMAL}
  "
  console.log "${YELLOW}from Source:
  $BACKUP_SOURCE
  "
  console.log "${CYAN}to Target:
  $BACKUP_TARGET
  "

  console.log "${NORMAL}Strategy:
  ${WHITE}$BACKUP_STRATEGY${NORMAL}
  "


  console.log "${NORMAL}Log Capture Mode:
  ${WHITE}$BACKUP_CAPTURE_LOG_MODE${NORMAL}
  "
}

backup.start()
{
  #echo "sourcing init"
  source this
  
  # Hierarchical config discovery - walk from pwd upward
  local config_file
  config_file=$(backup.config.discover)
  
  if [ -f "$config_file" ]; then
    debug.log "Using backup config: $config_file"
    source "$config_file"
  else
    # Fallback to global if nothing found
    if [ -f "$CONFIG_PATH/backup.env" ]; then
      debug.log "Using global backup config: $CONFIG_PATH/backup.env"
      source "$CONFIG_PATH/backup.env"
    fi
  fi
  
  private.check.log.mode

  this.start "$@"
}

backup.start "$@"

