//! System-wide degrees-of-freedom (DoF) bookkeeping.
//!
//! A thermodynamic cycle is a square nonlinear system:
//!
//! ```text
//! n_equations == n_unknowns
//! ```
//!
//! Every time a quantity is **fixed** (Dirichlet residual, outlet closure,
//! inverse constraint), either another quantity must be **freed** (actuator,
//! emergent pressure, free boundary) or an existing residual must be dropped.
//!
//! This module provides:
//! - re-export of component-level [`EquationRole`];
//! - unknown labels and a full system ledger;
//! - hard validation (`SystemDofBalance`) used as a pre-solve gate.
//!
//! # Design rules
//!
//! 1. Parameters outside the state vector are **not** free unknowns.
//! 2. Scalar secondary streams (`T_sec`, `C_sec`) are **rating-mode inputs**, not
//!    system-mode substitutes for a live secondary loop.
//! 3. Outlet closures (superheat / subcooling / quality / level) consume one DoF
//!    and must be paired with a free actuator or an intentional residual drop.
//! 4. Imbalance is an error, not a warning.

use std::fmt;

use thiserror::Error;

pub use entropyk_components::{unspecified_roles, EquationRole};

// ─────────────────────────────────────────────────────────────────────────────
// Unknown labels
// ─────────────────────────────────────────────────────────────────────────────

/// Kind of Newton unknown in the full state vector.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnknownKind {
    /// Shared branch mass-flow slot.
    BranchMassFlow {
        /// Branch id from topology presolve.
        branch_id: usize,
    },
    /// Edge pressure.
    EdgePressure {
        /// Edge ordinal in finalize order.
        edge_ordinal: usize,
    },
    /// Edge enthalpy.
    EdgeEnthalpy {
        /// Edge ordinal in finalize order.
        edge_ordinal: usize,
    },
    /// Hard inverse-control variable.
    InverseControl {
        /// Bounded-variable id string.
        id: String,
    },
    /// Thermal-coupling heat unknown Q [W].
    CouplingHeat {
        /// Coupling index.
        index: usize,
    },
    /// Saturated-PI actuator `u`.
    SaturatedActuator {
        /// Controller index.
        index: usize,
    },
    /// Saturated-PI internal state `x`.
    SaturatedIntegrator {
        /// Controller index.
        index: usize,
    },
    /// Physical free actuator (EXV opening, fan speed, …).
    FreeActuator {
        /// Bounded-variable id string.
        id: String,
    },
    /// Internal component state slot (macro-components / reserved).
    Internal {
        /// Slot index within the physical block.
        index: usize,
    },
}

impl fmt::Display for UnknownKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::BranchMassFlow { branch_id } => write!(f, "m_branch[{branch_id}]"),
            Self::EdgePressure { edge_ordinal } => write!(f, "P_edge[{edge_ordinal}]"),
            Self::EdgeEnthalpy { edge_ordinal } => write!(f, "h_edge[{edge_ordinal}]"),
            Self::InverseControl { id } => write!(f, "ctrl[{id}]"),
            Self::CouplingHeat { index } => write!(f, "Q_coupling[{index}]"),
            Self::SaturatedActuator { index } => write!(f, "u_sat[{index}]"),
            Self::SaturatedIntegrator { index } => write!(f, "x_sat[{index}]"),
            Self::FreeActuator { id } => write!(f, "actuator[{id}]"),
            Self::Internal { index } => write!(f, "internal[{index}]"),
        }
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Ledger entries
// ─────────────────────────────────────────────────────────────────────────────

/// One residual block contributed by a graph node.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComponentEquationBlock {
    /// Human-readable component name (or node index fallback).
    pub component_name: String,
    /// Graph node index.
    pub node_index: usize,
    /// Declared `n_equations()`.
    pub n_equations: usize,
    /// Semantic roles (length should equal `n_equations` when fully migrated).
    pub roles: Vec<EquationRole>,
}

/// Outcome of comparing equation count to unknown count.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SystemDofBalance {
    /// Square system — well posed at the dimension level.
    Balanced,
    /// More equations than unknowns.
    OverConstrained {
        /// `n_equations − n_unknowns`.
        excess_equations: usize,
    },
    /// Fewer equations than unknowns.
    UnderConstrained {
        /// `n_unknowns − n_equations`.
        free_dofs: usize,
    },
}

impl SystemDofBalance {
    /// Builds the balance enum from raw counts.
    pub fn from_counts(n_equations: usize, n_unknowns: usize) -> Self {
        match n_equations.cmp(&n_unknowns) {
            std::cmp::Ordering::Equal => Self::Balanced,
            std::cmp::Ordering::Greater => Self::OverConstrained {
                excess_equations: n_equations - n_unknowns,
            },
            std::cmp::Ordering::Less => Self::UnderConstrained {
                free_dofs: n_unknowns - n_equations,
            },
        }
    }

    /// Returns `true` when the system is square.
    pub fn is_balanced(self) -> bool {
        matches!(self, Self::Balanced)
    }
}

impl fmt::Display for SystemDofBalance {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Balanced => write!(f, "balanced"),
            Self::OverConstrained { excess_equations } => {
                write!(f, "over-constrained by {excess_equations}")
            }
            Self::UnderConstrained { free_dofs } => {
                write!(f, "under-constrained by {free_dofs}")
            }
        }
    }
}

