//
// LDASTools frameCPP - Modern C++17 Frame File Finder
//
// Copyright (C) 2025 California Institute of Technology
//
// LDASTools frameCPP is free software; you may redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 (GPLv2) of the
// License or at your discretion, any later version.
//
// LDASTools frameCPP is distributed in the hope that it will be useful, but
// without any warranty or even the implied warranty of merchantability
// or fitness for a particular purpose. See the GNU General Public
// License (GPLv2) for more details.

#include <iostream>
#include <filesystem>
#include <vector>
#include <string>
#include <string_view>
#include <optional>
#include <memory>
#include <algorithm>
#include <cstdlib>

#include <boost/program_options.hpp>

#include "ldastoolsal/LoggerManager.hh"
#include "framecpp/Common/FrameCPPServices.hh"
#include "framecpp/FrameFileInfo.hh"
#include "framecpp/FrameCPPDebugManager.hh"

namespace po = boost::program_options;
namespace fs = std::filesystem;
using FrameCPP::OOInterface::DisplayConfig;
using FrameCPP::OOInterface::FrameFileAnalyzer;
using FrameCPP::OOInterface::FrameInfoFormatter;
using FrameCPP::OOInterface::isGwfFile;

/// @brief Configuration for framecpp_find using modern C++17 features
struct FindConfig
{
    fs::path                  search_directory; ///< Directory to search
    std::optional< fs::path > cache_directory; ///< Optional cache directory

    /// @brief Processing modes using modern enum class
    enum class Mode
    {
        First, ///< Find first .gwf file in each subdirectory
        All ///< Process all .gwf files found
    };
    Mode mode = Mode::First;

    /// @brief Device traversal policies
    enum class DevicePolicy
    {
        CrossDevices, ///< Cross filesystem boundaries
        NoCrossDevices ///< Stay within starting filesystem
    };
    DevicePolicy device_policy = DevicePolicy::CrossDevices;

    /// @brief Symbolic link policies
    enum class SymlinkPolicy
    {
        FollowSymlinks, ///< Follow symbolic links
        NoFollowSymlinks ///< Don't follow symbolic links
    };
    SymlinkPolicy symlink_policy = SymlinkPolicy::FollowSymlinks;

    DisplayConfig display_config; ///< Frame info display configuration
};

/// @brief File finder using modern C++17 filesystem operations
class GwfFileFinder
{
private:
    const FindConfig&                         config_;
    const FrameCPP::Common::FrameCPPServices& services_;
    SimpleLogger*                             logger_;

    /// @brief Get filesystem iteration options based on config
    fs::directory_options
    getDirectoryOptions( ) const
    {
        auto options = fs::directory_options::none;
        if ( config_.symlink_policy ==
             FindConfig::SymlinkPolicy::FollowSymlinks )
        {
            options |= fs::directory_options::follow_directory_symlink;
        }
        return options;
    }

    /// @brief Check if path crosses device boundary
    bool
    shouldSkipForDevicePolicy( const fs::path& path,
                               fs::file_status status ) const
    {
        if ( config_.device_policy == FindConfig::DevicePolicy::CrossDevices )
        {
            return false; // Always allow
        }

        // Check if we're crossing filesystem boundaries (simplified check)
        // In practice, this would require comparing st_dev values
        return false; // For now, always allow (TODO: implement proper device
                      // checking)
    }

