//! Propagation pipeline as a free category.
//!
//! The SGP4/SDP4 computation is modeled as a directed graph where:
//! - Vertices are pipeline stages (data types)
//! - Edges are computation steps (functions between stages)
//! - Paths are complete propagation pipelines
//! - `interpret` from comp-cat-rs evaluates a path into a result
//!
//! ```text
//!   RawTle --(0: Extract)--> CommonElems --(1: InitSgp4)--> Initialized
//!                                        --(2: InitSdp4)-->
//!
//!   Initialized --(3: SecularSgp4)--> Secular
//!               --(4: SecularSdp4)-->
//!
//!   Secular --(5: PeriodicSgp4)--> Periodic
//!           --(6: PeriodicSdp4)-->
//!
//!   Periodic --(7: ComputeEci)--> EciOutput
//! ```

use comp_cat_rs::collapse::free_category::{
    Edge, FreeCategoryError, Graph, GraphMorphism, Path, Vertex, interpret,
};

use crate::error::Error;
use crate::propagation::classify::PropagatorKind;
use crate::propagation::sgp4::{EciOutput, SgpInit};
use crate::tle::Tle;
use crate::tle::newtypes::MinutesSinceEpoch;

// ---------------------------------------------------------------------------
// Graph definition
// ---------------------------------------------------------------------------

/// The propagation pipeline graph.
///
/// 6 vertices (stages), 8 edges (computation steps).
pub struct PropagationGraph;

/// Vertex indices for pipeline stages.
const V_RAW_TLE: usize = 0;
const V_COMMON: usize = 1;
const V_INIT: usize = 2;
const V_SECULAR: usize = 3;
const V_PERIODIC: usize = 4;
const V_ECI: usize = 5;

/// Edge indices for computation steps.
const E_EXTRACT: usize = 0;
const E_INIT_SGP4: usize = 1;
const E_INIT_SDP4: usize = 2;
const E_SECULAR_SGP4: usize = 3;
const E_SECULAR_SDP4: usize = 4;
const E_PERIODIC_SGP4: usize = 5;
const E_PERIODIC_SDP4: usize = 6;
const E_COMPUTE_ECI: usize = 7;

const VERTEX_COUNT: usize = 6;
const EDGE_COUNT: usize = 8;

/// Source vertices for each edge (indexed by edge index).
const EDGE_SOURCES: [usize; EDGE_COUNT] = [
    V_RAW_TLE, // 0: Extract
    V_COMMON,  // 1: InitSgp4
    V_COMMON,  // 2: InitSdp4
    V_INIT,    // 3: SecularSgp4
    V_INIT,    // 4: SecularSdp4
    V_SECULAR, // 5: PeriodicSgp4
    V_SECULAR, // 6: PeriodicSdp4
    V_PERIODIC, // 7: ComputeEci
];

/// Target vertices for each edge.
const EDGE_TARGETS: [usize; EDGE_COUNT] = [
    V_COMMON,   // 0: Extract
    V_INIT,     // 1: InitSgp4
    V_INIT,     // 2: InitSdp4
    V_SECULAR,  // 3: SecularSgp4
    V_SECULAR,  // 4: SecularSdp4
    V_PERIODIC, // 5: PeriodicSgp4
    V_PERIODIC, // 6: PeriodicSdp4
    V_ECI,      // 7: ComputeEci
];

impl Graph for PropagationGraph {
    fn vertex_count(&self) -> usize {
        VERTEX_COUNT
    }

    fn edge_count(&self) -> usize {
        EDGE_COUNT
    }

    fn source(&self, edge: Edge) -> Result<Vertex, FreeCategoryError> {
        EDGE_SOURCES
            .get(edge.index())
            .map(|&idx| Vertex::new(idx))
            .ok_or(FreeCategoryError::EdgeOutOfBounds {
                edge,
                count: EDGE_COUNT,
            })
    }

