import os
import time
import shutil
import re
import csv
import json
import tempfile
import subprocess
import sys
from PIL import Image

# Relative Imports
from .config import CONFIG, update_config
from .utils import save_state
from .module_transport import rclone_lsf_recursive, rclone_copy, rclone_delete, extract_archive, mount_dmg, unmount_dmg
from .module_scan import normalize_job_structure
from .phase_curate import interactive_curation
from .module_archive import build_master
from .phase_build import build_dist, sanitize_filename
from .phase_deploy import deploy
from .claude_helper import analyze_audio_metadata

# HIER EINFÜGEN:
def _get_targets():
    """Holt die verfügbaren Ziele aus der Config."""
    targets = CONFIG.get("video_targets", ["Allgemein"])[:] # Kopie erstellen
    music_target = CONFIG.get("music_target", "Musikvideos")
    if music_target and music_target not in targets:
        targets.append(music_target)
    
    # Add "Auto" option if classification is enabled
    if CONFIG.get("classification_enabled", False):
        if "Auto" not in targets:
            targets.append("Auto")
    
    return targets

MUSIC_IMPORT_DELAY_SEC = 2  # Delay before cleanup to give Music time to ingest the file.
PREFERRED_MEDIA_EXTS = (".mkv", ".mp4", ".webm", ".mov", ".m4v", ".mp3", ".m4a", ".wav", ".flac", ".aac", ".opus", ".ogg", ".aiff")
EXCLUDED_MEDIA_EXTS = (".json", ".jpg", ".jpeg", ".png", ".webp", ".vtt", ".srt", ".txt", ".md")

