//! Renders a parsed [`Dictionary`] to a Rust source file consumed by
//! the `radius-tokio-dict` crate's `generated` module.
//!
//! Used by `build.rs` at compile time and by the in-tree determinism /
//! snapshot tests. Output is deterministic: the parser preserves source
//! order, this renderer is a pure function of its input, and no
//! timestamps or absolute paths leak in.

use std::fmt::Write;

use super::{Dictionary, Flags, Type};

/// Render `dict` to a string suitable for `include!()` inside a
/// dictionary-group submodule of `radius_tokio_dict`.
///
/// The emitted source references `AttrInfo`, `AttrKind`, and
/// `VendorInfo` from the enclosing `generated` module (imported by
/// each per-group `use super::{...}` line in `generated.rs`), plus the
/// codec wire-shape markers in `crate::typed`.
///
/// `module_label` is embedded in the file header comment for human
/// orientation and has no semantic effect.
#[must_use]
pub fn render(module_label: &str, dict: &Dictionary) -> String {
    let mut out = String::with_capacity(estimate_capacity(dict));

    writeln!(
        out,
        "// @generated by build.rs from FreeRADIUS dictionary group `{module_label}`."
    )
    .unwrap();
    writeln!(
        out,
        "// Do not edit by hand; rerun `cargo build` to refresh."
    )
    .unwrap();
    writeln!(out).unwrap();

    write_attrs_module(&mut out, dict);
    writeln!(out).unwrap();
    write_values_module(&mut out, dict);

    out
}

/// Emit `pub mod attrs { ... }` with one typed const per attribute.
///
/// Top-level attributes become `Attr<T>`; vendor attributes become
/// `VsaAttr<T>`. Children of `tlv`-typed parents (2-component OIDs,
/// e.g. `173.1`, `146.1`) become `TlvAttr<T>` (top-level parent) or
/// `VsaTlvAttr<T>` (vendor parent). Deeper nesting and children of
/// non-`tlv` parents (RFC 6929 `extended` / `long-extended`) are
/// skipped — those framings need a separate API. Each emitted const
/// inherits its `T` from the dictionary's wire type via
/// [`wire_marker`].
fn write_attrs_module(out: &mut String, dict: &Dictionary) {
    // Build a quick lookup so we can validate that a 2-component
    // child's parent is a `tlv` container before emitting a TLV
    // handle for it. Keyed by `(vendor, root_oid)` so non-vendor and
    // vendor namespaces don't collide.
    use std::collections::HashMap;
    let mut parents: HashMap<(Option<u32>, u32), Type> = HashMap::new();
    for a in &dict.attributes {
        if a.oid.0.len() == 1 {
            parents.insert((a.vendor, a.oid.0[0]), a.typ);
        }
    }

    writeln!(
        out,
        "/// Typed handles for every attribute in this dictionary group."
    )
    .unwrap();
    writeln!(out, "///").unwrap();
    writeln!(
        out,
        "/// Top-level attributes are exposed as `Attr<T>`; attributes inside a"
    )
    .unwrap();
    writeln!(
        out,
        "/// `BEGIN-VENDOR` block are exposed as `VsaAttr<T>`. TLV sub-attributes"
    )
    .unwrap();
    writeln!(
        out,
        "/// (children of a `tlv`-typed parent) are exposed as `TlvAttr<T>` or"
    )
    .unwrap();
    writeln!(
        out,
        "/// `VsaTlvAttr<T>`. Use them with `RawAttribute::get` / `get_vsa` /"
    )
    .unwrap();
    writeln!(
        out,
        "/// `get_tlv` / `get_vsa_tlv` (in `radius_tokio::attributes`)."
    )
    .unwrap();
    writeln!(
        out,
        "#[allow(unused_imports, clippy::wildcard_imports, clippy::match_single_binding, clippy::too_many_lines)]"
    )
    .unwrap();
    writeln!(out, "pub mod attrs {{").unwrap();
    writeln!(out, "    use crate::typed::*;").unwrap();
    writeln!(out).unwrap();
    for a in &dict.attributes {
        let ident = const_ident(&a.name);
        match a.oid.0.as_slice() {
            // Top-level attribute.
            [root] => {
                let Ok(code) = u8::try_from(*root) else {
                    continue;
                };
                let marker = wire_marker(a.typ, a.flags);
                match a.vendor {
                    None => writeln!(
                        out,
                        "    pub const {ident}: Attr<{marker}> = Attr::new({code});",
                    )
                    .unwrap(),
                    Some(pen) => writeln!(
                        out,
                        "    pub const {ident}: VsaAttr<{marker}> = VsaAttr::new({pen}, {code});",
                    )
                    .unwrap(),
                }
            }
            // 2-component child (`parent.child`). Emit only when the
            // parent is a `tlv` container; the `extended` /
            // `long-extended` framings (RFC 6929) are out of scope
            // for the current TLV walker.
            [parent_root, child_sub] => {
                let Ok(parent_code) = u8::try_from(*parent_root) else {
                    continue;
                };
                let Ok(child_code) = u8::try_from(*child_sub) else {
                    continue;
                };
                if parents.get(&(a.vendor, *parent_root)) != Some(&Type::Tlv) {
                    continue;
                }
                let marker = wire_marker(a.typ, a.flags);
                match a.vendor {
                    None => writeln!(
                        out,
                        "    pub const {ident}: TlvAttr<{marker}> = TlvAttr::new({parent_code}, {child_code});",
                    )
                    .unwrap(),
                    Some(pen) => writeln!(
                        out,
                        "    pub const {ident}: VsaTlvAttr<{marker}> = VsaTlvAttr::new({pen}, {parent_code}, {child_code});",
                    )
                    .unwrap(),
                }
            }
            // Deeper nesting (3+ components) — RFC 6929 extended
            // children. Out of scope for now.
            _ => {}
        }
    }

    // Diagnostic dispatchers consumed by `crate::registry`.
    write_attr_lookups(out, dict);
    write_vendor_lookup(out, dict);

    writeln!(out, "}}").unwrap();
}

