//! PLCopen XML → Structured Text POUs.
//!
//! A PLCopen project stores each POU as `<pou name=".." pouType="..">` with an
//! `<interface>` (typed variable sections) and a `<body>` in one of the IEC
//! 61131-3 languages. We reconstruct an equivalent Structured-Text source for
//! each POU (a `VAR` block from the interface + statements from the body) and run
//! it through the ST parser, so raw `.st` files and PLCopen projects — textual or
//! graphical — flow through one analysis path.
//!
//! Body languages:
//! - **ST** — taken verbatim.
//! - **FBD / LD** — the graphical network is translated to synthetic ST: blocks
//!   become calls (`TypeName(pin := arg, …)`), out-variables / coils become
//!   assignments, with input pins resolved by tracing connections. This lets the
//!   semantic rules see comm calls, hardcoded arguments and safety writes that
//!   live in graphical logic, not just in text.
//! - **SFC** — the step/transition graph itself is skipped; the ST/FBD/LD bodies
//!   embedded in its actions and transitions are still translated.

use std::collections::HashMap;

use roxmltree::Node;

use super::ast::Pou;
use super::parser;

/// Parse every POU out of a PLCopen XML document (ST, FBD or LD bodies).
pub fn parse_plcopen(xml: &str) -> Vec<Pou> {
    let doc = match roxmltree::Document::parse(xml) {
        Ok(d) => d,
        Err(_) => return Vec::new(),
    };
    let mut pous = Vec::new();
    for pou in doc.descendants().filter(|n| n.has_tag_name("pou")) {
        let name = pou.attribute("name").unwrap_or("pou").to_string();
        let pou_type = pou.attribute("pouType").unwrap_or("program");

        let Some(body) = reconstruct_body(pou) else {
            continue;
        };
        if body.trim().is_empty() {
            continue;
        }

        let var_block = build_var_block(pou);
        let kw = match pou_type.to_ascii_lowercase().as_str() {
            "function" => "FUNCTION",
            "functionblock" | "functionblocktype" => "FUNCTION_BLOCK",
            _ => "PROGRAM",
        };
        let synthetic = format!("{kw} {name}\n{var_block}{body}\nEND_{kw}\n");
        pous.extend(parser::parse(&synthetic));
    }
    pous
}

/// Case-insensitive tag match (PLCopen uses `FBD`/`LD`/`ST`, CODESYS may vary).
fn tag_is(n: &Node, name: &str) -> bool {
    n.tag_name().name().eq_ignore_ascii_case(name)
}

/// Reconstruct a POU's body as Structured Text, whatever language it is written
/// in. Concatenates every language body found under `<body>` (SFC actions and
/// transitions carry their own ST/FBD/LD sub-bodies).
fn reconstruct_body(pou: Node) -> Option<String> {
    let mut out = String::new();
    for body in pou.descendants().filter(|n| tag_is(n, "body")) {
        for lang in body.children().filter(|n| n.is_element()) {
            let piece = match lang.tag_name().name().to_ascii_uppercase().as_str() {
                "ST" | "IL" => collect_text(lang),
                "FBD" | "LD" => translate_network(lang),
                _ => continue,
            };
            if !piece.trim().is_empty() {
                out.push_str(&piece);
                if !piece.ends_with('\n') {
                    out.push('\n');
                }
            }
        }
    }
    if out.trim().is_empty() {
        None
    } else {
        Some(out)
    }
}

// ── graphical (FBD / LD) → synthetic ST ────────────────────────────────

/// Translate one FBD/LD network into ST statements: blocks → calls,
/// out-variables and coils → assignments.
fn translate_network(net: Node) -> String {
    let by_id = index_local_ids(net);
    let mut out = String::new();
    for el in net.children().filter(|n| n.is_element()) {
        let stmt = match el.tag_name().name().to_ascii_lowercase().as_str() {
            "block" => block_call(el, &by_id).map(|c| format!("{c};")),
            "outvariable" => out_assignment(el, &by_id),
            "coil" => coil_assignment(el, &by_id),
            _ => None,
        };
        if let Some(s) = stmt {
            out.push_str(&s);
            out.push('\n');
        }
    }
    out
}

