# core/process_manager.py
"""Main processing logic for batch transcoding."""

import csv
import os
import shutil
import subprocess
import time
from pathlib import Path

from core.audio_handler import get_audio_streams
from core.encode_engine import run_ffmpeg
from core.logger_helper import setup_logger, setup_failure_logger
from core.video_handler import get_source_resolution, determine_target_resolution, get_source_bit_depth, has_forced_subtitles

logger = setup_logger(Path(__file__).parent.parent / "logs")
failure_logger = setup_failure_logger(Path(__file__).parent.parent / "logs")


def get_default_cq(folder: Path, config: dict, resolution: str, encoder: str = "hevc") -> int:
    """
    Get the default CQ value for a given resolution, encoder, and folder type.
    
    Args:
        folder: Input folder path (used to detect TV/anime/movie type)
        config: Configuration dictionary
        resolution: Resolution string ("720", "1080", etc.)
        encoder: Encoder type ("hevc" or "av1")
    
    Returns:
        Default CQ value for the given parameters
    """
    # Determine content type from folder path
    folder_lower = str(folder).lower()
    is_tv = "\\tv\\" in folder_lower or "/tv/" in folder_lower
    is_anime = "\\anime\\" in folder_lower or "/anime/" in folder_lower
    
    # Build the config key
    if is_tv:
        key = f"tv_{resolution}"
    elif is_anime:
        key = f"anime_{resolution}"
    else:
        key = f"movie_{resolution}"
    
    # Get CQ value from config
    cq_config = config.get("encode", {}).get("cq", {}).get(encoder, {})
    return cq_config.get(key, 28)  # Default fallback to 28


def _cleanup_temp_files(temp_input: Path, temp_output: Path):
    """Helper function to clean up temporary input and output files."""
    try:
        if temp_input.exists():
            temp_input.unlink()
            logger.debug(f"Cleaned up temp input: {temp_input.name}")
    except Exception as e:
        logger.warning(f"Could not delete temp input {temp_input.name}: {e}")
    
    try:
        if temp_output.exists():
            temp_output.unlink()
            logger.debug(f"Cleaned up temp output: {temp_output.name}")
    except Exception as e:
        logger.warning(f"Could not delete temp output {temp_output.name}: {e}")


def should_skip_file(file: Path, no_encode: bool, unforce_subs: bool, force_process: bool, ignore_tags: list, travel_output_folder: Path) -> tuple:
    """
    Determine if a file should be skipped from processing based on multiple criteria.
    
    Skip conditions (in order):
    1. If --no-encode + --unforce-subs: skip if file has no forced subtitles
    2. If --force-process NOT set: skip if filename contains any ignore_tags (e.g., [EHX])
    3. Travel mode always processes files (overrides ignore tags)
    
    Args:
        file: File path to check
        no_encode: True if --no-encode flag is set
        unforce_subs: True if --unforce-subs flag is set
        force_process: True if --force-process flag is set (bypass ignore_tags)
        ignore_tags: List of filename tags to skip (from config)
        travel_output_folder: If set, travel mode is active (process all files)
    
    Returns:
        tuple: (should_skip: bool, reason: str or None)
    """
    # Check for forced subtitles if using --no-encode + --unforce-subs
    if no_encode and unforce_subs:
        if not has_forced_subtitles(file):
            return True, "no forced subtitles found (--no-encode + --unforce-subs)"
    
    # Skip files with ignore tags (unless force_process is enabled)
    # In travel mode, don't skip files based on tags
    if not force_process and not travel_output_folder and any(tag.lower() in file.name.lower() for tag in ignore_tags):
        return True, "matches ignore tags"
    
    return False, None


