//! XML 1.0 grounding sources → Rust ontology data codegen.
//!
//! Loads two W3C-published files at build time:
//!
//! - **W3C XML namespace XSD** (`https://www.w3.org/2001/xml.xsd`) —
//!   the canonical machine-readable definition of `xml:lang`,
//!   `xml:space`, `xml:base`, `xml:id`. Text-scanned for
//!   `<xs:attribute name="...">` declarations.
//! - **W3C XML Information Set rec** (Cowan & Tobin 2004; XHTML 1.0
//!   Transitional publication format). The 11 information items are
//!   extracted by walking the `<h3><a name="infoitem.<id>">N.N. <Title></a></h3>`
//!   section-heading structure.
//!
//! Per "bottom-up loaded, never encoded": every Tier 2+ ontology
//! type derives from a registered authoritative source, not from
//! Rust enum variants written by hand. Both sources are hash-pinned
//! in `praxis.lock`.
//!
//! ## Citations
//!
//! - **Bray, Hollander, Layman, Tobin & Thompson (eds.) (2009)**
//!   *Namespaces in XML 1.0 (Third Edition)*, W3C Recommendation 8
//!   December 2009. <https://www.w3.org/TR/xml-names/>.
//! - **Bray, Paoli, Sperberg-McQueen, Maler & Yergeau (eds.) (2008)**
//!   *Extensible Markup Language (XML) 1.0 (Fifth Edition)*, W3C
//!   Recommendation 26 November 2008. <https://www.w3.org/TR/xml/>.
//! - **Cowan, J. & Tobin, R. (eds.) (2004)** *XML Information Set
//!   (Second Edition)*, W3C Recommendation 4 February 2004.
//!   <https://www.w3.org/TR/xml-infoset/>.
//! - **Marsh, J. & Tobin, R. (eds.) (2009)** *XML Base (Second
//!   Edition)*, W3C Recommendation 28 January 2009.
//!   <https://www.w3.org/TR/xmlbase/>.

use std::path::Path;

/// Errors returned by the XML-1.0 codegen entry points.
#[derive(Debug)]
pub enum XmlSchemaCodegenError {
    /// Could not read the source file from disk.
    ReadSource(String, std::io::Error),
    /// The expected scan produced zero hits — the source bundle is
    /// broken or the scan logic is out of date.
    EmptyScan(String),
}

impl core::fmt::Display for XmlSchemaCodegenError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::ReadSource(p, e) => write!(f, "read source {p}: {e}"),
            Self::EmptyScan(what) => write!(f, "empty scan for {what}"),
        }
    }
}

impl std::error::Error for XmlSchemaCodegenError {}

// =============================================================================
// xml.xsd → Rust source
// =============================================================================

/// Read `xml.xsd` from disk and emit a Rust source file declaring
/// `pub const XML_NAMESPACE_ATTRIBUTES: &[&str]` with the four
/// reserved attribute names (`base`, `id`, `lang`, `space`) in
/// alphabetical order.
///
/// Suitable for writing into `$OUT_DIR/xml_namespace_schema_generated.rs`
/// from a build script.
///
/// # Errors
///
/// Returns [`XmlSchemaCodegenError::ReadSource`] if the XSD can't be
/// read, or [`XmlSchemaCodegenError::EmptyScan`] if the file
/// contains no `<xs:attribute name=...>` declarations.
pub fn generate_xml_namespace_schema_source(
    xsd_path: &Path,
) -> Result<String, XmlSchemaCodegenError> {
    let xsd = std::fs::read_to_string(xsd_path)
        .map_err(|e| XmlSchemaCodegenError::ReadSource(xsd_path.display().to_string(), e))?;

    let names = scan_xsd_attribute_names(&xsd);
    if names.is_empty() {
        return Err(XmlSchemaCodegenError::EmptyScan(
            "xml.xsd <xs:attribute name=...>".to_string(),
        ));
    }

    let mut out = String::new();
    out.push_str(
        "// Generated by pr4xis::codegen::xml_schemas::generate_xml_namespace_schema_source.\n",
    );
    out.push_str(
        "// Source: W3C xml.xsd — the W3C-published xml namespace schema.\n\
         // Citation: Bray, Hollander, Layman, Tobin & Thompson (2009) Namespaces in XML 1.0\n\
         // (Third Edition), W3C Recommendation; Bray et al. (2008) XML 1.0 Fifth Edition\n\
         // §2.10 (xml:space) + §2.12 (xml:lang); Marsh & Tobin (2009) XML Base.\n",
    );
    out.push_str(&format!("// Attributes loaded: {}\n", names.len()));
    out.push_str("pub const XML_NAMESPACE_ATTRIBUTES: &[&str] = &[\n");
    for name in &names {
        out.push_str(&format!("    {:?},\n", name));
    }
    out.push_str("];\n");
    Ok(out)
}