/// Emit `pub const fn lookup(u8) -> Option<AttrInfo>`,
/// `pub const fn lookup_vsa(u32, u8) -> Option<AttrInfo>`, and
/// `pub const fn lookup_tlv(u8, u8) -> Option<AttrInfo>` inside the
/// surrounding `pub mod attrs { … }`.
///
/// `lookup` matches single-component, non-vendor attribute codes;
/// `lookup_vsa` matches single-component vendor attributes keyed by
/// `(pen, vendor-type)`; `lookup_tlv` matches two-component,
/// non-vendor children keyed by `(parent, child)` — used by the
/// dissector to recurse into RFC-namespace TLV containers. Deeper
/// (3+ component) and vendor-side TLV children remain reachable via
/// the typed `TlvAttr<T>` / `VsaTlvAttr<T>` consts above.
#[allow(clippy::too_many_lines)]
fn write_attr_lookups(out: &mut String, dict: &Dictionary) {
    use std::collections::BTreeMap;

    // Some FreeRADIUS dictionaries restate the same attribute code
    // across successor RFCs (or alias an old name). The legacy
    // linear-scan registry returned the *first* hit; preserve that by
    // building the lookup map with `entry().or_insert(...)`, which
    // keeps the earliest occurrence in source order. The `BTreeMap`
    // then yields the surviving entries in ascending key order, so
    // the generated `match` arms are sorted — both human-readable
    // and easier to diff across dictionary updates.

    // ── top-level (non-vendor, single-component) ───────────────────
    let mut by_code: BTreeMap<u8, &super::Attribute> = BTreeMap::new();
    for a in &dict.attributes {
        if a.vendor.is_some() || a.oid.0.len() != 1 {
            continue;
        }
        let Ok(code) = u8::try_from(a.oid.0[0]) else {
            continue;
        };
        by_code.entry(code).or_insert(a);
    }

    writeln!(out).unwrap();
    writeln!(
        out,
        "    /// Resolve a top-level attribute code (RFC namespace) to its diagnostic record."
    )
    .unwrap();
    writeln!(out, "    #[must_use]").unwrap();
    writeln!(
        out,
        "    pub const fn lookup(code: u8) -> Option<super::AttrInfo> {{"
    )
    .unwrap();
    writeln!(out, "        match code {{").unwrap();
    for (code, a) in &by_code {
        writeln!(
            out,
            "            {code}u8 => Some(super::AttrInfo {{ name: {name:?}, kind: {kind}, encrypted: {enc} }}),",
            name = a.name,
            kind = attr_kind_literal(a.typ),
            enc = a.flags.encrypt.is_some(),
        )
        .unwrap();
    }
    writeln!(out, "            _ => None,").unwrap();
    writeln!(out, "        }}").unwrap();
    writeln!(out, "    }}").unwrap();

    // ── single-component VSAs ──────────────────────────────────────
    let mut by_pen_vt: BTreeMap<(u32, u8), &super::Attribute> = BTreeMap::new();
    for a in &dict.attributes {
        let Some(pen) = a.vendor else {
            continue;
        };
        if a.oid.0.len() != 1 {
            continue;
        }
        let Ok(vt) = u8::try_from(a.oid.0[0]) else {
            continue;
        };
        by_pen_vt.entry((pen, vt)).or_insert(a);
    }

    writeln!(out).unwrap();
    writeln!(
        out,
        "    /// Resolve a Vendor-Specific attribute (`pen`, `vendor-type`) to its diagnostic record."
    )
    .unwrap();
    writeln!(out, "    #[must_use]").unwrap();
    writeln!(
        out,
        "    pub const fn lookup_vsa(pen: u32, vt: u8) -> Option<super::AttrInfo> {{"
    )
    .unwrap();
    writeln!(out, "        match (pen, vt) {{").unwrap();
    for ((pen, vt), a) in &by_pen_vt {
        writeln!(
            out,
            "            ({pen}u32, {vt}u8) => Some(super::AttrInfo {{ name: {name:?}, kind: {kind}, encrypted: {enc} }}),",
            name = a.name,
            kind = attr_kind_literal(a.typ),
            enc = a.flags.encrypt.is_some(),
        )
        .unwrap();
    }
    writeln!(out, "            _ => None,").unwrap();
    writeln!(out, "        }}").unwrap();
    writeln!(out, "    }}").unwrap();

    // ── top-level TLV children ─────────────────────────────────────
    // Children of a `type=tlv` parent in the RFC namespace. The
    // dictionary expresses these as a 2-component OID (parent.child)
    // with no owning vendor; e.g. `IPv6-6rd-Configuration.IPv4MaskLen`
    // is `173.1`. The dispatcher lets the dissector resolve each
    // inner TLV slot inside a `Tlv`-kind parent attribute.
    let mut by_parent_child: BTreeMap<(u8, u8), &super::Attribute> = BTreeMap::new();
    for a in &dict.attributes {
        if a.vendor.is_some() || a.oid.0.len() != 2 {
            continue;
        }
        let Ok(parent) = u8::try_from(a.oid.0[0]) else {
            continue;
        };
        let Ok(child) = u8::try_from(a.oid.0[1]) else {
            continue;
        };
        by_parent_child.entry((parent, child)).or_insert(a);
    }

    writeln!(out).unwrap();
    writeln!(
        out,
        "    /// Resolve a TLV child (`parent`, `child`) in the RFC namespace to its diagnostic record."
    )
    .unwrap();
    writeln!(out, "    #[must_use]").unwrap();
    writeln!(
        out,
        "    pub const fn lookup_tlv(parent: u8, child: u8) -> Option<super::AttrInfo> {{"
    )
    .unwrap();
    writeln!(out, "        match (parent, child) {{").unwrap();
    for ((parent, child), a) in &by_parent_child {
        writeln!(
            out,
            "            ({parent}u8, {child}u8) => Some(super::AttrInfo {{ name: {name:?}, kind: {kind}, encrypted: {enc} }}),",
            name = a.name,
            kind = attr_kind_literal(a.typ),
            enc = a.flags.encrypt.is_some(),
        )
        .unwrap();
    }
    writeln!(out, "            _ => None,").unwrap();
    writeln!(out, "        }}").unwrap();
    writeln!(out, "    }}").unwrap();
}

