//! Thermal coupling between circuits for heat transfer.
//!
//! This module provides the infrastructure for modeling heat exchange between
//! independent fluid circuits. Thermal couplings represent heat exchangers
//! that transfer heat from a "hot" circuit to a "cold" circuit without
//! fluid mixing.
//!
//! ## Sign Convention
//!
//! Heat transfer Q > 0 means heat flows INTO the cold circuit (out of hot circuit).
//! This follows the convention that the cold circuit receives heat.
//!
//! ## Coupling Graph and Circular Dependencies
//!
//! Thermal couplings form a directed graph where:
//! - Nodes are circuits (CircuitId)
//! - Edges point from hot_circuit to cold_circuit (direction of heat flow)
//!
//! Circular dependencies occur when circuits mutually heat each other (A→B and B→A).
//! Circuits in circular dependencies must be solved simultaneously by the solver.

use entropyk_core::{CircuitId, Temperature, ThermalConductance};
use petgraph::algo::{is_cyclic_directed, kosaraju_scc};
use petgraph::graph::{DiGraph, NodeIndex};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

fn default_duty_scale() -> f64 {
    1.0
}

/// Thermal coupling between two circuits via a heat exchanger.
///
/// Heat flows from `hot_circuit` to `cold_circuit` proportional to the
/// temperature difference and thermal conductance (UA value).
///
/// ## Physical (duty-transfer) mode
///
/// When [`hot_component`](Self::hot_component) and
/// [`cold_component`](Self::cold_component) are set, the coupling is **physical**:
/// the per-coupling state unknown is the transferred heat `Q` [W], closed by the
/// residual `r = Q − η·duty(hot_component)` where the duty is *measured* from the
/// solved state via the hot component's `measure_output(Capacity)` (e.g. the real
/// ε-NTU condenser duty). The cold-side component (a
/// [`ThermalLoad`](entropyk_components::ThermalLoad)) consumes `Q` in its energy
/// balance `ṁ·(h_out − h_in) = Q`, so the cold circuit genuinely warms up.
///
/// Without the component references the legacy MVP stub applies (the residual
/// simply pins `Q = 0`), preserved for backward compatibility.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ThermalCoupling {
    /// Circuit that supplies heat (higher temperature side).
    #[serde(alias = "hot_circuit")]
    pub hot_circuit: CircuitId,
    /// Circuit that receives heat (lower temperature side).
    #[serde(alias = "cold_circuit")]
    pub cold_circuit: CircuitId,
    /// Thermal conductance (UA) in W/K. Higher values = more heat transfer.
    pub ua: ThermalConductance,
    /// Efficiency factor (0.0 to 1.0). Default is 1.0 (no losses).
    pub efficiency: f64,
    /// Multiplier applied to the measured duty before it is injected into the
    /// receiver. Use `-1.0` when the receiver must be cooled by the source duty
    /// (for example, chilled water across an evaporator).
    #[serde(default = "default_duty_scale", alias = "duty_scale")]
    pub duty_scale: f64,
    /// Name of the registered component in the hot circuit whose *measured duty*
    /// (`measure_output(Capacity)`) is transferred (e.g. an ε-NTU `Condenser`).
    #[serde(default, alias = "hot_component")]
    pub hot_component: Option<String>,
    /// Name of the registered `ThermalLoad` component in the cold circuit that
    /// receives `Q` in its energy balance.
    #[serde(default, alias = "cold_component")]
    pub cold_component: Option<String>,
}

impl ThermalCoupling {
    /// Creates a new thermal coupling between two circuits.
    ///
    /// # Arguments
    ///
    /// * `hot_circuit` - Circuit at higher temperature (heat source)
    /// * `cold_circuit` - Circuit at lower temperature (heat sink)
    /// * `ua` - Thermal conductance in W/K
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_solver::{ThermalCoupling, CircuitId};
    /// use entropyk_core::ThermalConductance;
    ///
    /// let coupling = ThermalCoupling::new(
    ///     CircuitId(0),
    ///     CircuitId(1),
    ///     ThermalConductance::from_watts_per_kelvin(1000.0),
    /// );
    /// ```
    pub fn new(hot_circuit: CircuitId, cold_circuit: CircuitId, ua: ThermalConductance) -> Self {
        Self {
            hot_circuit,
            cold_circuit,
            ua,
            efficiency: 1.0,
            duty_scale: 1.0,
            hot_component: None,
            cold_component: None,
        }
    }

    /// Sets the efficiency factor for the coupling.
    ///
    /// Efficiency accounts for heat losses in the heat exchanger.
    /// A value of 0.9 means 90% of theoretical heat is transferred.
    pub fn with_efficiency(mut self, efficiency: f64) -> Self {
        self.efficiency = efficiency.clamp(0.0, 1.0);
        self
    }