/// Scan an XSD source string for `<xs:attribute name="...">`
/// declarations and return the names in alphabetical order. Mirrors
/// the same minimal text-scan that the XHTML loader uses; the XSD's
/// structural well-formedness is a build-time invariant.
fn scan_xsd_attribute_names(xsd: &str) -> Vec<String> {
    const NEEDLE: &str = "<xs:attribute name=\"";
    let mut out = std::collections::BTreeSet::new();
    let mut rest = xsd;
    while let Some(idx) = rest.find(NEEDLE) {
        let after = &rest[idx + NEEDLE.len()..];
        if let Some(end) = after.find('"') {
            out.insert(after[..end].to_string());
            rest = &after[end..];
        } else {
            break;
        }
    }
    out.into_iter().collect()
}

// =============================================================================
// xml-infoset.xhtml → Rust source
// =============================================================================

/// Read the XML Information Set rec (XHTML 1.0 Transitional) from
/// disk and emit a Rust source file declaring
/// `pub const XML_INFOSET_INFORMATION_ITEMS: &[InformationItemEntry]`
/// with the 11 information items in section order. Each entry
/// carries the section number (e.g. `"2.1"`), the section anchor
/// (e.g. `"infoitem.document"`), the canonical English name (e.g.
/// `"Document"`), and the Rust enum-variant identifier (e.g.
/// `"DocumentItem"`).
///
/// Suitable for writing into `$OUT_DIR/xml_infoset_generated.rs`
/// from a build script.
///
/// # Errors
///
/// Returns [`XmlSchemaCodegenError::ReadSource`] if the XHTML file
/// can't be read, or [`XmlSchemaCodegenError::EmptyScan`] if the
/// section-heading scan finds zero information items.
pub fn generate_xml_infoset_source(xhtml_path: &Path) -> Result<String, XmlSchemaCodegenError> {
    let xhtml = std::fs::read_to_string(xhtml_path)
        .map_err(|e| XmlSchemaCodegenError::ReadSource(xhtml_path.display().to_string(), e))?;

    let items = scan_infoset_items(&xhtml);
    if items.is_empty() {
        return Err(XmlSchemaCodegenError::EmptyScan(
            "xml-infoset.xhtml <h3><a name=\"infoitem.*\">...</a></h3>".to_string(),
        ));
    }

    let mut out = String::new();
    out.push_str("// Generated by pr4xis::codegen::xml_schemas::generate_xml_infoset_source.\n");
    out.push_str(
        "// Source: W3C XML Information Set (Second Edition), W3C Recommendation\n\
         // 4 February 2004. Editors: Cowan & Tobin.\n\
         // Information items per §2 — the 11-item conceptual taxonomy.\n",
    );
    out.push_str(&format!("// Items loaded: {}\n", items.len()));
    out.push_str(
        "/// One information item extracted from the published rec's section\n\
         /// hierarchy. `section` is the dotted number (e.g. `\"2.1\"`),\n\
         /// `anchor` is the `<a name=\"...\">` identifier (e.g.\n\
         /// `\"infoitem.document\"`), `english_name` is the canonical\n\
         /// English head-noun phrase (e.g. `\"Document\"`),\n\
         /// `variant_ident` is the Rust enum-variant identifier (e.g.\n\
         /// `\"DocumentItem\"`).\n\
         #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n\
         pub struct InformationItemEntry {\n    \
            pub section: &'static str,\n    \
            pub anchor: &'static str,\n    \
            pub english_name: &'static str,\n    \
            pub variant_ident: &'static str,\n\
         }\n\n",
    );
    out.push_str("pub const XML_INFOSET_INFORMATION_ITEMS: &[InformationItemEntry] = &[\n");
    for it in &items {
        out.push_str(&format!(
            "    InformationItemEntry {{ section: {:?}, anchor: {:?}, english_name: {:?}, variant_ident: {:?} }},\n",
            it.section, it.anchor, it.english_name, it.variant_ident,
        ));
    }
    out.push_str("];\n");
    Ok(out)
}

/// One information item extracted from the published rec's section
/// hierarchy. Internal to this module; the emitted const carries
/// the same fields via `XML_INFOSET_INFORMATION_ITEMS` (generated into
/// the consumer crate at build time — not a symbol in this crate).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScannedItem {
    pub section: String,
    pub anchor: String,
    pub english_name: String,
    pub variant_ident: String,
}

