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
from .utils import save_state
from .module_transport import rclone_lsf_recursive, rclone_copy, extract_archive, mount_dmg, unmount_dmg
from .module_scan import normalize_job_structure
from .phase_build import sanitize_filename
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)


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_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,
                target=target,
                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 _process_single_import(selected_item, source_root, is_rclone, target=None, batch_index=0):
    """
    Process a single import operation (restore mode).
    
    Args:
        selected_item: The item to import.
        source_root: Root path of the source archive.
        is_rclone: Whether the source is an rclone remote.
        target: Target category for the import.
        batch_index: Index for unique job IDs in batch operations.
    
    Returns:
        - job directory path on success
        - None on failure
        - "SKIPPED_DUPLICATE" if video already exists in metadata index
    """
    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)
    
    # 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,
            "target": target
        })
        print(f"✅ Job erfolgreich: {job_id} (Ziel: {target})")
        
        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)

