#!/usr/bin/env python3
"""
AV1 Batch Video Transcoder
Main entry point for batch video encoding with intelligent audio and resolution handling.
"""

import argparse
import csv
import shlex
from pathlib import Path

from core.config_helper import load_config_xml
from core.logger_helper import setup_logger
from core.process_manager import process_folder, get_default_cq

# =============================
# PATH NORMALIZATION
# =============================
def normalize_input_path(input_path: str, path_mappings: dict) -> Path:
    """
    Normalize input path from various formats to Windows path.
    
    Supports:
    - Windows paths: "P:\\tv\\show" or "P:/tv/show"
    - Linux paths: "/mnt/plex/tv/show" (maps to Windows equivalent if mapping exists)
    - Mixed separators: "P:/tv\\show"
    
    Args:
        input_path: Path string from user input
        path_mappings: Dict mapping Windows paths to Linux paths from config
        
    Returns:
        Path object pointing to the actual local folder
    """
    # First, try to map Linux paths to Windows paths (reverse mapping)
    # If user provides "/mnt/plex/tv", find the mapping and convert to "P:\\tv"
    if isinstance(path_mappings, list):
        # New format: list of dicts
        for mapping in path_mappings:
            if isinstance(mapping, dict):
                win_path = mapping.get("from")
                linux_path = mapping.get("to")
                if linux_path and input_path.lower().startswith(linux_path.lower()):
                    # Found a matching Linux path, convert to Windows
                    relative = input_path[len(linux_path):].lstrip("/").lstrip("\\")
                    result = Path(win_path) / relative if relative else Path(win_path)
                    logger.info(f"Path mapping: {input_path} -> {result}")
                    print(f"ℹ️  Mapped Linux path {input_path} to {result}")
                    return result
    else:
        # Old format: dict (for backwards compatibility)
        for win_path, linux_path in path_mappings.items():
            if input_path.lower().startswith(linux_path.lower()):
                # Found a matching Linux path, convert to Windows
                relative = input_path[len(linux_path):].lstrip("/").lstrip("\\")
                result = Path(win_path) / relative if relative else Path(win_path)
                logger.info(f"Path mapping: {input_path} -> {result}")
                print(f"ℹ️  Mapped Linux path {input_path} to {result}")
                return result
    
    # No mapping found, use path as-is (normalize separators to Windows)
    # Convert forward slashes to backslashes for Windows
    normalized = input_path.replace("/", "\\")
    result = Path(normalized)
    logger.info(f"Using path as-is: {result}")
    return result