/// Emit `pub const fn lookup_vendor(u32) -> Option<VendorInfo>` inside
/// `pub mod attrs { … }`. Maps an IANA Private Enterprise Number to a
/// human-readable vendor record. Vendor framing fields (`type_len`,
/// `length_len`, `has_continuation`) are not surfaced today; no
/// caller honours non-default framing yet. Reintroduce when the
/// dissector grows `format=t,l[,c]` support.
fn write_vendor_lookup(out: &mut String, dict: &Dictionary) {
    use std::collections::BTreeMap;

    // Same first-wins + sorted-by-key emission as `write_attr_lookups`.
    let mut by_pen: BTreeMap<u32, &super::Vendor> = BTreeMap::new();
    for v in &dict.vendors {
        by_pen.entry(v.id).or_insert(v);
    }

    writeln!(out).unwrap();
    writeln!(
        out,
        "    /// Resolve an IANA Private Enterprise Number to its vendor record."
    )
    .unwrap();
    writeln!(out, "    #[must_use]").unwrap();
    writeln!(
        out,
        "    pub const fn lookup_vendor(pen: u32) -> Option<super::VendorInfo> {{"
    )
    .unwrap();
    writeln!(out, "        match pen {{").unwrap();
    for (id, v) in &by_pen {
        writeln!(
            out,
            "            {id}u32 => Some(super::VendorInfo {{ name: {name:?}, id: {id}u32 }}),",
            name = v.name,
        )
        .unwrap();
    }
    writeln!(out, "            _ => None,").unwrap();
    writeln!(out, "        }}").unwrap();
    writeln!(out, "    }}").unwrap();
}

/// Map a dictionary [`Type`] to the runtime [`AttrKind`] literal as it
/// appears in generated source. Folds `Uint32` → `Integer` and any
/// `FixedOctets(_)` / `Abinary` → `Octets`, mirroring the loss-of-
/// detail in [`crate::AttrKind`] (the dissector treats them
/// identically anyway).
fn attr_kind_literal(t: Type) -> &'static str {
    match t {
        Type::String => "super::AttrKind::String",
        Type::Octets | Type::FixedOctets(_) | Type::Abinary => "super::AttrKind::Octets",
        Type::Ipaddr => "super::AttrKind::Ipaddr",
        Type::Ipv6addr => "super::AttrKind::Ipv6addr",
        Type::Ipv4prefix => "super::AttrKind::Ipv4prefix",
        Type::Ipv6prefix => "super::AttrKind::Ipv6prefix",
        Type::Byte => "super::AttrKind::Byte",
        Type::Short => "super::AttrKind::Short",
        Type::Integer | Type::Uint32 => "super::AttrKind::Integer",
        Type::Integer64 => "super::AttrKind::Integer64",
        Type::Signed => "super::AttrKind::Signed",
        Type::Date => "super::AttrKind::Date",
        Type::Ifid => "super::AttrKind::Ifid",
        Type::Ether => "super::AttrKind::Ether",
        Type::Tlv => "super::AttrKind::Tlv",
        Type::Vsa => "super::AttrKind::Vsa",
        Type::Evs => "super::AttrKind::Evs",
        Type::Extended => "super::AttrKind::Extended",
        Type::LongExtended => "super::AttrKind::LongExtended",
        Type::Struct => "super::AttrKind::Struct",
    }
}