def _lookup_urls_from_csv(video_ids):
    """
    Look up video metadata for given video IDs from the video_metadata_index.csv.
    
    This function finds metadata records in the CSV index that match the provided video IDs.
    It returns the DMG filepath, title, and comment for each matched video. The actual
    URL must be extracted separately using _extract_url_from_dmg().
    
    The CSV's comment field contains the platform_id (e.g., "youtube_LTaCZRsVIB0").
    Matching uses word boundaries to ensure exact matches and avoid false positives.
    
    Args:
        video_ids: List of video IDs (full platform_ids like "youtube_LTaCZRsVIB0")
        
    Returns:
        dict: Mapping of video_id -> {"dmg_filepath": path, "title": title, "comment": comment}
              Returns empty dict if CSV doesn't exist or no matches found.
    """
    work_dir = CONFIG.get("work_dir")
    if not work_dir:
        return {}
    
    csv_path = os.path.join(work_dir, "video_metadata_index.csv")
    
    if not os.path.exists(csv_path):
        return {}
    
    url_map = {}
    
    try:
        with open(csv_path, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                comment = row.get('comment', '').strip()
                dmg_filepath = row.get('dmg_filepath', '').strip()
                
                # Check if this row matches any of our video IDs
                for video_id in video_ids:
                    # Match with word boundaries to avoid false positives
                    # Use re.search with word boundary markers
                    pattern = r'\b' + re.escape(video_id) + r'\b'
                    if re.search(pattern, comment):
                        # Found a match - store the DMG filepath for later URL extraction
                        if video_id not in url_map:
                            url_map[video_id] = {
                                "dmg_filepath": dmg_filepath,
                                "title": row.get('title', ''),
                                "comment": comment
                            }
                        break
    except Exception as e:
        print(f"   ⚠️  Warnung: Konnte CSV nicht lesen: {e}")
    
    return url_map


def _search_video_by_title(search_term):
    """
    Search for videos by title in the video_metadata_index.csv.
    
    This function performs a case-insensitive partial match search on video titles.
    It returns all matching videos with their video IDs extracted from the comment field.
    
    Args:
        search_term: Title or partial title to search for
        
    Returns:
        list: List of dicts with 'video_id', 'title', 'dmg_filepath', and 'comment'
              Returns empty list if CSV doesn't exist or no matches found.
    """
    work_dir = CONFIG.get("work_dir")
    if not work_dir:
        return []
    
    csv_path = os.path.join(work_dir, "video_metadata_index.csv")
    
    if not os.path.exists(csv_path):
        return []
    
    matches = []
    search_lower = search_term.lower().strip()
    
    try:
        with open(csv_path, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                title = row.get('title', '').strip()
                comment = row.get('comment', '').strip()
                dmg_filepath = row.get('dmg_filepath', '').strip()
                
                # Case-insensitive partial match on title
                if search_lower in title.lower():
                    # Extract video ID from comment field (platform_id format)
                    # Comment usually contains the platform_id like "youtube_5fk8XXCw"
                    video_id = comment  # The comment field IS the video ID
                    
                    matches.append({
                        "video_id": video_id,
                        "title": title,
                        "dmg_filepath": dmg_filepath,
                        "comment": comment
                    })
    except Exception as e:
        print(f"   ⚠️  Warnung: Konnte CSV nicht lesen: {e}")
    
    return matches

def _extract_url_from_dmg(dmg_filepath):
    """
    Extract the original URL from a DMG archive's original.info.json.
    
    Args:
        dmg_filepath: Full rclone path to the DMG file (e.g., "lyssach-nas:/Archiv/2025/2025-01-15_youtube_abc.dmg")
        
    Returns:
        str: The webpage_url from original.info.json, or None if not found
    """
    if not dmg_filepath:
        return None
    
    is_rclone = ":" in dmg_filepath
    mount_point = None
    temp_dir = None
    local_dmg_path = dmg_filepath
    
    try:
        # Download DMG if remote
        if is_rclone:
            work_dir = CONFIG.get("work_dir") or tempfile.gettempdir()
            temp_dir = tempfile.mkdtemp(dir=work_dir, prefix="url_extract_")
            
            # Download the DMG
            rclone_copy(dmg_filepath, temp_dir)
            local_dmg_path = os.path.join(temp_dir, os.path.basename(dmg_filepath))
            
            # Verify download was successful
            if not os.path.exists(local_dmg_path):
                print(f"   ⚠️  DMG download fehlgeschlagen: {dmg_filepath}")
                return None
        
        # Verify local DMG exists
        if not os.path.exists(local_dmg_path):
            print(f"   ⚠️  DMG nicht gefunden: {local_dmg_path}")
            return None
        
        # Mount DMG
        mount_point = mount_dmg(local_dmg_path)
        if not mount_point:
            return None
        
        # Find original.info.json
        info_json_path = _find_original_info_json(mount_point)
        if not info_json_path or not os.path.exists(info_json_path):
            return None
        
        # Read and extract webpage_url
        with open(info_json_path, 'r', encoding='utf-8') as f:
            info = json.load(f)
            return info.get('webpage_url') or info.get('original_url')
    
    except Exception as e:
        print(f"   ⚠️  Fehler beim Extrahieren der URL aus DMG: {e}")
        return None
    
    finally:
        if mount_point:
            unmount_dmg(mount_point)
        if temp_dir and os.path.exists(temp_dir):
            shutil.rmtree(temp_dir)

# Auto-migration constants
AUTO_MIGRATION_BATCH_SIZE = 10  # Number of items to process per batch
AUTO_MIGRATION_BATCH_PAUSE_SEC = 2  # Pause between batches (set to 0 to disable)

def _extract_video_id_from_filename(filename):
    """
    Extract video ID from legacy archive filename.
    
    Supports formats like:
    - 2025-05-15_youtube_5fk8XXCw.dmg -> youtube_5fk8XXCw
    - youtube_abc123.zip -> youtube_abc123
    - 2024-01-01_vimeo_12345678.tar.gz -> vimeo_12345678
    
    Args:
        filename: Archive filename (can include path)
        
    Returns:
        Video ID in format "platform_id" or None if not found
    """
    # Get just the filename without path
    basename = os.path.basename(filename)
    
    # Remove extension(s) - check longer extensions first
    name_without_ext = basename
    for ext in ['.tar.gz', '.dmg', '.zip', '.tar', '.gz', '.rar']:
        if name_without_ext.lower().endswith(ext):
            name_without_ext = name_without_ext[:-len(ext)]
            break  # Only remove one extension
    
    # Pattern: optional date prefix, then platform_id
    # Platform must be at least 3 chars (youtube, vimeo, etc.)
    # ID must be at least 3 chars and contain alphanumeric characters
    # Examples: "2025-05-15_youtube_abc123" or "youtube_abc123"
    pattern = r'(?:\d{4}-\d{2}-\d{2}_)?([a-z]{3,}_[a-zA-Z0-9_-]{3,})$'
    match = re.search(pattern, name_without_ext)
    
    if match:
        return match.group(1)
    
    return None

def _check_video_exists_in_index(video_id):
    """
    Check if a video ID already exists in the video_metadata_index.csv.
    
    Args:
        video_id: Video ID in format "platform_id" (e.g., "youtube_5fk8XXCw")
        
    Returns:
        True if video exists in index, False otherwise
        
    Note:
        This performs a linear scan through the CSV file. For frequent lookups
        or large CSV files, consider caching the comment field values in a set
        or implementing a more efficient lookup mechanism.
    """
    if not video_id:
        return False
    
    work_dir = CONFIG.get("work_dir")
    if not work_dir:
        return False
    
    csv_path = os.path.join(work_dir, "video_metadata_index.csv")
    
    if not os.path.exists(csv_path):
        # If index doesn't exist, we can't check, so assume video doesn't exist
        return False
    
    try:
        with open(csv_path, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                # The comment field contains the video ID (platform_id)
                comment = row.get('comment', '').strip()
                if comment == video_id:
                    return True
        return False
    except Exception as e:
        print(f"   ⚠️  Warnung: Konnte Index nicht lesen: {e}")
        return False

def _ask_for_target():
    print("\n--- 🎯 ZIEL WÄHLEN ---")
    targets = _get_targets()
    for i, t in enumerate(targets, 1): print(f"{i}. {t}")
    sel = input(f"Wähle Ziel (1-{len(targets)}) [Default: {targets[0]}]: ").strip()
    idx = int(sel) - 1 if sel.isdigit() else 0
    return targets[idx] if 0 <= idx < len(targets) else targets[0]

def _is_valid_source_folder(folder_path):
    """Helper für lokale Ordner-Erkennung"""
    try:
        for f in os.listdir(folder_path):
            if ".source." in f and f.endswith((".mkv", ".mp4", ".webm", ".mov")):
                return True
    except: pass
    return False

def _is_video_id(input_str):
    """
    Determine if the input string is likely a video ID or a title.
    
    Video IDs typically:
    - Are short (less than 30 characters)
    - Contain underscores (e.g., youtube_abc123)
    - Don't contain spaces
    - Match pattern: platform_id format
    
    Args:
        input_str: Input string to check
        
    Returns:
        True if likely a video ID, False if likely a title
    """
    input_str = input_str.strip()
    
    # Empty input is not a valid ID
    if not input_str:
        return False
    
    # IDs typically don't have spaces
    if ' ' in input_str:
        return False
    
    # IDs are usually short
    if len(input_str) > 30:
        return False
    
    # Check for platform_id pattern (e.g., youtube_5fk8XXCw)
    # Platform must be at least 3 chars, ID at least 3 chars
    pattern = r'^[a-z]{3,}_[a-zA-Z0-9_-]{3,}$'
    if re.match(pattern, input_str):
        return True
    
    # Also accept just the ID part (without platform prefix)
    # e.g., "5fk8XXCw" is 8+ alphanumeric chars
    if len(input_str) >= 6 and input_str.replace('_', '').replace('-', '').isalnum():
        return True
    
    return False


def restore_multiple_archives(video_inputs):
    """
    Restore multiple archives from DMG archive by their IDs or titles.
    Similar to download queue functionality.
    
    Supports both video IDs (e.g., "youtube_5fk8XXCw" or "5fk8XXCw") 
    and titles (e.g., "Hydroelectric Turbine Manufacturing").
    
    Args:
        video_inputs: List of video IDs or titles to restore
        
    Returns:
        dict with 'successful', 'not_found', and 'failed' lists
        - successful: List of dicts with 'id', 'file', and 'job_path'
        - not_found: List of inputs not found in archive
        - failed: List of inputs that failed during import
    """
    source_root = CONFIG.get("rclone_master_root")
    
    if not source_root:
        print("❌ Kein Master-Archiv-Pfad konfiguriert.")
        return {"successful": [], "not_found": video_inputs, "failed": []}
    
    # Ask for target once for all videos
    target = _ask_for_target()
    if not target:
        print("❌ Kein Ziel ausgewählt. Abbruch.")
        return {"successful": [], "not_found": video_inputs, "failed": []}
    
    print(f"\n🚀 Starte Restore von {len(video_inputs)} Archiven...")
    print(f"   Ziel: {target}")
    print(f"   Quelle: {source_root}")
    
    # Step 1: Resolve all inputs to video IDs
    # Some inputs might be titles, others might be IDs
    print(f"\n🔍 Verarbeite Eingaben (IDs und Titel)...")
    
    video_ids_to_restore = []
    input_to_id_map = {}  # Map original input to resolved video ID
    title_search_needed = []
    
    for input_str in video_inputs:
        input_str = input_str.strip()
        if _is_video_id(input_str):
            # It's a video ID, use it directly
            video_ids_to_restore.append(input_str)
            input_to_id_map[input_str] = input_str
            print(f"   ✓ ID erkannt: {input_str}")
        else:
            # It's likely a title, needs search
            title_search_needed.append(input_str)
            print(f"   🔎 Titel erkannt: {input_str}")
    
    # Step 2: Search for titles in CSV
    if title_search_needed:
        print(f"\n🔍 Suche nach {len(title_search_needed)} Titel(n) im Metadaten-Index...")
        for title_input in title_search_needed:
            matches = _search_video_by_title(title_input)
            
            if not matches:
                print(f"   ⚠️  Kein Video gefunden für Titel: '{title_input}'")
                continue
            elif len(matches) == 1:
                # Exact match (or only one match)
                video_id = matches[0]['video_id']
                video_ids_to_restore.append(video_id)
                input_to_id_map[title_input] = video_id
                print(f"   ✅ Gefunden: '{matches[0]['title']}' → {video_id}")
            else:
                # Multiple matches - let user choose
                print(f"   📋 Mehrere Treffer für '{title_input}':")
                for i, match in enumerate(matches, 1):
                    print(f"      {i}. {match['title']} ({match['video_id']})")
                
                choice = input(f"   Auswahl (1-{len(matches)}) oder [s] überspringen: ").strip()
                
                if choice.lower() == 's':
                    print(f"   → Übersprungen: '{title_input}'")
                    continue
                
                if choice.isdigit():
                    idx = int(choice) - 1
                    if 0 <= idx < len(matches):
                        video_id = matches[idx]['video_id']
                        video_ids_to_restore.append(video_id)
                        input_to_id_map[title_input] = video_id
                        print(f"   ✅ Gewählt: '{matches[idx]['title']}' → {video_id}")
                    else:
                        print(f"   ⚠️  Ungültige Auswahl. Übersprungen: '{title_input}'")
                else:
                    print(f"   ⚠️  Ungültige Eingabe. Übersprungen: '{title_input}'")
    
    if not video_ids_to_restore:
        print("\n⚠️  Keine gültigen Video-IDs zum Restoren.")
        return {
            "successful": [],
            "not_found": video_inputs,
            "failed": [],
            "skipped_duplicates": [],
            "target": target
        }
    
    print(f"\n📦 Verarbeite {len(video_ids_to_restore)} Video(s)...")
    
    successful_restores = []
    not_found_ids = []
    failed_imports = []
    skipped_duplicates = []
    
    # Scan archive once for all files
    print(f"🔍 Scanne Archiv...")
    is_rclone = ":" in source_root
    
    try:
        if is_rclone:
            all_files = rclone_lsf_recursive(source_root)
        else:
            all_files = []
            for root, dirs, files in os.walk(source_root):
                for f in files:
                    rel_path = os.path.relpath(os.path.join(root, f), source_root)
                    all_files.append(rel_path)
    except Exception as e:
        print(f"❌ Fehler beim Scannen des Archivs: {e}")
        return {"successful": [], "not_found": video_inputs, "failed": []}
    
    # Process each video ID
    for idx, video_id in enumerate(video_ids_to_restore, 1):
        print(f"\n{'='*60}")
        print(f"📦 [{idx}/{len(video_ids_to_restore)}] Restore: {video_id}")
        print(f"{'='*60}")
        
        # Search for matching DMG file
        # Use regex pattern matching to avoid false positives
        # Match video_id that appears after a delimiter or at the start, and before a delimiter or end
        # Example: "2025-05-15_youtube_5fk8XXCw.dmg" should match "5fk8XXCw"
        # But "xyzabc.dmg" should NOT match "abc"
        matching_file = None
        for file in all_files:
            if file.lower().endswith(".dmg"):
                filename = os.path.basename(file)
                # Pattern: (start or delimiter) + video_id + (delimiter or extension)
                # This ensures video_id is a complete token, not a substring
                pattern = rf'(^|[_-]){re.escape(video_id)}([_.])'
                if re.search(pattern, filename):
                    matching_file = file
                    break
        
        if not matching_file:
            print(f"⚠️  Nicht gefunden: {video_id}")
            not_found_ids.append(video_id)
            continue
        
        print(f"✅ Gefunden: {matching_file}")
        
        # matching_file already contains the full relative path (e.g., "2025/2025-05-15_youtube_xYz123Ab.dmg")
        # Pass it directly to _process_single_import
        selected_item = matching_file
        
        try:
            # Use _process_single_import directly to avoid interactive prompts
            # migration_mode=False means it's a restore operation (no deletion)
            job_path = _process_single_import(
                selected_item=selected_item,
                source_root=source_root,
                is_rclone=is_rclone,
                migration_mode=False,
                target=target,
                ask_for_deletion=False,
                batch_index=idx
            )
            
            if job_path == "SKIPPED_DUPLICATE":
                skipped_duplicates.append(video_id)
                print(f"⏭️  Übersprungen (bereits im Archiv): {video_id}")
            elif job_path:
                successful_restores.append({
                    "id": video_id, 
                    "file": matching_file,
                    "job_path": job_path
                })
                print(f"✅ Erfolgreich importiert: {video_id}")
            else:
                print(f"❌ Import fehlgeschlagen: {video_id}")
                failed_imports.append(video_id)
        except Exception as e:
            print(f"❌ Fehler beim Import von {video_id}: {e}")
            failed_imports.append(video_id)
    
    return {
        "successful": successful_restores,
        "not_found": not_found_ids,
        "failed": failed_imports,
        "skipped_duplicates": skipped_duplicates,
        "target": target  # Include target for reuse in redownload
    }

def _find_matching_dmg_file(all_files, video_id):
    """Matches DMG names like YYYY-MM-DD_platform_id.dmg with ID separated by '_' or '-'."""
    for file in all_files:
        if file.lower().endswith(".dmg"):
            filename = os.path.basename(file)
            pattern = rf'(^|[_-]){re.escape(video_id)}([_.])'
            if re.search(pattern, filename):
                return file
    return None

def _find_original_info_json(root_dir):
    for root, _, files in os.walk(root_dir):
        for filename in files:
            if filename.lower() == "original.info.json":
                return os.path.join(root, filename)
    return None

def _select_original_media_file(root_dir):
    preferred = []
    fallback = []
    for root, _, files in os.walk(root_dir):
        for filename in files:
            if not filename.lower().startswith("original."):
                continue
            path = os.path.join(root, filename)
            ext = os.path.splitext(filename)[1].lower()
            if ext in PREFERRED_MEDIA_EXTS:
                preferred.append(path)
            elif ext not in EXCLUDED_MEDIA_EXTS:
                fallback.append(path)
    candidates = preferred or fallback
    if not candidates:
        return None
    return max(candidates, key=lambda path: os.path.getsize(path))

def _escape_ffmpeg_metadata(value):
    return str(value).replace('"', "'")

def _create_square_cropped_cover(input_image_path, output_image_path):
    """
    Creates a square (1:1) cropped version of an image, centered.
    
    Args:
        input_image_path: Path to the input image
        output_image_path: Path where the square-cropped image will be saved
    
    Returns:
        True if successful, False otherwise
    """
    try:
        if not os.path.exists(input_image_path):
            print(f"   ⚠️  Eingabebild nicht gefunden: {input_image_path}")
            return False
            
        with Image.open(input_image_path) as img:
            # Convert to RGB if image has transparency (RGBA) or other modes
            if img.mode not in ('RGB', 'L'):
                img = img.convert('RGB')
            
            width, height = img.size
            
            # Calculate the size of the square crop (use the smaller dimension)
            crop_size = min(width, height)
            
            # Calculate crop box (centered)
            left = (width - crop_size) // 2
            top = (height - crop_size) // 2
            right = left + crop_size
            bottom = top + crop_size
            
            # Crop to square
            img_cropped = img.crop((left, top, right, bottom))
            
            # Save as JPEG with high quality
            img_cropped.save(output_image_path, "JPEG", quality=95)
            
            return True
    except (OSError, IOError, ValueError) as e:
        # Handle PIL-specific errors and filesystem errors
        print(f"   ⚠️  Fehler beim Erstellen des quadratischen Covers für '{input_image_path}': {e}")
        return False
    except Exception as e:
        # Catch any other unexpected errors
        print(f"   ⚠️  Unerwarteter Fehler beim Erstellen des quadratischen Covers für '{input_image_path}': {e}")
        return False

def _extract_mp3_from_dmg(selected_item, source_root, is_rclone, video_id):
    if is_rclone:
        full_source_path = f"{source_root.rstrip('/')}/{selected_item}"
    else:
        full_source_path = os.path.join(source_root, selected_item)
    work_dir = CONFIG.get("work_dir") or tempfile.gettempdir()
    os.makedirs(work_dir, exist_ok=True)
    temp_dir = tempfile.TemporaryDirectory(dir=work_dir, prefix="mp3_extract_") if is_rclone else None
    mount_point = None
    local_dmg_path = full_source_path
    output_dir = os.path.join(work_dir, "mp3_extract")
    square_cover_path = None  # Initialize here for finally block

    try:
        if is_rclone:
            print("   📡 Lade DMG herunter...")
            rclone_copy(full_source_path, temp_dir.name)
            local_dmg_path = os.path.join(temp_dir.name, os.path.basename(selected_item))

        if not os.path.exists(local_dmg_path):
            raise FileNotFoundError(f"DMG nicht gefunden: {local_dmg_path}")

        print("   💿 Mounte DMG...")
        mount_point = mount_dmg(local_dmg_path)
        if not mount_point:
            raise RuntimeError("DMG konnte nicht gemountet werden.")

        media_path = _select_original_media_file(mount_point)
        if not media_path:
            raise FileNotFoundError("Keine originale Mediendatei im DMG gefunden.")

        info_json = _find_original_info_json(mount_point)
        meta = {}
        if info_json:
            with open(info_json, "r", encoding="utf-8") as f:
                meta = json.load(f)

        title = meta.get("title") or os.path.splitext(os.path.basename(media_path))[0]
        channel = meta.get("channel") or meta.get("uploader") or "Unknown"
        upload_date = meta.get("upload_date") or ""
        description = meta.get("description") or ""

        print("   🧠 Frage Claude nach Musikmetadaten...")
        ai_result = analyze_audio_metadata(title, channel, upload_date, description)
        artist = ai_result.get("artist", channel) if ai_result else channel
        song_title = ai_result.get("title", title) if ai_result else title
        release_year = ai_result.get("release_year") if ai_result else None
        album_name = ai_result.get("album") if ai_result else None
        track_number = ai_result.get("track_number") if ai_result else None
        is_music = ai_result.get("is_music", True) if ai_result else True

        if not is_music:
            print("   ℹ️  Inhalt als Nicht-Musik erkannt.")

        # For music, use only the year; otherwise use full date
        if is_music and release_year:
            metadata_date = str(release_year)
        else:
            # Fallback to upload date or upload year
            if upload_date and len(str(upload_date)) >= 4:
                metadata_date = str(upload_date)[:4]
            else:
                metadata_date = "2000"

        clean_artist = sanitize_filename(artist)
        clean_title = sanitize_filename(song_title)
        output_name = f"{clean_artist} - {clean_title}".strip(" -")
        if not output_name:
            output_name = sanitize_filename(video_id or "track")
        # Ensure output_name is filesystem-safe (should already be, but extra safety)
        output_name = output_name.replace("/", "-").replace("\\", "-")
        os.makedirs(output_dir, exist_ok=True)
        mp3_path = os.path.join(output_dir, f"{output_name}.mp3")

        metadata_artist = _escape_ffmpeg_metadata(artist)
        metadata_title = _escape_ffmpeg_metadata(song_title)
        metadata_date = _escape_ffmpeg_metadata(metadata_date)
        metadata_album = _escape_ffmpeg_metadata(album_name) if album_name else None
        metadata_track = _escape_ffmpeg_metadata(str(track_number)) if track_number is not None else None

        # Log metadata that will be written (use escaped values that will actually be written)
        print(f"   📝 ID3 Metadaten: Artist='{metadata_artist}', Title='{metadata_title}', Date='{metadata_date}'")
        if metadata_album:
            print(f"      Album='{metadata_album}'")
        if metadata_track:
            print(f"      Track={metadata_track}")

        # Look for curated poster in DMG and create square-cropped version
        curated_poster_path = os.path.join(mount_point, "poster_curated.jpg")
        
        if os.path.exists(curated_poster_path):
            print("   🖼️  Erstelle quadratisches Cover aus kuratiertem Poster...")
            temp_cover_path = os.path.join(output_dir, f"{output_name}_cover_square.jpg")
            if _create_square_cropped_cover(curated_poster_path, temp_cover_path):
                print("   ✅ Quadratisches Cover erstellt")
                square_cover_path = temp_cover_path

        print("   🎧 Extrahiere MP3...")
        ffmpeg_cmd = [
            CONFIG["ffmpeg"],
            "-i",
            media_path,
        ]
        
        # Add cover art as second input if available
        if square_cover_path:
            ffmpeg_cmd.extend(["-i", square_cover_path])
        
        ffmpeg_cmd.extend([
            "-vn",
            "-map",
            "0:a:0",  # Map audio from first input (media file)
        ])
        
        # Map the cover art from second input if available
        if square_cover_path:
            ffmpeg_cmd.extend(["-map", "1:0"])  # Map image from second input (cover art)
        
        ffmpeg_cmd.extend([
            "-c:a",
            "libmp3lame",
            "-q:a",
            "0",
        ])
        
        # Add cover art encoding settings if available
        if square_cover_path:
            ffmpeg_cmd.extend([
                "-c:v",
                "mjpeg",
                "-disposition:v",
                "attached_pic",  # Mark as attached picture (cover art)
            ])
        
        # Build metadata parameters for ID3v2 tags
        metadata_params = [
            "-metadata", f"artist={metadata_artist}",
            "-metadata", f"title={metadata_title}",
            "-metadata", f"date={metadata_date}",
        ]
        
        # Add optional metadata if available
        if metadata_album:
            metadata_params.extend(["-metadata", f"album={metadata_album}"])
        
        if metadata_track:
            metadata_params.extend(["-metadata", f"track={metadata_track}"])
        
        # Use ID3v2.3 for best iTunes/Apple Music compatibility
        metadata_params.extend(["-id3v2_version", "3"])
        
        ffmpeg_cmd.extend(metadata_params)
        
        ffmpeg_cmd.extend([
            "-y",
            mp3_path,
            "-hide_banner",
            "-loglevel",
            "error",
            "-stats",
        ])
        
        ffmpeg_result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True)
        if ffmpeg_result.returncode != 0:
            error_details = ffmpeg_result.stderr.strip() or ffmpeg_result.stdout.strip()
            raise RuntimeError(f"FFmpeg fehlgeschlagen: {error_details or 'Unbekannter Fehler'}")

        if sys.platform != "darwin":
            print("   ⚠️  Apple Music Import nur auf macOS verfügbar.")
            print(f"   📁 MP3 gespeichert: {mp3_path}")
            return True

        print("   🎵 Öffne in Apple Music...")
        open_result = subprocess.run(["open", "-a", "Music", mp3_path], check=False)
        if open_result.returncode == 0:
            time.sleep(MUSIC_IMPORT_DELAY_SEC)
            if os.path.exists(mp3_path):
                os.remove(mp3_path)
            print("   ✅ Import gestartet, MP3 entfernt.")
        else:
            print(f"   ⚠️  Music-Import fehlgeschlagen. Datei bleibt: {mp3_path}")
        return True
    except Exception as e:
        print(f"   ❌ MP3-Extraktion fehlgeschlagen: {e}")
        return False
    finally:
        # Clean up temporary cover file
        if square_cover_path and os.path.exists(square_cover_path):
            try:
                os.remove(square_cover_path)
            except (OSError, IOError) as e:
                # Log cleanup failure but don't fail the entire operation
                print(f"   ⚠️  Konnte temporäres Cover nicht löschen: {e}")
        if mount_point:
            unmount_dmg(mount_point)
        if temp_dir:
            temp_dir.cleanup()

def extract_mp3_from_dmg_archives(video_inputs):
    """
    Extract MP3 from DMG archives by their IDs or titles.
    
    Supports both video IDs (e.g., "youtube_5fk8XXCw" or "5fk8XXCw") 
    and titles (e.g., "Hydroelectric Turbine Manufacturing").
    
    Args:
        video_inputs: List of video IDs or titles
        
    Returns:
        dict with 'successful', 'not_found', and 'failed' lists
    """
    source_root = CONFIG.get("rclone_master_root")
    if not source_root:
        print("❌ Kein Master-Archiv-Pfad konfiguriert.")
        return {"successful": [], "not_found": video_inputs, "failed": []}

    print(f"\n🎵 Starte MP3-Extraktion für {len(video_inputs)} Eingabe(n)...")
    print(f"   Quelle: {source_root}")
    
    # Step 1: Resolve all inputs to video IDs
    print(f"\n🔍 Verarbeite Eingaben (IDs und Titel)...")
    
    video_ids_to_extract = []
    input_to_id_map = {}
    title_search_needed = []
    
    for input_str in video_inputs:
        input_str = input_str.strip()
        if _is_video_id(input_str):
            video_ids_to_extract.append(input_str)
            input_to_id_map[input_str] = input_str
            print(f"   ✓ ID erkannt: {input_str}")
        else:
            title_search_needed.append(input_str)
            print(f"   🔎 Titel erkannt: {input_str}")
    
    # Step 2: Search for titles in CSV
    if title_search_needed:
        print(f"\n🔍 Suche nach {len(title_search_needed)} Titel(n) im Metadaten-Index...")
        for title_input in title_search_needed:
            matches = _search_video_by_title(title_input)
            
            if not matches:
                print(f"   ⚠️  Kein Video gefunden für Titel: '{title_input}'")
                continue
            elif len(matches) == 1:
                video_id = matches[0]['video_id']
                video_ids_to_extract.append(video_id)
                input_to_id_map[title_input] = video_id
                print(f"   ✅ Gefunden: '{matches[0]['title']}' → {video_id}")
            else:
                print(f"   📋 Mehrere Treffer für '{title_input}':")
                for i, match in enumerate(matches, 1):
                    print(f"      {i}. {match['title']} ({match['video_id']})")
                
                choice = input(f"   Auswahl (1-{len(matches)}) oder [s] überspringen: ").strip()
                
                if choice.lower() == 's':
                    print(f"   → Übersprungen: '{title_input}'")
                    continue
                
                if choice.isdigit():
                    idx = int(choice) - 1
                    if 0 <= idx < len(matches):
                        video_id = matches[idx]['video_id']
                        video_ids_to_extract.append(video_id)
                        input_to_id_map[title_input] = video_id
                        print(f"   ✅ Gewählt: '{matches[idx]['title']}' → {video_id}")
                    else:
                        print(f"   ⚠️  Ungültige Auswahl. Übersprungen: '{title_input}'")
                else:
                    print(f"   ⚠️  Ungültige Eingabe. Übersprungen: '{title_input}'")
    
    if not video_ids_to_extract:
        print("\n⚠️  Keine gültigen Video-IDs zum Extrahieren.")
        return {"successful": [], "not_found": video_inputs, "failed": []}
    
    print(f"\n🎵 Verarbeite {len(video_ids_to_extract)} Video(s)...")

    successful = []
    not_found = []
    failed = []

    is_rclone = ":" in source_root
    try:
        if is_rclone:
            all_files = rclone_lsf_recursive(source_root)
        else:
            all_files = []
            for root, _, files in os.walk(source_root):
                for filename in files:
                    rel_path = os.path.relpath(os.path.join(root, filename), source_root)
                    all_files.append(rel_path)
    except Exception as e:
        print(f"❌ Fehler beim Scannen des Archivs: {e}")
        return {"successful": [], "not_found": video_inputs, "failed": []}

    for idx, video_id in enumerate(video_ids_to_extract, 1):
        print(f"\n{'='*60}")
        print(f"🎵 [{idx}/{len(video_ids_to_extract)}] MP3-Extraktion: {video_id}")
        print(f"{'='*60}")

        matching_file = _find_matching_dmg_file(all_files, video_id)
        if not matching_file:
            print(f"⚠️  Nicht gefunden: {video_id}")
            not_found.append(video_id)
            continue

        print(f"✅ Gefunden: {matching_file}")
        if _extract_mp3_from_dmg(matching_file, source_root, is_rclone, video_id):
            successful.append({"id": video_id, "file": matching_file})
        else:
            failed.append(video_id)

    return {
        "successful": successful,
        "not_found": not_found,
        "failed": failed
    }

def run_legacy_migration(search_term="", migration_mode=False):
    # 1. QUELLPFAD BESTIMMEN
    if migration_mode:
        default_src = CONFIG.get("migration_source_dir", "")
        if not default_src:
            print(f"\nPfad zum Quell-Ordner eingeben.")
            raw_input = input("   Pfad: ").strip().strip('"').strip("'")
            source_root = raw_input
            if source_root and os.path.exists(source_root):
                update_config("migration_source_dir", source_root)
        else:
            source_root = default_src
    else:
        source_root = CONFIG.get("legacy_root") or CONFIG.get("rclone_master_root")

    if not source_root: 
        print("❌ Kein Pfad angegeben."); return None

    # --- LAZY LOADING MENÜ ---
    auto_pick_next = False
    batch_mode = False
    batch_count = 10
    
    if migration_mode and not search_term:
        print(f"\n--- 🏗️  MIGRATION: {source_root} ---")
        print("   [ENTER] ⏭️  Nächstes Objekt migrieren (Alphabetisch 1.)")
        print("   [b]     📦 Batch: Nächste 10 Objekte migrieren")
        print("   [s]     🔍 Suchen / Alle auflisten")
        print("   [z]     🔙 Zurück")
        
        choice = input("   Wahl: ").strip().lower()
        
        if choice == 'z': return None
        if choice == 's': 
            inp = input("   Suchbegriff (optional): ").strip()
            if inp: search_term = inp
        elif choice == 'b':
            batch_mode = True
            auto_pick_next = True
        else:
            auto_pick_next = True

    # 2. SCAN & KANDIDATEN FINDEN (PERFORMANCE OPTIMIERT)
    print(f"🔍 Scanne '{source_root}'...")
    is_rclone = ":" in source_root
    candidates = set() # Set verhindert Duplikate bei Ordnern
    
    # Definitionen
    archive_exts = (".zip", ".tar", ".gz", ".rar", ".dmg")
    video_exts = (".mkv", ".mp4", ".webm", ".mov", ".avi", ".m4v")

    try:
        if is_rclone:
            # 🚀 ULTRA FAST REMOTE SCAN
            # Wir holen nur Dateipfade. Das ist 1 Request, egal ob 10 oder 10.000 Dateien.
            files = rclone_lsf_recursive(source_root)
            
            for f in files:
                # Fall A: Es ist ein Archiv
                if f.lower().endswith(archive_exts):
                    if not search_term or search_term.lower() in f.lower():
                        candidates.add(f)
                
                # Fall B: Es ist eine Videodatei in einem Ordner
                # rclone lsf -R gibt aus: "Ordnername/video.mp4"
                # Wir extrahieren "Ordnername/" als Kandidaten.
                elif f.lower().endswith(video_exts) and "/" in f:
                    folder_name = f.split("/")[0] + "/" # Nimm den Top-Level Ordner
                    if not search_term or search_term.lower() in folder_name.lower():
                        candidates.add(folder_name)

        else:
            # LOKALER SCAN (Klassisch)
            items = os.listdir(source_root)
            items.sort() # Alphabetisch ist default schnell
            
            for item in items:
                full_path = os.path.join(source_root, item)
                if search_term and search_term.lower() not in item.lower(): continue
                
                if os.path.isfile(full_path) and item.lower().endswith(archive_exts) and not item.startswith("."):
                    candidates.add(item)
                elif os.path.isdir(full_path) and _is_valid_source_folder(full_path):
                    candidates.add(item + "/")
                    
    except Exception as e:
        print(f"❌ Fehler beim Scan: {e}"); return None

    if not candidates: print(f"⚠️  Nichts gefunden."); return None

    # Set zurück in Liste wandeln und sortieren
    sorted_candidates = sorted(list(candidates))

    # 3. AUSWAHL
    selected_items = []
    
    if auto_pick_next and batch_mode:
        # Batch mode: select first N items
        selected_items = sorted_candidates[:batch_count]
        print(f"👉 Batch-Modus: {len(selected_items)} Objekte gewählt")
        for item in selected_items:
            print(f"   • {item}")
        
    elif auto_pick_next:
        # Single auto-pick: take first item
        selected_items = [sorted_candidates[0]]
        print(f"👉 Automatisch gewählt: {selected_items[0]}")
        
    elif len(sorted_candidates) == 1:
        selected_items = [sorted_candidates[0]]
        print(f"👉 Automatisch gewählt: {selected_items[0]}")
        
    else:
        # Manuelle Auswahl
        print(f"\nGefundene Kandidaten ({len(sorted_candidates)}):")
        display = sorted_candidates[:20]
        for i, f in enumerate(display, 1):
            print(f"{i}. {'wd' if f.endswith('/') else '📦'} {f}")
        if len(sorted_candidates) > 20: print(f"... ({len(sorted_candidates)-20} weitere)")
        
        sel = input("Nummer wählen: ").strip()
        if sel.isdigit() and 1 <= int(sel) <= len(display): 
            selected_items = [display[int(sel)-1]]
        else: 
            return None

    # 4. BATCH PROCESSING
    imported_jobs = []
    items_to_delete = []
    skipped_duplicates = []
    duplicate_items_to_delete = []  # Track duplicates for deletion at the end
    target = None  # Will be set by first import
    
    # Determine if we're in batch mode (multiple items selected)
    is_batch = len(selected_items) > 1
    
    for idx, selected_item in enumerate(selected_items, 1):
        if is_batch:
            print(f"\n{'='*60}")
            print(f"📦 BATCH [{idx}/{len(selected_items)}]: {selected_item}")
            print(f"{'='*60}")
        
        # Ask for target on first import, reuse for subsequent ones
        if idx == 1:
            try:
                target = _ask_for_target()
                if not target:
                    print("❌ Kein Ziel ausgewählt. Abbruch.")
                    break
            except Exception as e:
                print(f"❌ Fehler bei Zielauswahl: {e}")
                break
        
        job_path = _process_single_import(
            selected_item, 
            source_root, 
            is_rclone,
            migration_mode,
            target=target,
            ask_for_deletion=not is_batch,  # Only ask during import if NOT in batch mode
            batch_index=idx,  # Pass index for unique job IDs
            batch_mode=is_batch  # Pass batch mode flag
        )
        
        if job_path == "SKIPPED_DUPLICATE":
            skipped_duplicates.append(selected_item)
            # In batch mode with migration, collect duplicates for deletion at the end
            # Note: This only happens when migration_mode=True (batch menu only shown in migration mode)
            if is_batch and migration_mode:
                duplicate_items_to_delete.append((selected_item, is_rclone, source_root))
        elif job_path:
            imported_jobs.append(job_path)
            # Collect for potential deletion (only used if migration_mode=True)
            items_to_delete.append((selected_item, is_rclone, source_root))
        else:
            print(f"⚠️  Import fehlgeschlagen für: {selected_item}")
    
    # 5. MIGRATION CLEANUP - Only ask once at the end
    # Combine all items to delete (both successful imports and duplicates)
    all_items_to_delete = items_to_delete + duplicate_items_to_delete
    
    if migration_mode and all_items_to_delete:
        print(f"\n🧹 MIGRATION MODE AKTIV - {len(all_items_to_delete)} Objekte verarbeitet")
        if len(duplicate_items_to_delete) > 0:
            print(f"   ({len(items_to_delete)} importiert, {len(duplicate_items_to_delete)} Duplikate übersprungen)")
        if input(f"   ⚠️  Alle {len(all_items_to_delete)} Original-Objekte löschen? [y/N]: ").lower() == 'y':
            print("   🗑️  Lösche Quellen...")
            for selected_item, is_rclone, source_root in all_items_to_delete:
                is_folder_mode = selected_item.endswith("/")
                clean_name = selected_item.rstrip("/")
                full_source_path = f"{source_root}/{clean_name}" if is_rclone else os.path.join(source_root, clean_name)
                
                try:
                    if is_rclone:
                        rclone_delete(full_source_path, is_folder=is_folder_mode)
                    else:
                        if os.path.isfile(full_source_path): 
                            os.remove(full_source_path)
                        elif os.path.isdir(full_source_path): 
                            shutil.rmtree(full_source_path)
                    print(f"   ✅ Gelöscht: {selected_item}")
                except Exception as e:
                    print(f"   ❌ Fehler beim Löschen von {selected_item}: {e}")
    
    # Show summary of skipped duplicates
    if skipped_duplicates:
        print(f"\n📊 DUPLIKAT-ZUSAMMENFASSUNG")
        print(f"   {len(skipped_duplicates)} Objekte übersprungen (bereits im Archiv):")
        for item in skipped_duplicates:
            print(f"   • {item}")
    
    # Return single job path for backward compatibility, or list if multiple
    if len(imported_jobs) == 1:
        return imported_jobs[0]
    elif len(imported_jobs) > 1:
        return imported_jobs
    else:
        return None


def _process_single_import(selected_item, source_root, is_rclone, migration_mode, target=None, ask_for_deletion=False, batch_index=0, batch_mode=False):
    """
    Process a single import operation.
    
    Args:
        migration_mode: If True, checks for duplicates and skips/deletes legacy archives.
                       If False (restore mode), skips duplicate check to allow re-importing DMG archives.
        ask_for_deletion: If True, asks user for deletion immediately (single item mode).
                         If False AND batch_mode=False, auto-deletes without asking (auto-migration mode).
                         If False AND batch_mode=True, defers deletion to end (batch mode).
        batch_mode: If True, indicates we're in batch mode and deletion should be deferred to the end.
    
    Returns:
        - job directory path on success
        - None on failure
        - "SKIPPED_DUPLICATE" if video already exists in metadata index (only in migration_mode)
    """
    is_folder_mode = selected_item.endswith("/")
    clean_name = selected_item.rstrip("/")
    full_source_path = f"{source_root}/{clean_name}" if is_rclone else os.path.join(source_root, clean_name)
    
    # CHECK FOR DUPLICATES - Only for legacy archive migration, not for DMG restore
    # When restoring from DMG archives, there can be valid reasons to re-import
    if migration_mode:
        video_id = _extract_video_id_from_filename(clean_name)
        if video_id and _check_video_exists_in_index(video_id):
            print(f"🔍 Prüfe: {clean_name}")
            print(f"   ℹ️  Video ID '{video_id}' bereits im Archiv vorhanden")
            
            # In migration mode, handle the duplicate based on mode
            if ask_for_deletion:
                # Single item mode - ask user immediately
                print(f"   📋 Dieses Video existiert bereits im neuen DMG-Archiv.")
                response = input(f"   ⚠️  Legacy-Archiv '{clean_name}' löschen? [y/N]: ").lower()
                if response == 'y':
                    print("   🗑️  Lösche Legacy-Archiv...")
                    try:
                        if is_rclone:
                            rclone_delete(full_source_path, is_folder=is_folder_mode)
                        else:
                            if os.path.isfile(full_source_path):
                                os.remove(full_source_path)
                            elif os.path.isdir(full_source_path):
                                shutil.rmtree(full_source_path)
                        print("   ✅ Legacy-Archiv gelöscht.")
                    except Exception as e:
                        print(f"   ❌ Fehler beim Löschen: {e}")
                else:
                    print("   → Legacy-Archiv beibehalten.")
            elif batch_mode:
                # Batch mode - defer deletion to the end
                print(f"   📋 Dieses Video existiert bereits im neuen DMG-Archiv.")
                print(f"   → Löschung wird am Ende abgefragt")
            else:
                # Auto-migration mode - automatically delete without asking
                print(f"   🤖 Auto-Migration: Lösche Legacy-Archiv automatisch...")
                try:
                    if is_rclone:
                        rclone_delete(full_source_path, is_folder=is_folder_mode)
                    else:
                        if os.path.isfile(full_source_path):
                            os.remove(full_source_path)
                        elif os.path.isdir(full_source_path):
                            shutil.rmtree(full_source_path)
                    print("   ✅ Legacy-Archiv gelöscht.")
                except Exception as e:
                    print(f"   ❌ Fehler beim Löschen: {e}")
            
            print("   → Import übersprungen (Duplikat)")
            return "SKIPPED_DUPLICATE"
    
    # NOT A DUPLICATE - Proceed with normal import
    print(f"🚀 Importiere: {clean_name}")
    
    # JOB SETUP - Use batch index suffix for unique IDs
    work_dir = CONFIG.get("work_dir")
    if batch_index > 0:
        job_id = f"job_mig_{int(time.time())}_{batch_index}"
    else:
        job_id = f"job_mig_{int(time.time())}"
    target_job_dir = os.path.join(work_dir, job_id)
    
    temp_dl_dir = os.path.join(work_dir, "temp_import_dl")
    temp_extract_dir = os.path.join(work_dir, "temp_import_extract")

    try:
        final_extract_path = ""
        mount_point = None
        
        # TRANSPORT
        if is_rclone and not is_folder_mode:
            if os.path.exists(temp_dl_dir): shutil.rmtree(temp_dl_dir)
            os.makedirs(temp_dl_dir, exist_ok=True)
            print("   📡 Lade herunter...")
            rclone_copy(full_source_path, temp_dl_dir)
            import_source_path = os.path.join(temp_dl_dir, os.path.basename(clean_name))
        
        # Remote Ordner -> Laden
        elif is_rclone and is_folder_mode:
            if os.path.exists(temp_extract_dir): shutil.rmtree(temp_extract_dir)
            os.makedirs(temp_extract_dir, exist_ok=True)
            print("   📡 Lade Ordner herunter...")
            rclone_copy(full_source_path, temp_extract_dir)
            import_source_path = temp_extract_dir
            
        else:
            # Lokal
            import_source_path = full_source_path

        # ENTPACKEN (Nur bei Dateien)
        final_extract_path = import_source_path 
        
        if os.path.isfile(import_source_path):
            if import_source_path.lower().endswith(".dmg"):
                print("   💿 Mounte DMG...")
                mount_point = mount_dmg(import_source_path)
                if mount_point: final_extract_path = mount_point
            elif import_source_path.lower().endswith((".zip", ".tar", ".gz")):
                print("   📦 Entpacke Archiv...")
                if extract_archive(import_source_path, temp_extract_dir):
                    final_extract_path = temp_extract_dir

        # JOB ERSTELLEN
        # Use provided target or default to first target for backward compatibility
        if target is None:
            target = _get_targets()[0]
        
        normalize_job_structure(final_extract_path, target_job_dir)

        save_state(target_job_dir, {
            "status": "imported", 
            "original_source": full_source_path,
            "migration_mode": migration_mode,
            "target": target
        })
        print(f"✅ Job erfolgreich: {job_id} (Ziel: {target})")

        # MIGRATION CLEANUP - Only if asked (single mode)
        if migration_mode and ask_for_deletion:
            print("\n🧹 MIGRATION MODE AKTIV")
            if input(f"   ⚠️  Original '{clean_name}' löschen? [y/N]: ").lower() == 'y':
                print("   🗑️  Lösche Quelle...")
                if is_rclone:
                    rclone_delete(full_source_path, is_folder=is_folder_mode)
                else:
                    if os.path.isfile(full_source_path): os.remove(full_source_path)
                    elif os.path.isdir(full_source_path): shutil.rmtree(full_source_path)
                print("   ✅ Gelöscht.")
        
        return target_job_dir

    except Exception as e:
        print(f"❌ Fehler: {e}")
        if os.path.exists(target_job_dir): shutil.rmtree(target_job_dir)
        return None

    finally:
        if mount_point: unmount_dmg(mount_point)
        if 'temp_extract_dir' in locals() and os.path.exists(temp_extract_dir) and not is_folder_mode:
            shutil.rmtree(temp_extract_dir)
        if 'temp_dl_dir' in locals() and os.path.exists(temp_dl_dir):
            shutil.rmtree(temp_dl_dir)


def run_auto_migration():
    """
    Fully automatic migration: Import batches of 10 items from legacy archive,
    auto-curate, and deploy them continuously until all items are processed.
    
    This is the "royal discipline" - combines batch import, auto-curation, and deployment.
    """
    source_root = CONFIG.get("migration_source_dir", "")
    if not source_root:
        print("\n📥 AUTO-MIGRATION: LEGACY ARCHIV VOLLSTÄNDIG MIGRIEREN")
        print("Pfad zum Legacy-Archiv eingeben (z.B. lys-nas:/Archiv/Legacy).")
        raw_input = input("   Pfad: ").strip().strip('"').strip("'")
        source_root = raw_input
        if source_root and (os.path.exists(source_root) or ":" in source_root):
            update_config("migration_source_dir", source_root)
        else:
            print("❌ Ungültiger Pfad.")
            return
    
    print(f"\n{'='*70}")
    print("🤖 AUTO-MIGRATION GESTARTET")
    print(f"{'='*70}")
    print(f"📂 Quelle: {source_root}")
    print(f"📦 Batch-Größe: {AUTO_MIGRATION_BATCH_SIZE} Items pro Durchlauf")
    print(f"🤖 Modus: Automatische Kuratierung + Deployment")
    print(f"🔁 Retry: {'Aktiviert' if CONFIG.get('batch_auto_retry', True) else 'Deaktiviert'}")
    print()
    print("ℹ️  Der Prozess läuft kontinuierlich bis alle Items verarbeitet sind.")
    print("ℹ️  Bei Fehlern werden die nächsten Batches weiter verarbeitet.")
    print()
    
    confirm = input("Auto-Migration starten? [Y/n]: ").strip().lower()
    if confirm not in ['', 'y', 'j', 'ja', 'yes']:
        print("   Abgebrochen.")
        return
    
    # Ask for target once at the beginning
    print(f"\n--- 🎯 ZIEL FÜR MIGRATION WÄHLEN ---")
    targets = _get_targets()
    for i, t in enumerate(targets, 1):
        print(f"{i}. {t}")
    sel = input(f"Wähle Ziel (1-{len(targets)}) [Default: {targets[0]}]: ").strip()
    
    if sel.isdigit():
        idx = int(sel) - 1
        if 0 <= idx < len(targets):
            target = targets[idx]
        else:
            print(f"⚠️  Ungültige Auswahl. Verwende Standard: {targets[0]}")
            target = targets[0]
    else:
        target = targets[0]
    
    print(f"✅ Ziel gewählt: {target}")
    print(f"ℹ️  Dieses Ziel wird für alle Batches verwendet.")
    
    # Statistics tracking
    total_imported = 0
    total_curated = 0
    total_deployed = 0
    total_failed_import = 0
    total_failed_curation = 0
    total_failed_deployment = 0
    total_skipped_duplicates = 0
    batch_number = 0
    
    # Continuous batch processing
    while True:
        batch_number += 1
        
        print(f"\n{'='*70}")
        print(f"📦 BATCH #{batch_number}")
        print(f"{'='*70}")
        
        # 1. IMPORT BATCH (10 items)
        print(f"\n[1/3] Import: Hole nächste 10 Items aus Legacy-Archiv...")
        
        # Call run_legacy_migration with auto-pick and batch mode
        # We need to modify this to automatically pick next 10 items
        is_rclone = ":" in source_root
        
        # Get candidates
        print(f"🔍 Scanne '{source_root}'...")
        candidates = set()
        archive_exts = (".zip", ".tar", ".gz", ".rar", ".dmg")
        video_exts = (".mkv", ".mp4", ".webm", ".mov", ".avi", ".m4v")
        
        try:
            if is_rclone:
                files = rclone_lsf_recursive(source_root)
                for f in files:
                    if f.lower().endswith(archive_exts):
                        candidates.add(f)
                    elif f.lower().endswith(video_exts) and "/" in f:
                        folder_name = f.split("/")[0] + "/"
                        candidates.add(folder_name)
            else:
                items = os.listdir(source_root)
                items.sort()
                for item in items:
                    full_path = os.path.join(source_root, item)
                    if os.path.isfile(full_path) and item.lower().endswith(archive_exts) and not item.startswith("."):
                        candidates.add(item)
                    elif os.path.isdir(full_path) and _is_valid_source_folder(full_path):
                        candidates.add(item + "/")
        except Exception as e:
            print(f"❌ Fehler beim Scannen: {e}")
            break
        
        if not candidates:
            print("✅ Keine weiteren Items im Legacy-Archiv gefunden.")
            print("🎉 Auto-Migration abgeschlossen!")
            break
        
        # Select next batch of items
        sorted_candidates = sorted(list(candidates))
        selected_items = sorted_candidates[:AUTO_MIGRATION_BATCH_SIZE]
        
        print(f"👉 Batch #{batch_number}: {len(selected_items)} Items gewählt")
        for item in selected_items:
            print(f"   • {item}")
        
        # Import batch
        imported_jobs = []
        items_to_delete = []
        skipped_duplicates = []
        
        for idx, selected_item in enumerate(selected_items, 1):
            print(f"\n   [{idx}/{len(selected_items)}] Importiere: {selected_item}")
            
            try:
                # Use timestamp for uniqueness to avoid ID collisions
                job_path = _process_single_import(
                    selected_item,
                    source_root,
                    is_rclone,
                    migration_mode=True,
                    target=target,
                    ask_for_deletion=False,  # Auto-delete duplicates without asking in auto-migration mode
                    batch_index=idx  # Simple index within batch - combined with timestamp in _process_single_import
                )
                
                if job_path == "SKIPPED_DUPLICATE":
                    skipped_duplicates.append(selected_item)
                    total_skipped_duplicates += 1
                    print(f"   ⏭️  Übersprungen (Duplikat)")
                elif job_path:
                    imported_jobs.append(job_path)
                    items_to_delete.append((selected_item, is_rclone, source_root))
                    total_imported += 1
                else:
                    total_failed_import += 1
                    print(f"   ⚠️  Import fehlgeschlagen für: {selected_item}")
            except Exception as e:
                total_failed_import += 1
                print(f"   ❌ Fehler beim Import: {e}")
        
        if not imported_jobs:
            print(f"\n⚠️  Batch #{batch_number}: Alle Imports fehlgeschlagen. Überspringe zur nächsten Batch.")
            continue
        
        print(f"\n✅ Import abgeschlossen: {len(imported_jobs)}/{len(selected_items)} erfolgreich")
        
        # 2. AUTO-CURATE BATCH (with retry logic)
        print(f"\n[2/3] Auto-Kuratierung: Verarbeite {len(imported_jobs)} Jobs...")
        
        # Get retry settings from config
        max_attempts = CONFIG.get('max_retries', 3)  # Total number of attempts (not retries)
        retry_enabled = CONFIG.get('batch_auto_retry', True)
        
        curated_jobs = []
        failed_curation_jobs = []
        
        # Track jobs that need processing
        jobs_to_curate = list(imported_jobs)
        attempt_number = 1
        
        while jobs_to_curate and attempt_number <= max_attempts:
            if attempt_number > 1:
                print(f"\n🔁 Wiederholung: Versuch {attempt_number}/{max_attempts} für {len(jobs_to_curate)} Jobs...")
                if not retry_enabled:
                    print(f"   ⚠️  Retry deaktiviert in Config. Überspringe verbleibende Jobs.")
                    failed_curation_jobs.extend(jobs_to_curate)
                    break
            
            jobs_for_next_attempt = []
            
            for idx, job_path in enumerate(jobs_to_curate, 1):
                job_name = os.path.basename(job_path)
                if attempt_number == 1:
                    print(f"\n   [{idx}/{len(jobs_to_curate)}] Kuratiere: {job_name}")
                else:
                    print(f"\n   Versuch {attempt_number} [{idx}/{len(jobs_to_curate)}] Kuratiere: {job_name}")
                
                try:
                    interactive_curation(job_path, auto_mode=True)
                    curated_jobs.append(job_path)
                    total_curated += 1
                    print(f"   ✅ Kuratierung erfolgreich")
                except Exception as e:
                    print(f"   ❌ Kuratierung fehlgeschlagen: {e}")
                    if attempt_number < max_attempts and retry_enabled:
                        jobs_for_next_attempt.append(job_path)
                        print(f"   → Wird bei nächstem Versuch wiederholt...")
                    else:
                        failed_curation_jobs.append(job_path)
                        total_failed_curation += 1
                        print(f"   → Fahre mit nächstem Job fort...")
            
            jobs_to_curate = jobs_for_next_attempt
            attempt_number += 1
            
            # Break if no more attempts needed
            if not jobs_to_curate:
                break
        
        print(f"\n✅ Auto-Kuratierung abgeschlossen: {len(curated_jobs)}/{len(imported_jobs)} erfolgreich")
        
        # 3. DEPLOY BATCH
        deployed_in_batch = 0
        if curated_jobs:
            print(f"\n[3/3] Deployment: Deploye {len(curated_jobs)} kuratierte Jobs...")
            
            for idx, job_path in enumerate(curated_jobs, 1):
                job_name = os.path.basename(job_path)
                print(f"\n   [{idx}/{len(curated_jobs)}] Deploye: {job_name}")
                
                try:
                    build_master(job_path)
                    build_dist(job_path)
                    deploy(job_path)
                    total_deployed += 1
                    deployed_in_batch += 1
                    print(f"   ✅ Deployment erfolgreich")
                except Exception as e:
                    total_failed_deployment += 1
                    print(f"   ❌ Deployment fehlgeschlagen: {e}")
                    print(f"   → Job bleibt im Work-Dir für manuelle Nachbearbeitung")
            
            print(f"\n✅ Deployment abgeschlossen: {deployed_in_batch}/{len(curated_jobs)} erfolgreich")
        
        # 4. CLEANUP - Delete source items after successful processing
        if items_to_delete:
            print(f"\n🧹 Bereinigung: Lösche {len(items_to_delete)} verarbeitete Items aus Legacy-Archiv...")
            
            for selected_item, is_rclone_item, source_root_item in items_to_delete:
                is_folder_mode = selected_item.endswith("/")
                clean_name = selected_item.rstrip("/")
                full_source_path = f"{source_root_item}/{clean_name}" if is_rclone_item else os.path.join(source_root_item, clean_name)
                
                try:
                    if is_rclone_item:
                        rclone_delete(full_source_path, is_folder=is_folder_mode)
                    else:
                        if os.path.isfile(full_source_path):
                            os.remove(full_source_path)
                        elif os.path.isdir(full_source_path):
                            shutil.rmtree(full_source_path)
                    print(f"   ✅ Gelöscht: {selected_item}")
                except Exception as e:
                    print(f"   ⚠️  Fehler beim Löschen von {selected_item}: {e}")
        
        # 5. BATCH SUMMARY
        print(f"\n{'='*70}")
        print(f"📊 BATCH #{batch_number} ZUSAMMENFASSUNG")
        print(f"{'='*70}")
        print(f"📦 Import:         {len(imported_jobs)}/{len(selected_items)} erfolgreich")
        if skipped_duplicates:
            print(f"⏭️  Duplikate:      {len(skipped_duplicates)} übersprungen")
        print(f"🤖 Kuratierung:    {len(curated_jobs)}/{len(imported_jobs)} erfolgreich")
        print(f"🚀 Deployment:     {deployed_in_batch}/{len(curated_jobs)} erfolgreich")
        
        if failed_curation_jobs:
            print(f"\n⚠️  {len(failed_curation_jobs)} Jobs konnten nicht kuratiert werden:")
            for job_path in failed_curation_jobs:
                print(f"   • {os.path.basename(job_path)}")
            print(f"   → Diese Jobs verbleiben im Work-Dir für manuelle Nachbearbeitung")
        
        if skipped_duplicates:
            print(f"\n⏭️  {len(skipped_duplicates)} Duplikate übersprungen:")
            for item in skipped_duplicates:
                print(f"   • {item}")
            print(f"   → Diese Videos existieren bereits im DMG-Archiv")
        
        print(f"\n➡️  Fahre fort mit nächster Batch...")
        print()
        
        # Optional pause between batches to avoid overwhelming the system
        # Configurable via AUTO_MIGRATION_BATCH_PAUSE_SEC constant
        if AUTO_MIGRATION_BATCH_PAUSE_SEC > 0:
            time.sleep(AUTO_MIGRATION_BATCH_PAUSE_SEC)
    
    # FINAL SUMMARY
    print(f"\n{'='*70}")
    print("🎉 AUTO-MIGRATION ABGESCHLOSSEN")
    print(f"{'='*70}")
    print(f"📊 GESAMT-STATISTIK:")
    print(f"   Batches verarbeitet:    {batch_number}")
    print(f"   📦 Import:              {total_imported} erfolgreich, {total_failed_import} fehlgeschlagen")
    if total_skipped_duplicates > 0:
        print(f"   ⏭️  Duplikate:            {total_skipped_duplicates} übersprungen (bereits im Archiv)")
    print(f"   🤖 Kuratierung:         {total_curated} erfolgreich, {total_failed_curation} fehlgeschlagen")
    print(f"   🚀 Deployment:          {total_deployed} erfolgreich, {total_failed_deployment} fehlgeschlagen")
    print(f"{'='*70}")
    
    if total_failed_curation > 0 or total_failed_deployment > 0:
        print(f"\n💡 Hinweis: {total_failed_curation + total_failed_deployment} Jobs konnten nicht vollständig")
        print(f"   verarbeitet werden und verbleiben im Work-Dir für manuelle Nachbearbeitung.")
    
    if total_skipped_duplicates > 0:
        print(f"\n📋 Duplikat-Info: {total_skipped_duplicates} Legacy-Archive wurden automatisch gelöscht,")
        print(f"   da sie bereits im neuen DMG-Archiv vorhanden sind.")
    
    input("\n[ENTER] zum Fortfahren...")
