mod cache;
mod cli;
mod config;
mod debug;
mod generate;
mod init;
mod languages;
mod licenses;
mod manifest;
mod parser;
mod reporter;
mod sbom;
mod source_scan;
mod spdx;
mod table;
mod utils;
mod vendor_scan;
mod watch;

use clap::Parser;
use cli::{print_version_info, Cli, Commands};
use debug::{log, log_debug, set_debug_mode, FeludaError, FeludaResult, LogLevel};
use generate::handle_generate_command;
use init::handle_init_command;
use licenses::{
    detect_project_license, is_license_compatible, set_github_token, LicenseCompatibility,
    LicenseInfo,
};
use parser::parse_root;
use reporter::{generate_report, ReportConfig};
use sbom::handle_sbom_command;
use sbom::validate::handle_sbom_validate_command;
use std::env;
use std::path::Path;
use std::process;
use table::App;
use tempfile::TempDir;
use utils::clone_repository;

/// Configuration for the check command
#[derive(Debug)]
struct CheckConfig {
    path: String,
    json: bool,
    yaml: bool,
    verbose: bool,
    restrictive: bool,
    gui: bool,
    language: Option<String>,
    ci_format: Option<cli::CiFormat>,
    output_file: Option<String>,
    fail_on_restrictive: bool,
    incompatible: bool,
    fail_on_incompatible: bool,
    project_license: Option<String>,
    gist: bool,
    osi: Option<cli::OsiFilter>,
    strict: bool,
    no_local: bool,
    no_vendor_scan: bool,
}

fn main() {
    // Check if --version or -V is passed alone
    let args: Vec<String> = env::args().collect();
    if args.len() == 2 && (args[1] == "--version" || args[1] == "-V") {
        print_version_info();
        return;
    }

    match run() {
        Ok(_) => {}
        Err(e) => {
            e.log();
            process::exit(1);
        }
    }
}

fn run() -> FeludaResult<()> {
    let args = Cli::parse();

    // Debug mode
    if args.debug {
        set_debug_mode(true);
        log(
            LogLevel::Info,
            &format!("Starting Feluda with args: {args:?}"),
        );
    }

    // Set GitHub API token for authenticated requests
    set_github_token(args.github_token.clone());

    // Handle repository cloning if --repo is provided
    let (analysis_path, _temp_dir) = match &args.repo.clone() {
        Some(repo_url) => {
            log(
                LogLevel::Info,
                &format!("Attempting to clone repository: {repo_url}"),
            );
            let temp_dir = TempDir::new().map_err(|e| {
                FeludaError::TempDir(format!("Failed to create temporary directory: {e}"))
            })?;
            let repo_path = temp_dir.path();

            // Clone the repository
            if let Err(e) = clone_repository(&args, repo_path) {
                log(LogLevel::Error, &format!("Repository cloning failed: {e}"));
                return Err(e);
            }
            log(
                LogLevel::Info,
                &format!("Repository cloned to: {}", repo_path.display()),
            );
            (repo_path.to_path_buf(), Some(temp_dir))
        }
        None => {
            let path = Path::new(&args.path).to_path_buf();
            log(
                LogLevel::Info,
                &format!("Using local path for analysis: {}", path.display()),
            );
            (path, None)
        }
    };

    log(
        LogLevel::Info,
        &format!("Analysing project at: {}", analysis_path.display()),
    );

    // Handle the command based on whether a subcommand was provided
    if args.is_default_command() {
        // Default behavior: license analysis
        let config = CheckConfig {
            path: analysis_path.to_string_lossy().to_string(),
            json: args.json,
            yaml: args.yaml,
            verbose: args.verbose,
            restrictive: args.restrictive,
            gui: args.gui,
            language: args.language,
            ci_format: args.ci_format,
            output_file: args.output_file,
            fail_on_restrictive: args.fail_on_restrictive,
            incompatible: args.incompatible,
            fail_on_incompatible: args.fail_on_incompatible,
            project_license: args.project_license,
            gist: args.gist,
            osi: args.osi,
            strict: args.strict,
            no_local: args.no_local,
            no_vendor_scan: args.no_vendor_scan,
        };
        handle_check_command(config)
    } else {
        // Handle subcommands
        let command = args.get_command_args();
        match command {
            Commands::Generate {
                path,
                language,
                project_license,
            } => {
                handle_generate_command(path, language, project_license);
                Ok(())
            }
            Commands::Sbom {
                path,
                format,
                output,
            } => {
                // Determine which format to use
                match format {
                    Some(cli::SbomCommand::Spdx {
                        path: fmt_path,
                        output: fmt_output,
                    }) => {
                        // Use the subcommand path/output if provided, otherwise use the parent command's
                        let final_path = if fmt_path != "./" {
                            fmt_path
                        } else {
                            path.clone()
                        };
                        let final_output = fmt_output.or(output.clone());
                        handle_sbom_command(final_path, &cli::SbomFormat::Spdx, final_output)
                    }
                    Some(cli::SbomCommand::Cyclonedx {
                        path: fmt_path,
                        output: fmt_output,
                    }) => {
                        let final_path = if fmt_path != "./" {
                            fmt_path
                        } else {
                            path.clone()
                        };
                        let final_output = fmt_output.or(output.clone());
                        handle_sbom_command(final_path, &cli::SbomFormat::Cyclonedx, final_output)
                    }
                    Some(cli::SbomCommand::Validate {
                        sbom_file,
                        output: validation_output,
                        json,
                    }) => handle_sbom_validate_command(sbom_file, validation_output, json),
                    None => {
                        // Default: generate both formats
                        handle_sbom_command(path, &cli::SbomFormat::All, output)
                    }
                }
            }
            Commands::Cache { clear } => {
                handle_cache_command(clear)?;
                Ok(())
            }
            Commands::Init {
                path,
                force,
                no_pre_commit,
            } => {
                handle_init_command(path, force, no_pre_commit);
                Ok(())
            }
            Commands::Watch { path, debounce } => {
                if args.gui {
                    eprintln!(
                        "❌ Watch mode does not support --gui (TUI) output. \
                        Run `feluda watch` without --gui."
                    );
                    return Err(FeludaError::InvalidData(
                        "Watch mode does not support --gui (TUI) output".to_string(),
                    ));
                }
                if args.repo.is_some() {
                    eprintln!("❌ Watch mode operates on a local path; --repo is not supported.");
                    return Err(FeludaError::InvalidData(
                        "Watch mode operates on a local path; --repo is not supported".to_string(),
                    ));
                }

                let config = CheckConfig {
                    path,
                    json: args.json,
                    yaml: args.yaml,
                    verbose: args.verbose,
                    restrictive: args.restrictive,
                    gui: false,
                    language: args.language.clone(),
                    ci_format: args.ci_format.clone(),
                    output_file: args.output_file.clone(),
                    fail_on_restrictive: false,
                    incompatible: args.incompatible,
                    fail_on_incompatible: false,
                    project_license: args.project_license.clone(),
                    gist: args.gist,
                    osi: args.osi.clone(),
                    strict: args.strict,
                    no_local: args.no_local,
                    no_vendor_scan: args.no_vendor_scan,
                };
                watch::handle_watch_command(config, debounce)
            }
        }
    }
}

