//! Parsing of `AreaBriefingResponse` SOAP envelopes.
//!
//! The service reports business failures in-band: `returnStatus=false`
//! plus `returnCodedMessage` (or legacy `returnMessage`) elements inside
//! a 200 response. Genuine SOAP faults (`faultstring`) are reserved for
//! transport/auth-layer failures and usually ride on HTTP 500. Elements
//! are matched by local name so any namespace prefix the server picks is
//! accepted.

use quick_xml::Reader;
use quick_xml::events::{BytesRef, Event};

/// What an `AreaBriefing` response body contained.
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Outcome {
    /// `returnStatus=true`.
    Success {
        /// Text of the `SimpleWeatherBriefing` branch, when present.
        simple_briefing: Option<String>,
    },
    /// `returnStatus=false` with provider error messages.
    Failure {
        /// Combined `returnCodedMessage` / `returnMessage` texts.
        messages: Vec<String>,
    },
    /// A SOAP fault instead of an `AreaBriefingResponse`.
    Fault {
        /// The `faultstring` text.
        message: String,
    },
}

/// Failure to make sense of a response body.
#[derive(Debug, thiserror::Error)]
pub(crate) enum ParseError {
    /// The body is not well-formed XML.
    #[error("malformed XML in AreaBriefing response")]
    Xml(#[from] quick_xml::Error),
    /// Text content could not be decoded.
    #[error("undecodable text in AreaBriefing response")]
    Encoding(#[from] quick_xml::encoding::EncodingError),
    /// Neither a `returnStatus` nor a SOAP fault was found.
    #[error("response carries neither returnStatus nor a SOAP fault")]
    MissingReturnStatus,
    /// `returnStatus` was not a boolean.
    #[error("unrecognized returnStatus value {value:?}")]
    BadReturnStatus {
        /// The text actually found.
        value: String,
    },
    /// `returnStatus=true` but no briefing branch this adapter knows.
    #[error("success response carries no SimpleWeatherBriefing")]
    MissingBriefing,
}

#[derive(Default)]
struct Collector {
    return_status: Option<String>,
    simple_briefing: Option<String>,
    messages: Vec<String>,
    coded_code: Option<String>,
    coded_message: Option<String>,
    fault: Option<String>,
}

/// Parse a response body into an [`Outcome`].
///
/// Text is buffered per element because the reader splits content at
/// entity references (`&#xA;`, `&amp;`, …) and CDATA boundaries; the
/// buffer is dispatched when the element ends.
pub(crate) fn parse_area_briefing(xml: &str) -> Result<Outcome, ParseError> {
    let mut reader = Reader::from_str(xml);
    let mut path: Vec<String> = Vec::new();
    let mut text = String::new();
    let mut collected = Collector::default();
    loop {
        match reader.read_event()? {
            Event::Eof => break,
            Event::Start(start) => {
                path.push(local_name(start.local_name().as_ref()));
                text.clear();
            }
            Event::Text(event) => text.push_str(&event.xml_content()?),
            Event::CData(event) => text.push_str(&String::from_utf8_lossy(event.as_ref())),
            Event::GeneralRef(reference) => {
                if let Some(resolved) = resolve_reference(&reference)? {
                    text.push_str(&resolved);
                }
            }
            Event::End(_) => {
                dispatch(&mut collected, &path, std::mem::take(&mut text));
                path.pop();
            }
            _ => {}
        }
    }
    outcome(collected)
}

fn local_name(bytes: &[u8]) -> String {
    String::from_utf8_lossy(bytes).into_owned()
}

/// Character references resolve numerically; of named entities only the
/// XML built-ins exist here (the schema defines no others). Unknown
/// references are dropped rather than failing the briefing.
fn resolve_reference(reference: &BytesRef<'_>) -> Result<Option<String>, ParseError> {
    if let Some(resolved) = reference.resolve_char_ref()? {
        return Ok(Some(resolved.to_string()));
    }
    Ok(match reference.as_ref() {
        b"amp" => Some("&".to_owned()),
        b"lt" => Some("<".to_owned()),
        b"gt" => Some(">".to_owned()),
        b"apos" => Some("'".to_owned()),
        b"quot" => Some("\"".to_owned()),
        _ => None,
    })
}

/// Route one ended element's text into the collector. `path.last()` is
/// the element that just ended.
fn dispatch(collected: &mut Collector, path: &[String], text: String) {
    let Some(leaf) = path.last() else { return };
    let parent = path.len().checked_sub(2).and_then(|i| path.get(i));
    // Status and error strings are trimmed: a pretty-printing server
    // pads them with indentation whitespace. The briefing text is NOT
    // trimmed — its line structure is content.
    match leaf.as_str() {
        "returnStatus" => collected.return_status = Some(text.trim().to_owned()),
        "SimpleWeatherBriefing" => collected.simple_briefing = Some(text),
        "returnMessage" => collected.messages.push(text.trim().to_owned()),
        "code" if parent.is_some_and(|p| p == "returnCodedMessage") => {
            collected.coded_code = Some(text.trim().to_owned());
        }
        "message" if parent.is_some_and(|p| p == "returnCodedMessage") => {
            collected.coded_message = Some(text.trim().to_owned());
        }
        "returnCodedMessage" => {
            let code = collected.coded_code.take().unwrap_or_default();
            let message = collected.coded_message.take().unwrap_or_default();
            collected.messages.push(format!("{code}: {message}"));
        }
        "faultstring" => collected.fault = Some(text.trim().to_owned()),
        _ => {}
    }
}

fn outcome(collected: Collector) -> Result<Outcome, ParseError> {
    if let Some(status) = collected.return_status {
        return match status.as_str() {
            "true" | "1" => Ok(Outcome::Success {
                simple_briefing: collected.simple_briefing,
            }),
            "false" | "0" => Ok(Outcome::Failure {
                messages: collected.messages,
            }),
            _ => Err(ParseError::BadReturnStatus { value: status }),
        };
    }
    if let Some(message) = collected.fault {
        return Ok(Outcome::Fault { message });
    }
    Err(ParseError::MissingReturnStatus)
}

#[cfg(test)]
mod tests;
