//! Lenient decoding of AWC `format=json` payloads into [`Product`]s.
//!
//! Every field is optional at the serde layer: AWC's OpenAPI spec is
//! descriptive rather than contractual, so a missing field must never
//! fail a whole briefing. Entries without raw product text are dropped
//! (the domain model is text-centric) and counted via `tracing`.

use aerocontext_core::{GeoPoint, Product, ProductKind, ValidPeriod};
use chrono::{DateTime, NaiveDateTime, Utc};
use serde::Deserialize;
use tracing::warn;

#[derive(Debug, Deserialize)]
struct MetarJson {
    #[serde(rename = "icaoId", default)]
    icao_id: Option<String>,
    /// Raw METAR text. AWC names this `rawOb` for METARs and PIREPs.
    #[serde(rename = "rawOb", default)]
    raw_ob: Option<String>,
    /// Observation time, UNIX seconds.
    #[serde(rename = "obsTime", default)]
    obs_time: Option<i64>,
}

#[derive(Debug, Deserialize)]
struct TafJson {
    #[serde(rename = "icaoId", default)]
    icao_id: Option<String>,
    /// Raw TAF text. Named `rawTAF`, not `rawOb`.
    #[serde(rename = "rawTAF", default)]
    raw_taf: Option<String>,
    /// Issuance time, formatted string.
    #[serde(rename = "issueTime", default)]
    issue_time: Option<String>,
    /// Forecast validity window, UNIX seconds.
    #[serde(rename = "validTimeFrom", default)]
    valid_from: Option<i64>,
    #[serde(rename = "validTimeTo", default)]
    valid_to: Option<i64>,
}

#[derive(Debug, Deserialize)]
struct PirepJson {
    #[serde(rename = "icaoId", default)]
    icao_id: Option<String>,
    #[serde(rename = "rawOb", default)]
    raw_ob: Option<String>,
    /// Observation time, UNIX seconds. The spec types this `number`
    /// (unlike METAR's `integer`), so a fractional epoch must not fail
    /// the batch.
    #[serde(rename = "obsTime", default)]
    obs_time: Option<f64>,
}

#[derive(Debug, Deserialize)]
struct AirSigmetJson {
    /// Issuing office, e.g. `KKCI`.
    #[serde(rename = "icaoId", default)]
    icao_id: Option<String>,
    /// Raw text. Named `rawAirSigmet` on this endpoint.
    #[serde(rename = "rawAirSigmet", default)]
    raw_air_sigmet: Option<String>,
    /// Issuance time, formatted string.
    #[serde(rename = "creationTime", default)]
    creation_time: Option<String>,
    /// Hazard polygon as `{lat, lon}` pairs (plain JSON, not GeoJSON).
    #[serde(default)]
    coords: Vec<CoordJson>,
}

#[derive(Debug, Deserialize)]
struct CoordJson {
    #[serde(default)]
    lat: Option<f64>,
    #[serde(default)]
    lon: Option<f64>,
}

/// Decode a `metar` JSON array body.
pub(crate) fn metars(body: &str) -> Result<Vec<Product>, serde_json::Error> {
    let entries: Vec<MetarJson> = serde_json::from_str(body)?;
    Ok(collect(entries, "metar", |entry| {
        Some(
            Product::new(ProductKind::Metar, entry.raw_ob?)
                .with_location(entry.icao_id)
                .with_issued_at(entry.obs_time.and_then(|t| DateTime::from_timestamp(t, 0))),
        )
    }))
}

/// Decode a `taf` JSON array body.
pub(crate) fn tafs(body: &str) -> Result<Vec<Product>, serde_json::Error> {
    let entries: Vec<TafJson> = serde_json::from_str(body)?;
    Ok(collect(entries, "taf", |entry| {
        let valid = taf_validity(entry.valid_from, entry.valid_to);
        Some(
            Product::new(ProductKind::Taf, entry.raw_taf?)
                .with_location(entry.icao_id)
                .with_issued_at(entry.issue_time.as_deref().and_then(parse_awc_time))
                .with_valid(valid),
        )
    }))
}

/// Decode a `pirep` JSON array body.
pub(crate) fn pireps(body: &str) -> Result<Vec<Product>, serde_json::Error> {
    let entries: Vec<PirepJson> = serde_json::from_str(body)?;
    Ok(collect(entries, "pirep", |entry| {
        Some(
            Product::new(ProductKind::Pirep, entry.raw_ob?)
                .with_location(entry.icao_id)
                .with_issued_at(
                    entry
                        .obs_time
                        .and_then(|t| DateTime::from_timestamp(t.trunc() as i64, 0)),
                ),
        )
    }))
}

