//! Route alerts: adverse-condition alerts (TFR, SIGMET, AIRMET, …) along a
//! planned route, as a neutral model plus the [`RouteAlertProvider`] trait.
//!
//! An alert carries its category, verbatim text, the condition's lateral
//! geometry, where along the route it bites (distance), and its active window.
//! Filtering is the source's job — the query carries the route to alert
//! against.

use async_trait::async_trait;
use chrono::{DateTime, TimeDelta, Utc};

use crate::model::Area;
use crate::provider::{ContextProvider, ProviderError};
use crate::route_request::FlightRules;

/// The category of a route alert.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AlertCategory {
    /// Temporary Flight Restriction.
    Tfr,
    /// SIGMET (significant meteorological information).
    Sigmet,
    /// Convective SIGMET.
    ConvectiveSigmet,
    /// AIRMET.
    Airmet,
    /// Center Weather Advisory.
    CenterWeatherAdvisory,
    /// Aviation / public weather watch.
    WeatherWatch,
    /// Airport or runway restriction.
    AirportRunwayRestriction,
    /// Urgent PIREP / AIREP.
    UrgentPirep,
    /// Unmanned-aircraft-system activity.
    Uas,
    /// A category this crate does not model; the source's verbatim label.
    Other(String),
}

impl AlertCategory {
    /// Map a source category label (`TFR`, `CONVECTION_SIGMET`, …) to a kind.
    pub fn from_label(label: &str) -> Self {
        let folded: String = label
            .chars()
            .filter(|c| c.is_ascii_alphanumeric())
            .collect::<String>()
            .to_ascii_uppercase();
        match folded.as_str() {
            "TFR" => Self::Tfr,
            "SIGMET" => Self::Sigmet,
            "CONVECTIONSIGMET" | "CONVECTIVESIGMET" => Self::ConvectiveSigmet,
            "AIRMET" => Self::Airmet,
            "CENTERWEATHERADVISORY" | "CWA" => Self::CenterWeatherAdvisory,
            "WEATHERWATCH" | "AVIATIONWEATHERWATCH" | "PUBLICWEATHERWATCH" => Self::WeatherWatch,
            "AIRPORTRUNWAYRESTRICTION" => Self::AirportRunwayRestriction,
            "URGENTPIREP" | "URGENTAIREP" | "URGENTPIREPAIREP" => Self::UrgentPirep,
            "UAS" => Self::Uas,
            _ => Self::Other(label.trim().to_owned()),
        }
    }
}

/// One alert along the route.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct RouteAlert {
    /// Alert category.
    pub category: AlertCategory,
    /// Verbatim alert text.
    pub text: String,
    /// A short human summary, when the source supplies one.
    pub summary: Option<String>,
    /// Distance along the route (NM) at which the condition is encountered.
    pub distance_along_route_nm: Option<f64>,
    /// Shortest lateral distance (NM) from the route to the condition.
    pub lateral_distance_nm: Option<f64>,
    /// First instant the alert is in force, when carried.
    pub start: Option<DateTime<Utc>>,
    /// Last instant in force, when carried.
    pub end: Option<DateTime<Utc>>,
    /// The condition's lateral extent (a polygon), when carried.
    pub area: Option<Area>,
}

impl RouteAlert {
    /// An alert with its category and text; the rest unset.
    pub fn new(category: AlertCategory, text: impl Into<String>) -> Self {
        Self {
            category,
            text: text.into(),
            summary: None,
            distance_along_route_nm: None,
            lateral_distance_nm: None,
            start: None,
            end: None,
            area: None,
        }
    }

    /// Set the human summary.
    #[must_use]
    pub fn with_summary(mut self, summary: Option<String>) -> Self {
        self.summary = summary;
        self
    }

    /// Set the along-route and lateral distances (NM).
    #[must_use]
    pub fn with_distances(mut self, along_route: Option<f64>, lateral: Option<f64>) -> Self {
        self.distance_along_route_nm = along_route;
        self.lateral_distance_nm = lateral;
        self
    }

    /// Set the active window.
    #[must_use]
    pub fn with_period(mut self, start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>) -> Self {
        self.start = start;
        self.end = end;
        self
    }

    /// Set the lateral extent.
    #[must_use]
    pub fn with_area(mut self, area: Option<Area>) -> Self {
        self.area = area;
        self
    }
}

/// The route to alert against.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RouteAlertQuery {
    /// Departure aerodrome identifier.
    pub departure: String,
    /// Destination aerodrome identifier.
    pub destination: String,
    /// Enroute route string (space-separated fixes), when any.
    pub route: Option<String>,
    /// Flight rules.
    pub flight_rules: FlightRules,
    /// Cruise altitude in hundreds of feet (flight level), when known.
    pub altitude_fl: Option<u32>,
    /// Proposed departure time.
    pub departure_at: DateTime<Utc>,
    /// Estimated time en route.
    pub flight_duration: TimeDelta,
}

impl RouteAlertQuery {
    /// A query from `departure` to `destination`, departing at `departure_at`
    /// for `flight_duration`.
    pub fn new(
        departure: impl Into<String>,
        destination: impl Into<String>,
        departure_at: DateTime<Utc>,
        flight_duration: TimeDelta,
    ) -> Self {
        Self {
            departure: departure.into(),
            destination: destination.into(),
            route: None,
            flight_rules: FlightRules::Ifr,
            altitude_fl: None,
            departure_at,
            flight_duration,
        }
    }

    /// Set the enroute route string.
    #[must_use]
    pub fn with_route(mut self, route: Option<String>) -> Self {
        self.route = route;
        self
    }

    /// Set the flight rules.
    #[must_use]
    pub fn with_flight_rules(mut self, flight_rules: FlightRules) -> Self {
        self.flight_rules = flight_rules;
        self
    }

    /// Set the cruise altitude (flight level).
    #[must_use]
    pub fn with_altitude_fl(mut self, altitude_fl: Option<u32>) -> Self {
        self.altitude_fl = altitude_fl;
        self
    }
}

/// A provider that can serve adverse-condition alerts along a route.
#[async_trait]
pub trait RouteAlertProvider: ContextProvider {
    /// Fetch the alerts affecting `query`'s route.
    async fn route_alerts(&self, query: &RouteAlertQuery)
    -> Result<Vec<RouteAlert>, ProviderError>;
}

#[cfg(test)]
mod tests;
