use std::sync::OnceLock;

use pg_query::NodeEnum;
use pg_query::protobuf::Node;

use crate::errors::DeparseError;

// parsed once to get a valid ParseResult envelope; swapping ResTarget.val avoids hand-building protobuf sentinels
const WRAPPER_TEMPLATE_SQL: &str = "SELECT _";

static WRAPPER_PROTOTYPE: OnceLock<pg_query::protobuf::ParseResult> = OnceLock::new();

fn wrapper_clone() -> pg_query::protobuf::ParseResult {
    WRAPPER_PROTOTYPE
        .get_or_init(|| {
            pg_query::parse(WRAPPER_TEMPLATE_SQL)
                .expect("WRAPPER_TEMPLATE_SQL is a constant; deparse_* tests cover this path")
                .protobuf
        })
        .clone()
}

// two-part → (Some(schema), name), one-part → (None, name), else None
pub(crate) fn parse_name(nodes: &[Node]) -> Option<(Option<String>, String)> {
    let names: Vec<_> = nodes
        .iter()
        .map(|n| match &n.node {
            Some(NodeEnum::String(s)) => Some(s.sval.clone()),
            _ => None,
        })
        .collect();

    match names.as_slice() {
        [Some(schema), Some(name)] => Some((Some(schema.clone()), name.clone())),
        [Some(name)] => Some((None, name.clone())),
        _ => None,
    }
}

// wraps node in SELECT _ envelope, deparses, strips prefix — avoids touching source bytes
pub(crate) fn deparse_expr(node: &Node) -> Result<String, DeparseError> {
    let mut wrapper = wrapper_clone();
    let target_list = wrapper
        .stmts
        .first_mut()
        .and_then(|raw| raw.stmt.as_mut())
        .and_then(|stmt| match stmt.node.as_mut() {
            Some(NodeEnum::SelectStmt(s)) => Some(&mut s.target_list),
            _ => None,
        })
        .ok_or_else(|| DeparseError::UnexpectedShape {
            output: "wrapper template did not yield SelectStmt".to_owned(),
        })?;
    let res_target = target_list
        .first_mut()
        .and_then(|t| match t.node.as_mut() {
            Some(NodeEnum::ResTarget(r)) => Some(r),
            _ => None,
        })
        .ok_or_else(|| DeparseError::UnexpectedShape {
            output: "wrapper template did not yield ResTarget".to_owned(),
        })?;
    res_target.val = Some(Box::new(node.clone()));

    let raw = pg_query::deparse(&wrapper)?;
    strip_select_prefix(&raw).ok_or(DeparseError::UnexpectedShape { output: raw })
}

fn strip_select_prefix(raw: &str) -> Option<String> {
    let trimmed = raw.trim_start();
    let after = trimmed
        .get(..6)
        .filter(|head| head.eq_ignore_ascii_case("SELECT"))
        .map(|_| &trimmed[6..])?;
    let body = after.trim_start();
    Some(
        body.trim_end_matches(|c: char| c.is_whitespace() || c == ';')
            .to_owned(),
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    fn str_node(s: &str) -> Node {
        Node {
            node: Some(NodeEnum::String(pg_query::protobuf::String {
                sval: s.to_string(),
                ..Default::default()
            })),
        }
    }

    fn first_target_expr(sql: &str) -> Node {
        let result = pg_query::parse(sql).expect("parse");
        let stmt = result.protobuf.stmts[0]
            .stmt
            .as_ref()
            .expect("stmt")
            .as_ref();
        let select = match stmt.node.as_ref().expect("node") {
            NodeEnum::SelectStmt(s) => s,
            other => panic!("expected SelectStmt, got {other:?}"),
        };
        let target = match select.target_list[0].node.as_ref().expect("target node") {
            NodeEnum::ResTarget(t) => t,
            other => panic!("expected ResTarget, got {other:?}"),
        };
        target.val.as_ref().expect("val").as_ref().clone()
    }

    #[test]
    fn parse_name_two_part() {
        let nodes = vec![str_node("public"), str_node("users")];
        assert_eq!(
            parse_name(&nodes),
            Some((Some("public".to_string()), "users".to_string()))
        );
    }

    #[test]
    fn parse_name_one_part() {
        let nodes = vec![str_node("users")];
        assert_eq!(parse_name(&nodes), Some((None, "users".to_string())));
    }

    #[test]
    fn parse_name_empty_returns_none() {
        assert_eq!(parse_name(&[]), None);
    }

    #[test]
    fn parse_name_non_string_node_returns_none() {
        let nodes = vec![Node { node: None }];
        assert_eq!(parse_name(&nodes), None);
    }

    #[test]
    fn deparse_function_call_no_args() {
        let expr = first_target_expr("SELECT now()");
        assert_eq!(deparse_expr(&expr).expect("deparse"), "now()");
    }

    #[test]
    fn deparse_function_call_with_arg() {
        let expr = first_target_expr("SELECT gen_random_uuid()");
        assert_eq!(deparse_expr(&expr).expect("deparse"), "gen_random_uuid()");
    }

    #[test]
    fn deparse_string_literal_with_cast() {
        let expr = first_target_expr("SELECT 'pending'::text");
        assert_eq!(deparse_expr(&expr).expect("deparse"), "'pending'::text");
    }

    #[test]
    fn deparse_integer_literal() {
        let expr = first_target_expr("SELECT 0");
        assert_eq!(deparse_expr(&expr).expect("deparse"), "0");
    }

    #[test]
    fn deparse_is_null_predicate() {
        let expr = first_target_expr("SELECT deleted_at IS NULL");
        assert_eq!(deparse_expr(&expr).expect("deparse"), "deleted_at IS NULL");
    }

    #[test]
    fn deparse_function_over_column() {
        let expr = first_target_expr("SELECT lower(email)");
        assert_eq!(deparse_expr(&expr).expect("deparse"), "lower(email)");
    }

    #[test]
    fn deparse_round_trip_is_idempotent() {
        let expr = first_target_expr("SELECT lower(email) || '@example.com'");
        let first = deparse_expr(&expr).expect("first deparse");
        let reparsed_expr = first_target_expr(&format!("SELECT {first}"));
        let second = deparse_expr(&reparsed_expr).expect("second deparse");
        assert_eq!(first, second, "deparse must be idempotent under round-trip");
    }

    #[test]
    fn deparse_non_expression_node_returns_error() {
        let result = pg_query::parse("SELECT 1").expect("parse");
        let stmt_node = result.protobuf.stmts[0]
            .stmt
            .as_ref()
            .expect("stmt")
            .as_ref()
            .clone();
        let err = deparse_expr(&stmt_node).expect_err("must reject non-expression node");
        match err {
            DeparseError::PgQuery(_) | DeparseError::UnexpectedShape { .. } => {}
        }
    }
}