    fn target(&self, edge: Edge) -> Result<Vertex, FreeCategoryError> {
        EDGE_TARGETS
            .get(edge.index())
            .map(|&idx| Vertex::new(idx))
            .ok_or(FreeCategoryError::EdgeOutOfBounds {
                edge,
                count: EDGE_COUNT,
            })
    }
}

// ---------------------------------------------------------------------------
// Path builders
// ---------------------------------------------------------------------------

/// Build the SGP4 (near-earth) propagation path.
///
/// `RawTle -> CommonElems -> Initialized -> Secular -> Periodic -> EciOutput`
///
/// # Errors
///
/// Returns an error if path construction fails (should not happen
/// for well-formed edge indices).
pub fn sgp4_path() -> Result<Path, Error> {
    let graph = PropagationGraph;
    [E_EXTRACT, E_INIT_SGP4, E_SECULAR_SGP4, E_PERIODIC_SGP4, E_COMPUTE_ECI]
        .iter()
        .try_fold(
            Path::identity(Vertex::new(V_RAW_TLE)),
            |acc, &edge_idx| {
                Path::singleton(&graph, Edge::new(edge_idx))
                    .and_then(|step| acc.compose(step))
            },
        )
        .map_err(Error::from)
}

/// Build the SDP4 (deep-space) propagation path.
///
/// # Errors
///
/// Returns an error if path construction fails.
pub fn sdp4_path() -> Result<Path, Error> {
    let graph = PropagationGraph;
    [E_EXTRACT, E_INIT_SDP4, E_SECULAR_SDP4, E_PERIODIC_SDP4, E_COMPUTE_ECI]
        .iter()
        .try_fold(
            Path::identity(Vertex::new(V_RAW_TLE)),
            |acc, &edge_idx| {
                Path::singleton(&graph, Edge::new(edge_idx))
                    .and_then(|step| acc.compose(step))
            },
        )
        .map_err(Error::from)
}

/// Select the appropriate path based on propagator kind.
///
/// # Errors
///
/// Returns an error if path construction fails.
pub fn select_path(kind: PropagatorKind) -> Result<Path, Error> {
    match kind {
        PropagatorKind::Sgp4 => sgp4_path(),
        PropagatorKind::Sdp4 => sdp4_path(),
    }
}

// ---------------------------------------------------------------------------
// Graph morphism: interprets edges as actual computation
// ---------------------------------------------------------------------------

/// The morphism that maps pipeline edges to actual propagation functions.
///
/// Carries the TLE and time offset needed for evaluation.
pub struct PropagationInterpreter<'a> {
    tle: &'a Tle,
    tsince: MinutesSinceEpoch,
}

impl<'a> PropagationInterpreter<'a> {
    #[must_use]
    pub fn new(tle: &'a Tle, tsince: MinutesSinceEpoch) -> Self {
        Self { tle, tsince }
    }
}

/// A propagation step: a function that transforms pipeline state.
///
/// Each step captures enough context to execute when composed.
/// Composition chains steps sequentially.
pub enum PropagationStep<'a> {
    /// Identity: no-op.
    Identity,
    /// A single step that carries TLE + time and produces `EciOutput`.
    FullPipeline {
        tle: &'a Tle,
        tsince: MinutesSinceEpoch,
        kind: PropagatorKind,
    },
}

impl<'a> GraphMorphism<PropagationGraph> for PropagationInterpreter<'a> {
    type Object = Vertex;
    type Morphism = PropagationStep<'a>;

    fn map_vertex(&self, v: Vertex) -> Vertex {
        v
    }

    fn map_edge(&self, e: Edge) -> PropagationStep<'a> {
        // Each edge maps to a step that knows the TLE, time, and
        // which propagator branch to use.  The actual computation
        // happens when the composed result is `execute`d.
        let kind = match e.index() {
            E_INIT_SGP4 | E_SECULAR_SGP4 | E_PERIODIC_SGP4 => PropagatorKind::Sgp4,
            _ => PropagatorKind::Sdp4,
        };
        PropagationStep::FullPipeline {
            tle: self.tle,
            tsince: self.tsince,
            kind,
        }
    }
}

