#![doc = include_str!("../README.md")]
#![warn(
    elided_lifetimes_in_paths,
    explicit_outlives_requirements,
    missing_debug_implementations,
    missing_docs,
    noop_method_call,
    single_use_lifetimes,
    trivial_casts,
    trivial_numeric_casts,
    unreachable_pub,
    unsafe_code,
    unused_qualifications
)]
#![warn(clippy::pedantic, clippy::cargo)]

use std::path::Path;

use kdl_schema::Schema;
use knuffel::{ast::Document, span::Span};
use miette::{Diagnostic, NamedSource};
use thiserror::Error;

mod check;

/// a document as parsed by knuffel
pub type DocumentAst = Document<Span>;

/// a schema validation failure
#[derive(Debug, Error, Diagnostic)]
pub enum CheckFailure {
    /// an I/O failure while reading the document
    #[error("could not read file: {0}")]
    IoError(#[from] std::io::Error),
    /// a parse error while parsing the document
    #[error("could not parse file: {0}")]
    #[diagnostic(transparent)]
    ParseError(#[from] knuffel::Error<Span>),
    /// a validation error while checking the document against the schema
    #[error(transparent)]
    #[diagnostic(transparent)]
    ValidationFailure(#[from] check::CheckFailure),
    /// a validation error while checking the document against the schema,
    /// with more information for better reporting
    #[error("{failure}")]
    #[diagnostic(forward(failure))]
    SourcedValidationFailure {
        /// the raw text of the document
        #[source_code]
        source: NamedSource,
        /// what happened in validation
        #[source]
        failure: check::CheckFailure,
    },
}

impl CheckFailure {
    fn with_named_source(self, name: impl AsRef<str>, source: String) -> Self {
        match self {
            Self::ValidationFailure(failure) => Self::SourcedValidationFailure {
                source: NamedSource::new(name, source),
                failure,
            },
            _ => self,
        }
    }
}

/// allow a [Schema] to check a document for validity
pub trait CheckExt {
    /// check if the file at the given path matches this schema
    ///
    /// # Errors
    ///
    /// returns an error on IO failure, parsing failure, or validation failure
    fn check_file_matches(&self, file_path: impl AsRef<Path>) -> Result<(), CheckFailure>;
    /// check if the given document matches this schema
    ///
    /// document name is used for error reporting by both the knuffel parser and this crate
    ///
    /// # Errors
    ///
    /// returns an error on parsing failure or validation failure
    fn check_text_matches(
        &self,
        document_name: &str,
        document_text: &str,
    ) -> Result<(), CheckFailure>;
    /// check if the given already-parsed document matches this schema
    ///
    /// # Errors
    ///
    /// returns an error on validation failure
    fn check_ast_matches(&self, document_ast: DocumentAst) -> Result<(), CheckFailure>;
}

impl CheckExt for Schema {
    fn check_file_matches(&self, file_path: impl AsRef<Path>) -> Result<(), CheckFailure> {
        let file_path = file_path.as_ref();
        let file_name = file_path.display().to_string();
        let file_text = std::fs::read_to_string(file_path)?;
        let ast = knuffel::parse_ast(&file_name, &file_text)?;
        self.check_ast_matches(ast)
            .map_err(|err| err.with_named_source(file_name, file_text))
    }
    fn check_text_matches(
        &self,
        document_name: &str,
        document_text: &str,
    ) -> Result<(), CheckFailure> {
        let ast = knuffel::parse_ast(document_name, document_text)?;
        self.check_ast_matches(ast)
            .map_err(|err| err.with_named_source(document_name, document_text.to_string()))
    }
    fn check_ast_matches(&self, document_ast: DocumentAst) -> Result<(), CheckFailure> {
        check::check(&document_ast, self)?;
        Ok(())
    }
}