/// Emit `pub mod values { ... }` with one Rust newtype per
/// integer-typed attribute that carries `VALUE` enumerators, plus an
/// associated `pub const` per enumerator and an [`IntoWire`] impl
/// matching the attribute's wire shape.
///
/// Each newtype is `#[repr(transparent)]` and exposes its scalar via
/// `as_*()` / the public `.0` field. The universe is open: peers can
/// still send values that aren't in the dictionary, so the type wraps
/// the underlying scalar rather than being a closed Rust `enum`.
///
/// Attributes whose wire type is not integer-shaped (string, octets,
/// IP address, …) are skipped here — `VALUE` directives are rare on
/// those and a newtype would buy us little. Cross-dictionary `VALUE`
/// references (an enumerator pointing to an attribute defined in a
/// different group) are also skipped because the target type is
/// unknown at this codegen site.
#[allow(clippy::too_many_lines)]
fn write_values_module(out: &mut String, dict: &Dictionary) {
    use std::collections::BTreeMap;

    // Index attributes by name for value → attribute lookup. A
    // `BTreeMap` gives a deterministic iteration order matching the
    // dictionary, but we drive emission off the attribute list below
    // so we just need point lookups here.
    let mut attrs_by_name: std::collections::HashMap<&str, &super::Attribute> =
        std::collections::HashMap::with_capacity(dict.attributes.len());
    for a in &dict.attributes {
        attrs_by_name.insert(a.name.as_str(), a);
    }

    // Group values by attribute, preserving source order within each
    // group. Within a group, dedupe by enumerator name: dictionary
    // files frequently re-state the same `VALUE` line across
    // successor RFCs (e.g. `Error-Cause Invalid-Attribute-Value 407`
    // is defined identically in both rfc3576 and rfc5176). The first
    // occurrence wins; later restatements are silently dropped.
    let mut order: Vec<&str> = Vec::new();
    let mut groups: BTreeMap<&str, Vec<&super::Value>> = BTreeMap::new();
    let mut seen_names: std::collections::HashSet<(&str, &str)> = std::collections::HashSet::new();
    for v in &dict.values {
        let key = v.attribute.as_str();
        if !seen_names.insert((key, v.name.as_str())) {
            continue;
        }
        if !groups.contains_key(key) {
            order.push(key);
        }
        groups.entry(key).or_default().push(v);
    }

    writeln!(
        out,
        "/// Typed enumerators for every attribute in this dictionary group"
    )
    .unwrap();
    writeln!(out, "/// that carries one or more `VALUE` directives.").unwrap();
    writeln!(out, "///").unwrap();
    writeln!(
        out,
        "/// Each attribute becomes a `#[repr(transparent)]` newtype with one"
    )
    .unwrap();
    writeln!(
        out,
        "/// associated `pub const` per dictionary enumerator. The universe is"
    )
    .unwrap();
    writeln!(
        out,
        "/// open — peers may send unknown values, so the type wraps its scalar"
    )
    .unwrap();
    writeln!(
        out,
        "/// rather than being a closed Rust `enum`. Pattern matching against"
    )
    .unwrap();
    writeln!(
        out,
        "/// the associated consts works the same way as with `bitflags`-style"
    )
    .unwrap();
    writeln!(out, "/// types.").unwrap();
    writeln!(
        out,
        "#[allow(unused_imports, clippy::wildcard_imports, clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::match_single_binding, clippy::too_many_lines)]"
    )
    .unwrap();
    writeln!(out, "pub mod values {{").unwrap();
    writeln!(out, "    use crate::typed::*;").unwrap();

    // Record `(attribute_name, type_ident, inner_type)` for every
    // attribute we emit a newtype for, so the end-of-module dispatcher
    // can route a `(name, number)` pair to the right `T::name()` impl.
    let mut emitted: Vec<(&str, String, &'static str)> = Vec::new();

    for attr_name in order {
        let Some(attr) = attrs_by_name.get(attr_name).copied() else {
            // Cross-dictionary VALUE — emitted attribute lives in
            // a different group. The reverse-lookup table still
            // covers it via `registry::value_name`.
            continue;
        };
        let Some(shape) = value_shape(attr.typ, attr.flags) else {
            continue;
        };
        emitted.push((attr_name, pascal_ident(&attr.name), shape.inner));
        write_value_newtype(out, &attr.name, &shape, &groups[attr_name]);
    }

    // Per-group reverse-lookup dispatcher. Routes a dictionary
    // attribute name + raw integer to the matching newtype's
    // `name()` method. Used by `crate::registry::value_name` to
    // resolve enumerator names during packet dissection.
    writeln!(out).unwrap();
    writeln!(
        out,
        "    /// Resolve `attr`'s enumerator number to its dictionary name."
    )
    .unwrap();
    writeln!(out, "    ///").unwrap();
    writeln!(
        out,
        "    /// Returns `None` for unknown attributes / values. Generated"
    )
    .unwrap();
    writeln!(
        out,
        "    /// dispatcher used by `crate::registry::value_name`."
    )
    .unwrap();
    writeln!(out, "    #[must_use]").unwrap();
    writeln!(
        out,
        "    pub fn value_name(attr: &str, number: i64) -> Option<&'static str> {{"
    )
    .unwrap();
    if emitted.is_empty() {
        writeln!(out, "        let _ = (attr, number);").unwrap();
        writeln!(out, "        None").unwrap();
    } else {
        writeln!(out, "        match attr {{").unwrap();
        for (attr_name, type_ident, inner) in &emitted {
            writeln!(
                out,
                "            {attr_name:?} => {type_ident}(number as {inner}).name(),"
            )
            .unwrap();
        }
        writeln!(out, "            _ => None,").unwrap();
        writeln!(out, "        }}").unwrap();
    }
    writeln!(out, "    }}").unwrap();

    writeln!(out, "}}").unwrap();
}

/// Emit one `pub struct Foo(pub T);` newtype + its associated consts +
/// reverse-lookup + matching [`IntoWire`] impls. Called once per
/// integer-typed attribute that carries `VALUE` enumerators.
#[allow(clippy::too_many_lines)]
fn write_value_newtype(
    out: &mut String,
    attr_name: &str,
    shape: &ValueShape,
    values: &[&super::Value],
) {
    let type_ident = pascal_ident(attr_name);
    let inner = shape.inner;

    writeln!(out).unwrap();
    writeln!(
        out,
        "    /// Typed enumerator for the `{attr_name}` attribute."
    )
    .unwrap();
    writeln!(
        out,
        "    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]"
    )
    .unwrap();
    writeln!(out, "    #[repr(transparent)]").unwrap();
    writeln!(out, "    pub struct {type_ident}(pub {inner});").unwrap();
    writeln!(out).unwrap();
    writeln!(out, "    impl {type_ident} {{").unwrap();
    for v in values {
        let cident = value_const_ident(&v.name);
        let lit = scalar_literal(inner, v.number);
        writeln!(
            out,
            "        /// `{name}` (= `{n}`).",
            name = v.name,
            n = v.number
        )
        .unwrap();
        writeln!(out, "        pub const {cident}: Self = Self({lit});").unwrap();
    }
    writeln!(out).unwrap();
    writeln!(
        out,
        "        /// Underlying scalar (`{inner}`) as written on the wire."
    )
    .unwrap();
    writeln!(out, "        #[must_use]").unwrap();
    writeln!(
        out,
        "        pub const fn {as_fn}(self) -> {inner} {{ self.0 }}",
        as_fn = shape.as_fn
    )
    .unwrap();
    writeln!(out).unwrap();
    writeln!(
        out,
        "        /// Dictionary name for this value, if it matches a known enumerator."
    )
    .unwrap();
    writeln!(out, "        #[must_use]").unwrap();
    writeln!(out, "        pub fn name(self) -> Option<&'static str> {{").unwrap();
    writeln!(out, "            const TABLE: &[({inner}, &str)] = &[").unwrap();
    for v in values {
        let lit = scalar_literal(inner, v.number);
        writeln!(out, "                ({lit}, {name:?}),", name = v.name).unwrap();
    }
    writeln!(out, "            ];").unwrap();
    writeln!(
        out,
        "            TABLE.iter().find_map(|(n, s)| (*n == self.0).then_some(*s))"
    )
    .unwrap();
    writeln!(out, "        }}").unwrap();
    writeln!(out, "    }}").unwrap();
    writeln!(out).unwrap();

    // Symmetric `PartialEq` against the underlying scalar so callers
    // can write `st == ServiceType::LOGIN_USER` directly against the
    // raw integer returned by `attr.get(...)` — no `.as_u32()` round-trip.
    writeln!(out, "    impl PartialEq<{inner}> for {type_ident} {{").unwrap();
    writeln!(out, "        #[inline]").unwrap();
    writeln!(
        out,
        "        fn eq(&self, other: &{inner}) -> bool {{ self.0 == *other }}"
    )
    .unwrap();
    writeln!(out, "    }}").unwrap();
    writeln!(out, "    impl PartialEq<{type_ident}> for {inner} {{").unwrap();
    writeln!(out, "        #[inline]").unwrap();
    writeln!(
        out,
        "        fn eq(&self, other: &{type_ident}) -> bool {{ *self == other.0 }}"
    )
    .unwrap();
    writeln!(out, "    }}").unwrap();
    // `From` conversions in both directions so callers can move
    // between the newtype and its underlying scalar without poking
    // at the public `.0` field.
    writeln!(out, "    impl From<{inner}> for {type_ident} {{").unwrap();
    writeln!(out, "        #[inline]").unwrap();
    writeln!(
        out,
        "        fn from(value: {inner}) -> Self {{ Self(value) }}"
    )
    .unwrap();
    writeln!(out, "    }}").unwrap();
    writeln!(out, "    impl From<{type_ident}> for {inner} {{").unwrap();
    writeln!(out, "        #[inline]").unwrap();
    writeln!(
        out,
        "        fn from(value: {type_ident}) -> Self {{ value.0 }}"
    )
    .unwrap();
    writeln!(out, "    }}").unwrap();
    writeln!(out).unwrap();

    // IntoWire impls — one for the direct shape, plus a tagged
    // pair impl when the attribute has `has_tag`.
    let marker = shape.marker;
    if shape.tagged {
        writeln!(out, "    impl IntoWire<{marker}> for (u8, {type_ident}) {{").unwrap();
        writeln!(out, "        #[inline]").unwrap();
        writeln!(
            out,
            "        fn write_value(self, out: &mut ::std::vec::Vec<u8>) {{"
        )
        .unwrap();
        writeln!(
            out,
            "            <(u8, {inner}) as IntoWire<{marker}>>::write_value((self.0, self.1.0), out);"
        )
        .unwrap();
        writeln!(out, "        }}").unwrap();
        writeln!(out, "    }}").unwrap();
        writeln!(
            out,
            "    impl IntoWire<{marker}> for Tagged<{type_ident}> {{"
        )
        .unwrap();
        writeln!(out, "        #[inline]").unwrap();
        writeln!(
            out,
            "        fn write_value(self, out: &mut ::std::vec::Vec<u8>) {{"
        )
        .unwrap();
        writeln!(
            out,
            "            Tagged {{ tag: self.tag, value: self.value.0 }}.write_value(out);"
        )
        .unwrap();
        writeln!(out, "        }}").unwrap();
        writeln!(out, "    }}").unwrap();
    } else {
        writeln!(out, "    impl IntoWire<{marker}> for {type_ident} {{").unwrap();
        writeln!(out, "        #[inline]").unwrap();
        writeln!(
            out,
            "        fn write_value(self, out: &mut ::std::vec::Vec<u8>) {{"
        )
        .unwrap();
        writeln!(
            out,
            "            <{inner} as IntoWire<{marker}>>::write_value(self.0, out);"
        )
        .unwrap();
        writeln!(out, "        }}").unwrap();
        writeln!(out, "    }}").unwrap();
    }
}

/// Wire-shape descriptor for an attribute that carries `VALUE`
/// enumerators. Returned by [`value_shape`].
struct ValueShape {
    /// Rust scalar literal used as the newtype's inner type.
    inner: &'static str,
    /// Codec wire-type marker matching the attribute (`WInteger`, …).
    marker: &'static str,
    /// Whether the attribute is [`Flags::has_tag`] (changes the `IntoWire` shape).
    tagged: bool,
    /// Conventional `as_*` accessor name for the inner scalar.
    as_fn: &'static str,
}

/// Map an attribute's type+flags to a [`ValueShape`] when its wire
/// form is integer-like. Returns `None` for strings, octets, IP
/// addresses, etc. — those don't get a typed enumerator newtype.
fn value_shape(t: Type, flags: Flags) -> Option<ValueShape> {
    // Encrypted attributes ship cipher-shaped values; a typed
    // enumerator over their post-decrypt form is the user's job.
    if flags.encrypt.is_some() {
        return None;
    }
    if flags.has_tag {
        return match t {
            Type::Integer | Type::Uint32 | Type::Date => Some(ValueShape {
                inner: "u32",
                marker: "WTaggedInteger",
                tagged: true,
                as_fn: "as_u32",
            }),
            _ => None,
        };
    }
    Some(match t {
        Type::Byte => ValueShape {
            inner: "u8",
            marker: "WByte",
            tagged: false,
            as_fn: "as_u8",
        },
        Type::Short => ValueShape {
            inner: "u16",
            marker: "WShort",
            tagged: false,
            as_fn: "as_u16",
        },
        Type::Integer | Type::Uint32 | Type::Date => ValueShape {
            inner: "u32",
            marker: "WInteger",
            tagged: false,
            as_fn: "as_u32",
        },
        Type::Integer64 => ValueShape {
            inner: "u64",
            marker: "WInteger64",
            tagged: false,
            as_fn: "as_u64",
        },
        Type::Signed => ValueShape {
            inner: "i32",
            marker: "WSigned",
            tagged: false,
            as_fn: "as_i32",
        },
        _ => return None,
    })
}

/// Cast a dictionary `VALUE` number (always parsed as `i64`) to the
/// concrete Rust scalar literal matching the attribute's wire type.
/// Truncation / two's-complement reinterpretation follows the same
/// rules the codec uses on the wire.
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn scalar_literal(inner: &str, n: i64) -> String {
    match inner {
        "u8" => format!("{}u8", n as u8),
        "u16" => format!("{}u16", n as u16),
        "u32" => format!("{}u32", n as u32),
        "u64" => format!("{}u64", n as u64),
        "i32" => format!("{}i32", n as i32),
        _ => format!("{n}"),
    }
}

/// Convert a dictionary attribute name (`Tunnel-Type`,
/// `NAS-Port-Type`, `MS-CHAP2-Response`) into a `PascalCase` Rust
/// type identifier (`TunnelType`, `NasPortType`, `MsChap2Response`).
///
/// Each `-`-delimited segment is title-cased: first ASCII letter
/// uppercase, remaining ASCII letters lowercased. Digits and
/// non-ASCII characters pass through. Leading digits are prefixed
/// with `_` to keep the result a valid identifier.
fn pascal_ident(name: &str) -> String {
    let mut out = String::with_capacity(name.len());
    let mut first = true;
    for segment in name.split(['-', '.']) {
        if segment.is_empty() {
            continue;
        }
        let mut chars = segment.chars();
        if let Some(c) = chars.next() {
            if first && c.is_ascii_digit() {
                out.push('_');
            }
            first = false;
            if c.is_ascii_lowercase() {
                out.push(c.to_ascii_uppercase());
            } else {
                out.push(c);
            }
        }
        for c in chars {
            if c.is_ascii_uppercase() {
                out.push(c.to_ascii_lowercase());
            } else {
                out.push(c);
            }
        }
    }
    out
}

/// Convert a dictionary `VALUE` name into a `SCREAMING_SNAKE_CASE`
/// associated-const identifier. Same shape as [`const_ident`] but
/// also maps `.` to `_` and prefixes a leading digit with `_` so the
/// result is always a valid Rust identifier.
fn value_const_ident(name: &str) -> String {
    let mut out = String::with_capacity(name.len() + 1);
    let mut first = true;
    for ch in name.chars() {
        if first {
            if ch.is_ascii_digit() {
                out.push('_');
            }
            first = false;
        }
        match ch {
            '-' | '.' | '/' | ' ' => out.push('_'),
            c if c.is_ascii_lowercase() => out.push(c.to_ascii_uppercase()),
            c => out.push(c),
        }
    }
    out
}

/// Convert a dictionary attribute name (`Cisco-AVPair`) into a Rust
/// constant identifier (`CISCO_AVPAIR`). Replaces `-` with `_` and
/// upper-cases ASCII letters; non-ASCII bytes pass through unchanged.
fn const_ident(name: &str) -> String {
    let mut out = String::with_capacity(name.len());
    for ch in name.chars() {
        match ch {
            '-' => out.push('_'),
            c if c.is_ascii_lowercase() => out.push(c.to_ascii_uppercase()),
            c => out.push(c),
        }
    }
    out
}

/// Map a dictionary [`Type`] (plus its flags) to the codec wire-type
/// marker that decodes it.
///
/// Three shapes feed in here:
///
/// - **Encrypted** attributes (`encrypt=N`) carry a cipher-shaped
///   value (e.g. `User-Password`'s xor-MD5 stream, `Tunnel-Password`'s
///   `tag || salt || ciphertext`) that has no meaning as the parent
///   dictionary type. They map to [`WBytes`] so a typed `get(...)`
///   yields the raw octets and callers route plaintext through the
///   dedicated `Reply::add_*_password` helpers.
/// - **Tagged** attributes (`has_tag`, RFC 2868 §3) get the
///   tag-aware markers so consumers see a [`Tagged<V>`] view and
///   never have to peel the tag byte themselves.
/// - Everything else maps directly to its scalar marker; container /
///   variable-shape types fall back to [`WBytes`] until a TLV walker
///   lands.
fn wire_marker(t: Type, flags: Flags) -> &'static str {
    // Encrypted attributes ship cipher-shaped values that don't match
    // the source dictionary type. Force raw octets so callers route
    // through the dedicated decrypt helpers (e.g. add_tunnel_password)
    // instead of UTF-8-decoding ciphertext.
    if flags.encrypt.is_some() {
        return "WBytes";
    }
    if flags.has_tag {
        return match t {
            Type::String => "WTaggedText",
            Type::Integer | Type::Uint32 | Type::Date => "WTaggedInteger",
            // No other RFC-defined types currently combine with
            // `has_tag`; fall through to raw octets if a dictionary
            // surprises us so we don't silently strip a tag byte.
            _ => "WBytes",
        };
    }
    match t {
        Type::String => "WText",
        Type::Byte => "WByte",
        Type::Short => "WShort",
        Type::Integer | Type::Uint32 | Type::Date => "WInteger",
        Type::Integer64 => "WInteger64",
        Type::Signed => "WSigned",
        Type::Ipaddr => "WIpv4",
        Type::Ipv6addr => "WIpv6",
        Type::Ether => "WEther",
        Type::Ifid => "WIfid",
        // Container / variable-shape types decode as raw octets for now.
        Type::Octets
        | Type::FixedOctets(_)
        | Type::Abinary
        | Type::Ipv4prefix
        | Type::Ipv6prefix
        | Type::Tlv
        | Type::Vsa
        | Type::Evs
        | Type::Extended
        | Type::LongExtended
        | Type::Struct => "WBytes",
    }
}