impl PropagationStep<'_> {
    /// Execute the composed propagation step.
    ///
    /// # Errors
    ///
    /// Returns an error if initialization or propagation fails.
    pub fn execute(&self) -> Result<EciOutput, Error> {
        match self {
            Self::Identity => Err(Error::PropagationDiverged {
                context: "cannot execute identity step",
            }),
            Self::FullPipeline { tle, tsince, kind } => {
                let init = SgpInit::from_tle(tle)?;
                match kind {
                    PropagatorKind::Sgp4 => crate::propagation::sgp4::propagate(&init, *tsince),
                    PropagatorKind::Sdp4 => crate::propagation::sdp4::propagate(&init, *tsince),
                }
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Top-level pipeline execution via interpret
// ---------------------------------------------------------------------------

/// Propagate via the free category pipeline.
///
/// Builds the appropriate path (SGP4 or SDP4), interprets it
/// via the `PropagationInterpreter`, and executes the result.
///
/// # Errors
///
/// Returns an error if path construction, initialization, or
/// propagation fails.
pub fn propagate_via_pipeline(
    tle: &Tle,
    tsince: MinutesSinceEpoch,
) -> Result<EciOutput, Error> {
    let kind = crate::propagation::classify::select_propagator(tle.mean_motion());
    let path = select_path(kind)?;
    let interp = PropagationInterpreter::new(tle, tsince);

    let step = interpret::<PropagationGraph, _>(
        &interp,
        &path,
        |_| PropagationStep::Identity,
        |_prev, current| current,
    );

    step.execute()
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tle::parse::parse_tle;

    const ISS_L0: &str = "ISS (ZARYA)";
    const ISS_L1: &str =
        "1 25544U 98067A   24045.54339622  .00016717  00000-0  10270-3 0  9118";
    const ISS_L2: &str =
        "2 25544  51.6400 208.2546 0006703  44.2624  67.4637 15.50056823 23455";

    #[test]
    fn sgp4_path_has_5_edges() -> Result<(), Error> {
        let path = sgp4_path()?;
        assert_eq!(path.len(), 5);
        assert_eq!(path.source(), Vertex::new(V_RAW_TLE));
        assert_eq!(path.target(), Vertex::new(V_ECI));
        Ok(())
    }

    #[test]
    fn sdp4_path_has_5_edges() -> Result<(), Error> {
        let path = sdp4_path()?;
        assert_eq!(path.len(), 5);
        assert_eq!(path.source(), Vertex::new(V_RAW_TLE));
        assert_eq!(path.target(), Vertex::new(V_ECI));
        Ok(())
    }

    #[test]
    fn pipeline_propagation_matches_direct() -> Result<(), Error> {
        let tle = parse_tle(ISS_L0, ISS_L1, ISS_L2).run()?;
        let tsince = MinutesSinceEpoch::new(0.0);

        let pipeline_result = propagate_via_pipeline(&tle, tsince)?;
        let direct_result = crate::propagation::propagate(&tle, tsince)?;

        // Both paths should produce identical results for near-earth
        let diff = (pipeline_result.position_km() - direct_result.position_km()).magnitude();
        assert!(
            diff < 1e-10,
            "pipeline and direct results differ by {diff} km"
        );
        Ok(())
    }

    #[test]
    fn pipeline_at_60_min_produces_reasonable_output() -> Result<(), Error> {
        let tle = parse_tle(ISS_L0, ISS_L1, ISS_L2).run()?;
        let result = propagate_via_pipeline(&tle, MinutesSinceEpoch::new(60.0))?;

        let r = result.position_km().magnitude();
        assert!(
            r > 6400.0 && r < 7200.0,
            "radius {r} km out of range"
        );
        Ok(())
    }
}