/// Outcome of a single license analysis run.
///
/// Returned by [`report_analysis`] so callers (single-shot or watch) can decide
/// what to do — e.g. set an exit code — without the analysis itself terminating
/// the process.
#[derive(Debug, Default, Clone, Copy)]
struct ScanSummary {
    has_restrictive: bool,
    has_incompatible: bool,
}

/// Detect the project license and parse + analyze dependencies.
///
/// This is the shared front half of the check pipeline, reused by both the
/// single-shot command and `feluda watch`. It performs no terminal I/O beyond
/// logging and never exits the process.
fn analyze_dependencies(config: &CheckConfig) -> FeludaResult<(Vec<LicenseInfo>, Option<String>)> {
    log(
        LogLevel::Info,
        &format!("Executing check command with path: {}", config.path),
    );

    // Parse project dependencies
    log(
        LogLevel::Info,
        &format!("Parsing dependencies in path: {}", config.path),
    );

    let mut project_license = config.project_license.clone();

    // If no project license is provided via CLI, try to detect it
    if let Some(ref license) = project_license {
        log(
            LogLevel::Info,
            &format!("Using provided project license: {}", *license),
        );
    } else {
        log(
            LogLevel::Info,
            "No project license specified, attempting to detect",
        );
        match detect_project_license(&config.path) {
            Ok(Some(detected)) => {
                log(
                    LogLevel::Info,
                    &format!("Detected project license: {detected}"),
                );
                project_license = Some(detected);
            }
            Ok(None) => {
                log(LogLevel::Warn, "Could not detect project license");
            }
            Err(e) => {
                log(
                    LogLevel::Error,
                    &format!("Error detecting project license: {e}"),
                );
            }
        }
    }

    // Parse and analyze dependencies
    let mut analyzed_data = parse_root(
        &config.path,
        config.language.as_deref(),
        config.strict,
        config.no_local,
    )
    .map_err(|e| FeludaError::Parser(format!("Failed to parse dependencies: {e}")))?;

    log_debug("Analyzed dependencies", &analyzed_data);

    // Own-source header scan: flag project source files whose leading comments declare a
    // license different from the project's (code pasted in by AI tools or copied from other
    // projects without a manifest entry).
    let own_source_findings = cli::with_spinner("🔎: own source license headers", |indicator| {
        let findings = source_scan::scan_own_source_headers(
            Path::new(&config.path),
            project_license.as_deref(),
            config.strict,
        );
        indicator.update_progress(&format!(
            "{} finding{}",
            findings.len(),
            if findings.len() == 1 { "" } else { "s" }
        ));
        findings
    });
    analyzed_data.extend(own_source_findings);

    // Vendored/unmanaged scan: flag directories holding code no manifest records — libraries
    // copied into `vendor/`/`third_party/`, plus stray licensed directories elsewhere in the
    // tree. This walks the whole tree, so `--no-vendor-scan` opts large repos out.
    if config.no_vendor_scan {
        log(
            LogLevel::Info,
            "Skipping vendored/unmanaged dependency scan (--no-vendor-scan)",
        );
    } else {
        let known_names: Vec<String> = analyzed_data.iter().map(|info| info.name.clone()).collect();
        let vendored_findings = cli::with_spinner("📦: vendored dependencies", |indicator| {
            let findings = vendor_scan::scan_vendored_packages(
                Path::new(&config.path),
                &known_names,
                project_license.as_deref(),
                config.strict,
            );
            indicator.update_progress(&format!(
                "{} finding{}",
                findings.len(),
                if findings.len() == 1 { "" } else { "s" }
            ));
            findings
        });
        analyzed_data.extend(vendored_findings);
    }

    Ok((analyzed_data, project_license))
}