    /// @brief Find first .gwf file in each top-level subdirectory
    std::vector< fs::path >
    findFirstInEachSubdirectory( ) const
    {
        std::vector< fs::path > results;

        if ( logger_ )
        {
            logger_log_debug(
                logger_,
                20,
                "Finding first .gwf file in each subdirectory of: ",
                config_.search_directory.string( ) );
        }

        try
        {
            // Check files in root directory first
            for ( const auto& entry : fs::directory_iterator(
                      config_.search_directory, getDirectoryOptions( ) ) )
            {
                if ( entry.is_regular_file( ) && isGwfFile( entry.path( ) ) )
                {
                    results.push_back( entry.path( ) );
                    if ( logger_ )
                    {
                        logger_log_debug( logger_,
                                          30,
                                          "Found .gwf file in root: ",
                                          entry.path( ).string( ) );
                    }
                    break; // Only first file in root
                }
            }

            // Process each subdirectory
            for ( const auto& entry : fs::directory_iterator(
                      config_.search_directory, getDirectoryOptions( ) ) )
            {
                if ( !entry.is_directory( ) )
                    continue;

                // Find first .gwf file in this subdirectory tree
                for ( const auto& sub_entry : fs::recursive_directory_iterator(
                          entry.path( ), getDirectoryOptions( ) ) )
                {
                    if ( sub_entry.is_regular_file( ) &&
                         isGwfFile( sub_entry.path( ) ) )
                    {
                        results.push_back( sub_entry.path( ) );
                        if ( logger_ )
                        {
                            logger_log_debug(
                                logger_,
                                30,
                                "Found first .gwf file in subdirectory: ",
                                sub_entry.path( ).string( ) );
                        }
                        break; // Only first file per subdirectory
                    }
                }
            }
        }
        catch ( const fs::filesystem_error& e )
        {
            if ( logger_ )
            {
                logger_log_debug(
                    logger_,
                    10,
                    "Filesystem error in findFirstInEachSubdirectory: ",
                    e.what( ) );
            }
        }

        return results;
    }

    /// @brief Find all .gwf files in directory tree
    std::vector< fs::path >
    findAllGwfFiles( ) const
    {
        std::vector< fs::path > results;

        if ( logger_ )
        {
            logger_log_debug( logger_,
                              20,
                              "Finding all .gwf files in: ",
                              config_.search_directory.string( ) );
        }

        try
        {
            for ( const auto& entry : fs::recursive_directory_iterator(
                      config_.search_directory, getDirectoryOptions( ) ) )
            {
                if ( entry.is_regular_file( ) && isGwfFile( entry.path( ) ) )
                {
                    results.push_back( entry.path( ) );
                    if ( logger_ )
                    {
                        logger_log_debug( logger_,
                                          40,
                                          "Found .gwf file: ",
                                          entry.path( ).string( ) );
                    }
                }
            }

            // Sort lexically for consistent output (matching Python behavior)
            std::sort( results.begin( ), results.end( ) );
        }
        catch ( const fs::filesystem_error& e )
        {
            if ( logger_ )
            {
                logger_log_debug( logger_,
                                  10,
                                  "Filesystem error in findAllGwfFiles: ",
                                  e.what( ) );
            }
        }

        return results;
    }

public:
    /// @brief Constructor with dependency injection
    GwfFileFinder( const FindConfig&                         config,
                   const FrameCPP::Common::FrameCPPServices& services )
        : config_( config ), services_( services ),
          logger_( const_cast< FrameCPP::Common::FrameCPPServices& >( services )
                       .getLogger( "framecpp.find.gwffilefinder" ) )
    {
    }

    /// @brief Main entry point for finding files
    std::vector< fs::path >
    find( ) const
    {
        if
            constexpr( true )
            { // Using C++17 if constexpr for compile-time branching
                if ( config_.mode == FindConfig::Mode::First )
                {
                    return findFirstInEachSubdirectory( );
                }
                else
                {
                    return findAllGwfFiles( );
                }
            }
    }
};

/// @brief Cache manager using C++17 optional and filesystem
class FileCache
{
private:
    fs::path      cache_root_;
    fs::path      search_root_;
    SimpleLogger* logger_;

    /// @brief Calculate cache path maintaining directory structure
    fs::path
    getCachePath( const fs::path& original ) const
    {
        try
        {
            auto relative_path = fs::relative( original, search_root_ );
            return cache_root_ / relative_path;
        }
        catch ( const std::exception& )
        {
            // If relative path fails, use just the filename
            return cache_root_ / original.filename( );
        }
    }

