mod clippy;
mod eslint;
mod ruff;

use std::path::Path;
use std::time::Duration;

use compliance_core::models::ScanType;
use compliance_core::traits::{ScanOutput, Scanner};
use compliance_core::CoreError;

/// Timeout for each individual lint command
pub(crate) const LINT_TIMEOUT: Duration = Duration::from_secs(120);

pub struct LintScanner;

impl Scanner for LintScanner {
    fn name(&self) -> &str {
        "lint"
    }

    fn scan_type(&self) -> ScanType {
        ScanType::Lint
    }

    #[tracing::instrument(skip_all)]
    async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result<ScanOutput, CoreError> {
        let mut all_findings = Vec::new();

        // Detect which languages are present and run appropriate linters
        if has_rust_project(repo_path) {
            match clippy::run_clippy(repo_path, repo_id).await {
                Ok(findings) => all_findings.extend(findings),
                Err(e) => tracing::warn!("Clippy failed: {e}"),
            }
        }

        if has_js_project(repo_path) {
            match eslint::run_eslint(repo_path, repo_id).await {
                Ok(findings) => all_findings.extend(findings),
                Err(e) => tracing::warn!("ESLint failed: {e}"),
            }
        }

        if has_python_project(repo_path) {
            match ruff::run_ruff(repo_path, repo_id).await {
                Ok(findings) => all_findings.extend(findings),
                Err(e) => tracing::warn!("Ruff failed: {e}"),
            }
        }

        Ok(ScanOutput {
            findings: all_findings,
            sbom_entries: Vec::new(),
        })
    }
}

fn has_rust_project(repo_path: &Path) -> bool {
    repo_path.join("Cargo.toml").exists()
}

fn has_js_project(repo_path: &Path) -> bool {
    // Only run if eslint is actually installed in the project
    repo_path.join("package.json").exists() && repo_path.join("node_modules/.bin/eslint").exists()
}

fn has_python_project(repo_path: &Path) -> bool {
    repo_path.join("pyproject.toml").exists()
        || repo_path.join("setup.py").exists()
        || repo_path.join("requirements.txt").exists()
}

/// Run a command with a timeout, returning its output or an error
pub(crate) async fn run_with_timeout(
    child: tokio::process::Child,
    scanner_name: &str,
) -> Result<std::process::Output, CoreError> {
    let result = tokio::time::timeout(LINT_TIMEOUT, child.wait_with_output()).await;
    match result {
        Ok(Ok(output)) => Ok(output),
        Ok(Err(e)) => Err(CoreError::Scanner {
            scanner: scanner_name.to_string(),
            source: Box::new(e),
        }),
        Err(_) => {
            // Process is dropped here which sends SIGKILL on Unix
            Err(CoreError::Scanner {
                scanner: scanner_name.to_string(),
                source: Box::new(std::io::Error::new(
                    std::io::ErrorKind::TimedOut,
                    format!("{scanner_name} timed out after {}s", LINT_TIMEOUT.as_secs()),
                )),
            })
        }
    }
}