/// Scan the Infoset rec's XHTML body for `<h3>...<a name="infoitem.*">...</a></h3>`
/// section headings and return one [`ScannedItem`] per match in
/// document order.
///
/// The W3C-published XHTML uses a regular pattern:
///
/// ```html
/// <h3><a name="infoitem.document">2.1. The Document Information Item</a></h3>
/// <h3><a name="infoitem.element">2.2. Element Information Items</a></h3>
/// ```
///
/// We walk each occurrence of `<a name="infoitem.`, extract the
/// anchor suffix, then continue past `">` to the title text up to
/// `</a>`. The leading dotted section number is split off; the
/// remaining "The X Information Item" / "X Information Items"
/// pattern is reduced to its head-noun phrase.
pub fn scan_infoset_items(xhtml: &str) -> Vec<ScannedItem> {
    const ANCHOR_PREFIX: &str = "<a name=\"infoitem.";
    let mut out = Vec::new();
    let mut rest = xhtml;
    while let Some(idx) = rest.find(ANCHOR_PREFIX) {
        let after = &rest[idx + ANCHOR_PREFIX.len()..];
        // Anchor suffix up to the closing `"`.
        let Some(quote_end) = after.find('"') else {
            break;
        };
        let anchor_suffix = &after[..quote_end];
        // Skip the rest of the `<a ...>` opening tag — find the next `>`.
        let Some(gt_end) = after.find('>') else {
            break;
        };
        // Title text from `gt_end + 1` up to `</a>`.
        let body_start = gt_end + 1;
        let body = &after[body_start..];
        let Some(close_idx) = body.find("</a>") else {
            break;
        };
        let title_raw = body[..close_idx].trim();
        // Move `rest` past the close tag.
        rest = &body[close_idx + "</a>".len()..];

        // Parse the title: "2.1. The Document Information Item" /
        // "2.2 Element Information Items" / "2.10 Notation
        // Information Items" — the published rec mixes "X." and "X"
        // for the trailing dot on the section number.
        let (section, title_tail) = match split_section_number(title_raw) {
            Some(p) => p,
            None => continue,
        };
        let english_name = reduce_to_head_noun(&title_tail);
        let variant_ident = make_variant_ident(&english_name);
        let anchor = format!("infoitem.{}", anchor_suffix);
        out.push(ScannedItem {
            section,
            anchor,
            english_name,
            variant_ident,
        });
    }
    out
}

/// Split a section title into (number, rest-of-title), accepting
/// both `"2.1. Foo"` and `"2.1 Foo"` shapes. Returns `None` if the
/// title doesn't start with `<digit>(.<digit>)*[.]? <text>`.
fn split_section_number(s: &str) -> Option<(String, String)> {
    let mut chars = s.char_indices();
    let mut last_digit_or_dot = None;
    for (i, c) in chars.by_ref() {
        if c.is_ascii_digit() || c == '.' {
            last_digit_or_dot = Some(i);
            continue;
        }
        // First non-digit-non-dot character ends the number span.
        last_digit_or_dot?;
        // Number ends at `i`, trim trailing dot if any.
        let end = last_digit_or_dot.unwrap() + 1;
        let raw_num = s[..end].trim_end_matches('.');
        let tail = s[end..].trim_start();
        return Some((raw_num.to_string(), tail.to_string()));
    }
    None
}

/// Reduce "The Document Information Item" → "Document",
/// "Element Information Items" → "Element",
/// "The Document Type Declaration Information Item" → "Document Type Declaration",
/// etc. The "Information Item(s)" suffix is fixed; a leading "The"
/// article is dropped per English noun-phrase grammar (Huddleston &
/// Pullum 2002 §5).
fn reduce_to_head_noun(s: &str) -> String {
    let trimmed = s.trim();
    let without_suffix = trimmed
        .trim_end_matches("Information Items")
        .trim_end_matches("Information Item")
        .trim();
    without_suffix
        .strip_prefix("The ")
        .unwrap_or(without_suffix)
        .trim()
        .to_string()
}

/// Build a Rust-identifier-safe enum-variant name with the
/// `<HeadNoun>Item` shape: "Document" → "DocumentItem",
/// "Document Type Declaration" → "DocumentTypeDeclarationItem",
/// "Unexpanded Entity Reference" → "UnexpandedEntityReferenceItem".
fn make_variant_ident(english_name: &str) -> String {
    let mut out = String::new();
    let mut capitalize_next = true;
    for c in english_name.chars() {
        if c.is_ascii_whitespace() || c == '-' {
            capitalize_next = true;
            continue;
        }
        if capitalize_next {
            out.extend(c.to_uppercase());
            capitalize_next = false;
        } else {
            out.push(c);
        }
    }
    out.push_str("Item");
    out
}

