//! NOTAMs: the provider-neutral record, the query, the capability trait,
//! and cross-source reconciliation.
//!
//! Every source — FAA NMS (structured GeoJSON), Leidos (narrative), a future
//! foreign authority — "packs what it has" into a [`NotamRecord`]: `raw_text`
//! is always authoritative, the structured fields are filled when the source
//! provides them. [`reconcile`] is the one place that assembles records from
//! any mix of sources, deduplicates by the FAA NOTAM number, prefers the most
//! structured source, and flags when sources disagree or only one carries a
//! NOTAM — the same safety cross-check [`crate::compare()`] gives for weather.

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

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

/// Origin of a [`NotamRecord`]. Non-exhaustive: foreign authorities slot in
/// without breaking existing match sites.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NotamSource {
    /// FAA NOTAM Management System (NMS-API).
    FaaNms,
    /// Leidos Flight Service.
    Leidos,
    /// A source this crate does not name yet, by its own slug.
    Other(String),
}

impl NotamSource {
    /// How structured this source's records are, lowest = richest. Drives
    /// which record wins when sources carry the same NOTAM.
    fn fidelity_rank(&self) -> u8 {
        match self {
            Self::FaaNms => 0,
            Self::Leidos => 1,
            Self::Other(_) => 2,
        }
    }

    /// Stable lowercase slug, e.g. `"faa-nms"`.
    pub fn slug(&self) -> &str {
        match self {
            Self::FaaNms => "faa-nms",
            Self::Leidos => "leidos",
            Self::Other(slug) => slug,
        }
    }
}

/// FAA NOTAM classification (the `classification` filter / GeoJSON field).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NotamClassification {
    /// Domestic NOTAM.
    Domestic,
    /// Flight Data Center (regulatory) NOTAM.
    Fdc,
    /// International NOTAM.
    International,
    /// Military NOTAM.
    Military,
    /// Local military NOTAM.
    LocalMilitary,
    /// A classification this crate does not model, kept verbatim.
    Other(String),
}

/// The kind of facility a NOTAM concerns (the ICAO/`feature` category).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NotamFeature {
    /// Runway.
    Runway,
    /// Taxiway.
    Taxiway,
    /// Apron.
    Apron,
    /// Aerodrome.
    Aerodrome,
    /// Obstruction.
    Obstruction,
    /// Navigation aid.
    Navaid,
    /// Communication.
    Communication,
    /// Service.
    Service,
    /// Airspace.
    Airspace,
    /// Instrument or other procedure (ODP/SID/STAR/IAP/DVA/VFP).
    Procedure,
    /// Chart.
    Chart,
    /// Data.
    Data,
    /// Route.
    Route,
    /// Special.
    Special,
    /// Security.
    Security,
    /// A feature code this crate does not model, kept verbatim.
    Other(String),
}

/// Whether a NOTAM is new, a replacement, or a cancellation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NotamType {
    /// New NOTAM (`N`).
    New,
    /// Replaces a prior NOTAM (`R`).
    Replace,
    /// Cancels a prior NOTAM (`C`).
    Cancel,
    /// A type code this crate does not model, kept verbatim.
    Other(String),
}

/// A NOTAM's active window. `end` is `None` for a permanent NOTAM; an
/// estimated end carries `estimated = true` (a source-stated guess).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NotamValidity {
    /// First instant the NOTAM is in force.
    pub start: DateTime<Utc>,
    /// Last instant in force; `None` is permanent (`PERM`).
    pub end: Option<DateTime<Utc>>,
    /// The source marked the end estimated (`EST`).
    pub estimated: bool,
}

impl NotamValidity {
    /// A window from `start` to `end` (`None` = permanent).
    pub fn new(start: DateTime<Utc>, end: Option<DateTime<Utc>>) -> Self {
        Self {
            start,
            end,
            estimated: false,
        }
    }

    /// Mark the end as estimated.
    #[must_use]
    pub fn estimated(mut self, estimated: bool) -> Self {
        self.estimated = estimated;
        self
    }
}

/// One NOTAM, normalized across sources.
///
/// `source`, `number`, `location`, and `raw_text` are always present;
/// `(location, number)` is the cross-source identity. The rest is filled by
/// whatever the source carries — a structured source (NMS) fills nearly all
/// of it, a narrative source (Leidos) may fill only the text.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NotamRecord {
    /// Where this record came from.
    pub source: NotamSource,
    /// FAA NOTAM number, e.g. `"09/186"` — the cross-source key.
    pub number: String,
    /// Affected location (FAA LID / ICAO), e.g. `"KATL"`.
    pub location: String,
    /// Verbatim NOTAM text, exactly as the source published it.
    pub raw_text: String,
    /// Source-unique id when the source assigns one (NMS).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    /// New / replace / cancel.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub kind: Option<NotamType>,
    /// Classification (domestic/FDC/…).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub classification: Option<NotamClassification>,
    /// Facility category.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub feature: Option<NotamFeature>,
    /// ICAO selection (Q) code, e.g. `"QMRLC"`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub q_code: Option<String>,
    /// Affected FIR, e.g. `"ZTL"`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub affected_fir: Option<String>,
    /// Lower flight level, when the NOTAM states one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub min_fl: Option<u32>,
    /// Upper flight level, when the NOTAM states one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_fl: Option<u32>,
    /// When the NOTAM was issued.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub issued: Option<DateTime<Utc>>,
    /// Active window.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub valid: Option<NotamValidity>,
    /// Affected geometry (point/circle/polygon), when the source carries it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub geometry: Option<Area>,
}