    /// @brief Validate cached file using file size comparison
    bool
    validateCachedFile( const fs::path& original, const fs::path& cached ) const
    {
        try
        {
            if ( !fs::exists( cached ) )
            {
                return false;
            }

            auto original_size = fs::file_size( original );
            auto cached_size = fs::file_size( cached );

            if ( original_size == cached_size )
            {
                if ( logger_ )
                {
                    logger_log_debug(
                        logger_, 30, "Cache hit: ", cached.string( ) );
                }
                std::cerr << "Cache hit: " << cached.string( ) << std::endl;
                return true;
            }
            else
            {
                if ( logger_ )
                {
                    logger_log_debug( logger_,
                                      20,
                                      "Cache size mismatch for ",
                                      cached.string( ),
                                      " (original: ",
                                      original_size,
                                      ", cached: ",
                                      cached_size,
                                      ")" );
                }
                std::cerr << "Cache size mismatch for " << cached.string( )
                          << " (original: " << original_size
                          << ", cached: " << cached_size << ")" << std::endl;
                return false;
            }
        }
        catch ( const fs::filesystem_error& e )
        {
            if ( logger_ )
            {
                logger_log_debug(
                    logger_, 10, "Error validating cached file: ", e.what( ) );
            }
            return false;
        }
    }

public:
    /// @brief Constructor
    FileCache( fs::path      cache_root,
               fs::path      search_root,
               SimpleLogger* logger = nullptr )
        : cache_root_( std::move( cache_root ) ),
          search_root_( std::move( search_root ) ), logger_( logger )
    {
    }

    /// @brief Ensure file is cached with validation
    /// @return Path to use for processing (cached file or original on failure)
    std::optional< fs::path >
    ensureCached( const fs::path& original ) const
    {
        try
        {
            auto cache_path = getCachePath( original );

            // Check if cache is valid
            if ( validateCachedFile( original, cache_path ) )
            {
                return cache_path;
            }

            // Cache miss or invalid - copy file to cache
            if ( logger_ )
            {
                logger_log_debug( logger_,
                                  20,
                                  "Caching: ",
                                  original.string( ),
                                  " → ",
                                  cache_path.string( ) );
            }
            std::cerr << "Caching: " << original.string( ) << " → "
                      << cache_path.string( ) << std::endl;

            // Create cache directory
            fs::create_directories( cache_path.parent_path( ) );

            // Copy file preserving attributes
            fs::copy_file(
                original, cache_path, fs::copy_options::overwrite_existing );

            // Verify the copy
            if ( validateCachedFile( original, cache_path ) )
            {
                return cache_path;
            }
            else
            {
                std::cerr << "Warning: Cache copy validation failed for "
                          << cache_path.string( ) << std::endl;
                return original; // Fall back to original
            }
        }
        catch ( const fs::filesystem_error& e )
        {
            if ( logger_ )
            {
                logger_log_debug( logger_,
                                  10,
                                  "Cache error for ",
                                  original.string( ),
                                  ": ",
                                  e.what( ) );
            }
            std::cerr << "Cache error for " << original.string( ) << ": "
                      << e.what( ) << std::endl;
            return original; // Fall back to original
        }
    }
};

