//! The `route_brief` tool: import a Garmin `.fpl` flight plan and brief
//! its corridor across the providers.

use std::sync::Arc;

use aerocontext::{
    AircraftProfile, AwcClient, AwcConfig, FlightRules, GatherOutcome, RouteBriefingRequest,
    WeatherBriefingProvider, gather_route,
};
use aerocontext_navdata::crosssection::CrossSection;
use aerocontext_navdata::flightplan::FlightPlan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::error::ToolError;
use crate::tools::area_brief::{ProductReport, SourceStatus, shape_outcomes};

/// Default corridor half-width, nautical miles.
const DEFAULT_CORRIDOR_HALF_NM: f64 = 25.0;

/// Parameters for the `route_brief` tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct RouteBriefParams {
    /// The Garmin FlightPlan v1 `.fpl` document, as text (the file's
    /// contents; UTF-8 or UTF-16 both parse).
    pub fpl: String,
    /// Corridor half-width in nautical miles around the route centerline.
    /// Defaults to 25 (a 50 NM corridor).
    pub corridor_half_nm: Option<f64>,
    /// Flight rules: `"ifr"` (default) or `"vfr"`. IFR pulls the wider
    /// NOTAM/FDC scope.
    pub rules: Option<String>,
    /// Products to request: `"metar"`, `"taf"`, `"pirep"`, `"sigmet"`.
    /// Empty means the source's default set; the narrative carries
    /// everything else.
    pub products: Option<Vec<String>>,
    /// Optional aircraft performance profile, for the vertical
    /// cross-section (climb/cruise/descent, time en route, fuel).
    pub profile: Option<ProfileParams>,
}

/// Aircraft performance, mirroring
/// [`aerocontext::AircraftProfile`] with a JSON schema
/// for the tool surface. All figures are the operator's own POH/AFM data.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct ProfileParams {
    /// Cruise true airspeed, knots.
    pub cruise_tas_kt: f64,
    /// Average climb rate, feet per minute.
    pub climb_rate_fpm: f64,
    /// Climb true airspeed, knots.
    pub climb_tas_kt: f64,
    /// Average descent rate, feet per minute.
    pub descent_rate_fpm: f64,
    /// Descent true airspeed, knots.
    pub descent_tas_kt: f64,
    /// Cruise fuel burn per hour (operator's unit).
    pub cruise_burn_per_hour: f64,
    /// Start/taxi/takeoff fuel (same unit).
    pub taxi_fuel: f64,
    /// Usable fuel capacity (same unit).
    pub fuel_capacity: f64,
    /// Required reserve, minutes of cruise burn.
    pub reserve_minutes: f64,
}

/// One sample of the vertical profile.
#[derive(Debug, Serialize, JsonSchema)]
pub struct CrossSectionReport {
    /// Route waypoint identifier.
    pub ident: Option<String>,
    /// Estimated altitude, feet MSL.
    pub altitude_ft: f64,
    /// Distance from departure, nautical miles.
    pub along_track_nm: f64,
    /// Minutes from ETD to here.
    pub eta_minutes: u64,
    /// Flight phase: `"Climb"`, `"Cruise"`, or `"Descent"`.
    pub phase: String,
}

/// What the `route_brief` tool returns.
#[derive(Debug, Serialize, JsonSchema)]
pub struct RouteBriefReport {
    /// Departure identifier.
    pub departure: Option<String>,
    /// Destination identifier.
    pub destination: Option<String>,
    /// Number of route waypoints.
    pub waypoint_count: usize,
    /// Great-circle route length, nautical miles.
    pub distance_nm: f64,
    /// Corridor full width used, nautical miles.
    pub corridor_nm: f64,
    /// ETD (RFC 3339) from the plan, when present.
    pub etd: Option<String>,
    /// Planned cruise altitude, feet, when present.
    pub cruise_altitude_ft: Option<i32>,
    /// Vertical profile, when an aircraft profile was supplied.
    pub cross_section: Vec<CrossSectionReport>,
    /// Estimated time en route, minutes (with a profile).
    pub time_en_route_minutes: Option<u64>,
    /// Estimated trip fuel incl. taxi and reserve (with a profile).
    pub fuel_required: Option<f64>,
    /// Whether the trip fuel fits the profile's usable capacity.
    pub fuel_within_capacity: Option<bool>,
    /// Every product gathered, attributed to its source.
    pub products: Vec<ProductReport>,
    /// Per-source outcome, including failures and full narratives.
    pub sources: Vec<SourceStatus>,
    /// Operational notes (e.g. a source skipped for missing credentials).
    pub notes: Vec<String>,
}