# =============================
# BATCH PROCESSING
# =============================
def parse_batch_file(file_path: Path) -> list:
    """
    Parse batch file (.txt or .csv) and extract paths with optional per-row parameters.
    
    Formats:
    - Simple list (.txt): One path per line, optional space-separated parameters
      Example: P:\movies\Movie1 --r 720 --cq 28
    - CSV (.csv): First column is path, remaining columns are optional parameters
      Example: "P:\movies\Movie1","--r 720","--cq 28"
    
    Args:
        file_path: Path to batch file
        
    Returns:
        List of tuples: [(path_str, params_str), ...]
    """
    batch_items = []
    
    if file_path.suffix.lower() == ".csv":
        # CSV format
        with open(file_path, "r", encoding="utf-8") as f:
            reader = csv.reader(f)
            for row_idx, row in enumerate(reader):
                if not row or not row[0].strip():
                    continue  # Skip empty rows
                
                path_str = row[0].strip()
                
                # Skip header row (if it starts with "path")
                if row_idx == 0 and path_str.lower().startswith("path"):
                    continue
                
                # Combine remaining columns as parameters
                params = " ".join(col.strip() for col in row[1:] if col.strip())
                batch_items.append((path_str, params))
    else:
        # Simple list format (.txt or others)
        with open(file_path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                
                # Skip empty lines and comments
                if not line or line.startswith("#"):
                    continue
                
                # Split path from parameters
                # Handle quoted paths like: "C:\path with spaces" --r 720
                if line.startswith('"'):
                    # Find closing quote
                    end_quote = line.find('"', 1)
                    if end_quote != -1:
                        path_str = line[1:end_quote]
                        params = line[end_quote+1:].strip()
                    else:
                        path_str = line
                        params = ""
                else:
                    # Unquoted path, split on first space
                    parts = line.split(None, 1)
                    path_str = parts[0]
                    params = parts[1] if len(parts) > 1 else ""
                
                batch_items.append((path_str, params))
    
    return batch_items


def merge_batch_args(base_args, batch_params_str: str) -> argparse.Namespace:
    """
    Merge batch file row parameters with base CLI parameters.
    Row parameters override base parameters for that specific path.
    
    Args:
        base_args: Base argparse.Namespace from CLI
        batch_params_str: Parameter string from batch file row
        
    Returns:
        New argparse.Namespace with merged parameters
    """
    # Create a copy of base args
    merged = argparse.Namespace(**vars(base_args))
    
    if not batch_params_str:
        return merged
    
    # Parse row parameters as if they were CLI arguments
    try:
        param_list = shlex.split(batch_params_str)
    except ValueError:
        # If shlex fails, try simple split
        param_list = batch_params_str.split()
    
    # Build a parser just for these arguments (reuse the same one)
    # We'll manually apply known arguments
    i = 0
    while i < len(param_list):
        arg = param_list[i]
        
        if arg == "--cq":
            if i + 1 < len(param_list):
                merged.cq = int(param_list[i + 1])
                i += 2
            else:
                i += 1
        elif arg in ["--m", "--mode"]:
            if i + 1 < len(param_list):
                merged.transcode_mode = param_list[i + 1]
                i += 2
            else:
                i += 1
        elif arg == "--encoder":
            if i + 1 < len(param_list):
                merged.encoder = param_list[i + 1]
                i += 2
            else:
                i += 1
        elif arg in ["--r", "--resolution"]:
            if i + 1 < len(param_list):
                merged.resolution = param_list[i + 1]
                i += 2
            else:
                i += 1
        elif arg == "--test":
            merged.test_mode = True
            i += 1
        elif arg == "--language":
            if i + 1 < len(param_list):
                merged.audio_language = param_list[i + 1]
                i += 2
            else:
                i += 1
        elif arg == "--filter-audio":
            merged.filter_audio = True
            i += 1
        elif arg == "--audio-select":
            if i + 1 < len(param_list):
                merged.audio_select = param_list[i + 1]
                i += 2
            else:
                i += 1
        elif arg == "--audio-titles":
            if i + 1 < len(param_list):
                merged.audio_titles = param_list[i + 1]
                i += 2
            else:
                i += 1
        elif arg == "--audio-channels":
            if i + 1 < len(param_list):
                merged.audio_channels = param_list[i + 1]
                i += 2
            else:
                i += 1
        elif arg == "--keep-all-titles":
            merged.strip_all_titles = False
            i += 1
        elif arg == "--strip-all-titles":
            merged.strip_all_titles = True
            i += 1
        elif arg == "--unforce-subs":
            merged.unforce_subs = True
            i += 1
        elif arg == "--no-encode":
            merged.no_encode = True
            i += 1
        elif arg == "--force-process":
            merged.force_process = True
            i += 1
        elif arg == "--replace":
            merged.replace_file = True
            i += 1
        else:
            i += 1
    
    return merged


def parse_audio_dict(audio_str: str, dict_type: str) -> dict:
    """
    Parse audio titles or channels string into dictionary.
    
    Args:
        audio_str: String like "0:English,1:Commentary" or "0:2,1:6"
        dict_type: "titles" or "channels"
        
    Returns:
        Dictionary mapping stream indices to values, or empty dict on error
    """
    if not audio_str:
        return {}
    
    result = {}
    try:
        pairs = audio_str.split(",")
        for pair in pairs:
            stream_idx, value = pair.split(":")
            stream_idx = int(stream_idx.strip())
            
            if dict_type == "channels":
                # Validate channels: only 2 or 6 allowed
                channels = int(value.strip())
                if channels not in (2, 6):
                    logger.warning(f"Invalid channel count: {channels}. Only 2 or 6 allowed. Skipping.")
                    continue
                result[stream_idx] = channels
            else:  # titles
                result[stream_idx] = value.strip()
    except (ValueError, IndexError) as e:
        logger.warning(f"Error parsing audio {dict_type}: {e}")
    
    return result

# =============================
# Setup
# =============================
LOG_FOLDER = Path(__file__).parent / "logs"
logger = setup_logger(LOG_FOLDER)

TRACKER_FILE = Path(__file__).parent / "conversion_tracker.csv"
if not TRACKER_FILE.exists():
    with open(TRACKER_FILE, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow([
            "type", "show", "filename", "original_size_MB", "processed_size_MB", "percentage", 
            "source_resolution", "target_resolution", "audio_streams", "cq_value", "method"
        ])


# =============================
# MAIN
# =============================
def main():
    parser = argparse.ArgumentParser(
        description="Batch AV1 encode videos with intelligent audio and resolution handling",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s "C:\\Videos\\Movies"                          # Smart mode (preserve resolution, 4K->1080p)
  %(prog)s "C:\\Videos\\TV" --r 720 --m bitrate          # Force 720p, bitrate mode
  %(prog)s "C:\\Videos\\Anime" --cq 28 --r 1080          # Force 1080p, CQ=28
  %(prog)s "C:\\Videos\\Low-Res" --r 480                 # Force 480p for low-res content
        """
    )
    
    parser.add_argument("folder", nargs="?", default=None, help="Input folder containing video files (required unless using --paths-file)")
    parser.add_argument("--cq", type=int, help="Override default CQ value")
    parser.add_argument(
        "--m", "--mode", dest="transcode_mode", default="cq",
        choices=["cq", "bitrate"],
        help="Encode mode: CQ (constant quality) or Bitrate mode"
    )
    parser.add_argument(
        "--encoder", dest="encoder", default="hevc",
        choices=["hevc", "av1"],
        help="Video encoder: 'hevc' for HEVC NVENC 10-bit (default), 'av1' for AV1 NVENC 8-bit. Auto-selected based on source bit depth if not specified"
    )
    parser.add_argument(
        "--r", "--resolution", dest="resolution", default=None,
        choices=["480", "720", "1080"],
        help="Target resolution (acts as max, downscales if source is larger). If not specified: 4K→1080p, else preserve source"
    )
    parser.add_argument(
        "--test", dest="test_mode", default=False, action="store_true",
        help="Test mode: encode only first file, show ratio, don't move or delete (default: False)"
    )
    parser.add_argument(
        "--language", dest="audio_language", default=None,
        help="Tag audio streams with language code (e.g., eng, spa, fra). If not set, audio language is unchanged"
    )
    parser.add_argument(
        "--filter-audio", dest="filter_audio", default=None, action="store_true",
        help="Interactive audio selection: show audio streams and let user choose which to keep (overrides config setting)"
    )
    parser.add_argument(
        "--audio-select", dest="audio_select", default=None,
        help="Pre-select audio streams to keep (comma-separated, e.g., 1,2). Skips interactive prompt"
    )
    parser.add_argument(
        "--audio-titles", dest="audio_titles", default=None,
        help="Set custom titles for audio streams (e.g., '0:English,1:Commentary'). Format: stream_index:title,stream_index:title"
    )
    parser.add_argument(
        "--audio-channels", dest="audio_channels", default=None,
        help="Set channel count for specific audio streams (e.g., '0:2,1:6'). Format: stream_index:channels,stream_index:channels. Only 2 or 6 channels allowed"
    )
    parser.add_argument(
        "--keep-all-titles", dest="strip_all_titles", default=True, action="store_false",
        help="Preserve title metadata on audio tracks (default: titles are stripped)"
    )
    parser.add_argument(
        "--unforce-subs", dest="unforce_subs", default=False, action="store_true",
        help="Remove forced flag from all subtitle tracks"
    )
    parser.add_argument(
        "--no-encode", dest="no_encode", default=False, action="store_true",
        help="Skip encoding: copy video/audio streams as-is. Useful with --unforce-subs to only re-mux subtitles"
    )
    parser.add_argument(
        "--force-process", dest="force_process", default=False, action="store_true",
        help="Process files even if they contain ignore tags (e.g., already encoded files with suffix)"
    )
    parser.add_argument(
        "--replace", dest="replace_file", default=False, action="store_true",
        help="Replace original file instead of creating suffix version. Requires --no-encode"
    )
    parser.add_argument(
        "--wait", "-w", dest="wait_seconds", type=int, nargs='?', const=-1, default=None,
        help="Wait after each file (default: 30s with --no-encode, 0s otherwise). Gives Plex time to detect changes"
    )
    parser.add_argument(
        "--travel", dest="travel_mode", default=False, action="store_true",
        help="Travel mode: force 720p resolution and CQ+2, requires --output flag"
    )
    parser.add_argument(
        "--output", dest="output_folder", default=None,
        help="Output folder for travel mode (creates subfolder based on input folder name)"
    )
    parser.add_argument(
        "--paths-file", dest="paths_file", default=None,
        help="Batch mode: Read paths from file (.txt or .csv). One path per line with optional per-row parameters"
    )
    args = parser.parse_args()

    # Load configuration
    config_path = Path(__file__).parent / "config.xml"
    config = load_config_xml(config_path)
    
    # =============================
    # BATCH MODE
    # =============================
    if args.paths_file:
        paths_file = Path(args.paths_file)
        if not paths_file.exists():
            print(f"❌ Paths file not found: {paths_file}")
            logger.error(f"Paths file not found: {paths_file}")
            return
        
        # Track what we've already processed
        processed_items = set()
        batch_queue = []
        completed = 0
        failed = 0
        total_attempted = 0
        
        print("=" * 80)
        print(f"BATCH MODE: Processing paths from {paths_file.name}")
        print(f"ℹ️  File will be rechecked after each item for new additions")
        print("=" * 80)
        logger.info(f"BATCH MODE: Starting batch processing from {paths_file}")
        logger.info("File monitoring enabled - will check for new additions after each item")
        
        # Initial load
        batch_items = parse_batch_file(paths_file)
        if not batch_items:
            print(f"❌ No valid paths found in {paths_file}")
            logger.error(f"No valid paths in batch file: {paths_file}")
            return
        
        print(f"📋 Found {len(batch_items)} initial path(s)\n")
        
        # Create item signatures for tracking (path + params combo)
        for path_str, params_str in batch_items:
            item_sig = f"{path_str}|{params_str}"
            if item_sig not in processed_items:
                batch_queue.append((path_str, params_str))
        
        batch_num = 1
        
        # Process batch items with recheck after each
        while batch_queue:
            path_str, params_str = batch_queue.pop(0)
            item_sig = f"{path_str}|{params_str}"
            total_attempted += 1
            
            print()
            print("-" * 80)
            print(f"BATCH [{batch_num}]: {path_str}")
            if params_str:
                print(f"Parameters: {params_str}")
            print("-" * 80)
            logger.info(f"[BATCH {batch_num}] Processing: {path_str}")
            
            try:
                # Normalize path
                folder = normalize_input_path(path_str, config.get("path_mappings", {}))
                
                # Check if folder exists
                if not folder.exists():
                    print(f"❌ [BATCH {batch_num}] Folder not found: {folder}")
                    logger.error(f"[BATCH {batch_num}] Folder not found: {folder}")
                    processed_items.add(item_sig)
                    failed += 1
                    batch_num += 1
                    
                    # Recheck file for new items
                    print(f"\n🔄 Rechecking {paths_file.name} for new additions...")
                    new_batch_items = parse_batch_file(paths_file)
                    for path, params in new_batch_items:
                        sig = f"{path}|{params}"
                        if sig not in processed_items and sig not in [f"{p}|{pr}" for p, pr in batch_queue]:
                            batch_queue.append((path, params))
                            print(f"   ✨ New item found: {path}")
                    
                    continue
                
                # Merge batch parameters with base CLI parameters
                merged_args = merge_batch_args(args, params_str)
                
                # Handle travel mode
                travel_output_folder = None
                if merged_args.travel_mode:
                    if not merged_args.output_folder:
                        print(f"❌ [BATCH {batch_num}] --travel requires --output folder")
                        logger.error(f"[BATCH {batch_num}] --travel requires --output folder")
                        processed_items.add(item_sig)
                        failed += 1
                        batch_num += 1
                        
                        # Recheck file for new items
                        print(f"\n🔄 Rechecking {paths_file.name} for new additions...")
                        new_batch_items = parse_batch_file(paths_file)
                        for path, params in new_batch_items:
                            sig = f"{path}|{params}"
                            if sig not in processed_items and sig not in [f"{p}|{pr}" for p, pr in batch_queue]:
                                batch_queue.append((path, params))
                                print(f"   ✨ New item found: {path}")
                        
                        continue
                    
                    output_base = Path(merged_args.output_folder)
                    input_folder_name = folder.name
                    travel_output_folder = output_base / input_folder_name
                    travel_output_folder.mkdir(parents=True, exist_ok=True)
                    
                    merged_args.resolution = "720"
                    default_cq = get_default_cq(folder, config, "720", merged_args.encoder)
                    merged_args.cq = default_cq + 2
                
                # Validate --replace requires --no-encode
                if merged_args.replace_file and not merged_args.no_encode:
                    print(f"❌ [BATCH {batch_num}] --replace requires --no-encode")
                    logger.error(f"[BATCH {batch_num}] --replace requires --no-encode")
                    processed_items.add(item_sig)
                    failed += 1
                    batch_num += 1
                    
                    # Recheck file for new items
                    print(f"\n🔄 Rechecking {paths_file.name} for new additions...")
                    new_batch_items = parse_batch_file(paths_file)
                    for path, params in new_batch_items:
                        sig = f"{path}|{params}"
                        if sig not in processed_items and sig not in [f"{p}|{pr}" for p, pr in batch_queue]:
                            batch_queue.append((path, params))
                            print(f"   ✨ New item found: {path}")
                    
                    continue
                
                # Set wait time
                if merged_args.wait_seconds is None:
                    merged_args.wait_seconds = 0
                elif merged_args.wait_seconds == -1:
                    merged_args.wait_seconds = 30 if merged_args.no_encode else 0
                
                # Parse audio dicts from merged args
                audio_titles_dict = parse_audio_dict(merged_args.audio_titles, "titles") if merged_args.audio_titles else {}
                audio_channels_dict = parse_audio_dict(merged_args.audio_channels, "channels") if merged_args.audio_channels else {}
                
                # Process folder
                process_folder(
                    folder, merged_args.cq, merged_args.transcode_mode, merged_args.resolution,
                    config, TRACKER_FILE, merged_args.test_mode, merged_args.audio_language,
                    merged_args.filter_audio, merged_args.audio_select, merged_args.encoder,
                    merged_args.strip_all_titles, travel_output_folder, merged_args.unforce_subs,
                    merged_args.no_encode, merged_args.force_process, merged_args.replace_file,
                    merged_args.wait_seconds, audio_titles=audio_titles_dict, audio_channels=audio_channels_dict
                )
                
                print(f"✓ [BATCH {batch_num}] Completed: {folder.name}")
                logger.info(f"[BATCH {batch_num}] Completed successfully")
                processed_items.add(item_sig)
                completed += 1
                batch_num += 1
                
                # Recheck file for new items after successful completion
                print(f"\n🔄 Rechecking {paths_file.name} for new additions...")
                new_batch_items = parse_batch_file(paths_file)
                new_count = 0
                for path, params in new_batch_items:
                    sig = f"{path}|{params}"
                    if sig not in processed_items and sig not in [f"{p}|{pr}" for p, pr in batch_queue]:
                        batch_queue.append((path, params))
                        print(f"   ✨ New item found: {path}")
                        new_count += 1
                
                if new_count == 0:
                    print(f"   (no new additions)")
                
            except Exception as e:
                print(f"❌ [BATCH {batch_num}] Error: {e}")
                logger.error(f"[BATCH {batch_num}] Error: {e}", exc_info=True)
                processed_items.add(item_sig)
                failed += 1
                batch_num += 1
                
                # Recheck file for new items even on error
                print(f"\n🔄 Rechecking {paths_file.name} for new additions...")
                try:
                    new_batch_items = parse_batch_file(paths_file)
                    new_count = 0
                    for path, params in new_batch_items:
                        sig = f"{path}|{params}"
                        if sig not in processed_items and sig not in [f"{p}|{pr}" for p, pr in batch_queue]:
                            batch_queue.append((path, params))
                            print(f"   ✨ New item found: {path}")
                            new_count += 1
                    
                    if new_count == 0:
                        print(f"   (no new additions)")
                except Exception as recheck_error:
                    print(f"⚠️  Error rechecking file: {recheck_error}")
                    logger.warning(f"Error during file recheck: {recheck_error}")
        
        # Final check for any items added while processing the last batch
        print()
        print("=" * 80)
        print("✓ BATCH QUEUE COMPLETE - Performing final file check...")
        print("=" * 80)
        final_items = parse_batch_file(paths_file)
        final_new = []
        for path, params in final_items:
            sig = f"{path}|{params}"
            if sig not in processed_items:
                final_new.append((path, params))
        
        if final_new:
            print(f"📋 Found {len(final_new)} new item(s) added during processing!\n")
            
            for path_str, params_str in final_new:
                item_sig = f"{path_str}|{params_str}"
                total_attempted += 1
                
                print("-" * 80)
                print(f"BATCH [{batch_num}]: {path_str}")
                if params_str:
                    print(f"Parameters: {params_str}")
                print("-" * 80)
                logger.info(f"[BATCH {batch_num}] Processing: {path_str}")
                
                try:
                    folder = normalize_input_path(path_str, config.get("path_mappings", {}))
                    
                    if not folder.exists():
                        print(f"❌ [BATCH {batch_num}] Folder not found: {folder}")
                        logger.error(f"[BATCH {batch_num}] Folder not found: {folder}")
                        failed += 1
                        batch_num += 1
                        continue
                    
                    merged_args = merge_batch_args(args, params_str)
                    
                    travel_output_folder = None
                    if merged_args.travel_mode:
                        if not merged_args.output_folder:
                            print(f"❌ [BATCH {batch_num}] --travel requires --output folder")
                            logger.error(f"[BATCH {batch_num}] --travel requires --output folder")
                            failed += 1
                            batch_num += 1
                            continue
                        
                        output_base = Path(merged_args.output_folder)
                        input_folder_name = folder.name
                        travel_output_folder = output_base / input_folder_name
                        travel_output_folder.mkdir(parents=True, exist_ok=True)
                        
                        merged_args.resolution = "720"
                        default_cq = get_default_cq(folder, config, "720", merged_args.encoder)
                        merged_args.cq = default_cq + 2
                    
                    if merged_args.replace_file and not merged_args.no_encode:
                        print(f"❌ [BATCH {batch_num}] --replace requires --no-encode")
                        logger.error(f"[BATCH {batch_num}] --replace requires --no-encode")
                        failed += 1
                        batch_num += 1
                        continue
                    
                    if merged_args.wait_seconds is None:
                        merged_args.wait_seconds = 0
                    elif merged_args.wait_seconds == -1:
                        merged_args.wait_seconds = 30 if merged_args.no_encode else 0
                    
                    # Parse audio dicts from merged args
                    audio_titles_dict = parse_audio_dict(merged_args.audio_titles, "titles") if merged_args.audio_titles else {}
                    audio_channels_dict = parse_audio_dict(merged_args.audio_channels, "channels") if merged_args.audio_channels else {}
                    
                    process_folder(
                        folder, merged_args.cq, merged_args.transcode_mode, merged_args.resolution,
                        config, TRACKER_FILE, merged_args.test_mode, merged_args.audio_language,
                        merged_args.filter_audio, merged_args.audio_select, merged_args.encoder,
                        merged_args.strip_all_titles, travel_output_folder, merged_args.unforce_subs,
                        merged_args.no_encode, merged_args.force_process, merged_args.replace_file,
                        merged_args.wait_seconds, audio_titles=audio_titles_dict, audio_channels=audio_channels_dict
                    )
                    
                    print(f"✓ [BATCH {batch_num}] Completed: {folder.name}")
                    logger.info(f"[BATCH {batch_num}] Completed successfully")
                    completed += 1
                    batch_num += 1
                    
                except Exception as e:
                    print(f"❌ [BATCH {batch_num}] Error: {e}")
                    logger.error(f"[BATCH {batch_num}] Error: {e}", exc_info=True)
                    failed += 1
                    batch_num += 1
        else:
            print("(no new items found)")
        
        # Final summary
        print()
        print("=" * 80)
        print(f"✓ BATCH PROCESSING COMPLETE")
        print(f"  Total items processed: {total_attempted}")
        print(f"  ✓ Succeeded: {completed}")
        if failed > 0:
            print(f"  ❌ Failed: {failed}")
        print("=" * 80)
        logger.info(f"Batch processing finished: {completed} succeeded, {failed} failed out of {total_attempted}")
        
        return
    
    # =============================
    # SINGLE MODE
    # =============================
    if not args.folder:
        parser.print_help()
        return
    
    # Normalize input path (handle Linux paths, mixed separators, etc.)
    folder = normalize_input_path(args.folder, config.get("path_mappings", {}))
    
    # Verify folder exists
    if not folder.exists():
        print(f"❌ Folder not found: {folder}")
        logger.error(f"Folder not found: {folder}")
        return

    # Handle travel mode
    travel_output_folder = None
    if args.travel_mode:
        if not args.output_folder:
            print("❌ --travel flag requires --output folder to be specified")
            logger.error("--travel flag used without --output folder")
            return
        
        # Parse output folder and create subfolder based on input folder name
        output_base = Path(args.output_folder)
        input_folder_name = folder.name
        travel_output_folder = output_base / input_folder_name
        
        # Create the output folder structure
        travel_output_folder.mkdir(parents=True, exist_ok=True)
        print(f"✅ Travel mode: Output folder set to {travel_output_folder}")
        logger.info(f"Travel mode enabled: {folder} -> {travel_output_folder}")
        
        # Set resolution to 720 in travel mode
        args.resolution = "720"
        
        # Get default CQ for 720p and add 2
        default_cq = get_default_cq(folder, config, "720", args.encoder)
        args.cq = default_cq + 2
        print(f"✅ Travel mode: Resolution=720p, CQ={args.cq} (default {default_cq} + 2)")
        logger.info(f"Travel mode: CQ set to {args.cq}")

    # Validate --replace flag requires --no-encode
    if args.replace_file and not args.no_encode:
        print("❌ --replace requires --no-encode flag")
        logger.error("--replace flag used without --no-encode")
        return
    
    # Parse audio titles from CLI argument
    audio_titles_dict = {}
    if args.audio_titles:
        try:
            # Format: "0:English,1:Commentary,2:Descriptive Audio"
            pairs = args.audio_titles.split(",")
            for pair in pairs:
                stream_idx, title = pair.split(":")
                audio_titles_dict[int(stream_idx.strip())] = title.strip()
            logger.info(f"Audio titles: {audio_titles_dict}")
        except (ValueError, IndexError):
            print("❌ Invalid --audio-titles format. Use: '0:English,1:Commentary'")
            logger.error(f"Invalid audio titles format: {args.audio_titles}")
            return
    
    # Parse audio channels from CLI argument
    audio_channels_dict = {}
    if args.audio_channels:
        try:
            # Format: "0:2,1:6" (stream_index:channel_count)
            pairs = args.audio_channels.split(",")
            for pair in pairs:
                stream_idx, channels = pair.split(":")
                stream_idx = int(stream_idx.strip())
                channels = int(channels.strip())
                
                # Validate that only 2 or 6 channels are allowed
                if channels not in (2, 6):
                    print(f"❌ Invalid channel count: {channels}. Only 2 or 6 channels allowed")
                    logger.error(f"Invalid channel count: {channels}. Only 2 or 6 channels allowed")
                    return
                
                audio_channels_dict[stream_idx] = channels
            logger.info(f"Audio channels: {audio_channels_dict}")
        except (ValueError, IndexError):
            print("❌ Invalid --audio-channels format. Use: '0:2,1:6'")
            logger.error(f"Invalid audio channels format: {args.audio_channels}")
            return
    
    # Set wait time default: 30s if --no-encode and --wait used, 0 otherwise
    # -1 means --wait was used without a value (use intelligent default)
    if args.wait_seconds is None:
        args.wait_seconds = 0  # No --wait flag provided
    elif args.wait_seconds == -1:
        args.wait_seconds = 30 if args.no_encode else 0  # --wait used without value
    
    # Process folder
    process_folder(folder, args.cq, args.transcode_mode, args.resolution, config, TRACKER_FILE, args.test_mode, args.audio_language, args.filter_audio, args.audio_select, args.encoder, args.strip_all_titles, travel_output_folder, args.unforce_subs, args.no_encode, args.force_process, args.replace_file, args.wait_seconds, audio_titles=audio_titles_dict, audio_channels=audio_channels_dict)

if __name__ == "__main__":
    main()