/// Full DoF report for a finalized [`crate::System`].
#[derive(Debug, Clone, PartialEq)]
pub struct DofReport {
    /// Total residual equations assembled by the solver.
    pub n_equations: usize,
    /// Total Newton unknowns (`full_state_vector_len`).
    pub n_unknowns: usize,
    /// Balance classification.
    pub balance: SystemDofBalance,
    /// Per-component residual blocks.
    pub components: Vec<ComponentEquationBlock>,
    /// System-level residual roles (constraints, couplings, saturated).
    pub system_equations: Vec<EquationRole>,
    /// Catalog of unknowns (may be compact when edge map is large).
    pub unknowns: Vec<UnknownKind>,
    /// Human diagnostics (pairing warnings, role-length mismatches, …).
    pub diagnostics: Vec<String>,
}

impl DofReport {
    /// Renders a concise multi-line summary suitable for logs and CLI.
    pub fn summary(&self) -> String {
        let mut lines = Vec::new();
        lines.push(format!(
            "DoF: equations={} unknowns={} → {}",
            self.n_equations, self.n_unknowns, self.balance
        ));
        for block in &self.components {
            let roles = if block.roles.is_empty() {
                "(no roles declared)".to_string()
            } else {
                block
                    .roles
                    .iter()
                    .map(|r| r.to_string())
                    .collect::<Vec<_>>()
                    .join(", ")
            };
            lines.push(format!(
                "  node {} `{}`: {} eqs — {}",
                block.node_index, block.component_name, block.n_equations, roles
            ));
        }
        if !self.system_equations.is_empty() {
            lines.push(format!(
                "  system-level: {}",
                self.system_equations
                    .iter()
                    .map(|r| r.to_string())
                    .collect::<Vec<_>>()
                    .join(", ")
            ));
        }
        for d in &self.diagnostics {
            lines.push(format!("  ! {d}"));
        }
        lines.join("\n")
    }
}

/// Errors raised by the hard DoF gate.
#[derive(Error, Debug, Clone, PartialEq)]
pub enum SystemDofError {
    /// Square-system check failed.
    #[error(
        "System DoF imbalance: {n_equations} equations vs {n_unknowns} unknowns ({balance}).\n{summary}"
    )]
    Imbalance {
        /// Equation count.
        n_equations: usize,
        /// Unknown count.
        n_unknowns: usize,
        /// Balance tag.
        balance: SystemDofBalance,
        /// Full summary text.
        summary: String,
    },
    /// Component declared roles that disagree with `n_equations()`.
    #[error(
        "Component `{component}` equation_roles length {roles_len} != n_equations {n_equations}"
    )]
    RoleCountMismatch {
        /// Component name.
        component: String,
        /// Roles length.
        roles_len: usize,
        /// Declared equation count.
        n_equations: usize,
    },
}

/// Pads or trims roles to `n`, recording a diagnostic on mismatch.
pub fn align_roles(
    component: &str,
    n_equations: usize,
    mut roles: Vec<EquationRole>,
    diagnostics: &mut Vec<String>,
) -> Vec<EquationRole> {
    if roles.len() == n_equations {
        return roles;
    }
    if roles.is_empty() {
        return unspecified_roles(n_equations);
    }
    diagnostics.push(format!(
        "component `{component}`: equation_roles len {} != n_equations {n_equations} (aligned)",
        roles.len()
    ));
    if roles.len() < n_equations {
        let start = roles.len();
        roles.extend(unspecified_roles(n_equations - start));
    } else {
        roles.truncate(n_equations);
    }
    roles
}

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

    #[test]
    fn balance_from_counts() {
        assert_eq!(
            SystemDofBalance::from_counts(9, 9),
            SystemDofBalance::Balanced
        );
        assert_eq!(
            SystemDofBalance::from_counts(10, 9),
            SystemDofBalance::OverConstrained {
                excess_equations: 1
            }
        );
        assert_eq!(
            SystemDofBalance::from_counts(8, 9),
            SystemDofBalance::UnderConstrained { free_dofs: 1 }
        );
    }

    #[test]
    fn align_roles_empty_becomes_unspecified() {
        let mut diag = Vec::new();
        let roles = align_roles("x", 3, Vec::new(), &mut diag);
        assert_eq!(roles.len(), 3);
        assert!(diag.is_empty());
        assert!(matches!(
            roles[2],
            EquationRole::Unspecified { local_index: 2 }
        ));
    }

    #[test]
    fn summary_mentions_balance() {
        let report = DofReport {
            n_equations: 2,
            n_unknowns: 1,
            balance: SystemDofBalance::OverConstrained {
                excess_equations: 1,
            },
            components: vec![ComponentEquationBlock {
                component_name: "c".into(),
                node_index: 0,
                n_equations: 2,
                roles: vec![
                    EquationRole::EnergyBalance {
                        stream: "refrigerant",
                    },
                    EquationRole::OutletClosure { kind: "quality" },
                ],
            }],
            system_equations: vec![],
            unknowns: vec![],
            diagnostics: vec!["quality residual without free actuator".into()],
        };
        let s = report.summary();
        assert!(s.contains("over-constrained"));
        assert!(s.contains("quality"));
    }
}