/// Annotate each dependency with license-compatibility information relative to
/// the project license. Mutates `analyzed_data` in place.
fn annotate_compatibility(
    analyzed_data: &mut [LicenseInfo],
    project_license: &Option<String>,
    strict: bool,
) {
    // Update each dependency with compatibility information if project license is known
    if let Some(proj_license) = project_license {
        log(
            LogLevel::Info,
            &format!("Checking license compatibility against project license: {proj_license}"),
        );

        for info in analyzed_data.iter_mut() {
            if let Some(ref dep_license) = info.license {
                info.compatibility = is_license_compatible(dep_license, proj_license, strict);

                log(
                    LogLevel::Info,
                    &format!(
                        "License compatibility for {} ({}): {:?}",
                        info.name, dep_license, info.compatibility
                    ),
                );
            } else {
                info.compatibility = if strict {
                    LicenseCompatibility::Incompatible
                } else {
                    LicenseCompatibility::Unknown
                };

                log(
                    LogLevel::Info,
                    &format!(
                        "License compatibility for {} {} (no license info)",
                        info.name,
                        if strict { "incompatible" } else { "unknown" }
                    ),
                );
            }
        }
    } else {
        // If no project license is known, mark all as unknown compatibility
        log(
            LogLevel::Warn,
            "No project license specified or detected, marking all dependencies as unknown compatibility",
        );

        for info in analyzed_data.iter_mut() {
            info.compatibility = LicenseCompatibility::Unknown;
        }
    }
}