def process_folder(folder: Path, cq: int, transcode_mode: str, resolution: str, config: dict, tracker_file: Path, test_mode: bool = False, audio_language: str = None, filter_audio: bool = None, audio_select: str = None, encoder: str = "hevc", strip_all_titles: bool = False, travel_output_folder: Path = None, unforce_subs: bool = False, no_encode: bool = False, force_process: bool = False, replace_file: bool = False, wait_seconds: int = 0, color_bit: int = None, crop_height: int = None, audio_titles: dict = None, audio_channels: dict = None, title_suffix: str = None, default_language: str = None, no_replace_und: bool = False):
    """
    Process all video files in folder with appropriate encoding settings.
    
    Args:
        folder: Input folder path
        cq: CQ override value
        transcode_mode: "cq" or "bitrate"
        resolution: Explicit resolution override ("480", "720", "1080", or None for smart)
        config: Configuration dictionary
        tracker_file: Path to CSV tracker file
        test_mode: If True, only encode first file and skip final move/cleanup
        audio_language: Optional language code to tag audio (e.g., 'eng', 'spa'). If None, no tagging applied.
        filter_audio: If True, show interactive audio selection prompt. If None, use config setting.
        audio_select: Pre-selected audio streams (comma-separated, e.g., "1,2"). Skips interactive prompt.
        encoder: Video encoder to use - "hevc" for HEVC NVENC 10-bit (default) or "av1" for AV1 NVENC 8-bit.
        strip_all_titles: If True, strip all title metadata from all audio tracks.
        unforce_subs: If True, remove forced flag from all subtitle tracks.
        no_encode: If True, skip encoding and copy video/audio streams as-is. Useful with --unforce-subs for re-muxing only.
        force_process: If True, process files even if they match ignore_tags (e.g., already encoded files).
        crop_height: Optional crop height in pixels (e.g., 816 for 1920x816 crop from 1920x1080 source). If None, no crop applied.
        replace_file: If True, replace original file instead of creating suffix version. Requires no_encode=True.
        wait_seconds: Seconds to wait after each successful file (for Plex detection). 0 = no wait.
        travel_output_folder: If provided, move encoded files to this folder instead of original location.
        color_bit: If specified (8 or 10), forces HEVC color bit depth. 8-bit uses yuv420p, 10-bit uses p010le. Only valid with hevc encoder.
        title_suffix: Optional text to insert before main suffix (e.g., "1080p" or "v2"). If None, uses config file setting.
        default_language: Default language code for undefined (und) audio tracks (e.g., 'eng', 'spa'). If None, uses config default.
        no_replace_und: If True, disable replacement of undefined language tags with default language.
    """
    if not folder.exists():
        print(f"❌ Folder not found: {folder}")
        logger.error(f"Folder not found: {folder}")
        return

    audio_config = config["audio"]
    bitrate_config = config["encode"]["fallback"]
    filters_config = config["encode"]["filters"]
    suffix = config["suffix"]
    # Use provided title_suffix, or get from config, or use empty string
    if title_suffix is None:
        title_suffix = config.get("title_suffix", "")
    # Create combined suffix (title_suffix appears before main suffix)
    combined_suffix = title_suffix + suffix if title_suffix else suffix
    extensions = config["extensions"]
    ignore_tags = config["ignore_tags"]
    reduction_ratio_threshold = config["reduction_ratio_threshold"]
    
    # Resolution logic: explicit arg takes precedence, else use smart defaults
    explicit_resolution = resolution  # Will be None if not specified
    
    filter_flags = filters_config.get("default","lanczos")
    folder_lower = str(folder).lower()
    is_tv = "\\tv\\" in folder_lower or "/tv/" in folder_lower
    is_anime = "\\anime\\" in folder_lower or "/anime/" in folder_lower
    if is_tv:
        filter_flags = filters_config.get("tv","bicubic")
    elif is_anime:
        filter_flags = filters_config.get("anime", filters_config.get("default","lanczos"))

    processing_folder = Path(config["processing_folder"])
    processing_folder.mkdir(parents=True, exist_ok=True)

    # ===== Handle default language for undefined (und) audio tracks =====
    # Determine effective default language
    effective_default_lang = None
    if not no_replace_und:  # Only if replacement is enabled
        # Priority: CLI arg > config setting > 'und' (disabled)
        if default_language:
            effective_default_lang = default_language
            print(f"✅ Using CLI default language: {effective_default_lang}")
            logger.info(f"Default language from CLI: {effective_default_lang}")
        else:
            # Get from config
            config_default_lang = config.get("general", {}).get("default_language", "eng")
            if config_default_lang and config_default_lang != "und":
                effective_default_lang = config_default_lang
                print(f"✅ Using config default language: {effective_default_lang}")
                logger.info(f"Default language from config: {effective_default_lang}")
    
    if no_replace_und:
        print(f"⏸️  Undefined (und) language tag replacement is DISABLED (--no-replace-und)")
        logger.info("und→default language replacement is disabled")
    elif effective_default_lang:
        print(f"ℹ️  Will replace 'und' audio language with: {effective_default_lang}")
        logger.info(f"und→default language enabled: und → {effective_default_lang}")
    else:
        print(f"ℹ️  und audio language replacement is disabled or configured as 'und'")
        logger.info("und→default language disabled (configured as 'und')")

    # Determine if we're in smart mode (no explicit mode specified)

    is_smart_mode = transcode_mode not in ["cq", "bitrate"]  # Default/smart mode
    is_forced_cq = transcode_mode == "cq"
    is_forced_bitrate = transcode_mode == "bitrate"
    
    # Track files for potential retry in smart mode
    failed_cq_files = []  # List of (file_path, metadata) for CQ failures in smart mode
    consecutive_failures = 0
    max_consecutive = 3
    
    # Phase 1: Process files with initial mode strategy
    print(f"\n{'='*60}")
    if is_smart_mode:
        print("📋 MODE: Smart (Try CQ first, retry with Bitrate if needed)")
    elif is_forced_cq:
        print("📋 MODE: Forced CQ (skip failures, log them)")
    else:
        print("📋 MODE: Forced Bitrate (skip failures, log them)")
    print(f"{'='*60}\n")
    
    skipped_count = 0
    for file in folder.rglob("*"):
        if file.suffix.lower() not in extensions:
            continue
        
        # Check if file should be skipped
        should_skip, skip_reason = should_skip_file(file, no_encode, unforce_subs, force_process, ignore_tags, travel_output_folder)
        if should_skip:
            logger.info(f"Skipping {file.name}: {skip_reason}")
            print(f"⏭️ Skipping {file.name}: {skip_reason}")
            skipped_count += 1
            continue
        
        if skipped_count > 0:
            print(f"⏭️ Skipped {skipped_count} file(s)")
            logger.info(f"Skipped {skipped_count} file(s)")
            skipped_count = 0

        print("="*60)
        logger.info(f"Processing: {file.name}")
        print(f"📁 Processing: {file.name}")

        temp_input = (processing_folder / file.name).resolve()
        
        # Check if file already exists in processing folder
        if temp_input.exists() and os.access(temp_input, os.R_OK):
            source_size = file.stat().st_size
            temp_size = temp_input.stat().st_size
            
            # Verify it's complete (same size as source)
            if source_size == temp_size:
                print(f"✓ Found existing copy in processing folder (verified complete)")
                logger.info(f"File already in processing: {file.name} ({temp_size/1e6:.2f} MB verified complete)")
            else:
                # File exists but incomplete - recopy
                print(f"⚠️  Existing copy incomplete ({temp_size/1e6:.2f} MB vs {source_size/1e6:.2f} MB source). Re-copying...")
                logger.warning(f"Incomplete copy detected for {file.name}. Re-copying.")
                shutil.copy2(file, temp_input)
                logger.info(f"Re-copied {file.name} → {temp_input.name}")
        else:
            # File doesn't exist or not accessible - copy it
            shutil.copy2(file, temp_input)
            logger.info(f"Copied {file.name} → {temp_input.name}")
        
        # Verify file is accessible
        for attempt in range(3):
            if temp_input.exists() and os.access(temp_input, os.R_OK):
                break
        
        # Check for matching subtitle file
        subtitle_file = None
        if config.get("general", {}).get("subtitles", {}).get("enabled", True):
            subtitle_exts = config.get("general", {}).get("subtitles", {}).get("extensions", ".vtt,.srt,.ass,.ssa,.sub").split(",")
            # Look for subtitle with same base name (e.g., movie.vtt or movie.en.vtt)
            for ext in subtitle_exts:
                ext = ext.strip()
                # Try exact match first (movie.vtt)
                potential_sub = file.with_suffix(ext)
                if potential_sub.exists():
                    subtitle_file = potential_sub
                    print(f"📝 Found subtitle: {subtitle_file.name}")
                    logger.info(f"Found subtitle file: {subtitle_file.name}")
                    break
                
                # Try language prefix variants (movie.en.vtt, movie.eng.vtt, etc.)
                # Look for files matching the pattern basename.*language*.ext
                parent_dir = file.parent
                base_name = file.stem
                for item in parent_dir.glob(f"{base_name}.*{ext}"):
                    subtitle_file = item
                    print(f"📝 Found subtitle: {subtitle_file.name}")
                    logger.info(f"Found subtitle file: {subtitle_file.name}")
                    break
                
                if subtitle_file:
                    break
        
        try:
            # Detect source resolution and determine target resolution
            src_width, src_height = get_source_resolution(temp_input)
            src_bit_depth = get_source_bit_depth(temp_input)
            res_width, res_height, target_resolution = determine_target_resolution(
                src_width, src_height, explicit_resolution
            )
            
            # Auto-select encoder based on detected source bit depth
            if src_bit_depth >= 10:
                # Source is 10-bit or higher - use HEVC NVENC
                selected_encoder = "hevc"
            else:
                # Source is 8-bit - use AV1 NVENC
                selected_encoder = "av1"
            logger.info(f"Auto-selected {selected_encoder.upper()} encoder for detected {src_bit_depth}-bit source")
            
            # Log resolution decision
            if explicit_resolution:
                logger.info(f"Using explicitly specified resolution: {res_width}x{res_height}")
            else:
                if src_height > 1080:
                    print(f"⚠️  Source {src_width}x{src_height} is above 1080p. Scaling down to 1080p.")
                elif src_height <= 720:
                    print(f"ℹ️  Source {src_width}x{src_height} is 720p or lower. Preserving resolution.")
                else:
                    print(f"ℹ️  Source {src_width}x{src_height} is at or below 1080p. Preserving resolution.")
            
            # Set CQ based on content type, target resolution, and encoder
            if is_anime:
                cq_key = f"anime_{target_resolution}"
            elif is_tv:
                cq_key = f"tv_{target_resolution}"
            else:
                cq_key = f"movie_{target_resolution}"
            # Look up CQ from encoder-specific section (using auto-selected encoder)
            encoder_cq_config = config["encode"]["cq"].get(selected_encoder, {})
            content_cq = encoder_cq_config.get(cq_key, 32)
            file_cq = cq if cq is not None else content_cq
            
            # Use the auto-selected encoder for the rest of processing
            actual_encoder = selected_encoder
            content_cq = encoder_cq_config.get(cq_key, 32)
            file_cq = cq if cq is not None else content_cq
            
            # Output file with title_suffix (if any) and main suffix in processing folder (always .mkv container)
            combined_suffix = title_suffix + suffix if title_suffix else suffix
            temp_output = (processing_folder / f"{file.stem}{combined_suffix}.mkv").resolve()
            
            # Determine which method to try first
            if is_forced_bitrate:
                method = "Bitrate"
            elif is_forced_cq:
                method = "CQ"
            else:  # Smart mode
                method = "CQ"  # Always try CQ first in smart mode
            
            # Attempt encoding
            try:
                # Determine audio_filter config (CLI arg overrides config file)
                # --filter-audio flag means: show interactive prompt
                if filter_audio:
                    audio_filter_config = {"enabled": True, "interactive": True}
                    # If --audio-select provided, skip interactive and use pre-selected streams
                    if audio_select:
                        audio_filter_config["preselected"] = audio_select
                elif audio_select:
                    # If --audio-select provided (without --filter-audio), use preselected streams
                    audio_filter_config = {"enabled": True, "preselected": audio_select}
                else:
                    # Use config file setting (if present)
                    audio_filter_config = config.get("general", {}).get("audio_filter", {})
                
                # ===== Determine effective audio language to apply =====
                # Priority: CLI --language > (detected und + default_language) > detected language
                effective_audio_language = audio_language
                
                # If no explicit CLI language, check if we should replace 'und' with default
                if not audio_language and effective_default_lang:
                    # Get audio streams to check their languages
                    streams = get_audio_streams(temp_input)
                    if streams:
                        # Check if the first (or any primary) audio stream is 'und'
                        first_stream_lang = streams[0][3]  # 4th element is language
                        if first_stream_lang == "und":
                            effective_audio_language = effective_default_lang
                            logger.info(f"First audio stream is 'und', replacing with default language: {effective_audio_language}")
                            print(f"🔄 Audio stream detected as 'und', will tag as: {effective_audio_language}")
                
                orig_size, out_size, reduction_ratio = run_ffmpeg(
                    temp_input, temp_output, file_cq, res_width, res_height, src_width, src_height,
                    filter_flags, audio_config, method, bitrate_config, actual_encoder, [subtitle_file] if subtitle_file else None, effective_audio_language,
                    audio_filter_config, test_mode, strip_all_titles, src_bit_depth, unforce_subs, no_encode, color_bit, crop_height, audio_titles, audio_channels
                )
                
                # Check if encode met size target
                # Skip size check if --no-encode is used (file size will be nearly identical)
                encode_succeeded = True
                if not no_encode:
                    if method == "CQ" and reduction_ratio >= reduction_ratio_threshold:
                        encode_succeeded = False
                    elif method == "Bitrate" and reduction_ratio >= reduction_ratio_threshold:
                        encode_succeeded = False
                
                if not encode_succeeded:
                    # Size threshold not met
                    error_msg = f"Size threshold not met ({reduction_ratio:.1%})"
                    
                    if test_mode:
                        # In test mode, stop immediately and keep temp files
                        print(f"❌ Test mode: {method} failed: {error_msg}")
                        print(f"   Temp input preserved at: {temp_input}")
                        print(f"   Temp output preserved at: {temp_output}")
                        logger.error(f"Test mode: {method} size threshold failed for {file.name}: {error_msg}")
                        raise RuntimeError(error_msg)
                    
                    if is_smart_mode and method == "CQ":
                        # In smart mode CQ failure, mark for bitrate retry
                        print(f"⚠️  CQ failed size target ({reduction_ratio:.1%}). Will retry with Bitrate.")
                        failure_logger.warning(f"{file.name} | CQ failed size target ({reduction_ratio:.1%})")
                        failed_cq_files.append({
                            'file': file,
                            'temp_input': temp_input,
                            'temp_output': temp_output,
                            'src_width': src_width,
                            'src_height': src_height,
                            'res_width': res_width,
                            'res_height': res_height,
                            'target_resolution': target_resolution,
                            'file_cq': file_cq,
                            'is_tv': is_tv,
                            'subtitle_file': subtitle_file,
                            'src_bit_depth': src_bit_depth,
                            'encoder': actual_encoder,
                            'effective_audio_language': effective_audio_language
                        })
                        consecutive_failures += 1
                        if consecutive_failures >= max_consecutive:
                            print(f"\n⚠️  {max_consecutive} consecutive CQ failures. Moving to Phase 2: Bitrate retry.")
                            logger.warning(f"{max_consecutive} consecutive CQ failures. Moving to Phase 2.")
                            break  # Move to Phase 2
                        continue
                    elif is_forced_cq or is_forced_bitrate:
                        # In forced mode, skip the file
                        print(f"❌ {method} failed: {error_msg}")
                        failure_logger.warning(f"{file.name} | {method} failed: {error_msg}")
                        consecutive_failures += 1
                        if consecutive_failures >= max_consecutive:
                            print(f"\n❌ {max_consecutive} consecutive failures in forced {method} mode. Stopping.")
                            logger.error(f"{max_consecutive} consecutive failures. Stopping process.")
                            _cleanup_temp_files(temp_input, temp_output)
                            break
                        _cleanup_temp_files(temp_input, temp_output)
                        continue
                
                # Encoding succeeded - reset failure counter
                consecutive_failures = 0
                
            except subprocess.CalledProcessError as e:
                # FFmpeg execution failed
                error_msg = str(e).split('\n')[0][:100]  # First 100 chars of error
                
                if test_mode:
                    # In test mode, stop immediately on any error and keep temp files
                    print(f"❌ Test mode: Encode failed. Stopping script.")
                    print(f"   Temp input preserved at: {temp_input}")
                    print(f"   Temp output preserved at: {temp_output}")
                    logger.error(f"Test mode: Encode failed for {file.name}: {error_msg}")
                    raise
                
                if is_smart_mode and method == "CQ":
                    # In smart mode, log and retry with bitrate
                    print(f"❌ CQ encode error. Will retry with Bitrate.")
                    failure_logger.warning(f"{file.name} | CQ error: {error_msg}")
                    failed_cq_files.append({
                        'file': file,
                        'temp_input': temp_input,
                        'temp_output': temp_output,
                        'src_width': src_width,
                        'src_height': src_height,
                        'res_width': res_width,
                        'res_height': res_height,
                        'target_resolution': target_resolution,
                        'file_cq': file_cq,
                        'is_tv': is_tv,
                        'subtitle_file': subtitle_file,
                        'effective_audio_language': effective_audio_language
                    })
                    consecutive_failures += 1
                    if consecutive_failures >= max_consecutive:
                        print(f"\n⚠️  {max_consecutive} consecutive CQ failures. Moving to Phase 2: Bitrate retry.")
                        logger.warning(f"{max_consecutive} consecutive CQ failures. Moving to Phase 2.")
                        break
                    continue
                elif is_forced_cq or is_forced_bitrate:
                    # In forced mode, skip and log
                    if test_mode:
                        # In test mode, stop immediately and keep temp files
                        print(f"❌ Test mode: {method} encode failed. Stopping script.")
                        print(f"   Temp input preserved at: {temp_input}")
                        print(f"   Temp output preserved at: {temp_output}")
                        logger.error(f"Test mode: {method} encode failed for {file.name}: {error_msg}")
                        raise
                    
                    print(f"❌ {method} encode failed: {error_msg}")
                    failure_logger.warning(f"{file.name} | {method} error: {error_msg}")
                    consecutive_failures += 1
                    if consecutive_failures >= max_consecutive:
                        print(f"\n❌ {max_consecutive} consecutive failures in forced {method} mode. Stopping.")
                        logger.error(f"{max_consecutive} consecutive failures. Stopping process.")
                        _cleanup_temp_files(temp_input, temp_output)
                        break
                    _cleanup_temp_files(temp_input, temp_output)
                    continue
            
            # If we get here, encoding succeeded - save file and log
            _save_successful_encoding(
                file, temp_input, temp_output, orig_size, out_size, 
                reduction_ratio, method, src_width, src_height, res_width, res_height,
                file_cq, tracker_file, folder, is_tv, suffix, config, test_mode, subtitle_file, travel_output_folder, replace_file, wait_seconds, combined_suffix
            )
            
            # In test mode, stop after first successful file
            if test_mode:
                print(f"\n✅ TEST MODE: File processed. Encoded file is in temp folder for inspection.")
                break
            
        except Exception as e:
            # Unexpected error
            error_msg = str(e)[:100]
            print(f"❌ Unexpected error: {error_msg}")
            failure_logger.warning(f"{file.name} | Unexpected error: {error_msg}")
            consecutive_failures += 1
            logger.error(f"Unexpected error processing {file.name}: {e}")
            _cleanup_temp_files(temp_input, temp_output)
            
            if is_forced_cq or is_forced_bitrate:
                if consecutive_failures >= max_consecutive:
                    print(f"\n❌ {max_consecutive} consecutive failures. Stopping.")
                    break
            else:
                if consecutive_failures >= max_consecutive:
                    print(f"\n⚠️  {max_consecutive} consecutive failures. Moving to Phase 2.")
                    break
    
    # Phase 2: Retry failed CQ files with Bitrate mode (smart mode only)
    if is_smart_mode and failed_cq_files:
        print(f"\n{'='*60}")
        print(f"📋 PHASE 2: Retrying {len(failed_cq_files)} failed files with Bitrate mode")
        print(f"{'='*60}\n")
        
        consecutive_failures = 0
        
        for file_data in failed_cq_files:
            file = file_data['file']
            temp_input = file_data['temp_input']
            temp_output = file_data['temp_output']
            
            try:
                print(f"🔄 Retrying: {file.name} with Bitrate")
                logger.info(f"Phase 2 Retry: {file.name} with Bitrate mode")
                
                # Clean up old output if it exists
                if temp_output.exists():
                    temp_output.unlink()
                
                # Retry with bitrate
                orig_size, out_size, reduction_ratio = run_ffmpeg(
                    temp_input, temp_output, file_data['file_cq'], 
                    file_data['res_width'], file_data['res_height'],
                    file_data['src_width'], file_data['src_height'],
                    filter_flags, audio_config, "Bitrate", bitrate_config, file_data.get('encoder', encoder),
                    [file_data.get('subtitle_file')] if file_data.get('subtitle_file') else None, file_data.get('effective_audio_language'), None, test_mode, strip_all_titles,
                    file_data.get('src_bit_depth'), unforce_subs, no_encode, color_bit, crop_height, audio_titles, audio_channels
                )
                
                # Check if bitrate also failed
                # Skip size check if --no-encode is used (file size will be nearly identical)
                if not no_encode and reduction_ratio >= reduction_ratio_threshold:
                    print(f"⚠️  Bitrate also failed size target ({reduction_ratio:.1%}). Skipping.")
                    failure_logger.warning(f"{file.name} | Bitrate retry also failed ({reduction_ratio:.1%})")
                    consecutive_failures += 1
                    _cleanup_temp_files(temp_input, temp_output)
                    if consecutive_failures >= max_consecutive:
                        print(f"\n⚠️  {max_consecutive} consecutive Phase 2 failures. Stopping retries.")
                        break
                    continue
                
                # Bitrate succeeded
                consecutive_failures = 0
                _save_successful_encoding(
                    file, temp_input, temp_output,
                    orig_size, out_size, reduction_ratio, "Bitrate",
                    file_data['src_width'], file_data['src_height'],
                    file_data['res_width'], file_data['res_height'],
                    file_data['file_cq'], tracker_file, 
                    folder, file_data['is_tv'], suffix, config, False, 
                    file_data.get('subtitle_file'), travel_output_folder, replace_file, wait_seconds, combined_suffix
                )
                
            except subprocess.CalledProcessError as e:
                error_msg = str(e).split('\n')[0][:100]
                
                if test_mode:
                    # In test mode, stop immediately on any error and keep temp files
                    print(f"❌ Test mode: Bitrate retry failed. Stopping script.")
                    print(f"   Temp input preserved at: {temp_input}")
                    print(f"   Temp output preserved at: {temp_output}")
                    logger.error(f"Test mode: Bitrate retry failed for {file.name}: {error_msg}")
                    raise
                
                print(f"❌ Bitrate retry failed: {error_msg}")
                failure_logger.warning(f"{file.name} | Bitrate retry error: {error_msg}")
                consecutive_failures += 1
                logger.error(f"Bitrate retry failed for {file.name}: {e}")
                _cleanup_temp_files(temp_input, temp_output)
                
                if consecutive_failures >= max_consecutive:
                    print(f"\n⚠️  {max_consecutive} consecutive Phase 2 failures. Stopping retries.")
                    break
            except Exception as e:
                error_msg = str(e)[:100]
                
                if test_mode:
                    # In test mode, stop immediately on any error and keep temp files
                    print(f"❌ Test mode: Unexpected error in Phase 2. Stopping script.")
                    print(f"   Temp input preserved at: {temp_input}")
                    print(f"   Temp output preserved at: {temp_output}")
                    logger.error(f"Test mode: Phase 2 error for {file.name}: {error_msg}")
                    raise
                
                print(f"❌ Unexpected error in Phase 2: {error_msg}")
                failure_logger.warning(f"{file.name} | Phase 2 error: {error_msg}")
                consecutive_failures += 1
                _cleanup_temp_files(temp_input, temp_output)
                if consecutive_failures >= max_consecutive:
                    print(f"\n⚠️  {max_consecutive} consecutive Phase 2 failures. Stopping retries.")
                    break
    
    print(f"\n{'='*60}")
    print("✅ Batch processing complete")
    logger.info("Batch processing complete")


