//! The `area_brief` tool: live weather products around an airport.

use std::sync::Arc;

use aerocontext::{
    Area, AreaBriefingRequest, AwcClient, AwcConfig, GatherOutcome, ProductKind,
    WeatherBriefingProvider, gather,
};
use aerocontext_core::NavDataSnapshot;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::error::{ToolError, render_chain};

/// Default briefing radius in nautical miles.
pub const DEFAULT_RADIUS_NM: f64 = 30.0;
/// Smallest accepted briefing radius in nautical miles.
pub const MIN_RADIUS_NM: f64 = 2.0;
/// Largest accepted briefing radius in nautical miles.
pub const MAX_RADIUS_NM: f64 = 100.0;

/// Parameters for the `area_brief` tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct AreaBriefParams {
    /// Airport identifier the briefing centers on, e.g. `"KSFO"`.
    pub airport: String,
    /// Briefing radius in nautical miles. Defaults to 30; clamped to
    /// 2..=100.
    pub radius_nm: Option<f64>,
    /// Products to request: `"metar"`, `"taf"`, `"pirep"`, `"sigmet"`.
    /// Defaults to `["metar"]` — each additional kind is one more
    /// upstream AWC request.
    pub products: Option<Vec<String>>,
    /// Intended departure time (ETD), RFC 3339, e.g.
    /// `"2026-06-15T14:30:00Z"`. Anchors navdata cycle selection and
    /// departure-relative sources; observation products stay current.
    pub etd: Option<String>,
}

/// An [`AreaBriefParams`] after defaulting, clamping, and parsing.
#[derive(Debug, Clone, PartialEq)]
pub struct ValidatedAreaBrief {
    /// Uppercased airport identifier.
    pub airport: String,
    /// Radius in nautical miles, within 2..=100.
    pub radius_nm: f64,
    /// Parsed product kinds, deduplicated, never empty.
    pub products: Vec<ProductKind>,
    /// Parsed ETD when given.
    pub departure_at: Option<chrono::DateTime<chrono::Utc>>,
}

/// What the `area_brief` tool returns.
#[derive(Debug, Serialize, JsonSchema)]
pub struct AreaBriefReport {
    /// Airport the briefing centers on, as requested.
    pub airport: String,
    /// Radius actually used, after defaulting and clamping.
    pub radius_nm: f64,
    /// Product kinds actually requested, after defaulting.
    pub products_requested: Vec<String>,
    /// Every product gathered, each attributed to its source.
    pub products: Vec<ProductReport>,
    /// Per-source outcome, including failures.
    pub sources: Vec<SourceStatus>,
    /// Operational notes (e.g. a source skipped for missing credentials).
    pub notes: Vec<String>,
    /// The ETD this brief was anchored to (RFC 3339), echoed from the
    /// request; absent means "conditions now".
    pub etd: Option<String>,
}

/// One weather product with attribution. Quote `raw_text` verbatim.
#[derive(Debug, Serialize, JsonSchema)]
pub struct ProductReport {
    /// Source that published this product, e.g. `"awc"`.
    pub source: String,
    /// Product kind, e.g. `"metar"`.
    pub kind: String,
    /// Station or location identifier, when the product is tied to one.
    pub location: Option<String>,
    /// Issue/observation time (RFC 3339), when the source provides one.
    pub issued_at: Option<String>,
    /// Validity window start (RFC 3339), e.g. a TAF forecast period —
    /// check it against the ETD.
    pub valid_from: Option<String>,
    /// Validity window end (RFC 3339).
    pub valid_to: Option<String>,
    /// Verbatim product text as published by the source.
    pub raw_text: String,
}

