use crate::snippets::error::Result;
use crate::snippets::types::{Language, Snippet, SnippetStatus, ValidationLevel};
use crate::snippets::validators::{SnippetValidator, run_command};
use std::io::Write;
use tempfile::NamedTempFile;

pub struct CValidator;

fn compiler() -> Option<String> {
    for candidate in ["cc", "clang", "gcc"] {
        if which::which(candidate).is_ok() {
            return Some(candidate.to_string());
        }
    }
    None
}

impl SnippetValidator for CValidator {
    fn language(&self) -> Language {
        Language::C
    }

    fn is_available(&self) -> bool {
        compiler().is_some()
    }

    fn validate(
        &self,
        snippet: &Snippet,
        level: ValidationLevel,
        timeout_secs: u64,
    ) -> Result<(SnippetStatus, Option<String>)> {
        let Some(cc) = compiler() else {
            return Ok((SnippetStatus::Unavailable, Some("no C compiler on PATH".into())));
        };

        let mut source = NamedTempFile::with_suffix(".c")?;
        source.write_all(snippet.code.as_bytes())?;
        source.flush()?;
        let source_path = source.path().to_string_lossy().to_string();

        let mut command = std::process::Command::new(&cc);
        match level {
            ValidationLevel::Syntax => {
                command.args(["-fsyntax-only", &source_path]);
            }
            ValidationLevel::Compile | ValidationLevel::Run => {
                let out = NamedTempFile::new()?;
                let out_path = out.path().to_string_lossy().to_string();
                drop(out);
                command.args(["-o", &out_path, &source_path]);
                let (success, output) = run_command(&mut command, timeout_secs)?;
                if !success {
                    return Ok((SnippetStatus::Fail, Some(output)));
                }
                if matches!(level, ValidationLevel::Run) {
                    let mut run = std::process::Command::new(&out_path);
                    let (ran_ok, run_output) = run_command(&mut run, timeout_secs)?;
                    let _ = std::fs::remove_file(&out_path);
                    return Ok(if ran_ok {
                        (SnippetStatus::Pass, None)
                    } else {
                        (SnippetStatus::Fail, Some(run_output))
                    });
                }
                let _ = std::fs::remove_file(&out_path);
                return Ok((SnippetStatus::Pass, None));
            }
        }

        let (success, output) = run_command(&mut command, timeout_secs)?;
        if success {
            Ok((SnippetStatus::Pass, None))
        } else {
            Ok((SnippetStatus::Fail, Some(output)))
        }
    }

    fn max_level(&self) -> ValidationLevel {
        ValidationLevel::Run
    }

    fn is_dependency_error(&self, output: &str) -> bool {
        output.contains("file not found")
            || output.contains("No such file or directory")
            || output.contains("undeclared identifier")
            || output.contains("implicit declaration")
            || output.contains("unknown type name")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::snippets::types::{Snippet, SnippetMetadata};
    use std::path::PathBuf;

    fn snippet(code: &str) -> Snippet {
        Snippet {
            id: None,
            path: PathBuf::from("test.c"),
            language: Language::C,
            title: None,
            code: code.to_string(),
            start_line: 1,
            block_index: 0,
            annotation: None,
            metadata: SnippetMetadata::default(),
        }
    }

    #[test]
    fn syntax_ok() {
        let v = CValidator;
        if !v.is_available() {
            return;
        }
        let s = snippet("int main(void) { return 0; }\n");
        let (status, _) = v.validate(&s, ValidationLevel::Syntax, 30).unwrap();
        assert_eq!(status, SnippetStatus::Pass);
    }

    #[test]
    fn syntax_fail() {
        let v = CValidator;
        if !v.is_available() {
            return;
        }
        let s = snippet("int main(void) { @@@ }\n");
        let (status, _) = v.validate(&s, ValidationLevel::Syntax, 30).unwrap();
        assert_eq!(status, SnippetStatus::Fail);
    }
}
