//! S-expression formatter for kaish AST.
//!
//! Converts AST nodes to the S-expression format used in test snapshots.
//! S-expressions provide a stable, readable format that's easier to diff
//! than Debug output.

use super::*;

/// Format a Program as an S-expression.
/// For single-statement programs, formats just the statement.
/// For multi-statement programs, formats as a sequence.
pub fn format_program(program: &Program) -> String {
    let stmts: Vec<_> = program
        .statements
        .iter()
        .filter(|s| !matches!(s, Stmt::Empty))
        .collect();

    match stmts.len() {
        0 => "(program)".to_string(),
        1 => format_stmt(stmts[0]),
        _ => {
            let parts: Vec<String> = stmts.iter().map(|s| format_stmt(s)).collect();
            format!("(program {})", parts.join(" "))
        }
    }
}

/// Format a statement as an S-expression.
pub fn format_stmt(stmt: &Stmt) -> String {
    match stmt {
        Stmt::Assignment(a) => format_assignment(a),
        Stmt::Command(cmd) => format_command(cmd),
        Stmt::Pipeline(p) => format_pipeline(p),
        Stmt::If(if_stmt) => format_if(if_stmt),
        Stmt::For(for_loop) => format_for(for_loop),
        Stmt::While(while_loop) => format_while(while_loop),
        Stmt::Case(case_stmt) => format_case(case_stmt),
        Stmt::Break(n) => match n {
            Some(level) => format!("(break {})", level),
            None => "(break)".to_string(),
        },
        Stmt::Continue(n) => match n {
            Some(level) => format!("(continue {})", level),
            None => "(continue)".to_string(),
        },
        Stmt::Return(expr) => match expr {
            Some(e) => format!("(return {})", format_expr(e)),
            None => "(return)".to_string(),
        },
        Stmt::Exit(expr) => match expr {
            Some(e) => format!("(exit {})", format_expr(e)),
            None => "(exit)".to_string(),
        },
        Stmt::ToolDef(tool) => format_tooldef(tool),
        Stmt::Test(test_expr) => format!("(test {})", format_test_expr(test_expr)),
        Stmt::AndChain { left, right } => {
            format!("(and-chain {} {})", format_stmt(left), format_stmt(right))
        }
        Stmt::OrChain { left, right } => {
            format!("(or-chain {} {})", format_stmt(left), format_stmt(right))
        }
        Stmt::EnvScoped { assignments, body } => {
            let assigns: Vec<String> = assignments.iter().map(format_assignment).collect();
            format!("(env-scoped ({}) {})", assigns.join(" "), format_stmt(body))
        }
        Stmt::Empty => "(empty)".to_string(),
    }
}

/// Format an assignment as an S-expression.
fn format_assignment(a: &Assignment) -> String {
    let value = format_expr(&a.value);
    format!("(assign {} {} local={})", format_varpath(&a.path), value, a.local)
}

/// Format a command as an S-expression.
pub fn format_command(cmd: &Command) -> String {
    let mut parts = vec![format!("(cmd {}", cmd.name)];

    for arg in &cmd.args {
        parts.push(format_arg(arg));
    }

    for redir in &cmd.redirects {
        parts.push(format_redirect(redir));
    }

    format!("{})", parts.join(" "))
}

/// Format an argument as an S-expression.
fn format_arg(arg: &Arg) -> String {
    match arg {
        Arg::Positional(expr) => format!("(pos {})", format_expr(expr)),
        Arg::Named { key, value } => format!("(named {} {})", key, format_expr(value)),
        Arg::WordAssign { key, value } => format!("(wordassign {} {})", key, format_expr(value)),
        Arg::ShortFlag(f) => format!("(shortflag {})", f),
        Arg::LongFlag(f) => format!("(longflag {})", f),
        Arg::DoubleDash => "(doubledash)".to_string(),
    }
}

/// Format a redirect as an S-expression.
fn format_redirect(redir: &Redirect) -> String {
    let kind = match redir.kind {
        RedirectKind::StdoutOverwrite => ">",
        RedirectKind::StdoutAppend => ">>",
        RedirectKind::Stdin => "<",
        RedirectKind::HereDoc => "<<",
        RedirectKind::HereString => "<<<",
        RedirectKind::Stderr => "2>",
        RedirectKind::Both => "&>",
        RedirectKind::MergeStderr => "2>&1",
        RedirectKind::MergeStdout => "1>&2",
    };
    format!("(redir {} {})", kind, format_expr(&redir.target))
}