/// Outcome of one source's gather.
#[derive(Debug, Serialize, JsonSchema)]
pub struct SourceStatus {
    /// Source name, e.g. `"awc"`.
    pub source: String,
    /// How many products this source contributed.
    pub products: usize,
    /// Why the source failed, when it did.
    pub error: Option<String>,
    /// The departure time this source anchored its answer to (RFC 3339);
    /// absent means it served current conditions.
    pub answered_for: Option<String>,
    /// The source's full narrative briefing document, when it produces
    /// one (Leidos: adverse conditions, NOTAMs, TFRs, winds aloft beyond
    /// the extracted products). Advisory, quote verbatim.
    pub narrative: Option<String>,
}

/// Validate and normalize raw tool parameters.
pub fn validate(params: AreaBriefParams) -> Result<ValidatedAreaBrief, ToolError> {
    let airport = params.airport.trim().to_uppercase();
    if airport.is_empty() {
        return Err(ToolError::InvalidParam {
            message: "airport must be a non-empty identifier, e.g. \"KSFO\"".to_owned(),
        });
    }
    let radius_nm = params.radius_nm.unwrap_or(DEFAULT_RADIUS_NM);
    if !radius_nm.is_finite() {
        return Err(ToolError::InvalidParam {
            message: format!(
                "radius_nm must be a finite number of nautical miles, got {radius_nm}"
            ),
        });
    }
    let radius_nm = radius_nm.clamp(MIN_RADIUS_NM, MAX_RADIUS_NM);
    let products = match params.products {
        Some(names) if !names.is_empty() => {
            let mut kinds: Vec<ProductKind> = Vec::with_capacity(names.len());
            for name in &names {
                let kind = parse_product(name)?;
                if !kinds.contains(&kind) {
                    kinds.push(kind);
                }
            }
            kinds
        }
        _ => vec![ProductKind::Metar],
    };
    let departure_at = match &params.etd {
        Some(text) => Some(text.parse().map_err(|_| ToolError::InvalidParam {
            message: format!("etd must be RFC 3339, e.g. \"2026-06-15T14:30:00Z\", got {text:?}"),
        })?),
        None => None,
    };
    Ok(ValidatedAreaBrief {
        airport,
        radius_nm,
        products,
        departure_at,
    })
}

/// Parse one product name, case-insensitively.
fn parse_product(name: &str) -> Result<ProductKind, ToolError> {
    Ok(match name.trim().to_ascii_lowercase().as_str() {
        "metar" => ProductKind::Metar,
        "taf" => ProductKind::Taf,
        "pirep" => ProductKind::Pirep,
        "sigmet" => ProductKind::Sigmet,
        other => {
            return Err(ToolError::InvalidParam {
                message: format!(
                    "unknown product {other:?}; expected one of metar, taf, pirep, sigmet"
                ),
            });
        }
    })
}

/// Stable lowercase label for a product kind.
fn product_label(kind: &ProductKind) -> String {
    match kind {
        ProductKind::Metar => "metar".to_owned(),
        ProductKind::Taf => "taf".to_owned(),
        ProductKind::Pirep => "pirep".to_owned(),
        ProductKind::Sigmet => "sigmet".to_owned(),
        ProductKind::Airmet => "airmet".to_owned(),
        ProductKind::GAirmet => "gairmet".to_owned(),
        ProductKind::Cwa => "cwa".to_owned(),
        ProductKind::Notam => "notam".to_owned(),
        ProductKind::Other(name) => name.clone(),
        _ => "other".to_owned(),
    }
}

/// Gather the briefing from every available source. Network path; the
/// shaping it delegates to is unit-tested separately.
pub async fn run(request: &ValidatedAreaBrief) -> Result<AreaBriefReport, ToolError> {
    let mut notes = Vec::new();
    let brief_date = request
        .departure_at
        .map_or_else(|| chrono::Utc::now().date_naive(), |etd| etd.date_naive());
    let snapshot = match crate::store::load_snapshot_for(brief_date).await {
        Ok(snapshot) => Some(snapshot),
        Err(error) => {
            notes.push(format!(
                "navdata unavailable, so AWC cannot resolve identifier areas: {}",
                render_chain(&error)
            ));
            None
        }
    };
    let gather_request = request.clone();
    let (outcomes, mut gather_notes) =
        tokio::task::spawn_blocking(move || gather_blocking(gather_request, snapshot))
            .await
            .map_err(|cause| ToolError::BriefingTask {
                cause: Box::new(cause),
            })??;
    notes.append(&mut gather_notes);
    Ok(shape_report(request, &outcomes, notes))
}