/// Render the interactive TUI table for the analyzed dependencies.
///
/// GUI mode is single-shot only (it takes over the terminal and `color_eyre`
/// can only be installed once per process), so it is intentionally not used by
/// `feluda watch`.
fn run_gui(
    mut analyzed_data: Vec<LicenseInfo>,
    project_license: Option<String>,
    config: &CheckConfig,
) -> FeludaResult<()> {
    let original_count = analyzed_data.len();

    // Filter for restrictive and incompatible
    if config.restrictive || config.incompatible {
        if project_license.is_some() {
            log(
                LogLevel::Info,
                "Restrictive and incompatible mode enabled, filtering for restrictive and incompatible licenses",
            );
            analyzed_data.retain(|info| {
                (config.restrictive && *info.is_restrictive())
                    || (config.incompatible
                        && info.compatibility == LicenseCompatibility::Incompatible)
            });

            log(
                LogLevel::Info,
                &format!(
                    "Filtered for restrictive and incompatible licenses: {} of {} dependencies",
                    analyzed_data.len(),
                    original_count
                ),
            );
        } else {
            log(
                LogLevel::Warn,
                "Incompatible mode enabled but no project license specified, cannot filter for incompatible licenses",
            );
        }
    } else if config.restrictive {
        // Filter for restrictive
        log(
            LogLevel::Info,
            "Restrictive mode enabled, filtering for restrictive licenses",
        );
        analyzed_data.retain(|info| *info.is_restrictive());

        log(
            LogLevel::Info,
            &format!(
                "Filtered for restrictive licenses: {} of {} dependencies",
                analyzed_data.len(),
                original_count
            ),
        );
    } else if config.incompatible {
        // Filter for incompatible if requested
        if project_license.is_some() {
            log(
                LogLevel::Info,
                "Incompatible mode enabled, filtering for incompatible licenses",
            );
            analyzed_data.retain(|info| info.compatibility == LicenseCompatibility::Incompatible);

            log(
                LogLevel::Info,
                &format!(
                    "Filtered for incompatible licenses: {} of {} dependencies",
                    analyzed_data.len(),
                    original_count
                ),
            );
        } else {
            log(
                LogLevel::Warn,
                "Incompatible mode enabled but no project license specified, cannot filter for incompatible licenses",
            );
        }
    }

    // Apply OSI filtering
    if let Some(osi_filter) = &config.osi {
        let before_count = analyzed_data.len();
        match osi_filter {
            cli::OsiFilter::Approved => {
                analyzed_data.retain(|info| info.osi_status == licenses::OsiStatus::Approved);
                log(
                    LogLevel::Info,
                    &format!(
                        "Filtered for OSI approved licenses: {} of {} dependencies",
                        analyzed_data.len(),
                        before_count
                    ),
                );
            }
            cli::OsiFilter::NotApproved => {
                analyzed_data.retain(|info| info.osi_status == licenses::OsiStatus::NotApproved);
                log(
                    LogLevel::Info,
                    &format!(
                        "Filtered for non-OSI approved licenses: {} of {} dependencies",
                        analyzed_data.len(),
                        before_count
                    ),
                );
            }
            cli::OsiFilter::Unknown => {
                analyzed_data.retain(|info| info.osi_status == licenses::OsiStatus::Unknown);
                log(
                    LogLevel::Info,
                    &format!(
                        "Filtered for unknown OSI status licenses: {} of {} dependencies",
                        analyzed_data.len(),
                        before_count
                    ),
                );
            }
        }
    }

    log(LogLevel::Info, "Starting TUI mode");

    // Initialize the terminal
    color_eyre::install()
        .map_err(|e| FeludaError::TuiInit(format!("Failed to initialize color_eyre: {e}")))?;

    let terminal = ratatui::init();
    log(LogLevel::Info, "Terminal initialized for TUI");

    // TUI app with project license info
    let app_result = App::new(analyzed_data, project_license).run(terminal);
    ratatui::restore();

    // Handle any errors from the TUI
    app_result.map_err(|e| FeludaError::TuiRuntime(format!("TUI error: {e}")))?;

    log(LogLevel::Info, "TUI session completed successfully");

    Ok(())
}

/// Generate a (non-interactive) dependency report and return the outcome.
///
/// Unlike the previous inline implementation, this never calls `process::exit`;
/// the caller inspects the returned [`ScanSummary`] to decide on exit codes.
/// This makes it safe to call repeatedly from `feluda watch`.
fn report_analysis(
    analyzed_data: Vec<LicenseInfo>,
    project_license: Option<String>,
    config: &CheckConfig,
) -> ScanSummary {
    log(LogLevel::Info, "Generating dependency report");

    // Create ReportConfig from CLI arguments
    let report_config = ReportConfig::new(
        config.json,
        config.yaml,
        config.verbose,
        config.restrictive,
        config.incompatible,
        config.ci_format.clone(),
        config.output_file.clone(),
        project_license,
        config.gist,
        config.osi.clone(),
    );

    // Generate a report based on the analyzed data
    let (has_restrictive, has_incompatible) = generate_report(analyzed_data, report_config);

    log(
        LogLevel::Info,
        &format!(
            "Report generated, has_restrictive: {has_restrictive}, has_incompatible: {has_incompatible}"
        ),
    );

    ScanSummary {
        has_restrictive,
        has_incompatible,
    }
}

fn handle_check_command(config: CheckConfig) -> FeludaResult<()> {
    let (mut analyzed_data, project_license) = analyze_dependencies(&config)?;

    if analyzed_data.is_empty() {
        log(LogLevel::Warn, "No dependencies found to analyze. Exiting.");
        return Ok(());
    }

    annotate_compatibility(&mut analyzed_data, &project_license, config.strict);

    // Either run the GUI or generate a report
    if config.gui {
        run_gui(analyzed_data, project_license, &config)?;
    } else {
        let summary = report_analysis(analyzed_data, project_license, &config);

        if (config.fail_on_restrictive && summary.has_restrictive)
            || (config.fail_on_incompatible && summary.has_incompatible)
        {
            log(
                LogLevel::Warn,
                "Exiting with non-zero status due to license issues",
            );
            process::exit(1);
        }
    }

    log(LogLevel::Info, "Feluda completed successfully");

    Ok(())
}

fn handle_cache_command(clear: bool) -> FeludaResult<()> {
    if clear {
        cache::clear_github_licenses_cache()?;
        println!("✓ Cache cleared successfully\n");
    } else {
        let status = cache::get_cache_status()?;
        status.print_status();
    }
    Ok(())
}