def _save_successful_encoding(file, temp_input, temp_output, orig_size, out_size, 
                              reduction_ratio, method, src_width, src_height, res_width, res_height,
                              file_cq, tracker_file, folder, is_tv, suffix, config=None, test_mode=False, subtitle_file=None, travel_output_folder=None, replace_file: bool = False, wait_seconds: int = 0, combined_suffix: str = None):
    """Helper function to save successfully encoded files with [EHX] tag and clean up subtitle files."""
    
    # In test mode, show ratio and skip file move/cleanup
    if test_mode:
        orig_size_mb = round(orig_size / 1e6, 2)
        out_size_mb = round(out_size / 1e6, 2)
        percentage = round(out_size_mb / orig_size_mb * 100, 1)
        
        print(f"\n{'='*60}")
        print(f"📊 TEST MODE RESULTS:")
        print(f"{'='*60}")
        print(f"Original:  {orig_size_mb} MB")
        print(f"Encoded:   {out_size_mb} MB")
        print(f"Ratio:     {percentage}% ({reduction_ratio:.1%} reduction)")
        print(f"Method:    {method} (CQ={file_cq if method == 'CQ' else 'N/A'})")
        print(f"{'='*60}")
        print(f"📁 Encoded file location: {temp_output}")
        logger.info(f"TEST MODE - File: {file.name} | Ratio: {percentage}% | Method: {method}")
        return
    
    # Check if file is in a Featurettes folder - if so, remove suffix from destination filename
    folder_parts = [p.lower() for p in file.parent.parts]
    is_featurette = "featurettes" in folder_parts
    
    # Use provided combined_suffix if available, otherwise use regular suffix
    effective_suffix = combined_suffix if combined_suffix else suffix
    
    if replace_file:
        # Use original filename (no suffix)
        dest_file = file.parent / file.name
    elif is_featurette:
        # Remove effective suffix from temp_output.name for Featurettes
        output_name = temp_output.name
        if effective_suffix in output_name:
            output_name = output_name.replace(effective_suffix, "")
        dest_file = file.parent / output_name
    else:
        dest_file = file.parent / temp_output.name
    
    # If travel mode is active, use travel output folder instead
    if travel_output_folder:
        # Preserve relative directory structure from input folder
        relative_path = file.parent.relative_to(folder)
        travel_dest_dir = travel_output_folder / relative_path
        travel_dest_dir.mkdir(parents=True, exist_ok=True)
        dest_file = travel_dest_dir / temp_output.name
        print(f"🧳 Travel mode: Moving to {dest_file}")
        logger.info(f"Travel mode destination: {dest_file}")
    
    shutil.move(temp_output, dest_file)
    print(f"🚚 Moved {temp_output.name} → {dest_file.name}")
    logger.info(f"Moved {temp_output.name} → {dest_file.name}")

    # Classify file type based on folder (folder_parts already defined earlier)
    if "tv" in folder_parts:
        f_type = "tv"
        tv_index = folder_parts.index("tv")
        show = folder.parts[tv_index + 1] if len(folder.parts) > tv_index + 1 else "Unknown"
    elif "anime" in folder_parts:
        f_type = "anime"
        anime_index = folder_parts.index("anime")
        show = folder.parts[anime_index + 1] if len(folder.parts) > anime_index + 1 else "Unknown"
    else:
        f_type = "movie"
        show = "N/A"

    orig_size_mb = round(orig_size / 1e6, 2)
    proc_size_mb = round(out_size / 1e6, 2)
    percentage = round(proc_size_mb / orig_size_mb * 100, 1)
    
    # Get audio stream count for tracking
    try:
        audio_streams = get_audio_streams(temp_input)
        audio_stream_count = len(audio_streams)
    except:
        audio_stream_count = 0
    
    # Format resolutions for tracking
    src_resolution = f"{src_width}x{src_height}"
    target_res = f"{res_width}x{res_height}"
    cq_str = str(file_cq) if method == "CQ" else "N/A"

    with open(tracker_file, "a", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow([
            f_type, show, dest_file.name, orig_size_mb, proc_size_mb, percentage, 
            src_resolution, target_res, audio_stream_count, cq_str, method
        ])

    # Enhanced logging with all conversion details
    logger.info(f"\n✅ CONVERSION COMPLETE: {dest_file.name}")
    logger.info(f"  Type: {f_type.upper()} | Show: {show}")
    logger.info(f"  Size: {orig_size_mb}MB → {proc_size_mb}MB ({percentage}% of original, {100-percentage:.1f}% reduction)")
    logger.info(f"  Method: {method} | Status: SUCCESS")
    print(f"📝 Logged conversion: {dest_file.name} ({percentage}%), method={method}")

    try:
        temp_input.unlink()
        
        # Keep original file if in travel mode, replace mode, or if in Featurettes folder
        if travel_output_folder:
            logger.info(f"Travel mode: Kept original file {file.name}")
        elif replace_file:
            logger.info(f"Replace mode: Original file has been replaced with processed version at {file.name}")
        elif not is_featurette:
            file.unlink()
            logger.info(f"Deleted original and processing copy for {file.name}")
        else:
            logger.info(f"Featurettes file preserved at origin: {file.name}")
        
        # Clean up subtitle file if it was embedded
        if subtitle_file and subtitle_file.exists():
            try:
                subtitle_file.unlink()
                print(f"🗑️  Removed embedded subtitle: {subtitle_file.name}")
                logger.info(f"Removed embedded subtitle: {subtitle_file.name}")
            except Exception as e:
                logger.warning(f"Could not delete subtitle file {subtitle_file.name}: {e}")
    except Exception as e:
        print(f"⚠️ Could not delete files: {e}")
        logger.warning(f"Could not delete files: {e}")
    
    # Wait if specified (for Plex detection)
    if wait_seconds > 0:
        import time
        print(f"⏱️  Waiting {wait_seconds}s for Plex to detect changes...")
        time.sleep(wait_seconds)