fn estimate_capacity(dict: &Dictionary) -> usize {
    // Rough lower bound; saves repeated reallocations on the larger
    // generated files (RFC: ~1k attributes, vendor: several thousand).
    256 + dict.vendors.len() * 96 + dict.attributes.len() * 192 + dict.values.len() * 96
}

#[cfg(test)]
mod tests {
    use super::super::{Attribute, Dictionary, Flags, Oid, Type, Value, Vendor};
    use super::*;

    fn fixture() -> Dictionary {
        Dictionary {
            vendors: vec![Vendor {
                name: "Acme".into(),
                id: 99,
                type_len: 1,
                length_len: 1,
                has_continuation: false,
            }],
            attributes: vec![
                Attribute {
                    name: "User-Name".into(),
                    oid: Oid(vec![1]),
                    vendor: None,
                    typ: Type::String,
                    flags: Flags::default(),
                },
                Attribute {
                    name: "Service-Type".into(),
                    oid: Oid(vec![6]),
                    vendor: None,
                    typ: Type::Integer,
                    flags: Flags::default(),
                },
                Attribute {
                    name: "Acme-Token".into(),
                    oid: Oid(vec![1]),
                    vendor: Some(99),
                    typ: Type::Octets,
                    flags: Flags {
                        encrypt: Some(2),
                        has_tag: true,
                        ..Flags::default()
                    },
                },
            ],
            values: vec![Value {
                attribute: "Service-Type".into(),
                name: "Framed-User".into(),
                number: 2,
            }],
        }
    }