    /// Sets the signed duty multiplier applied before injecting heat into the
    /// receiver component.
    pub fn with_duty_scale(mut self, duty_scale: f64) -> Self {
        self.duty_scale = if duty_scale.is_finite() {
            duty_scale
        } else {
            1.0
        };
        self
    }

    /// Enables the **physical duty-transfer mode**: the measured duty of
    /// `hot_component` (via `measure_output(Capacity)`) is transferred, scaled
    /// by `efficiency`, into `cold_component`'s energy balance (a `ThermalLoad`).
    pub fn with_interface_components(
        mut self,
        hot_component: impl Into<String>,
        cold_component: impl Into<String>,
    ) -> Self {
        self.hot_component = Some(hot_component.into());
        self.cold_component = Some(cold_component.into());
        self
    }

    /// Returns `true` when the coupling is in physical duty-transfer mode.
    pub fn is_physical(&self) -> bool {
        self.hot_component.is_some() && self.cold_component.is_some()
    }
}

/// Computes heat transfer for a thermal coupling.
///
/// # Formula
///
/// Q = η × UA × (T_hot - T_cold)
///
/// Where:
/// - Q is the heat transfer rate (W), positive means heat INTO cold circuit
/// - η is the efficiency factor
/// - UA is the thermal conductance (W/K)
/// - T_hot, T_cold are temperatures (K)
///
/// # Sign Convention
///
/// - Q > 0: Heat flows from hot to cold (normal operation)
/// - Q = 0: No temperature difference
/// - Q < 0: Cold is hotter than hot (reverse flow, unusual)
///
/// # Example
///
/// ```
/// use entropyk_solver::{ThermalCoupling, CircuitId, compute_coupling_heat};
/// use entropyk_core::{Temperature, ThermalConductance};
///
/// let coupling = ThermalCoupling::new(
///     CircuitId(0),
///     CircuitId(1),
///     ThermalConductance::from_watts_per_kelvin(1000.0),
/// );
///
/// let t_hot = Temperature::from_kelvin(350.0);
/// let t_cold = Temperature::from_kelvin(300.0);
///
/// let q = compute_coupling_heat(&coupling, t_hot, t_cold);
/// assert!(q > 0.0, "Heat should flow from hot to cold");
/// ```
pub fn compute_coupling_heat(
    coupling: &ThermalCoupling,
    t_hot: Temperature,
    t_cold: Temperature,
) -> f64 {
    coupling.efficiency
        * coupling.ua.to_watts_per_kelvin()
        * (t_hot.to_kelvin() - t_cold.to_kelvin())
}

/// Builds a coupling graph for dependency analysis.
///
/// Returns a directed graph where:
/// - Nodes are CircuitIds present in any coupling
/// - Edges point from hot_circuit to cold_circuit
fn build_coupling_graph(couplings: &[ThermalCoupling]) -> DiGraph<CircuitId, ()> {
    let mut graph = DiGraph::new();
    let mut circuit_to_node: HashMap<CircuitId, NodeIndex> = HashMap::new();

    for coupling in couplings {
        // Add hot_circuit node if not present
        let hot_node = *circuit_to_node
            .entry(coupling.hot_circuit)
            .or_insert_with(|| graph.add_node(coupling.hot_circuit));

        // Add cold_circuit node if not present
        let cold_node = *circuit_to_node
            .entry(coupling.cold_circuit)
            .or_insert_with(|| graph.add_node(coupling.cold_circuit));

        // Add directed edge: hot -> cold
        graph.add_edge(hot_node, cold_node, ());
    }

    graph
}

/// Checks if the coupling graph contains circular dependencies.
///
/// Circular dependencies occur when circuits are mutually thermally coupled
/// (e.g., A heats B, and B heats A). When circular dependencies exist,
/// the solver must solve those circuits simultaneously rather than sequentially.
///
/// # Example
///
/// ```
/// use entropyk_solver::{ThermalCoupling, CircuitId, has_circular_dependencies};
/// use entropyk_core::ThermalConductance;
///
/// // No circular dependency: A → B → C
/// let couplings = vec![
///     ThermalCoupling::new(CircuitId(0), CircuitId(1), ThermalConductance::from_watts_per_kelvin(100.0)),
///     ThermalCoupling::new(CircuitId(1), CircuitId(2), ThermalConductance::from_watts_per_kelvin(100.0)),
/// ];
/// assert!(!has_circular_dependencies(&couplings));
///
/// // Circular dependency: A → B and B → A
/// let couplings_circular = vec![
///     ThermalCoupling::new(CircuitId(0), CircuitId(1), ThermalConductance::from_watts_per_kelvin(100.0)),
///     ThermalCoupling::new(CircuitId(1), CircuitId(0), ThermalConductance::from_watts_per_kelvin(100.0)),
/// ];
/// assert!(has_circular_dependencies(&couplings_circular));
/// ```
pub fn has_circular_dependencies(couplings: &[ThermalCoupling]) -> bool {
    if couplings.is_empty() {
        return false;
    }
    let graph = build_coupling_graph(couplings);
    is_cyclic_directed(&graph)
}

