use camino::Utf8Path;
use pg_query::NodeEnum;
use text_size::{TextRange, TextSize};

use crate::errors::ParseError;

/// A single parsed SQL statement with its byte range in the original source.
pub struct ParsedStatement {
    /// byte range in the source string.
    pub range: TextRange,
    /// root AST node.
    pub root: NodeEnum,
}

/// A statement that could not be parsed.
#[derive(Clone)]
pub struct ParseFailure {
    /// byte range of the failed statement in the source string.
    pub range: TextRange,
    /// parser error message.
    pub message: String,
}

fn into_root(result: pg_query::ParseResult) -> Option<NodeEnum> {
    result
        .protobuf
        .stmts
        .into_iter()
        .next()
        .and_then(|raw| raw.stmt)
        .and_then(|boxed| boxed.node)
}

// pointer arithmetic: stmt_text is a sub-slice of sql, so pointer distance = start offset
fn stmt_range(sql: &str, stmt_text: &str) -> TextRange {
    let start = stmt_text.as_ptr() as usize - sql.as_ptr() as usize;
    let end = start + stmt_text.len();
    // SAFETY: SQL files exceeding 4 GiB are not a realistic concern.
    #[allow(clippy::cast_possible_truncation)]
    TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32))
}

/// Split `sql` into individual statements and parse each one.
///
/// Failures carry the byte range and error message for IDE diagnostics.
///
/// # Errors
///
/// Returns [`ParseError::EmptyInput`] if `sql` is blank.
/// Returns [`ParseError::Scanner`] if the statement scanner itself fails.
///
/// # Examples
///
/// ```
/// use psql_scip::parse::split_and_parse;
///
/// let (stmts, failures) = split_and_parse("SELECT 1; SELECT 2;").unwrap();
/// assert_eq!(stmts.len(), 2);
/// assert!(failures.is_empty());
/// ```
pub fn split_and_parse(sql: &str) -> Result<(Vec<ParsedStatement>, Vec<ParseFailure>), ParseError> {
    if sql.trim().is_empty() {
        return Err(ParseError::EmptyInput);
    }

    let stmt_slices =
        pg_query::split_with_scanner(sql).map_err(|e| ParseError::Scanner(e.to_string()))?;

    let mut stmts = Vec::with_capacity(stmt_slices.len());
    let mut failures = Vec::new();

    for stmt_text in stmt_slices {
        let range = stmt_range(sql, stmt_text);
        match pg_query::parse(stmt_text) {
            Ok(parse_result) => match into_root(parse_result) {
                Some(root) => stmts.push(ParsedStatement { range, root }),
                None => {
                    tracing::warn!(?range, "statement produced no root node, skipping");
                }
            },
            Err(err) => {
                tracing::debug!(?range, %err, "skipping unparseable statement");
                failures.push(ParseFailure {
                    range,
                    message: err.to_string(),
                });
            }
        }
    }

    Ok((stmts, failures))
}

/// Read a UTF-8 SQL file at `path` and parse all statements in it.
///
/// # Errors
///
/// - [`ParseError::Io`] if the file cannot be read.
/// - [`ParseError::EmptyInput`] if the file is blank.
/// - [`ParseError::Scanner`] if statement scanning fails.
pub fn parse_file(
    path: &Utf8Path,
) -> Result<(Vec<ParsedStatement>, Vec<ParseFailure>), ParseError> {
    let sql = std::fs::read_to_string(path).map_err(|source| ParseError::Io {
        path: path.to_string(),
        source,
    })?;
    split_and_parse(&sql)
}

#[cfg(test)]
mod tests {
    use pg_query::NodeEnum;

    use super::*;

    #[test]
    fn parses_simple_select() {
        let (stmts, failures) = split_and_parse("SELECT 1; SELECT 2;")
            .expect("split_and_parse must not fail on valid SQL");

        assert_eq!(stmts.len(), 2, "expected two statements");
        assert!(failures.is_empty(), "expected no failures");

        for stmt in &stmts {
            assert!(
                matches!(stmt.root, NodeEnum::SelectStmt(_)),
                "each root must be a SelectStmt"
            );
        }
    }

    #[test]
    fn returns_error_on_empty_input() {
        let result = split_and_parse("   ");
        assert!(
            matches!(result, Err(ParseError::EmptyInput)),
            "blank input must return EmptyInput error"
        );
    }

    #[test]
    fn skips_unparseable_and_returns_valid() {
        // The scanner returns "SELECT 1 FROM" as a statement (truncated at `;`)
        // but the parser rejects it — verifies the ParseFailure path.
        let sql = "SELECT 1; SELECT 1 FROM; SELECT 2;";
        let (stmts, failures) =
            split_and_parse(sql).expect("function must not return Err for partial input");

        assert!(
            stmts.len() >= 2,
            "at least two parseable statements must be returned, got {}",
            stmts.len()
        );
        assert_eq!(
            failures.len(),
            1,
            "must have one failure, got {}",
            failures.len()
        );
        assert!(
            !failures[0].message.is_empty(),
            "failure message must be non-empty"
        );
    }

    #[test]
    fn root_is_accessible_as_node_enum() {
        let (stmts, _) = split_and_parse("SELECT 42;").expect("must parse");
        assert_eq!(stmts.len(), 1);
        assert!(
            matches!(stmts[0].root, NodeEnum::SelectStmt(_)),
            "root must be SelectStmt"
        );
    }

    #[test]
    fn stmt_range_points_into_source() {
        let sql = "SELECT 1; SELECT 2;";
        let (stmts, _) = split_and_parse(sql).expect("must parse");
        assert_eq!(stmts.len(), 2);
        // The range for the first statement must point to valid SQL text.
        let range = stmts[0].range;
        let text = &sql[std::ops::Range::<usize>::from(range)];
        assert!(text.contains("SELECT"), "range must cover SELECT keyword");
    }
}