    #[test]
    fn render_is_deterministic() {
        let d = fixture();
        assert_eq!(render("test", &d), render("test", &d));
    }

    #[test]
    fn render_emits_expected_sections() {
        let s = render("test", &fixture());
        // Diagnostic dispatchers live inside `pub mod attrs`.
        assert!(s.contains("pub mod attrs"));
        assert!(s.contains("pub const fn lookup(code: u8) -> Option<super::AttrInfo>"));
        assert!(s.contains("pub const fn lookup_vsa(pen: u32, vt: u8) -> Option<super::AttrInfo>"));
        assert!(s.contains("pub const fn lookup_vendor(pen: u32) -> Option<super::VendorInfo>"));
        // The old `pub static` slices and `Type`/`Flags`/`AttributeEntry`
        // record literals have been retired.
        assert!(!s.contains("pub static VENDORS"));
        assert!(!s.contains("pub static ATTRIBUTES"));
        assert!(!s.contains("pub static VALUES"));
        assert!(!s.contains("AttributeEntry"));
        assert!(!s.contains("VendorEntry"));
        assert!(!s.contains("super::Type::"));
        assert!(!s.contains("super::Flags"));
        // Values dispatcher is unchanged.
        assert!(s.contains("pub mod values"));
        assert!(s.contains("pub fn value_name"));
        // Attribute and vendor names appear inside the lookup matches.
        assert!(s.contains(r#"name: "Acme""#));
        assert!(s.contains(r#"name: "User-Name""#));
        // Vendor record dispatch.
        assert!(s.contains("99u32 => Some(super::VendorInfo"));
        // VSA dispatch keyed by (pen, vt).
        assert!(s.contains("(99u32, 1u8) => Some(super::AttrInfo"));
        // Encrypted attribute surfaces `encrypted: true`.
        assert!(s.contains("encrypted: true"));
        // Enumerator name lives in the per-newtype reverse-lookup table.
        assert!(s.contains("Framed-User"));
    }

    #[test]
    fn render_emits_typed_attrs_module() {
        let s = render("test", &fixture());
        // Module wrapper.
        assert!(s.contains("pub mod attrs"));
        // Top-level attribute → Attr<WText>.
        assert!(s.contains("pub const USER_NAME: Attr<WText> = Attr::new(1);"));
        // Vendor attribute → VsaAttr<WBytes> (octets); PEN + code in order.
        assert!(s.contains("pub const ACME_TOKEN: VsaAttr<WBytes> = VsaAttr::new(99, 1);"));
    }

    #[test]
    fn const_ident_normalizes_names() {
        assert_eq!(super::const_ident("User-Name"), "USER_NAME");
        assert_eq!(super::const_ident("Cisco-AVPair"), "CISCO_AVPAIR");
        assert_eq!(super::const_ident("MS-CHAP2-Response"), "MS_CHAP2_RESPONSE");
    }

    #[test]
    fn pascal_ident_pascalizes_names() {
        assert_eq!(super::pascal_ident("Tunnel-Type"), "TunnelType");
        assert_eq!(super::pascal_ident("NAS-Port-Type"), "NasPortType");
        assert_eq!(super::pascal_ident("MS-CHAP2-Response"), "MsChap2Response");
        assert_eq!(super::pascal_ident("Acct-Status-Type"), "AcctStatusType");
        // Leading digit gets prefixed so the result is a valid ident.
        assert_eq!(super::pascal_ident("3GPP-IMSI"), "_3gppImsi");
    }

    #[test]
    fn value_const_ident_normalizes_value_names() {
        assert_eq!(super::value_const_ident("Framed-User"), "FRAMED_USER");
        assert_eq!(super::value_const_ident("IEEE-802"), "IEEE_802");
        assert_eq!(super::value_const_ident("IP-in-IP"), "IP_IN_IP");
        // Leading digit gets prefixed.
        assert_eq!(super::value_const_ident("3DES"), "_3DES");
    }

    #[test]
    fn render_emits_values_module_with_newtypes() {
        // Two attributes with VALUE enumerators: one tagged
        // (`Tunnel-Type` → `WTaggedInteger`) and one plain
        // (`Service-Type` → `WInteger`).
        let d = Dictionary {
            vendors: vec![],
            attributes: vec![
                Attribute {
                    name: "Service-Type".into(),
                    oid: Oid(vec![6]),
                    vendor: None,
                    typ: Type::Integer,
                    flags: Flags::default(),
                },
                Attribute {
                    name: "Tunnel-Type".into(),
                    oid: Oid(vec![64]),
                    vendor: None,
                    typ: Type::Integer,
                    flags: Flags {
                        has_tag: true,
                        ..Flags::default()
                    },
                },
            ],
            values: vec![
                Value {
                    attribute: "Service-Type".into(),
                    name: "Framed-User".into(),
                    number: 2,
                },
                Value {
                    attribute: "Tunnel-Type".into(),
                    name: "VLAN".into(),
                    number: 13,
                },
                Value {
                    attribute: "Tunnel-Type".into(),
                    name: "PPTP".into(),
                    number: 1,
                },
            ],
        };
        let s = render("test", &d);
        // Module wrapper present.
        assert!(s.contains("pub mod values"));
        // Newtypes for each attribute, PascalCased.
        assert!(s.contains("pub struct ServiceType(pub u32);"));
        assert!(s.contains("pub struct TunnelType(pub u32);"));
        // Associated consts present with the right values.
        assert!(s.contains("pub const FRAMED_USER: Self = Self(2u32);"));
        assert!(s.contains("pub const VLAN: Self = Self(13u32);"));
        assert!(s.contains("pub const PPTP: Self = Self(1u32);"));
        // Plain integer attribute → IntoWire<WInteger> impl on the newtype.
        assert!(s.contains("impl IntoWire<WInteger> for ServiceType"));
        // Symmetric PartialEq with the inner scalar so callers can
        // compare an `attr.get(...)` result directly against a const.
        assert!(s.contains("impl PartialEq<u32> for ServiceType"));
        assert!(s.contains("impl PartialEq<ServiceType> for u32"));
        // `From` round-trips between the newtype and its inner scalar.
        assert!(s.contains("impl From<u32> for ServiceType"));
        assert!(s.contains("impl From<ServiceType> for u32"));
        // Tagged attribute → IntoWire<WTaggedInteger> for (u8, NewType).
        assert!(s.contains("impl IntoWire<WTaggedInteger> for (u8, TunnelType)"));
        assert!(s.contains("impl IntoWire<WTaggedInteger> for Tagged<TunnelType>"));
        // Reverse-lookup accessor.
        assert!(s.contains("pub fn name(self) -> Option<&'static str>"));
        // Group-level dispatcher routes (name, number) into the right
        // newtype's `name()` impl.
        assert!(s.contains("pub fn value_name(attr: &str, number: i64) -> Option<&'static str>"));
        assert!(s.contains("\"Service-Type\" => ServiceType(number as u32).name()"));
        assert!(s.contains("\"Tunnel-Type\" => TunnelType(number as u32).name()"));
    }

    #[test]
    fn values_module_skips_non_integer_attributes() {
        // String attribute with a (rare) VALUE entry — we currently
        // skip these; the reverse-lookup table still covers them.
        let d = Dictionary {
            vendors: vec![],
            attributes: vec![Attribute {
                name: "Login-Service".into(),
                oid: Oid(vec![15]),
                vendor: None,
                typ: Type::String,
                flags: Flags::default(),
            }],
            values: vec![Value {
                attribute: "Login-Service".into(),
                name: "Telnet".into(),
                number: 0,
            }],
        };
        let s = render("test", &d);
        assert!(!s.contains("pub struct LoginService"));
    }

    #[test]
    fn values_module_skips_cross_dictionary_values() {
        // VALUE pointing to an attribute defined in some other
        // dictionary group. The codegen here can't know the target
        // type, so the newtype is skipped.
        let d = Dictionary {
            vendors: vec![],
            attributes: vec![],
            values: vec![Value {
                attribute: "Unknown-Foreign-Attr".into(),
                name: "Whatever".into(),
                number: 1,
            }],
        };
        let s = render("test", &d);
        assert!(!s.contains("UnknownForeignAttr"));
    }

    #[test]
    fn tagged_attributes_get_tagged_markers() {
        let d = Dictionary {
            vendors: vec![],
            attributes: vec![
                Attribute {
                    name: "Tunnel-Type".into(),
                    oid: Oid(vec![64]),
                    vendor: None,
                    typ: Type::Integer,
                    flags: Flags {
                        has_tag: true,
                        ..Flags::default()
                    },
                },
                Attribute {
                    name: "Tunnel-Client-Endpoint".into(),
                    oid: Oid(vec![66]),
                    vendor: None,
                    typ: Type::String,
                    flags: Flags {
                        has_tag: true,
                        ..Flags::default()
                    },
                },
                // `has_tag,encrypt=2` — encryption wins; the typed handle
                // exposes raw octets so callers go through the dedicated
                // tunnel-password helper instead.
                Attribute {
                    name: "Tunnel-Password".into(),
                    oid: Oid(vec![69]),
                    vendor: None,
                    typ: Type::String,
                    flags: Flags {
                        has_tag: true,
                        encrypt: Some(2),
                        ..Flags::default()
                    },
                },
            ],
            values: vec![],
        };
        let s = render("test", &d);
        assert!(s.contains("pub const TUNNEL_TYPE: Attr<WTaggedInteger>"));
        assert!(s.contains("pub const TUNNEL_CLIENT_ENDPOINT: Attr<WTaggedText>"));
        assert!(s.contains("pub const TUNNEL_PASSWORD: Attr<WBytes>"));
    }

    #[test]
    fn tlv_children_emit_typed_handles() {
        let d = Dictionary {
            vendors: vec![Vendor {
                name: "Ruckus".into(),
                id: 25053,
                type_len: 1,
                length_len: 1,
                has_continuation: false,
            }],
            attributes: vec![
                // Top-level `tlv` parent (RFC 6930 IPv6-6rd-Configuration).
                Attribute {
                    name: "IPv6-6rd-Configuration".into(),
                    oid: Oid(vec![173]),
                    vendor: None,
                    typ: Type::Tlv,
                    flags: Flags::default(),
                },
                // Child of the top-level parent.
                Attribute {
                    name: "IPv6-6rd-IPv4MaskLen".into(),
                    oid: Oid(vec![173, 1]),
                    vendor: None,
                    typ: Type::Integer,
                    flags: Flags::default(),
                },
                // Vendor TLV parent.
                Attribute {
                    name: "Ruckus-TC-Attr-Ids-With-Quota".into(),
                    oid: Oid(vec![146]),
                    vendor: Some(25053),
                    typ: Type::Tlv,
                    flags: Flags::default(),
                },
                // Vendor TLV child.
                Attribute {
                    name: "Ruckus-TC-Name-Quota".into(),
                    oid: Oid(vec![146, 1]),
                    vendor: Some(25053),
                    typ: Type::String,
                    flags: Flags::default(),
                },
                // Child of a non-tlv parent — must be skipped.
                Attribute {
                    name: "Skip-Me".into(),
                    oid: Oid(vec![200]),
                    vendor: None,
                    typ: Type::Extended,
                    flags: Flags::default(),
                },
                Attribute {
                    name: "Skip-Me-Child".into(),
                    oid: Oid(vec![200, 1]),
                    vendor: None,
                    typ: Type::Integer,
                    flags: Flags::default(),
                },
            ],
            values: vec![],
        };
        let s = render("test", &d);
        // Top-level TLV child gets a `TlvAttr<T>`.
        assert!(
            s.contains("pub const IPV6_6RD_IPV4MASKLEN: TlvAttr<WInteger> = TlvAttr::new(173, 1);")
        );
        // Vendor TLV child gets a `VsaTlvAttr<T>` carrying PEN +
        // parent type + child sub-type.
        assert!(s.contains(
            "pub const RUCKUS_TC_NAME_QUOTA: VsaTlvAttr<WText> = VsaTlvAttr::new(25053, 146, 1);"
        ));
        // Children of non-`tlv` parents (e.g. `extended`) are not
        // emitted yet.
        assert!(
            !s.contains("SKIP_ME_CHILD"),
            "non-tlv extended children must be skipped",
        );
    }
}