/// Run the provider fan-out on a dedicated current-thread runtime.
///
/// The provider futures are `?Send` (the adapters stay wasm-compatible),
/// so they cannot live on the multi-threaded MCP runtime; gathered
/// outcomes are plain `Send` data and cross back safely.
fn gather_blocking(
    request: ValidatedAreaBrief,
    snapshot: Option<NavDataSnapshot>,
) -> Result<(Vec<GatherOutcome>, Vec<String>), ToolError> {
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .map_err(|cause| ToolError::BriefingTask {
            cause: Box::new(cause),
        })?;
    runtime.block_on(async move {
        let mut notes = Vec::new();
        let mut awc = AwcClient::new(AwcConfig {
            user_agent: crate::USER_AGENT.to_owned(),
            ..AwcConfig::default()
        })
        .map_err(|cause| ToolError::WeatherClient {
            provider: "awc".to_owned(),
            cause: Box::new(cause),
        })?;
        if let Some(snapshot) = snapshot {
            awc = awc.with_nav_data(snapshot);
        }
        let mut providers: Vec<Arc<dyn WeatherBriefingProvider>> = vec![Arc::new(awc)];
        if let Some(note) = aerocontext::register_leidos_from_env(&mut providers) {
            notes.push(note);
        }
        let briefing_request = AreaBriefingRequest::new(Area::LocationRadius {
            ident: request.airport.clone(),
            radius_nm: request.radius_nm,
        })
        .with_products(request.products)
        .with_departure_at(request.departure_at);
        let outcomes = gather(&providers, &briefing_request).await;
        Ok((outcomes, notes))
    })
}

/// Shape gathered outcomes into the wire report.
pub fn shape_report(
    request: &ValidatedAreaBrief,
    outcomes: &[GatherOutcome],
    notes: Vec<String>,
) -> AreaBriefReport {
    let (products, sources) = shape_outcomes(outcomes);
    AreaBriefReport {
        airport: request.airport.clone(),
        radius_nm: request.radius_nm,
        products_requested: request.products.iter().map(product_label).collect(),
        products,
        sources,
        notes,
        etd: request.departure_at.map(|at| at.to_rfc3339()),
    }
}

/// Split gathered outcomes into attributed products and per-source
/// status. Shared by the area and route briefing surfaces.
pub fn shape_outcomes(outcomes: &[GatherOutcome]) -> (Vec<ProductReport>, Vec<SourceStatus>) {
    let mut products = Vec::new();
    let mut sources = Vec::new();
    for outcome in outcomes {
        match &outcome.result {
            Ok(briefing) => {
                for product in &briefing.products {
                    products.push(ProductReport {
                        source: outcome.source.clone(),
                        kind: product_label(&product.kind),
                        location: product.location.clone(),
                        issued_at: product.issued_at.map(|at| at.to_rfc3339()),
                        valid_from: product.valid.map(|v| v.start.to_rfc3339()),
                        valid_to: product.valid.map(|v| v.end.to_rfc3339()),
                        raw_text: product.raw_text.clone(),
                    });
                }
                sources.push(SourceStatus {
                    source: outcome.source.clone(),
                    products: briefing.products.len(),
                    error: None,
                    answered_for: briefing.departure_at.map(|at| at.to_rfc3339()),
                    narrative: briefing.narrative.clone(),
                });
            }
            Err(error) => sources.push(SourceStatus {
                source: outcome.source.clone(),
                products: 0,
                error: Some(render_chain(error)),
                answered_for: None,
                narrative: None,
            }),
        }
    }
    (products, sources)
}

#[cfg(test)]
mod tests;