/// Index every element in a network by its `localId` so connections resolve.
fn index_local_ids<'a, 'input>(net: Node<'a, 'input>) -> HashMap<String, Node<'a, 'input>> {
    net.descendants()
        .filter(|n| n.is_element())
        .filter_map(|n| n.attribute("localId").map(|id| (id.to_string(), n)))
        .collect()
}

/// Build a call expression for a block: `TypeName(pin := arg, …)`.
fn block_call(block: Node, by_id: &HashMap<String, Node>) -> Option<String> {
    let ty = block.attribute("typeName")?;
    let mut args = Vec::new();
    if let Some(inputs) = block.children().find(|n| tag_is(n, "inputVariables")) {
        for v in inputs.children().filter(|n| tag_is(n, "variable")) {
            let Some(expr) = input_expr(v, by_id, 0) else {
                continue;
            };
            match v.attribute("formalParameter") {
                Some(pin) if !pin.is_empty() => args.push(format!("{pin} := {expr}")),
                _ => args.push(expr),
            }
        }
    }
    Some(format!("{ty}({})", args.join(", ")))
}

/// `target := <traced expression>;` for an FBD out-variable.
fn out_assignment(outvar: Node, by_id: &HashMap<String, Node>) -> Option<String> {
    let target = expression_text(outvar)?;
    let value = input_expr(outvar, by_id, 0).unwrap_or_else(|| "0".to_string());
    Some(format!("{target} := {value};"))
}

/// `coil := <traced rung expression>;` for an LD coil (negated → `NOT (…)`).
fn coil_assignment(coil: Node, by_id: &HashMap<String, Node>) -> Option<String> {
    let target = child_text(coil, "variable")?;
    let rung = input_expr(coil, by_id, 0).unwrap_or_else(|| "TRUE".to_string());
    let negated = matches!(coil.attribute("negated"), Some(v) if v.eq_ignore_ascii_case("true"));
    let rhs = if negated {
        format!("NOT ({rung})")
    } else {
        rung
    };
    Some(format!("{target} := {rhs};"))
}

/// Resolve the expression feeding `node`'s single input connection.
fn input_expr(node: Node, by_id: &HashMap<String, Node>, depth: u8) -> Option<String> {
    let refid = ref_local_id(node)?;
    Some(expr_for(&refid, by_id, depth))
}

/// Build the ST expression produced by the element with this `localId`.
fn expr_for(local_id: &str, by_id: &HashMap<String, Node>, depth: u8) -> String {
    if depth > 24 {
        return "0".to_string();
    }
    let Some(node) = by_id.get(local_id) else {
        return format!("__net{local_id}");
    };
    match node.tag_name().name().to_ascii_lowercase().as_str() {
        "invariable" | "inoutvariable" => {
            expression_text(*node).unwrap_or_else(|| format!("__net{local_id}"))
        }
        // A block feeding another element: reference it by a synthetic result
        // name; the block is emitted as its own call statement, so we neither
        // duplicate the call nor lose it.
        "block" => format!("__blk{local_id}"),
        "contact" => {
            let var = child_text(*node, "variable").unwrap_or_else(|| "TRUE".to_string());
            let negated =
                matches!(node.attribute("negated"), Some(v) if v.eq_ignore_ascii_case("true"));
            let term = if negated { format!("NOT {var}") } else { var };
            match ref_local_id(*node) {
                Some(up) => {
                    let upstream = expr_for(&up, by_id, depth + 1);
                    if upstream == "TRUE" {
                        term
                    } else {
                        format!("({upstream} AND {term})")
                    }
                }
                None => term,
            }
        }
        "leftpowerrail" => "TRUE".to_string(),
        _ => format!("__net{local_id}"),
    }
}

/// The `refLocalId` of `node`'s first input connection, if any.
fn ref_local_id(node: Node) -> Option<String> {
    node.descendants()
        .find(|n| tag_is(n, "connectionPointIn"))
        .and_then(|cpi| cpi.descendants().find(|n| tag_is(n, "connection")))
        .and_then(|c| c.attribute("refLocalId"))
        .map(|s| s.to_string())
}