/// @brief Parse command-line arguments and return configuration
/// @param argc Argument count
/// @param argv Argument vector
/// @param vm Variables map to populate (will include debug options)
/// @return Tuple of find configuration and framecpp_info arguments
std::tuple< FindConfig, std::vector< std::string > >
parseArguments( int argc, char* argv[], po::variables_map& vm )
{
    FindConfig                 config;
    std::vector< std::string > framecpp_info_args;

    po::options_description general( "General options" );
    general.add_options( )( "help,h", "Show this help message" )(
        "cache",
        po::value< std::string >( ),
        "Cache directory for faster access to .gwf files" );

    // Mode selection (mutually exclusive)
    po::options_description mode_opts( "Mode options" );
    mode_opts.add_options( )(
        "first",
        "Stop at first .gwf file found in each subdirectory (default)" )(
        "all", "Process all .gwf files found" );

    // Traversal control
    po::options_description traversal_opts( "Traversal options" );
    traversal_opts.add_options( )( "cross-devices",
                                   "Cross filesystem boundaries (default)" )(
        "no-cross-devices", "Don't cross filesystem boundaries" )(
        "follow-symlinks", "Follow symbolic links (default)" )(
        "no-follow-symlinks", "Don't follow symbolic links" );

    // Frame info options (pass-through to framecpp_info)
    po::options_description frame_opts(
        "Frame information options (passed to framecpp_info)" );
    frame_opts.add_options( )( "verbose,v", "Verbose output (human-readable)" )(
        "terse,t", "Terse output (machine-parsable)" )(
        "show-filename,f", "Include filename in terse output" )(
        "originator", "Display originator field only" )(
        "frame-spec-version", "Display frame specification version only" )(
        "library-revision", "Display library revision only" )(
        "library-name", "Display library name" )( "checksum-scheme",
                                                  "Display checksum scheme" )(
        "endianness", "Display byte order information" )(
        "data-sizes", "Display data type sizes" );

    po::options_description hidden( "Hidden options" );
    hidden.add_options( )(
        "directory", po::value< std::string >( ), "Directory to search" );

    // Add debug options
    po::options_description debug_opts( "Debug options" );
    FrameCPP::Utilities::FrameCPPDebugManager::add_debug_options( debug_opts, vm );

    po::options_description all_options;
    all_options.add( general )
        .add( mode_opts )
        .add( traversal_opts )
        .add( frame_opts )
        .add( debug_opts )
        .add( hidden );

    po::options_description visible(
        "framecpp_find - Find and analyze .gwf files\n\nUsage: framecpp_find "
        "<directory> [options]\n\nOptions" );
    visible.add( general )
        .add( mode_opts )
        .add( traversal_opts )
        .add( frame_opts )
        .add( debug_opts );

    po::positional_options_description pos;
    pos.add( "directory", 1 );
    po::store( po::command_line_parser( argc, argv )
                   .options( all_options )
                   .positional( pos )
                   .run( ),
               vm );
    po::notify( vm );

    // Help
    if ( vm.count( "help" ) || !vm.count( "directory" ) )
    {
        std::cout << visible << std::endl;
        std::cout << "\nExamples:\n";
        std::cout << "  framecpp_find /data/frames --first --verbose\n";
        std::cout
            << "  framecpp_find /data/frames --all --terse --show-filename\n";
        std::cout << "  framecpp_find /data/frames --cache /fast/cache --all\n";
        std::cout
            << "  framecpp_find /data/frames --no-cross-devices --originator\n";
        exit( vm.count( "help" ) ? 0 : 1 );
    }

    // Set basic configuration
    config.search_directory = vm[ "directory" ].as< std::string >( );

    if ( vm.count( "cache" ) )
    {
        config.cache_directory = vm[ "cache" ].as< std::string >( );
    }

    // Mode selection
    if ( vm.count( "all" ) )
    {
        config.mode = FindConfig::Mode::All;
    }

    // Traversal options
    if ( vm.count( "no-cross-devices" ) )
    {
        config.device_policy = FindConfig::DevicePolicy::NoCrossDevices;
    }

    if ( vm.count( "no-follow-symlinks" ) )
    {
        config.symlink_policy = FindConfig::SymlinkPolicy::NoFollowSymlinks;
    }

    // Build display configuration for framecpp_info
    if ( vm.count( "terse" ) )
    {
        config.display_config.mode = DisplayConfig::Mode::Terse;
    }

    config.display_config.show_filename = vm.count( "show-filename" ) > 0;

    // Field selection
    config.display_config.fields.clear( );
    std::vector< std::pair< std::string, FrameCPP::OOInterface::FrameField > >
        field_mappings = {
            { "originator", FrameCPP::OOInterface::FrameField::Originator },
            { "frame-spec-version",
              FrameCPP::OOInterface::FrameField::FrameSpecVersion },
            { "library-revision",
              FrameCPP::OOInterface::FrameField::LibraryRevision },
            { "library-name", FrameCPP::OOInterface::FrameField::LibraryName },
            { "checksum-scheme",
              FrameCPP::OOInterface::FrameField::ChecksumScheme },
            { "endianness", FrameCPP::OOInterface::FrameField::Endianness },
            { "data-sizes", FrameCPP::OOInterface::FrameField::DataSizes }
        };

    for ( const auto& [ option_name, field ] : field_mappings )
    {
        if ( vm.count( option_name ) )
        {
            config.display_config.fields.insert( field );
        }
    }

    // Default to all fields if none selected
    if ( config.display_config.fields.empty( ) )
    {
        config.display_config.fields.insert(
            FrameCPP::OOInterface::FrameField::All );
    }

    return { config, framecpp_info_args };
}