/// Returns groups of circuits that must be solved simultaneously.
///
/// Groups are computed using strongly connected components (SCC) analysis
/// of the coupling graph. Circuits in the same SCC have circular thermal
/// dependencies and must be solved together.
///
/// # Returns
///
/// A vector of vectors, where each inner vector contains CircuitIds that
/// must be solved simultaneously. Single-element vectors indicate circuits
/// that can be solved independently (in topological order).
///
/// # Example
///
/// ```
/// use entropyk_solver::{ThermalCoupling, CircuitId, coupling_groups};
/// use entropyk_core::ThermalConductance;
///
/// // A → B, B and C independent
/// let couplings = vec![
///     ThermalCoupling::new(CircuitId(0), CircuitId(1), ThermalConductance::from_watts_per_kelvin(100.0)),
/// ];
/// let groups = coupling_groups(&couplings);
/// // Groups will contain individual circuits since there's no cycle
/// ```
pub fn coupling_groups(couplings: &[ThermalCoupling]) -> Vec<Vec<CircuitId>> {
    if couplings.is_empty() {
        return Vec::new();
    }

    let graph = build_coupling_graph(couplings);
    let sccs = kosaraju_scc(&graph);

    sccs.into_iter()
        .map(|node_indices| node_indices.into_iter().map(|idx| graph[idx]).collect())
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use approx::assert_relative_eq;

    fn make_coupling(hot: u16, cold: u16, ua_w_per_k: f64) -> ThermalCoupling {
        ThermalCoupling::new(
            CircuitId(hot),
            CircuitId(cold),
            ThermalConductance::from_watts_per_kelvin(ua_w_per_k),
        )
    }

    #[test]
    fn test_thermal_coupling_creation() {
        let coupling = ThermalCoupling::new(
            CircuitId(0),
            CircuitId(1),
            ThermalConductance::from_watts_per_kelvin(1000.0),
        );

        assert_eq!(coupling.hot_circuit, CircuitId(0));
        assert_eq!(coupling.cold_circuit, CircuitId(1));
        assert_relative_eq!(coupling.ua.to_watts_per_kelvin(), 1000.0, epsilon = 1e-10);
        assert_relative_eq!(coupling.efficiency, 1.0, epsilon = 1e-10);
    }

    #[test]
    fn test_thermal_coupling_with_efficiency() {
        let coupling = ThermalCoupling::new(
            CircuitId(0),
            CircuitId(1),
            ThermalConductance::from_watts_per_kelvin(1000.0),
        )
        .with_efficiency(0.85);

        assert_relative_eq!(coupling.efficiency, 0.85, epsilon = 1e-10);
    }

    #[test]
    fn test_efficiency_clamped() {
        let coupling = make_coupling(0, 1, 100.0).with_efficiency(1.5);
        assert_relative_eq!(coupling.efficiency, 1.0, epsilon = 1e-10);

        let coupling = make_coupling(0, 1, 100.0).with_efficiency(-0.5);
        assert_relative_eq!(coupling.efficiency, 0.0, epsilon = 1e-10);
    }

    #[test]
    fn test_compute_coupling_heat_positive() {
        let coupling = make_coupling(0, 1, 1000.0);
        let t_hot = Temperature::from_kelvin(350.0);
        let t_cold = Temperature::from_kelvin(300.0);

        let q = compute_coupling_heat(&coupling, t_hot, t_cold);

        // Q = 1.0 * 1000 * (350 - 300) = 50000 W
        assert_relative_eq!(q, 50000.0, epsilon = 1e-10);
        assert!(q > 0.0, "Heat should be positive (into cold circuit)");
    }

    #[test]
    fn test_compute_coupling_heat_zero() {
        let coupling = make_coupling(0, 1, 1000.0);
        let t_hot = Temperature::from_kelvin(300.0);
        let t_cold = Temperature::from_kelvin(300.0);

        let q = compute_coupling_heat(&coupling, t_hot, t_cold);

        assert_relative_eq!(q, 0.0, epsilon = 1e-10);
    }

    #[test]
    fn test_compute_coupling_heat_negative() {
        let coupling = make_coupling(0, 1, 1000.0);
        let t_hot = Temperature::from_kelvin(280.0);
        let t_cold = Temperature::from_kelvin(300.0);

        let q = compute_coupling_heat(&coupling, t_hot, t_cold);

        // Q = 1000 * (280 - 300) = -20000 W (reverse flow)
        assert_relative_eq!(q, -20000.0, epsilon = 1e-10);
        assert!(q < 0.0, "Heat should be negative (reverse flow)");
    }

    #[test]
    fn test_compute_coupling_heat_with_efficiency() {
        let coupling = make_coupling(0, 1, 1000.0).with_efficiency(0.9);
        let t_hot = Temperature::from_kelvin(350.0);
        let t_cold = Temperature::from_kelvin(300.0);

        let q = compute_coupling_heat(&coupling, t_hot, t_cold);

        // Q = 0.9 * 1000 * 50 = 45000 W
        assert_relative_eq!(q, 45000.0, epsilon = 1e-10);
    }

    #[test]
    fn test_energy_conservation() {
        // For two circuits coupled, Q_hot = -Q_cold
        // This means the heat leaving hot circuit equals heat entering cold circuit
        let coupling = make_coupling(0, 1, 1000.0);
        let t_hot = Temperature::from_kelvin(350.0);
        let t_cold = Temperature::from_kelvin(300.0);

        let q_into_cold = compute_coupling_heat(&coupling, t_hot, t_cold);
        let q_out_of_hot = -q_into_cold; // By convention

        // Heat into cold = - (heat out of hot)
        assert_relative_eq!(q_into_cold, -q_out_of_hot, epsilon = 1e-10);
        assert!(q_into_cold > 0.0, "Cold circuit receives heat");
        assert!(q_out_of_hot < 0.0, "Hot circuit loses heat");
    }

    #[test]
    fn test_no_circular_dependency() {
        // Linear chain: A → B → C
        let couplings = vec![make_coupling(0, 1, 100.0), make_coupling(1, 2, 100.0)];

        assert!(!has_circular_dependencies(&couplings));
    }

    #[test]
    fn test_circular_dependency_detection() {
        // Mutual: A → B and B → A
        let couplings = vec![make_coupling(0, 1, 100.0), make_coupling(1, 0, 100.0)];

        assert!(has_circular_dependencies(&couplings));
    }

    #[test]
    fn test_circular_dependency_complex() {
        // Triangle: A → B → C → A
        let couplings = vec![
            make_coupling(0, 1, 100.0),
            make_coupling(1, 2, 100.0),
            make_coupling(2, 0, 100.0),
        ];

        assert!(has_circular_dependencies(&couplings));
    }

    #[test]
    fn test_empty_couplings_no_cycle() {
        let couplings: Vec<ThermalCoupling> = vec![];
        assert!(!has_circular_dependencies(&couplings));
    }

    #[test]
    fn test_single_coupling_no_cycle() {
        let couplings = vec![make_coupling(0, 1, 100.0)];
        assert!(!has_circular_dependencies(&couplings));
    }

    #[test]
    fn test_coupling_groups_no_cycle() {
        // A → B, C independent
        let couplings = vec![make_coupling(0, 1, 100.0)];

        let groups = coupling_groups(&couplings);

        // With no cycles, each circuit is its own group
        assert_eq!(groups.len(), 2);

        // Each group should have exactly one circuit
        for group in &groups {
            assert_eq!(group.len(), 1);
        }

        // Collect all circuit IDs
        let all_circuits: std::collections::HashSet<CircuitId> =
            groups.iter().flat_map(|g| g.iter().copied()).collect();
        assert!(all_circuits.contains(&CircuitId(0)));
        assert!(all_circuits.contains(&CircuitId(1)));
    }

    #[test]
    fn test_coupling_groups_with_cycle() {
        // A ↔ B (mutual), C → D
        let couplings = vec![
            make_coupling(0, 1, 100.0),
            make_coupling(1, 0, 100.0),
            make_coupling(2, 3, 100.0),
        ];

        let groups = coupling_groups(&couplings);

        // Should have 3 groups: [A, B] as one, C as one, D as one
        assert_eq!(groups.len(), 3);

        // Find the group with 2 circuits (A and B)
        let large_group: Vec<&Vec<CircuitId>> = groups.iter().filter(|g| g.len() == 2).collect();
        assert_eq!(large_group.len(), 1);

        let ab_group = large_group[0];
        assert!(ab_group.contains(&CircuitId(0)));
        assert!(ab_group.contains(&CircuitId(1)));
    }

    #[test]
    fn test_coupling_groups_empty() {
        let couplings: Vec<ThermalCoupling> = vec![];
        let groups = coupling_groups(&couplings);
        assert!(groups.is_empty());
    }
}