/// Text of a node's `<expression>` child (variable name or literal).
fn expression_text(node: Node) -> Option<String> {
    let e = node.children().find(|n| tag_is(n, "expression"))?;
    let t = collect_text(e).trim().to_string();
    if t.is_empty() {
        None
    } else {
        Some(t)
    }
}

/// Text of a named child element (e.g. `<variable>` of a contact/coil).
fn child_text(node: Node, name: &str) -> Option<String> {
    let c = node.children().find(|n| tag_is(n, name))?;
    let t = collect_text(c).trim().to_string();
    if t.is_empty() {
        None
    } else {
        Some(t)
    }
}

/// Concatenate the text of a node's descendant text nodes (bodies are often
/// wrapped in `<xhtml>` and may contain multiple text runs). Only text nodes are
/// gathered: an element's `.text()` would re-yield its first child's text, which
/// (with the text node itself) would duplicate every value.
fn collect_text(node: Node) -> String {
    node.descendants()
        .filter(|n| n.is_text())
        .filter_map(|n| n.text())
        .collect::<String>()
}

/// Build an ST `VAR … END_VAR` block from a POU's `<interface>` variable
/// sections, so declarations (types, initial values) reach the rules.
fn build_var_block(pou: Node) -> String {
    let Some(interface) = pou.children().find(|n| n.has_tag_name("interface")) else {
        return String::new();
    };
    let mut out = String::from("VAR\n");
    let mut any = false;
    for container in interface.children().filter(|n| n.is_element()) {
        // localVars / inputVars / outputVars / inOutVars / tempVars / globalVars / externalVars
        if !container.tag_name().name().ends_with("Vars") {
            continue;
        }
        for var in container.children().filter(|n| n.has_tag_name("variable")) {
            let Some(vname) = var.attribute("name") else {
                continue;
            };
            let ty = var
                .children()
                .find(|n| n.has_tag_name("type"))
                .map(type_name)
                .unwrap_or_else(|| "BOOL".to_string());
            let init = var
                .children()
                .find(|n| n.has_tag_name("initialValue"))
                .and_then(initial_value);
            match init {
                Some(v) => out.push_str(&format!("  {vname} : {ty} := {v};\n")),
                None => out.push_str(&format!("  {vname} : {ty};\n")),
            }
            any = true;
        }
    }
    out.push_str("END_VAR\n");
    if any {
        out
    } else {
        String::new()
    }
}

/// Render a PLCopen `<type>` element as an ST type string.
fn type_name(type_node: Node) -> String {
    let Some(inner) = type_node.children().find(|n| n.is_element()) else {
        return "BOOL".to_string();
    };
    let tag = inner.tag_name().name();
    match tag {
        "derived" => inner.attribute("name").unwrap_or("DERIVED").to_string(),
        "array" => {
            let dim = inner.children().find(|n| n.has_tag_name("dimension"));
            let (lo, hi) = dim
                .map(|d| {
                    (
                        d.attribute("lower").unwrap_or("0").to_string(),
                        d.attribute("upper").unwrap_or("0").to_string(),
                    )
                })
                .unwrap_or_else(|| ("0".to_string(), "0".to_string()));
            let base = inner
                .children()
                .find(|n| n.has_tag_name("baseType"))
                .map(type_name)
                .unwrap_or_else(|| "INT".to_string());
            format!("ARRAY[{lo}..{hi}] OF {base}")
        }
        "string" | "wstring" => "STRING".to_string(),
        // BOOL, INT, DINT, REAL, TIME, ... — the tag name is the ST type.
        other => other.to_ascii_uppercase(),
    }
}

