use std::path::PathBuf;
use std::time::Instant;

use anyhow::Result;
use log::{debug, error};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;

use ruff_linter::SuppressionKind;
use ruff_linter::linter::add_suppressions_to_path;
use ruff_linter::source_kind::SourceKind;
use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{
    PyprojectConfig, ResolvedFile, match_exclusion, project_files_in_path,
};

use crate::args::ConfigArguments;

/// Add suppression directives to a collection of files.
pub(crate) fn add_noqa(
    files: &[PathBuf],
    pyproject_config: &PyprojectConfig,
    config_arguments: &ConfigArguments,
    reason: Option<&str>,
    suppression_kind: SuppressionKind,
) -> Result<usize> {
    // Collect all the files to check.
    let start = Instant::now();
    let (mut paths, resolver) = project_files_in_path(files, pyproject_config, config_arguments)?;
    let duration = start.elapsed();
    debug!("Identified files to lint in: {duration:?}");

    // Filter out paths for file types not supported for linting
    paths.retain(|path| {
        if let Ok(ResolvedFile::Root(path) | ResolvedFile::Nested(path)) = path {
            matches!(
                SourceType::from(path),
                SourceType::Python(PySourceType::Python | PySourceType::Stub)
            )
        } else {
            true
        }
    });

    if paths.is_empty() {
        warn_user_once!("No Python files found under the given path(s)");
        return Ok(0);
    }

    // Discover the package root for each Python file.
    let package_roots = resolver.package_roots(
        &paths
            .iter()
            .flatten()
            .map(ResolvedFile::path)
            .collect::<Vec<_>>(),
    );

    let start = Instant::now();
    let modifications: usize = paths
        .par_iter()
        .flatten()
        .map(|resolved_file| -> Result<usize> {
            let source_type = SourceType::from(resolved_file.path());
            let path = resolved_file.path();
            let package = resolved_file
                .path()
                .parent()
                .and_then(|parent| package_roots.get(parent))
                .and_then(|package| *package);
            let settings = resolver.resolve(path);
            if (settings.file_resolver.force_exclude || !resolved_file.is_root())
                && match_exclusion(
                    resolved_file.path(),
                    resolved_file.file_name(),
                    &settings.linter.exclude,
                )
            {
                return Ok(0);
            }
            let source_kind = match SourceKind::from_path(path, source_type) {
                Ok(Some(source_kind)) => source_kind,
                Ok(None) => return Ok(0),
                Err(e) => {
                    error!("Failed to extract source from {}: {e}", path.display());
                    return Ok(0);
                }
            };
            match add_suppressions_to_path(
                path,
                package,
                &source_kind,
                source_type.expect_python(),
                &settings.linter,
                reason,
                suppression_kind,
            ) {
                Ok(count) => Ok(count),
                Err(e) => {
                    error!("Failed to add suppression to {}: {e}", path.display());
                    Ok(0)
                }
            }
        })
        .sum::<Result<usize>>()?;

    let duration = start.elapsed();
    debug!("Added suppressions to files in: {duration:?}");

    Ok(modifications)
}
