#!/usr/bin/python3
"""
framecpp_find - Find and process .gwf files using framecpp_info

This utility walks directory trees to find .gwf files and processes them
using framecpp_info with passed-through command line options.

Usage:
    framecpp_find <directory> [options] [framecpp_info_options...]
    
Modes:
    --first (default): Stop at first .gwf file found
    --all: Process all .gwf files found
    
Traversal Control:
    --cross-devices (default): Cross filesystem boundaries
    --no-cross-devices: Don't cross filesystem boundaries
    --follow-symlinks (default): Follow symbolic links
    --no-follow-symlinks: Don't follow symbolic links

Finding framecpp_info:
    The script searches for framecpp_info in this order:
    1. FRAMECPP_BUILD_DIR environment variable (if set)
    2. Current working directory
    3. PATH environment variable
    4. Same directory as this script
    
    For development, you can set: export FRAMECPP_BUILD_DIR=/path/to/build
"""

import os
import sys
import argparse
import subprocess
import shutil
from pathlib import Path


def create_arg_parser():
    """Create argument parser with mutually exclusive groups and proper defaults."""
    parser = argparse.ArgumentParser(
        description="Find .gwf files and process them with framecpp_info",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
    framecpp_find /data/frames --first --verbose
    framecpp_find /data/frames --all --terse --show-filename
    framecpp_find /slow/tape/archive --cache /fast/cache --all --verbose
    framecpp_find /data/frames --no-cross-devices --originator
    framecpp_find /data/frames --no-follow-symlinks --endianness
        """
    )
    
    # Required positional argument
    parser.add_argument('directory', 
                       help='Directory to search for .gwf files')
    
    # Cache option
    parser.add_argument('--cache', 
                       type=str,
                       help='Cache directory for faster access to .gwf files')
    
    # Mode selection (mutually exclusive)
    mode_group = parser.add_mutually_exclusive_group()
    mode_group.add_argument('--first', 
                           action='store_true', 
                           default=True,
                           help='Stop at first .gwf file found (default)')
    mode_group.add_argument('--all', 
                           action='store_true',
                           help='Process all .gwf files found')
    
    # Device traversal (mutually exclusive)
    device_group = parser.add_mutually_exclusive_group()
    device_group.add_argument('--cross-devices', 
                             action='store_true', 
                             default=True,
                             help='Cross filesystem boundaries (default)')
    device_group.add_argument('--no-cross-devices', 
                             action='store_true',
                             help='Don\'t cross filesystem boundaries')
    
    # Symlink following (mutually exclusive)
    symlink_group = parser.add_mutually_exclusive_group()
    symlink_group.add_argument('--follow-symlinks', 
                              action='store_true', 
                              default=True,
                              help='Follow symbolic links (default)')
    symlink_group.add_argument('--no-follow-symlinks', 
                              action='store_true',
                              help='Don\'t follow symbolic links')
    
    return parser


def find_framecpp_info():
    """Find framecpp_info executable using flexible search strategy for arbitrary build locations."""
    
    # 1. Check environment variable (if set by user/build system)
    if 'FRAMECPP_BUILD_DIR' in os.environ:
        build_dir = Path(os.environ['FRAMECPP_BUILD_DIR'])
        potential_paths = [
            build_dir / 'src' / 'Utilities' / 'framecpp_info',  # Common CMake layout
            build_dir / 'framecpp_info',                        # Direct in build dir
        ]
        for framecpp_info_path in potential_paths:
            if framecpp_info_path.is_file() and os.access(framecpp_info_path, os.X_OK):
                return str(framecpp_info_path)
    
    # 2. Check current working directory (if run from build directory)
    cwd = Path.cwd()
    framecpp_info_path = cwd / 'framecpp_info'
    if framecpp_info_path.is_file() and os.access(framecpp_info_path, os.X_OK):
        return str(framecpp_info_path)
    
    # 3. Check PATH (most reliable for installed or development with PATH setup)
    for path_dir in os.environ.get('PATH', '').split(os.pathsep):
        if not path_dir.strip():  # Skip empty path entries
            continue
        framecpp_info_path = Path(path_dir) / 'framecpp_info'
        if framecpp_info_path.is_file() and os.access(framecpp_info_path, os.X_OK):
            return str(framecpp_info_path)
    
    # 4. Check same directory as script (fallback for when script is copied to build location)
    script_dir = Path(__file__).resolve().parent
    framecpp_info_path = script_dir / 'framecpp_info'
    if framecpp_info_path.is_file() and os.access(framecpp_info_path, os.X_OK):
        return str(framecpp_info_path)
    
    # 5. Default fallback - let subprocess handle the error with useful message
    return 'framecpp_info'


def get_cache_path(original_path, search_directory, cache_directory):
    """Calculate cache file path maintaining relative directory structure."""
    try:
        original_path = Path(original_path).resolve()
        search_directory = Path(search_directory).resolve()
        cache_directory = Path(cache_directory)
        
        # Get relative path from search directory to the file
        relative_path = original_path.relative_to(search_directory)
        
        # Create cache path maintaining directory structure
        cache_path = cache_directory / relative_path
        
        return cache_path
    except ValueError:
        # If file is not under search directory, use just the filename
        return cache_directory / Path(original_path).name


def ensure_cached_file(original_path, cache_path):
    """Ensure file is cached with size validation. Returns path to use for processing."""
    try:
        # Get original file size
        original_stat = original_path.stat()
        original_size = original_stat.st_size
        
        # Check if cache file exists and has same size
        if cache_path.exists():
            cache_stat = cache_path.stat()
            if cache_stat.st_size == original_size:
                print(f"Cache hit: {cache_path}", file=sys.stderr)
                return cache_path
            else:
                print(f"Cache size mismatch for {cache_path} (original: {original_size}, cached: {cache_stat.st_size})", 
                      file=sys.stderr)
        
        # Cache miss or size mismatch - copy file to cache
        print(f"Caching: {original_path} → {cache_path}", file=sys.stderr)
        
        # Create cache directory if needed
        cache_path.parent.mkdir(parents=True, exist_ok=True)
        
        # Copy file preserving timestamps
        shutil.copy2(str(original_path), str(cache_path))
        
        # Verify the copy
        cache_stat = cache_path.stat()
        if cache_stat.st_size != original_size:
            print(f"Warning: Cache copy size mismatch for {cache_path}", file=sys.stderr)
            return original_path  # Fall back to original
        
        return cache_path
        
    except (OSError, IOError) as e:
        print(f"Cache error for {original_path}: {e}", file=sys.stderr)
        return original_path  # Fall back to original


def process_gwf_file(filepath, framecpp_info_cmd, framecpp_info_args, cache_directory=None, search_directory=None):
    """Process a single .gwf file with framecpp_info, using cache if available."""
    try:
        # Determine which file to process (cached or original)
        process_path = filepath
        
        if cache_directory:
            cache_path = get_cache_path(filepath, search_directory, cache_directory)
            process_path = ensure_cached_file(Path(filepath), cache_path)
        
        # Run framecpp_info on the selected file
        cmd = [framecpp_info_cmd] + framecpp_info_args + [str(process_path)]
        result = subprocess.run(cmd, check=True)
        return True
        
    except subprocess.CalledProcessError as e:
        print(f"Error processing {filepath}: framecpp_info returned {e.returncode}", 
              file=sys.stderr)
        return False
    except Exception as e:
        print(f"Error processing {filepath}: {e}", file=sys.stderr)
        return False


def find_first_in_subtree(root_dir, cross_devices, follow_symlinks, start_device):
    """Find the first .gwf file in a subtree. Returns (found_file_path, success) or (None, False)."""
    try:
        for root, dirs, files in os.walk(root_dir, topdown=True, followlinks=follow_symlinks):
            # Check device boundary if --no-cross-devices specified
            if not cross_devices:
                try:
                    current_device = os.stat(root).st_dev
                    if current_device != start_device:
                        dirs[:] = []  # Don't recurse into subdirectories
                        continue
                except (OSError, IOError):
                    # Skip directories we can't stat
                    dirs[:] = []
                    continue
            
            # Process files in current directory (lexically sorted)
            for filename in sorted(files):
                if filename.endswith('.gwf'):
                    filepath = Path(root) / filename
                    return filepath, True
        
        return None, False
        
    except Exception as e:
        print(f"Error walking subtree {root_dir}: {e}", file=sys.stderr)
        return None, False


def find_and_process_first(directory, cross_devices, follow_symlinks, 
                          framecpp_info_cmd, framecpp_info_args, cache_directory=None):
    """Find and process the first .gwf file in each top-level subdirectory."""
    try:
        start_device = os.stat(directory).st_dev if not cross_devices else None
        success_count = 0
        total_branches = 0
        
        # Get list of top-level subdirectories and process them independently
        try:
            items = sorted(os.listdir(directory))
        except (OSError, IOError) as e:
            print(f"Error listing directory {directory}: {e}", file=sys.stderr)
            return False
        
        # First check files in the root directory itself
        root_files_found = False
        for item in items:
            item_path = Path(directory) / item
            if item_path.is_file() and item.endswith('.gwf'):
                if not root_files_found:  # Only process first file in root
                    print(f"Found: {item_path}")
                    if process_gwf_file(item_path, framecpp_info_cmd, framecpp_info_args, 
                                      cache_directory, directory):
                        success_count += 1
                    total_branches += 1
                    root_files_found = True
                    break  # Only process first .gwf file in root directory
        
        # Then process each subdirectory independently - find first .gwf in each
        for item in items:
            item_path = Path(directory) / item
            if item_path.is_dir():
                # Check device boundary for the subdirectory
                if not cross_devices:
                    try:
                        item_device = os.stat(item_path).st_dev
                        if item_device != start_device:
                            continue  # Skip this subdirectory
                    except (OSError, IOError):
                        continue  # Skip directories we can't stat
                
                # Search this subtree for the first .gwf file
                found_file, file_found = find_first_in_subtree(item_path, cross_devices, follow_symlinks, start_device)
                if found_file:
                    print(f"Found: {found_file}")
                    total_branches += 1
                    if process_gwf_file(found_file, framecpp_info_cmd, framecpp_info_args, 
                                      cache_directory, directory):
                        success_count += 1
        
        if total_branches == 0:
            print(f"No .gwf files found in {directory}")
            return False
        else:
            print(f"Processed {success_count}/{total_branches} first files successfully", file=sys.stderr)
            return success_count > 0
        
    except Exception as e:
        print(f"Error walking directory {directory}: {e}", file=sys.stderr)
        return False


def find_and_process_all(directory, cross_devices, follow_symlinks,
                        framecpp_info_cmd, framecpp_info_args, cache_directory=None):
    """Find and process all .gwf files using directory-level processing algorithm."""
    try:
        start_device = os.stat(directory).st_dev if not cross_devices else None
        success_count = 0
        total_count = 0
        
        for root, dirs, files in os.walk(directory, followlinks=follow_symlinks):
            # Check device boundary if --no-cross-devices specified
            if not cross_devices:
                try:
                    current_device = os.stat(root).st_dev
                    if current_device != start_device:
                        dirs[:] = []  # Don't recurse into subdirectories
                        continue
                except (OSError, IOError):
                    # Skip directories we can't stat
                    dirs[:] = []
                    continue
            
            # Find all .gwf files in current directory
            gwf_files = [f for f in files if f.endswith('.gwf')]
            
            if gwf_files:
                # Sort lexically within this directory
                gwf_files.sort()
                
                # Process files in this directory
                for filename in gwf_files:
                    filepath = Path(root) / filename
                    total_count += 1
                    
                    if process_gwf_file(filepath, framecpp_info_cmd, framecpp_info_args,
                                      cache_directory, directory):
                        success_count += 1
        
        if total_count == 0:
            print(f"No .gwf files found in {directory}")
            return False
        else:
            print(f"Processed {success_count}/{total_count} .gwf files successfully", 
                  file=sys.stderr)
            return success_count > 0
        
    except Exception as e:
        print(f"Error walking directory {directory}: {e}", file=sys.stderr)
        return False


def main():
    """Main entry point."""
    # Parse known arguments (ours) and unknown arguments (for framecpp_info)
    parser = create_arg_parser()
    args, unknown_args = parser.parse_known_args()
    
    # Validate directory
    if not os.path.isdir(args.directory):
        print(f"Error: Directory not found: {args.directory}", file=sys.stderr)
        return 1
    
    # Validate cache directory if specified
    if args.cache:
        cache_dir = Path(args.cache)
        if cache_dir.exists() and not cache_dir.is_dir():
            print(f"Error: Cache path exists but is not a directory: {args.cache}", file=sys.stderr)
            return 1
        # Create cache directory if it doesn't exist
        try:
            cache_dir.mkdir(parents=True, exist_ok=True)
        except (OSError, IOError) as e:
            print(f"Error: Cannot create cache directory {args.cache}: {e}", file=sys.stderr)
            return 1
    
    # Find framecpp_info executable
    framecpp_info_cmd = find_framecpp_info()
    
    # Determine traversal settings based on arguments
    cross_devices = not args.no_cross_devices
    follow_symlinks = not args.no_follow_symlinks
    
    # Choose processing mode
    if args.all:
        success = find_and_process_all(
            args.directory, cross_devices, follow_symlinks,
            framecpp_info_cmd, unknown_args, args.cache
        )
    else:  # --first mode (default)
        success = find_and_process_first(
            args.directory, cross_devices, follow_symlinks,
            framecpp_info_cmd, unknown_args, args.cache
        )
    
    return 0 if success else 1


if __name__ == '__main__':
    sys.exit(main())