/// Extract an initial value as an ST literal (quoting strings).
fn initial_value(iv: Node) -> Option<String> {
    let simple = iv.descendants().find(|n| n.has_tag_name("simpleValue"))?;
    let raw = simple.attribute("value")?.trim().to_string();
    if raw.is_empty() {
        return None;
    }
    // Numbers / booleans / time literals pass through; everything else is a
    // string literal.
    let is_scalar = raw.eq_ignore_ascii_case("true")
        || raw.eq_ignore_ascii_case("false")
        || raw.starts_with(['T', 't', 'D', 'd']) && raw.contains('#')
        || raw
            .chars()
            .all(|c| c.is_ascii_digit() || c == '.' || c == '-' || c == '+');
    if is_scalar || raw.starts_with('\'') || raw.starts_with('"') {
        Some(raw)
    } else {
        Some(format!("'{}'", raw.replace('\'', "''")))
    }
}

#[cfg(test)]
mod tests {
    use super::parse_plcopen;
    use crate::pipeline::plc::rules;
    use std::collections::HashSet;

    fn rule_ids(xml: &str) -> HashSet<&'static str> {
        parse_plcopen(xml)
            .iter()
            .flat_map(rules::analyze)
            .map(|h| h.rule_id)
            .collect()
    }

    /// A Ladder Diagram network: a rung (power rail → contact → coil) plus an
    /// insecure comm block. Coils/contacts translate to assignments; the block
    /// translates to a call so the port rule fires.
    #[test]
    fn ld_coil_and_block_translate_and_are_analysed() {
        let xml = r#"<?xml version="1.0"?>
<project xmlns="http://www.plcopen.org/xml/tc6_0201">
 <types><pous>
  <pou name="Rung" pouType="program">
   <interface><localVars>
     <variable name="Motor"><type><BOOL/></type></variable>
   </localVars></interface>
   <body><LD>
     <leftPowerRail localId="0"/>
     <contact localId="1"><variable>Start</variable>
       <connectionPointIn><connection refLocalId="0"/></connectionPointIn></contact>
     <coil localId="2"><variable>Motor</variable>
       <connectionPointIn><connection refLocalId="1"/></connectionPointIn></coil>
     <inVariable localId="3"><expression>21</expression></inVariable>
     <inVariable localId="4"><expression>FALSE</expression></inVariable>
     <block localId="10" typeName="Ftp_Send">
       <inputVariables>
         <variable formalParameter="PORT">
           <connectionPointIn><connection refLocalId="3"/></connectionPointIn></variable>
         <variable formalParameter="ENCRYPT">
           <connectionPointIn><connection refLocalId="4"/></connectionPointIn></variable>
       </inputVariables>
     </block>
   </LD></body>
  </pou>
 </pous></types>
</project>"#;
        let ids = rule_ids(xml);
        // Ftp_Send(PORT := 21, ENCRYPT := FALSE) — port 21 is an insecure protocol.
        assert!(
            ids.contains("plc-insecure-protocol-port"),
            "LD block should flag port 21; got {ids:?}"
        );
    }

    /// Doubled-text regression: a graphical expression must be extracted once,
    /// so literals like `502` and `FALSE` stay intact (not `502502`/`FALSEFALSE`).
    #[test]
    fn graphical_expression_text_is_not_duplicated() {
        let xml = r#"<?xml version="1.0"?>
<project xmlns="http://www.plcopen.org/xml/tc6_0201">
 <types><pous>
  <pou name="Comm" pouType="program">
   <body><FBD>
     <inVariable localId="1"><expression>502</expression></inVariable>
     <inVariable localId="2"><expression>FALSE</expression></inVariable>
     <block localId="10" typeName="Modbus_TCP_Master">
       <inputVariables>
         <variable formalParameter="PORT">
           <connectionPointIn><connection refLocalId="1"/></connectionPointIn></variable>
         <variable formalParameter="AUTH">
           <connectionPointIn><connection refLocalId="2"/></connectionPointIn></variable>
       </inputVariables>
     </block>
   </FBD></body>
  </pou>
 </pous></types>
</project>"#;
        let ids = rule_ids(xml);
        assert!(ids.contains("plc-insecure-protocol-port")); // PORT := 502 (not 502502)
        assert!(ids.contains("plc-insecure-comm")); // AUTH := FALSE (not FALSEFALSE)
    }
}