/// Parse the plan, gather the route briefing, and shape the report.
pub async fn run(params: RouteBriefParams) -> Result<RouteBriefReport, ToolError> {
    let plan = FlightPlan::from_fpl_bytes(params.fpl.as_bytes()).map_err(|cause| {
        ToolError::InvalidParam {
            message: format!("could not parse the .fpl flight plan: {cause}"),
        }
    })?;
    let corridor_half = params.corridor_half_nm.unwrap_or(DEFAULT_CORRIDOR_HALF_NM);
    let rules = match params
        .rules
        .as_deref()
        .map(str::to_ascii_lowercase)
        .as_deref()
    {
        Some("vfr") => FlightRules::Vfr,
        _ => FlightRules::Ifr,
    };
    let products = parse_products(params.products.as_deref().unwrap_or(&[]))?;
    let request = plan
        .to_route_briefing_request(corridor_half, rules)
        .with_products(products);

    let (cross_section, ete, fuel, within) = match params.profile {
        Some(profile) => build_cross_section(&plan, profile)?,
        None => (Vec::new(), None, None, None),
    };

    let gather_request = request.clone();
    let (outcomes, notes) = tokio::task::spawn_blocking(move || gather_blocking(gather_request))
        .await
        .map_err(|cause| ToolError::BriefingTask {
            cause: Box::new(cause),
        })??;
    let (product_reports, sources) = shape_outcomes(&outcomes);

    Ok(RouteBriefReport {
        departure: plan.departure().map(ToOwned::to_owned),
        destination: plan.destination().map(ToOwned::to_owned),
        waypoint_count: plan.route.len(),
        distance_nm: request.total_distance_nm(),
        corridor_nm: corridor_half * 2.0,
        etd: plan.etd.map(|at| at.to_rfc3339()),
        cruise_altitude_ft: plan.cruise_altitude_ft,
        cross_section,
        time_en_route_minutes: ete,
        fuel_required: fuel,
        fuel_within_capacity: within,
        products: product_reports,
        sources,
        notes,
    })
}

type CrossSectionParts = (
    Vec<CrossSectionReport>,
    Option<u64>,
    Option<f64>,
    Option<bool>,
);

/// Build the vertical profile from a supplied aircraft profile.
fn build_cross_section(
    plan: &FlightPlan,
    profile: ProfileParams,
) -> Result<CrossSectionParts, ToolError> {
    // ProfileParams mirrors AircraftProfile field-for-field; round-trip
    // through JSON since AircraftProfile is non-exhaustive.
    let profile: AircraftProfile = serde_json::to_value(&profile)
        .and_then(serde_json::from_value)
        .map_err(|cause| ToolError::InvalidParam {
            message: format!("invalid aircraft profile: {cause}"),
        })?;
    let xs = CrossSection::build(plan, &profile).map_err(|cause| ToolError::InvalidParam {
        message: format!("cross-section unavailable: {cause}"),
    })?;
    let samples = xs
        .samples
        .iter()
        .map(|s| CrossSectionReport {
            ident: s.ident.clone(),
            altitude_ft: s.altitude_ft,
            along_track_nm: s.along_track_nm,
            eta_minutes: s.eta_from_etd.as_secs() / 60,
            phase: format!("{:?}", s.phase),
        })
        .collect();
    Ok((
        samples,
        Some(xs.time_en_route.as_secs() / 60),
        Some(xs.fuel_required),
        Some(xs.fuel_within_capacity),
    ))
}

/// Run the provider fan-out on a dedicated current-thread runtime (the
/// adapter futures are `?Send`), exactly as the area brief does.
fn gather_blocking(
    request: RouteBriefingRequest,
) -> 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();
        // The route carries its own coordinates, so AWC needs no snapshot.
        let 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),
        })?;
        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 outcomes = gather_route(&providers, &request).await;
        Ok((outcomes, notes))
    })
}

/// Parse product-kind names; only METAR/TAF are mapped today.
fn parse_products(names: &[String]) -> Result<Vec<aerocontext::ProductKind>, ToolError> {
    names
        .iter()
        .map(|name| match name.trim().to_ascii_lowercase().as_str() {
            "metar" => Ok(aerocontext::ProductKind::Metar),
            "taf" => Ok(aerocontext::ProductKind::Taf),
            "pirep" => Ok(aerocontext::ProductKind::Pirep),
            "sigmet" => Ok(aerocontext::ProductKind::Sigmet),
            other => Err(ToolError::InvalidParam {
                message: format!("unknown product {other:?}"),
            }),
        })
        .collect()
}

#[cfg(test)]
mod tests;