impl NotamRecord {
    /// A record with only its identity and verbatim text; fill the rest with
    /// the `with_*` setters.
    pub fn new(
        source: NotamSource,
        number: impl Into<String>,
        location: impl Into<String>,
        raw_text: impl Into<String>,
    ) -> Self {
        Self {
            source,
            number: number.into(),
            location: location.into(),
            raw_text: raw_text.into(),
            id: None,
            kind: None,
            classification: None,
            feature: None,
            q_code: None,
            affected_fir: None,
            min_fl: None,
            max_fl: None,
            issued: None,
            valid: None,
            geometry: None,
        }
    }

    /// Set the source-unique id.
    #[must_use]
    pub fn with_id(mut self, id: Option<String>) -> Self {
        self.id = id;
        self
    }

    /// Set new/replace/cancel.
    #[must_use]
    pub fn with_kind(mut self, kind: Option<NotamType>) -> Self {
        self.kind = kind;
        self
    }

    /// Set the classification.
    #[must_use]
    pub fn with_classification(mut self, classification: Option<NotamClassification>) -> Self {
        self.classification = classification;
        self
    }

    /// Set the facility feature.
    #[must_use]
    pub fn with_feature(mut self, feature: Option<NotamFeature>) -> Self {
        self.feature = feature;
        self
    }

    /// Set the ICAO selection (Q) code.
    #[must_use]
    pub fn with_q_code(mut self, q_code: Option<String>) -> Self {
        self.q_code = q_code;
        self
    }

    /// Set the affected FIR.
    #[must_use]
    pub fn with_affected_fir(mut self, affected_fir: Option<String>) -> Self {
        self.affected_fir = affected_fir;
        self
    }

    /// Set the lower/upper flight levels.
    #[must_use]
    pub fn with_flight_levels(mut self, min_fl: Option<u32>, max_fl: Option<u32>) -> Self {
        self.min_fl = min_fl;
        self.max_fl = max_fl;
        self
    }

    /// Set the issue time.
    #[must_use]
    pub fn with_issued(mut self, issued: Option<DateTime<Utc>>) -> Self {
        self.issued = issued;
        self
    }

    /// Set the active window.
    #[must_use]
    pub fn with_valid(mut self, valid: Option<NotamValidity>) -> Self {
        self.valid = valid;
        self
    }

    /// Set the affected geometry.
    #[must_use]
    pub fn with_geometry(mut self, geometry: Option<Area>) -> Self {
        self.geometry = geometry;
        self
    }

    /// Count of populated structured fields — the richness used to pick a
    /// winner when two sources carry the same NOTAM.
    fn richness(&self) -> u8 {
        u8::from(self.id.is_some())
            + u8::from(self.kind.is_some())
            + u8::from(self.classification.is_some())
            + u8::from(self.feature.is_some())
            + u8::from(self.q_code.is_some())
            + u8::from(self.affected_fir.is_some())
            + u8::from(self.min_fl.is_some() || self.max_fl.is_some())
            + u8::from(self.issued.is_some())
            + u8::from(self.valid.is_some())
            + u8::from(self.geometry.is_some())
    }
}

/// Where a [`NotamProvider`] should look. Atomic by design: a route is tiled
/// into several scopes by the caller (the FAA radius cap is 100 NM), so this
/// trait never has to know about corridors.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NotamScope {
    /// A single FAA LID / ICAO location, e.g. `"KBOS"`.
    Location(String),
    /// A circle around a coordinate, radius in nautical miles.
    PointRadius {
        /// Circle center.
        center: GeoPoint,
        /// Radius in nautical miles (FAA caps this at 100).
        radius_nm: f64,
    },
}

/// A NOTAM query: where to look, plus optional relevance filters.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NotamQuery {
    /// Where to look.
    pub scope: NotamScope,
    /// Restrict to these classifications; empty means the source default.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub classifications: Vec<NotamClassification>,
    /// Restrict to these features; empty means all.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub features: Vec<NotamFeature>,
}

impl NotamQuery {
    /// A query over `scope` with no filters.
    pub fn new(scope: NotamScope) -> Self {
        Self {
            scope,
            classifications: Vec::new(),
            features: Vec::new(),
        }
    }

    /// Restrict to these classifications.
    #[must_use]
    pub fn with_classifications(mut self, classifications: Vec<NotamClassification>) -> Self {
        self.classifications = classifications;
        self
    }

    /// Restrict to these features.
    #[must_use]
    pub fn with_features(mut self, features: Vec<NotamFeature>) -> Self {
        self.features = features;
        self
    }
}

/// A provider that can serve NOTAMs for a [`NotamQuery`].
///
/// Each call is one atomic scope (a location or a point-radius). Callers tile
/// a route into several queries and pass the union to [`reconcile`].
#[async_trait]
pub trait NotamProvider: ContextProvider {
    /// Fetch the NOTAMs matching `query`.
    async fn notams(&self, query: &NotamQuery) -> Result<Vec<NotamRecord>, ProviderError>;
}

mod reconcile;

pub use reconcile::{NotamReconciliation, ReconciledNotam, reconcile};

#[cfg(test)]
mod tests;