/// Format a pipeline as an S-expression.
/// Format a command-substitution body — a block of statements — as an s-expr.
/// A one-statement block formats as that statement; multiple are wrapped in a
/// `(block …)` so the sequence is visible in snapshots.
pub fn format_stmt_block(stmts: &[Stmt]) -> String {
    if stmts.len() == 1 {
        format_stmt(&stmts[0])
    } else {
        let inner: Vec<String> = stmts.iter().map(format_stmt).collect();
        format!("(block {})", inner.join(" "))
    }
}

pub fn format_pipeline(p: &Pipeline) -> String {
    let cmds: Vec<String> = p.commands.iter().map(format_command).collect();

    if p.background {
        if cmds.len() == 1 {
            format!("(background {})", cmds[0])
        } else {
            format!("(background (pipeline {}))", cmds.join(" "))
        }
    } else {
        format!("(pipeline {})", cmds.join(" "))
    }
}

/// Format an if statement as an S-expression.
fn format_if(if_stmt: &IfStmt) -> String {
    let cond = format_expr(&if_stmt.condition);
    let then_stmts: Vec<String> = if_stmt
        .then_branch
        .iter()
        .filter(|s| !matches!(s, Stmt::Empty))
        .map(format_stmt)
        .collect();
    let then_part = format!("(then {})", then_stmts.join(" "));

    match &if_stmt.else_branch {
        Some(else_stmts) => {
            let else_inner: Vec<String> = else_stmts
                .iter()
                .filter(|s| !matches!(s, Stmt::Empty))
                .map(format_stmt)
                .collect();
            if else_inner.is_empty() {
                format!("(if {} {} (else))", cond, then_part)
            } else {
                format!("(if {} {} (else {}))", cond, then_part, else_inner.join(" "))
            }
        }
        None => format!("(if {} {} (else))", cond, then_part),
    }
}

/// Format a for loop as an S-expression.
fn format_for(for_loop: &ForLoop) -> String {
    let items: Vec<String> = for_loop.items.iter().map(format_expr).collect();
    let body_stmts: Vec<String> = for_loop
        .body
        .iter()
        .filter(|s| !matches!(s, Stmt::Empty))
        .map(format_stmt)
        .collect();
    format!(
        "(for {} (in {}) (do {}))",
        for_loop.variable,
        items.join(" "),
        body_stmts.join(" ")
    )
}

/// Format a while loop as an S-expression.
fn format_while(while_loop: &WhileLoop) -> String {
    let cond = format_expr(&while_loop.condition);
    let body_stmts: Vec<String> = while_loop
        .body
        .iter()
        .filter(|s| !matches!(s, Stmt::Empty))
        .map(format_stmt)
        .collect();
    format!("(while {} (do {}))", cond, body_stmts.join(" "))
}

/// Format a case statement as an S-expression.
fn format_case(case_stmt: &CaseStmt) -> String {
    let expr = format_expr(&case_stmt.expr);
    let branches: Vec<String> = case_stmt
        .branches
        .iter()
        .map(format_case_branch)
        .collect();
    format!("(case {} ({}))", expr, branches.join(" "))
}

/// Format a case branch as an S-expression.
fn format_case_branch(branch: &CaseBranch) -> String {
    let patterns = branch.patterns.join("|");
    let body_stmts: Vec<String> = branch
        .body
        .iter()
        .filter(|s| !matches!(s, Stmt::Empty))
        .map(format_stmt)
        .collect();
    format!("(branch \"{}\" ({}))", patterns, body_stmts.join(" "))
}

/// Format a tool definition as an S-expression.
fn format_tooldef(tool: &ToolDef) -> String {
    let params: Vec<String> = tool.params.iter().map(format_param).collect();
    let body_stmts: Vec<String> = tool
        .body
        .iter()
        .filter(|s| !matches!(s, Stmt::Empty))
        .map(format_stmt)
        .collect();
    format!(
        "(tooldef {} ({}) ({}))",
        tool.name,
        params.join(" "),
        body_stmts.join(" ")
    )
}

/// Format a parameter definition as an S-expression.
fn format_param(param: &ParamDef) -> String {
    let type_str = param
        .param_type
        .as_ref()
        .map(|t| match t {
            ParamType::String => "string",
            ParamType::Int => "int",
            ParamType::Float => "float",
            ParamType::Bool => "bool",
        })
        .unwrap_or("any");

    match &param.default {
        Some(default) => format!("(param {} {} {})", param.name, type_str, format_expr(default)),
        None => format!("(param {} {})", param.name, type_str),
    }
}