/// Decode an `airsigmet` JSON array body, keeping the hazard polygon for
/// client-side clipping.
pub(crate) fn air_sigmets(body: &str) -> Result<Vec<(Product, Vec<GeoPoint>)>, serde_json::Error> {
    let entries: Vec<AirSigmetJson> = serde_json::from_str(body)?;
    Ok(collect(entries, "airsigmet", |entry| {
        let decoded: Vec<GeoPoint> = entry
            .coords
            .iter()
            .filter_map(|c| {
                Some(GeoPoint {
                    lat: c.lat?,
                    lon: c.lon?,
                })
            })
            .collect();
        // All-or-nothing: a partially decoded ring could lose a bbox
        // extreme and let clipping exclude a hazard that intersects the
        // request area. No usable geometry means "always keep".
        let polygon = if decoded.len() == entry.coords.len() {
            decoded
        } else {
            warn!(
                dropped = entry.coords.len() - decoded.len(),
                "SIGMET ring vertices failed to decode; keeping the product without geometry"
            );
            Vec::new()
        };
        Some((
            Product::new(ProductKind::Sigmet, entry.raw_air_sigmet?)
                .with_location(entry.icao_id)
                .with_issued_at(entry.creation_time.as_deref().and_then(parse_awc_time)),
            polygon,
        ))
    }))
}

/// Keep SIGMETs whose polygon's bounding box intersects the request box.
///
/// A SIGMET without usable geometry is kept: silently dropping a hazard
/// because its shape did not decode would make the briefing unsafe.
/// Antimeridian-crossing shapes are treated naively (CONUS products do
/// not cross it).
pub(crate) fn clip_to_bbox(
    entries: Vec<(Product, Vec<GeoPoint>)>,
    (south_west, north_east): (GeoPoint, GeoPoint),
) -> Vec<Product> {
    entries
        .into_iter()
        .filter_map(|(product, polygon)| {
            let keep = match polygon_bbox(&polygon) {
                None => true,
                Some((min, max)) => {
                    min.lat <= north_east.lat
                        && max.lat >= south_west.lat
                        && min.lon <= north_east.lon
                        && max.lon >= south_west.lon
                }
            };
            keep.then_some(product)
        })
        .collect()
}

fn polygon_bbox(polygon: &[GeoPoint]) -> Option<(GeoPoint, GeoPoint)> {
    let first = polygon.first()?;
    let mut min = *first;
    let mut max = *first;
    for point in polygon {
        min.lat = min.lat.min(point.lat);
        min.lon = min.lon.min(point.lon);
        max.lat = max.lat.max(point.lat);
        max.lon = max.lon.max(point.lon);
    }
    Some((min, max))
}

fn collect<E, T>(entries: Vec<E>, what: &str, convert: impl Fn(E) -> Option<T>) -> Vec<T> {
    let total = entries.len();
    let kept: Vec<T> = entries.into_iter().filter_map(convert).collect();
    if kept.len() < total {
        warn!(
            what,
            dropped = total - kept.len(),
            "dropped entries without raw product text"
        );
    }
    kept
}

/// Build a [`ValidPeriod`] from a TAF's `validTimeFrom`/`validTimeTo` epoch
/// seconds; `None` unless both are present and decode.
fn taf_validity(from: Option<i64>, to: Option<i64>) -> Option<ValidPeriod> {
    Some(ValidPeriod::new(
        DateTime::from_timestamp(from?, 0)?,
        DateTime::from_timestamp(to?, 0)?,
    ))
}

/// Best-effort parse of AWC's assorted timestamp strings; `None` rather
/// than an error, because a bad timestamp must not fail a briefing.
fn parse_awc_time(text: &str) -> Option<DateTime<Utc>> {
    if let Ok(parsed) = DateTime::parse_from_rfc3339(text) {
        return Some(parsed.to_utc());
    }
    // Space-separated variants seen in spec examples, with and without
    // fractional seconds / trailing Z.
    for format in [
        "%Y-%m-%d %H:%M:%S%.fZ",
        "%Y-%m-%d %H:%M:%S%.f",
        "%Y-%m-%d %H:%M:%S",
    ] {
        if let Ok(naive) = NaiveDateTime::parse_from_str(text, format) {
            return Some(naive.and_utc());
        }
    }
    None
}

#[cfg(test)]
mod tests;