// =============================================================================
// Tests
// =============================================================================

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

    #[test]
    fn scan_xsd_attribute_names_finds_lang_space_base_id() {
        let xsd = r#"<?xml version="1.0"?>
            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
              <xs:attribute name="lang" />
              <xs:attribute name="space" />
              <xs:attribute name="base" type="xs:anyURI" />
              <xs:attribute name="id" type="xs:ID" />
            </xs:schema>"#;
        let names = scan_xsd_attribute_names(xsd);
        assert_eq!(names, vec!["base", "id", "lang", "space"]);
    }

    #[test]
    fn scan_xsd_attribute_names_empty_on_empty_input() {
        assert!(scan_xsd_attribute_names("").is_empty());
    }

    #[test]
    fn split_section_number_handles_dot_form() {
        assert_eq!(
            split_section_number("2.1. The Document Information Item"),
            Some((
                "2.1".to_string(),
                "The Document Information Item".to_string()
            ))
        );
    }

    #[test]
    fn split_section_number_handles_no_trailing_dot() {
        assert_eq!(
            split_section_number("2.6 Character Information Items"),
            Some(("2.6".to_string(), "Character Information Items".to_string()))
        );
    }

    #[test]
    fn reduce_to_head_noun_drops_the_and_suffix() {
        assert_eq!(
            reduce_to_head_noun("The Document Information Item"),
            "Document"
        );
        assert_eq!(reduce_to_head_noun("Element Information Items"), "Element");
        assert_eq!(
            reduce_to_head_noun("The Document Type Declaration Information Item"),
            "Document Type Declaration"
        );
        assert_eq!(
            reduce_to_head_noun("Unexpanded Entity Reference Information Items"),
            "Unexpanded Entity Reference"
        );
    }

    #[test]
    fn make_variant_ident_camel_cases_with_item_suffix() {
        assert_eq!(make_variant_ident("Document"), "DocumentItem");
        assert_eq!(
            make_variant_ident("Document Type Declaration"),
            "DocumentTypeDeclarationItem"
        );
        assert_eq!(
            make_variant_ident("Unexpanded Entity Reference"),
            "UnexpandedEntityReferenceItem"
        );
    }

    #[test]
    fn scan_infoset_items_yields_eleven_in_section_order() {
        // Mirror the W3C-published rec's heading pattern. The actual
        // rec is bundled at crates/domains/data/markup-schemas/xml/.
        let sample = r#"
            <h3><a name="infoitem.document">2.1. The Document Information Item</a></h3>
            <h3><a name="infoitem.element">2.2. Element Information Items</a></h3>
            <h3><a name="infoitem.attribute">2.3. Attribute Information Items</a></h3>
            <h3><a name="infoitem.pi">2.4. Processing Instruction Information Items</a></h3>
            <h3><a name="infoitem.rse">2.5. Unexpanded Entity Reference Information Items</a></h3>
            <h3><a name="infoitem.character">2.6. Character Information Items</a></h3>
            <h3><a name="infoitem.comment">2.7. Comment Information Items</a></h3>
            <h3><a name="infoitem.doctype">2.8. The Document Type Declaration Information Item</a></h3>
            <h3><a name="infoitem.entity.unparsed">2.9. Unparsed Entity Information Items</a></h3>
            <h3><a name="infoitem.notation">2.10. Notation Information Items</a></h3>
            <h3><a name="infoitem.namespace">2.11. Namespace Information Items</a></h3>
        "#;
        let items = scan_infoset_items(sample);
        assert_eq!(items.len(), 11);
        assert_eq!(items[0].english_name, "Document");
        assert_eq!(items[0].variant_ident, "DocumentItem");
        assert_eq!(items[0].section, "2.1");
        assert_eq!(items[10].english_name, "Namespace");
        assert_eq!(items[10].variant_ident, "NamespaceItem");
        // Spot-check the multi-word names.
        let dtd = items
            .iter()
            .find(|i| i.anchor == "infoitem.doctype")
            .unwrap();
        assert_eq!(dtd.english_name, "Document Type Declaration");
        assert_eq!(dtd.variant_ident, "DocumentTypeDeclarationItem");
        let rse = items.iter().find(|i| i.anchor == "infoitem.rse").unwrap();
        assert_eq!(rse.english_name, "Unexpanded Entity Reference");
        assert_eq!(rse.variant_ident, "UnexpandedEntityReferenceItem");
    }
}