/// Format an expression as an S-expression.
pub fn format_expr(expr: &Expr) -> String {
    match expr {
        Expr::Literal(value) => format_value(value),
        Expr::VarRef(path) => format!("(varref {})", format_varpath(path)),
        Expr::Interpolated(parts) => {
            let parts_str: Vec<String> = parts
                .iter()
                .map(format_string_part)
                .collect();
            format!("(interpolated {})", parts_str.join(" "))
        }
        Expr::HereDocBody { parts, strip_tabs } => {
            let parts_str: Vec<String> = parts
                .iter()
                .map(|sp| format_string_part(&sp.part))
                .collect();
            format!(
                "(heredoc-body strip-tabs={} {})",
                strip_tabs,
                parts_str.join(" ")
            )
        }
        Expr::BinaryOp { left, op, right } => {
            let op_str = match op {
                BinaryOp::And => "and",
                BinaryOp::Or => "or",
            };
            format!("({} {} {})", op_str, format_expr(left), format_expr(right))
        }
        Expr::CommandSubst(stmts) => {
            format!("(cmdsubst {})", format_stmt_block(stmts))
        }
        Expr::Test(test_expr) => format!("(test {})", format_test_expr(test_expr)),
        Expr::Positional(n) => format!("(positional {})", n),
        Expr::AllArgs => "(all-args)".to_string(),
        Expr::ArgCount => "(arg-count)".to_string(),
        Expr::VarLength(path) => format!("(var-length {})", format_varpath(path)),
        Expr::VarWithDefault { path, default } => {
            let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
            format!("(var-default {} ({}))", format_varpath(path), default_parts.join(" "))
        }
        Expr::Arithmetic(expr_str) => format!("(arithmetic \"{}\")", expr_str),
        Expr::Command(cmd) => format_command(cmd),
        Expr::LastExitCode => "(last-exit-code)".to_string(),
        Expr::CurrentPid => "(current-pid)".to_string(),
        Expr::GlobPattern(s) => format!("(glob \"{}\")", s),
        Expr::ListLiteral(elems) => {
            let parts: Vec<String> = elems
                .iter()
                .map(|elem| match elem {
                    ListElem::Item(e) => format_expr(e),
                    ListElem::Spread(e) => format!("(spread {})", format_expr(e)),
                })
                .collect();
            format!("(list {})", parts.join(" "))
        }
        Expr::RecordLiteral(entries) => {
            let parts: Vec<String> = entries
                .iter()
                .map(|entry| {
                    let key = match &entry.key {
                        RecordKey::Bare(s) => s.clone(),
                        RecordKey::Quoted(s) => format!("\"{}\"", s),
                        RecordKey::Interpolated(parts) => {
                            let parts_str: Vec<String> =
                                parts.iter().map(format_string_part).collect();
                            format!("(interpolated {})", parts_str.join(" "))
                        }
                    };
                    format!("({} {})", key, format_expr(&entry.value))
                })
                .collect();
            format!("(record {})", parts.join(" "))
        }
    }
}

