//! SOAP 1.1 request envelope construction for the `AreaBriefing`
//! operation.
//!
//! The schema (`elementFormDefault="qualified"`) requires every child to
//! be namespace-qualified, and the `AreaBriefingRequest` complex type is
//! an `xs:sequence` — element order is part of the contract and must
//! follow the WSDL exactly.

use aerocontext_core::GeoPoint;
use chrono::{DateTime, SecondsFormat, Utc};
use quick_xml::escape::escape;

/// Target namespace of the FFSP web-service schema.
const SCHEMA_NS: &str = "https://lmfsweb.afss.com/Website/schemas";
/// SOAP 1.1 envelope namespace.
const SOAP_NS: &str = "http://schemas.xmlsoap.org/soap/envelope/";
/// `notABriefing` requires at least schema version 20160225; sending a
/// version also switches errors to structured `returnCodedMessage`s.
const VERSION_REQUESTED: &str = "20160225";

/// Inputs of one `AreaBriefing` call, already validated by the client.
pub(crate) struct AreaBriefingParams<'a> {
    /// Center of the area: a 2–5 char location identifier, an identifier
    /// with radial/distance, or a `DDMM(N|S)DDDMM(E|W)` coordinate.
    pub location_identifier: &'a str,
    /// Proposed departure time; the briefing window derives from it.
    pub departure: DateTime<Utc>,
    /// Area radius in nautical miles (schema range 2–100).
    pub radius_nm: u32,
}

/// Render the request envelope. Child elements follow the WSDL
/// `xs:sequence` order: includeCodedMessages, notABriefing,
/// versionRequested, briefingType, fixName, departureInstant,
/// briefingRadius.
pub(crate) fn area_briefing_envelope(params: &AreaBriefingParams<'_>) -> String {
    format!(
        concat!(
            r#"<soapenv:Envelope xmlns:soapenv="{soap_ns}" xmlns:lfs="{schema_ns}">"#,
            "<soapenv:Header/><soapenv:Body><lfs:AreaBriefingRequest>",
            "<lfs:includeCodedMessages>true</lfs:includeCodedMessages>",
            "<lfs:notABriefing>true</lfs:notABriefing>",
            "<lfs:versionRequested>{version}</lfs:versionRequested>",
            "<lfs:briefingType>SIMPLE</lfs:briefingType>",
            "<lfs:fixName><lfs:locationIdentifier>{location}</lfs:locationIdentifier></lfs:fixName>",
            "<lfs:departureInstant>{departure}</lfs:departureInstant>",
            "<lfs:briefingRadius>{radius}</lfs:briefingRadius>",
            "</lfs:AreaBriefingRequest></soapenv:Body></soapenv:Envelope>",
        ),
        soap_ns = SOAP_NS,
        schema_ns = SCHEMA_NS,
        version = VERSION_REQUESTED,
        location = escape(params.location_identifier),
        departure = params.departure.to_rfc3339_opts(SecondsFormat::Secs, true),
        radius = params.radius_nm,
    )
}

/// Format a coordinate as the schema's `DDMM(N|S)DDDMM(E|W)` location
/// string: latitude as 2-digit degrees + 2-digit minutes, longitude as
/// 3-digit degrees + 2-digit minutes, minutes rounded to the nearest
/// whole minute (carrying into degrees at 60).
pub(crate) fn ddmm_location(point: GeoPoint) -> String {
    let (lat_deg, lat_min) = degrees_minutes(point.lat.abs());
    let (lon_deg, lon_min) = degrees_minutes(point.lon.abs());
    let ns = if point.lat < 0.0 { 'S' } else { 'N' };
    let ew = if point.lon < 0.0 { 'W' } else { 'E' };
    format!("{lat_deg:02}{lat_min:02}{ns}{lon_deg:03}{lon_min:02}{ew}")
}

fn degrees_minutes(degrees_abs: f64) -> (u32, u32) {
    // `as` saturates on overflow/NaN, so hostile floats degrade to a
    // clamped value rather than a panic.
    let total_minutes = (degrees_abs * 60.0).round().max(0.0) as u64;
    ((total_minutes / 60) as u32, (total_minutes % 60) as u32)
}

#[cfg(test)]
mod tests;
