//! Diagnostic helpers that stringify AST fragments.
//!
//! Form (A) — the round-trippable authoring surface — lives in
//! `ailang-surface::print`. This module is the asymmetric, lossy
//! companion: stringification routines used inside error messages
//! and the manifest summary, where one-line ML-style notation
//! (`forall a. List<a> -> Int`) reads better than form (A)'s
//! `(forall (vars a) (fn-type ...))`.
//!
//! Public entry points:
//! - [`manifest`] — one line per def, used by `ail manifest`.
//! - [`type_to_string`] — single-line ML-style type, used by
//!   `ailang-check` diagnostics, `ailang-codegen` mismatch errors,
//!   and `ail describe`'s text projection.
//! - [`pattern_to_string`] — single-line pattern, used by
//!   `ailang-check` diagnostics.
//!
//! All output is deterministic. None of it round-trips back to AST.

use crate::ast::*;
use std::fmt::Write;

/// Render a one-line-per-def manifest of a [`Module`].
///
/// Each line carries the def kind keyword (`fn` / `const` / `type`),
/// the name padded to a fixed width, the rendered type, and the
/// 16-hex-char [`crate::def_hash`] in brackets. Used by `ail manifest`
/// and as the canonical "what's in this module" summary.
pub fn manifest(m: &Module) -> String {
    let mut s = String::new();
    writeln!(s, "module {}", m.name).unwrap();
    let max_name = m.defs.iter().map(|d| d.name().len()).max().unwrap_or(0);
    for def in &m.defs {
        let h = crate::hash::def_hash(def);
        let (kw, ty) = match def {
            Def::Fn(f) => ("fn", type_to_string(&f.ty)),
            Def::Const(c) => ("const", type_to_string(&c.ty)),
            Def::Type(t) => {
                let ctors = t
                    .ctors
                    .iter()
                    .map(|c| {
                        if c.fields.is_empty() {
                            c.name.clone()
                        } else {
                            format!(
                                "{}({})",
                                c.name,
                                c.fields
                                    .iter()
                                    .map(type_to_string)
                                    .collect::<Vec<_>>()
                                    .join(", ")
                            )
                        }
                    })
                    .collect::<Vec<_>>()
                    .join(" | ");
                let body = if t.vars.is_empty() {
                    ctors
                } else {
                    format!("forall {}. {}", t.vars.join(" "), ctors)
                };
                ("type", body)
            }
            // Iter 22b.1: one-line summary `class C a` / `instance C T`.
            // Full pretty rendering of method signatures and bodies
            // lands in 22b.4 alongside the prose projection arms.
            Def::Class(c) => ("class", format!("{} {}", c.name, c.param)),
            Def::Instance(i) => ("instance", format!("{} {}", i.class, type_to_string(&i.type_))),
        };
        writeln!(
            s,
            "  {kw:5} {name:<width$} :: {ty}    [{h}]",
            kw = kw,
            name = def.name(),
            width = max_name,
            ty = ty,
            h = h,
        )
        .unwrap();
    }
    s
}

/// Render a [`Pattern`] as a single-line string.
///
/// Used by `ailang-check` when constructing diagnostic messages.
pub fn pattern_to_string(p: &Pattern) -> String {
    match p {
        Pattern::Wild => "_".into(),
        Pattern::Var { name } => name.clone(),
        Pattern::Lit { lit } => lit_to_string(lit),
        Pattern::Ctor { ctor, fields } => {
            if fields.is_empty() {
                ctor.clone()
            } else {
                let fs = fields
                    .iter()
                    .map(pattern_to_string)
                    .collect::<Vec<_>>()
                    .join(" ");
                format!("({ctor} {fs})")
            }
        }
    }
}

fn lit_to_string(l: &Literal) -> String {
    match l {
        Literal::Int { value } => value.to_string(),
        Literal::Bool { value } => value.to_string(),
        Literal::Str { value } => {
            // serde_json escapes for us; the result is a valid
            // JSON string literal, which is enough as our canonical form.
            serde_json::to_string(value).unwrap()
        }
        Literal::Unit => "()".to_string(),
        Literal::Float { bits } => format!("(float-bits 0x{:016x})", bits),
    }
}

/// Render a [`Type`] as a single-line string.
///
/// The format mirrors typical ML-family notation: type-constructor
/// applications use angle brackets (`List<Int>`), function types use
/// arrow syntax (`(Int, Int) -> Int`) with optional `!eff1,eff2`
/// effect suffixes, and `Forall` becomes `forall a b. ...`. Used by
/// [`manifest`] and by `ailang-check` diagnostics.
pub fn type_to_string(t: &Type) -> String {
    match t {
        Type::Con { name, args } => {
            if args.is_empty() {
                name.clone()
            } else {
                let xs = args
                    .iter()
                    .map(type_to_string)
                    .collect::<Vec<_>>()
                    .join(", ");
                format!("{name}<{xs}>")
            }
        }
        Type::Var { name } => name.clone(),
        Type::Fn { params, ret, effects, .. } => {
            let p = params
                .iter()
                .map(type_to_string)
                .collect::<Vec<_>>()
                .join(", ");
            let eff = if effects.is_empty() {
                String::new()
            } else {
                format!(" !{}", effects.join(","))
            };
            format!("({p}) -> {ret}{eff}", ret = type_to_string(ret))
        }
        Type::Forall { vars, constraints: _, body } => {
            format!("forall {}. {}", vars.join(" "), type_to_string(body))
        }
    }
}

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

    fn sample_module() -> Module {
        Module {
            schema: crate::SCHEMA.into(),
            name: "sample".into(),
            imports: vec![],
            defs: vec![
                Def::Fn(FnDef {
                    name: "add".into(),
                    ty: Type::Fn {
                        params: vec![Type::int(), Type::int()],
                        ret: Box::new(Type::int()),
                        effects: vec![],
                        param_modes: vec![],
                        ret_mode: ParamMode::Implicit,
                    },
                    params: vec!["a".into(), "b".into()],
                    body: Term::App {
                        callee: Box::new(Term::Var { name: "+".into() }),
                        args: vec![
                            Term::Var { name: "a".into() },
                            Term::Var { name: "b".into() },
                        ],
                        tail: false,
                    },
                    suppress: vec![],
                    doc: None,
                }),
            ],
        }
    }

    #[test]
    fn manifest_contains_type_and_hash() {
        let s = manifest(&sample_module());
        assert!(s.contains("add"));
        assert!(s.contains("(Int, Int) -> Int"));
    }
}