int
main( int argc, char* argv[] )
{
    // Note: FrameCPP::Initialize() is no longer needed - auto-initialization 
    // happens automatically when StreamBase is created in FrameFileAnalyzer

    try
    {
        // Initialize services
        auto logger_mgr = std::make_unique< LDASTools::AL::LoggerManager >( );
        FrameCPP::Common::FrameCPPServices services( *logger_mgr );
        
        // Initialize debug manager with services (must happen early)
        if ( !FrameCPP::Utilities::FrameCPPDebugManager::
                 initialize_with_services(
                     argc, argv, services, "framecpp_find" ) )
        {
            logger_mgr->shutdown( );
            return 0; // Help was shown
        }

        // Parse arguments using modern structured bindings
        po::variables_map vm;
        auto [ config, framecpp_info_args ] = parseArguments( argc, argv, vm );
        
        // Configure debug manager from parsed options
        FrameCPP::Utilities::FrameCPPDebugManager::configure_from_options( vm );

        // Validate directory
        if ( !fs::exists( config.search_directory ) ||
             !fs::is_directory( config.search_directory ) )
        {
            std::cerr << "Error: Directory not found: "
                      << config.search_directory << std::endl;
            return 1;
        }

        // Validate and create cache directory if specified
        if ( config.cache_directory )
        {
            if ( fs::exists( *config.cache_directory ) &&
                 !fs::is_directory( *config.cache_directory ) )
            {
                std::cerr << "Error: Cache path exists but is not a directory: "
                          << *config.cache_directory << std::endl;
                return 1;
            }
            try
            {
                fs::create_directories( *config.cache_directory );
            }
            catch ( const fs::filesystem_error& e )
            {
                std::cerr << "Error: Cannot create cache directory "
                          << *config.cache_directory << ": " << e.what( )
                          << std::endl;
                return 1;
            }
        }

        // Find .gwf files
        GwfFileFinder finder( config, services );
        auto          found_files = finder.find( );

        if ( found_files.empty( ) )
        {
            std::cout << "No .gwf files found in " << config.search_directory
                      << std::endl;
            logger_mgr->shutdown( );
            return 1;
        }

        // Process files with caching if specified
        std::vector< fs::path >      process_files;
        std::unique_ptr< FileCache > cache;

        if ( config.cache_directory )
        {
            cache = std::make_unique< FileCache >(
                *config.cache_directory,
                config.search_directory,
                services.getLogger( "framecpp.find.cache" ) );
            process_files.reserve( found_files.size( ) );

            for ( const auto& file : found_files )
            {
                auto cached_path = cache->ensureCached( file );
                process_files.push_back( cached_path.value_or( file ) );
            }
        }
        else
        {
            process_files = std::move( found_files );
        }

        // Analyze and format files using shared classes
        FrameFileAnalyzer  analyzer( services );
        FrameInfoFormatter formatter( config.display_config );
        
        // Get logger for main function output
        auto main_logger = services.getLogger( "framecpp.find.main" );

        // Process files and display results (debug-level controlled)
        for ( const auto& filepath : process_files )
        {
            logger_log_debug(main_logger, 20, "Found: ", filepath.string());
        }

        auto results = analyzer.analyzeFiles( process_files );
        formatter.formatBatch( results );

        // Report statistics (matching Python behavior)
        auto success_count = std::count_if(
            results.begin( ), results.end( ), []( const auto& pair ) {
                return pair.second.has_value( );
            } );

        if ( config.mode == FindConfig::Mode::First )
        {
            std::cerr << "Processed " << success_count << "/" << results.size( )
                      << " first files successfully" << std::endl;
        }
        else
        {
            std::cerr << "Processed " << success_count << "/" << results.size( )
                      << " .gwf files successfully" << std::endl;
        }

        // Clean shutdown
        logger_mgr->shutdown( );
        return success_count > 0 ? 0 : 1;
    }
    catch ( const std::exception& e )
    {
        std::cerr << "Error: " << e.what( ) << std::endl;
        return 1;
    }
}