/// Format a test expression as an S-expression.
pub fn format_test_expr(test: &TestExpr) -> String {
    match test {
        TestExpr::FileTest { op, path } => {
            let op_str = match op {
                FileTestOp::Exists => "-e",
                FileTestOp::IsFile => "-f",
                FileTestOp::IsDir => "-d",
                FileTestOp::Readable => "-r",
                FileTestOp::Writable => "-w",
                FileTestOp::Executable => "-x",
            };
            format!("(file {} {})", op_str, format_expr(path))
        }
        TestExpr::StringTest { op, value } => {
            let op_str = match op {
                StringTestOp::IsEmpty => "-z",
                StringTestOp::IsNonEmpty => "-n",
                StringTestOp::IsList => "-list",
                StringTestOp::IsRecord => "-record",
            };
            format!("(string {} {})", op_str, format_expr(value))
        }
        TestExpr::Comparison { left, op, right } => {
            let op_str = match op {
                TestCmpOp::Eq => "==",
                TestCmpOp::NotEq => "!=",
                TestCmpOp::Match => "=~",
                TestCmpOp::NotMatch => "!~",
                TestCmpOp::Gt => ">",
                TestCmpOp::Lt => "<",
                TestCmpOp::GtEq => ">=",
                TestCmpOp::LtEq => "<=",
                TestCmpOp::NumEq => "-eq",
                TestCmpOp::NumNotEq => "-ne",
                TestCmpOp::NumGt => "-gt",
                TestCmpOp::NumLt => "-lt",
                TestCmpOp::NumGtEq => "-ge",
                TestCmpOp::NumLtEq => "-le",
            };
            format!(
                "(cmp {} {} {})",
                op_str,
                format_expr(left),
                format_expr(right)
            )
        }
        TestExpr::And { left, right } => {
            format!("(and {} {})", format_test_expr(left), format_test_expr(right))
        }
        TestExpr::Or { left, right } => {
            format!("(or {} {})", format_test_expr(left), format_test_expr(right))
        }
        TestExpr::Not { expr } => {
            format!("(not {})", format_test_expr(expr))
        }
        TestExpr::In { left, right } => {
            format!("(in {} {})", format_expr(left), format_expr(right))
        }
        TestExpr::NotIn { left, right } => {
            format!("(not-in {} {})", format_expr(left), format_expr(right))
        }
    }
}

/// Format a StringPart as an S-expression.
fn format_string_part(part: &StringPart) -> String {
    match part {
        StringPart::Literal(s) => format!("\"{}\"", escape_for_display(s)),
        StringPart::Var(path) => format!("(varref {})", format_varpath(path)),
        StringPart::VarWithDefault { path, default } => {
            let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
            format!("(vardefault {} ({}))", format_varpath(path), default_parts.join(" "))
        }
        StringPart::VarLength(path) => format!("(varlength {})", format_varpath(path)),
        StringPart::Positional(n) => format!("(positional {})", n),
        StringPart::AllArgs => "(allargs)".to_string(),
        StringPart::ArgCount => "(argcount)".to_string(),
        StringPart::Arithmetic(expr) => format!("(arith \"{}\")", expr),
        StringPart::CommandSubst(stmts) => format!("(cmdsubst {})", format_stmt_block(stmts)),
        StringPart::LastExitCode => "(last-exit-code)".to_string(),
        StringPart::CurrentPid => "(current-pid)".to_string(),
    }
}

/// Escape control characters for display in test output.
fn escape_for_display(s: &str) -> String {
    s.replace('\n', "\\n")
        .replace('\t', "\\t")
        .replace('\r', "\\r")
}

/// Format a value as an S-expression.
pub fn format_value(value: &Value) -> String {
    match value {
        Value::Null => "(null)".to_string(),
        Value::Bool(b) => format!("(bool {})", b),
        Value::Int(n) => format!("(int {})", n),
        Value::Float(f) => format!("(float {})", f),
        Value::String(s) => format!("(string \"{}\")", escape_for_display(s)),
        Value::Json(json) => format!("(json {})", json),
        Value::Bytes(b) => format!("(bytes len={})", b.len()),
    }
}

/// Format a variable path as an S-expression.
pub fn format_varpath(path: &VarPath) -> String {
    path.segments
        .iter()
        .map(|seg| match seg {
            VarSegment::Field(name) => name.clone(),
            VarSegment::Index(i) => format!("[{i}]"),
            VarSegment::Key(k) => format!("[{k}]"),
            VarSegment::Dynamic(v) => format!("[${v}]"),
            VarSegment::Slice(a, b) => format!(
                "[{}:{}]",
                a.map(|n| n.to_string()).unwrap_or_default(),
                b.map(|n| n.to_string()).unwrap_or_default()
            ),
        })
        .collect::<Vec<_>>()
        .join(".")
}

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

    #[test]
    fn format_simple_int() {
        assert_eq!(format_value(&Value::Int(42)), "(int 42)");
    }

    #[test]
    fn format_simple_string() {
        assert_eq!(format_value(&Value::String("hello".to_string())), "(string \"hello\")");
    }

    #[test]
    fn format_varpath_simple() {
        let path = VarPath::simple("X");
        assert_eq!(format_varpath(&path), "X");
    }

    #[test]
    fn format_varpath_nested() {
        let path = VarPath {
            segments: vec![
                VarSegment::Field("VAR".to_string()),
                VarSegment::Field("field".to_string()),
            ],
        };
        assert_eq!(format_varpath(&path), "VAR.field");
    }
}
