//! REST request building for the FFSP briefing operations.
//!
//! The 1800wxbrief web service is REST: each operation is a
//! `application/x-www-form-urlencoded` `POST` to a resource URI
//! (`FP/areaBriefing`, `FP/routeBriefing`) with Basic vendor auth. This
//! module turns a validated request into the ordered form parameters; the
//! [`client`](crate::client) posts them. No SOAP envelopes are ever built.

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

/// REST resource for the area briefing operation.
pub(crate) const AREA_BRIEFING_RESOURCE: &str = "FP/areaBriefing";
/// REST resource for the route briefing operation.
pub(crate) const ROUTE_BRIEFING_RESOURCE: &str = "FP/routeBriefing";
/// `notABriefing` (and structured error messages) need at least this
/// schema version.
const VERSION_REQUESTED: &str = "20160225";

/// Inputs of one area briefing call, already validated by the client.
pub(crate) struct AreaBriefingForm<'a> {
    /// 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,
}

/// Inputs of one route briefing call.
pub(crate) struct RouteBriefingForm<'a> {
    /// Flight rules the route is flown under.
    pub flight_rules: FlightRules,
    /// Departure location identifier.
    pub departure: &'a str,
    /// Destination location identifier.
    pub destination: &'a str,
    /// Proposed departure time.
    pub departure_at: DateTime<Utc>,
    /// Enroute route string (space-separated fixes / slashless
    /// `DDMM(N|S)DDDMM(E|W)` coordinates), excluding departure and
    /// destination.
    pub route: &'a str,
    /// Corridor width in nautical miles (schema range 25–100).
    pub corridor_width_nm: u32,
    /// Cruise altitude in hundreds of feet (flight level), when known.
    pub altitude_fl: Option<u32>,
    /// Cruise true airspeed in knots, when known.
    pub speed_kt: Option<u32>,
}

/// Form parameters common to every SIMPLE, non-briefing request.
fn common() -> Vec<(&'static str, String)> {
    vec![
        ("briefingType", "SIMPLE".to_owned()),
        ("notABriefing", "true".to_owned()),
        ("includeCodedMessages", "true".to_owned()),
        ("versionRequested", VERSION_REQUESTED.to_owned()),
    ]
}

/// Form parameters for an area briefing request.
pub(crate) fn area_briefing_form(form: &AreaBriefingForm<'_>) -> Vec<(&'static str, String)> {
    let mut params = common();
    params.push(("fixName", form.location_identifier.to_owned()));
    params.push(("briefingRadius", form.radius_nm.to_string()));
    params.push(("departureInstant", instant(form.departure)));
    params
}

/// Form parameters for a route briefing request. REST flight plans need
/// `type=DOMESTIC|ICAO`; coordinate fixes use the same slashless
/// `DDMM(N|S)DDDMM(E|W)` form as the area location.
pub(crate) fn route_briefing_form(form: &RouteBriefingForm<'_>) -> Vec<(&'static str, String)> {
    let mut params = common();
    params.push(("type", "DOMESTIC".to_owned()));
    params.push((
        "flightRules",
        match form.flight_rules {
            FlightRules::Vfr => "VFR",
            // IFR is the wider briefing scope (it pulls FDC/route NOTAMs a
            // VFR brief omits), so an unmodeled future rule defaults to it.
            _ => "IFR",
        }
        .to_owned(),
    ));
    params.push(("departure", form.departure.to_owned()));
    params.push(("destination", form.destination.to_owned()));
    params.push(("departureInstant", instant(form.departure_at)));
    if !form.route.is_empty() {
        params.push(("route", form.route.to_owned()));
    }
    params.push(("routeCorridorWidth", form.corridor_width_nm.to_string()));
    // NASAltitude/NASSpeed are xs:choice wrappers; the REST wire carries
    // the chosen leaf — altitudeFL (hundreds of feet) and speedKnots.
    if let Some(fl) = form.altitude_fl {
        params.push(("altitudeFL", fl.to_string()));
    }
    if let Some(speed) = form.speed_kt {
        params.push(("speedKnots", speed.to_string()));
    }
    params
}

fn instant(time: DateTime<Utc>) -> String {
    time.to_rfc3339_opts(SecondsFormat::Secs, true)
}

/// 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;
