################################################################################
#                                                                              #
# R functions for the smfa package                                             #
#                                                                              #
################################################################################

#------------------------------------------------------------------------------#
# Model: Stochastic Metafrontier Analysis                                      #
# Data: Cross-sectional or pooled data with group variable                     #
#------------------------------------------------------------------------------#

#' Stochastic metafrontier estimation
#'
#' @description
#' \code{\link{smfa}} estimates a stochastic metafrontier model
#' for cross-sectional or pooled data. The function follows the theoretical
#' frameworks of Battese, Rao, and O'Donnell (2004) and O'Donnell, Rao, and
#' Battese (2008), and additionally implements the two-stage stochastic approach
#' of Huang, Huang, and Liu (2014). Three types of group-level frontier models
#' are supported: standard stochastic frontier analysis
#' (\code{\link[sfaR]{sfacross}}), sample selection stochastic frontier
#' analysis (\code{\link[sfaR]{sfaselectioncross}}), and latent class stochastic
#' frontier analysis (\code{\link[sfaR]{sfalcmcross}}).
#'
#' @aliases smfa print.smfa
#'
#' @param formula A symbolic description of the frontier model to be estimated,
#'   based on the generic function \code{\link[stats]{formula}}. For
#'   \code{groupType = "sfaselectioncross"}, this argument specifies the
#'   frontier (outcome) equation and must be a standard formula whose left-hand
#'   side is the output (or cost) variable and whose right-hand side contains
#'   the frontier regressors (see also \code{selectionF}).
#' @param muhet A one-part formula to account for heterogeneity in the mean of
#'   the pre-truncated normal distribution. Applicable only when
#'   \code{groupType = "sfacross"} and \code{udist = "tnormal"}. The variables
#'   specified model the conditional mean
#'   \eqn{\mu_i = \bm{\omega}'\mathbf{Z}_{\mu}} of the truncated normal
#'   inefficiency distribution (see section \sQuote{Details}).
#' @param uhet A one-part formula to account for heteroscedasticity in the
#'   one-sided error variance. Applicable for all three model types. The
#'   variance of the inefficiency term is modelled as
#'   \eqn{\sigma^2_u = \exp(\bm{\delta}'\mathbf{Z}_u)}, where
#'   \eqn{\mathbf{Z}_u} are the inefficiency drivers and \eqn{\bm{\delta}}
#'   the associated coefficients (see section \sQuote{Details}).
#' @param vhet A one-part formula to account for heteroscedasticity in the
#'   two-sided error variance. Applicable for all three model types. The
#'   variance of the noise term is modelled as
#'   \eqn{\sigma^2_v = \exp(\bm{\phi}'\mathbf{Z}_v)}, where
#'   \eqn{\mathbf{Z}_v} are the heteroscedasticity variables and
#'   \eqn{\bm{\phi}} the coefficients (see section \sQuote{Details}).
#' @param thet A one-part formula to account for technological heterogeneity
#'   in the construction of the latent classes. Applicable only when
#'   \code{groupType = "sfalcmcross"}. The variables specified enter the logit
#'   formulation that determines the prior class membership probabilities
#'   \eqn{\pi(i,j)} (see section \sQuote{Details}).
#' @param logDepVar Logical. Informs whether the dependent variable is logged
#'   (\code{TRUE}) or not (\code{FALSE}). Default \code{TRUE}. Must match the
#'   transformation applied to the left-hand side of \code{formula}.
#' @param data A data frame containing all variables referenced in
#'   \code{formula}, \code{selectionF}, \code{muhet}, \code{uhet}, \code{vhet},
#'   \code{thet}, and \code{group}.
#' @param subset An optional vector specifying a subset of observations to be
#'   used in the estimation process.
#' @param weights An optional vector of weights to be used for weighted
#'   log-likelihood estimation. Should be \code{NULL} or a numeric vector with
#'   strictly positive values. When \code{NULL} (default), all observations
#'   receive equal weight.
#' @param wscale Logical. When \code{weights} is not \code{NULL}, a scaling
#'   transformation is applied such that the weights sum to the sample size:
#'   \deqn{w_{\mathrm{new}} = n \times
#'     \frac{w_{\mathrm{old}}}{\sum w_{\mathrm{old}}}}
#'   Default \code{TRUE}. When \code{FALSE}, the raw weights are used without
#'   scaling.
#' @param group Character string. The name of the column in \code{data}
#'   identifying the technology group of each observation. The column is coerced
#'   to a factor internally and must have at least two unique values. When
#'   \code{groupType = "sfalcmcross"} and \code{group} is \code{NULL}, a single
#'   pooled latent class model is estimated and class assignments serve as groups
#'   (see section \sQuote{Details}).
#' @param S Integer. Frontier orientation.
#'   \itemize{
#'     \item \code{S = 1} (default): production or profit frontier,
#'       \eqn{\varepsilon_i = v_i - u_i}.
#'     \item \code{S = -1}: cost frontier,
#'       \eqn{\varepsilon_i = v_i + u_i}.
#'   }
#' @param udist Character string. Distribution for the one-sided error term
#'   \eqn{u_i \ge 0}. The following distributions are available for
#'   \code{groupType = "sfacross"}:
#'   \itemize{
#'     \item \code{"hnormal"} (default): half-normal distribution
#'       (Aigner \emph{et al.}, 1977; Meeusen and van den Broeck, 1977).
#'     \item \code{"exponential"}: exponential distribution.
#'     \item \code{"tnormal"}: truncated normal distribution (Stevenson, 1980).
#'     \item \code{"rayleigh"}: Rayleigh distribution (Hajargasht, 2015).
#'     \item \code{"uniform"}: uniform distribution (Li, 1996; Nguyen, 2010).
#'     \item \code{"gamma"}: Gamma distribution, estimated by maximum simulated
#'       likelihood (Greene, 2003).
#'     \item \code{"lognormal"}: log-normal distribution, estimated by maximum
#'       simulated likelihood (Migon and Medici, 2001; Wang and Ye, 2020).
#'     \item \code{"weibull"}: Weibull distribution, estimated by maximum
#'       simulated likelihood (Tsionas, 2007).
#'     \item \code{"genexponential"}: generalised exponential distribution
#'       (Papadopoulos, 2020).
#'     \item \code{"tslaplace"}: truncated skewed Laplace distribution
#'       (Wang, 2012).
#'   }
#'   For \code{groupType = "sfaselectioncross"} and \code{"sfalcmcross"}, only
#'   \code{"hnormal"} is currently supported.
#' @param start Numeric vector. Optional starting values for the maximum
#'   likelihood (ML) or maximum simulated likelihood (MSL) estimation of the
#'   group-level frontier models. When \code{NULL} (default), starting values
#'   are computed automatically. For \code{groupType = "sfacross"}, they are
#'   derived from OLS residuals. For \code{groupType = "sfalcmcross"}, they
#'   depend on \code{whichStart}.
#' @param scaling Logical. Applicable only when \code{groupType = "sfacross"}
#'   and \code{udist = "tnormal"}. When \code{TRUE}, the scaling property model
#'   (Wang and Schmidt, 2002) is estimated, whereby
#'   \eqn{u_i = h(\mathbf{Z}_u, \bm{\delta}) u^*_i} and \eqn{u^*_i} follows
#'   a truncated normal distribution \eqn{N^+(\tau, \exp(c_u))}. Default
#'   \code{FALSE}.
#' @param modelType Character string. Applicable only when
#'   \code{groupType = "sfaselectioncross"}. Specifies the model used to correct
#'   for selection bias. Currently, only \code{"greene10"} (default) is
#'   supported, corresponding to the two-step approach of Greene (2010): a
#'   probit model is estimated for the selection equation, and its inverse Mills
#'   ratio is included as a correction term in the stochastic frontier second
#'   step.
#' @param groupType Character string. Type of frontier model estimated for each
#'   technology group. Three options are available:
#'   \itemize{
#'     \item \code{"sfacross"} (default): standard cross-sectional stochastic
#'       frontier analysis (\code{\link[sfaR]{sfacross}}). Groups are defined
#'       by the \code{group} variable. All 10 distributions for \code{udist}
#'       are supported, along with heteroscedasticity in both error components
#'       (\code{uhet}, \code{vhet}), heterogeneity in the truncated mean
#'       (\code{muhet}), and the scaling property.
#'     \item \code{"sfaselectioncross"}: sample selection stochastic frontier
#'       analysis (\code{\link[sfaR]{sfaselectioncross}}). Corrects for sample
#'       selection bias via the generalised Heckman approach (Greene, 2010).
#'       Requires \code{selectionF}. Only observations for which the selection
#'       indicator equals one enter the frontier and metafrontier; efficiency
#'       estimates for non-selected observations are \code{NA}. Only
#'       \code{udist = "hnormal"} is supported.
#'     \item \code{"sfalcmcross"}: latent class stochastic frontier analysis
#'       (\code{\link[sfaR]{sfalcmcross}}). Estimates a finite mixture of
#'       frontier models with the number of classes determined by
#'       \code{lcmClasses}. When \code{group} is supplied, a separate latent
#'       class model is estimated per group-stratum and combined for the
#'       metafrontier. When \code{group} is omitted, a single pooled model is
#'       estimated and class assignments serve as technology groups. Supports
#'       \code{thet} for class-membership covariates and \code{uhet},
#'       \code{vhet} for within-class heteroscedasticity. Only
#'       \code{udist = "hnormal"} is supported.
#'   }
#' @param metaMethod Character string. Method for estimating the global
#'   metafrontier that envelopes all group frontiers. Three options are
#'   available:
#'   \itemize{
#'     \item \code{"lp"} (default): deterministic linear programming envelope.
#'       Finds the parameter vector \eqn{\bm{\beta}^*} minimising
#'       \eqn{\sum_i |\ln \hat{f}(x_i, \bm{\beta}^*) - \ln \hat{f}(x_i,
#'       \hat{\bm{\beta}}_{(g)})|}
#'       subject to \eqn{\ln \hat{f}(x_i, \bm{\beta}^*) \ge \ln \hat{f}(x_i,
#'       \hat{\bm{\beta}}_{(g)})} for all observations and all groups
#'       (Battese \emph{et al.}, 2004).
#'     \item \code{"qp"}: deterministic quadratic programming envelope.
#'       Minimises the sum of squared deviations under the same envelope
#'       constraint.
#'     \item \code{"sfa"}: stochastic metafrontier estimated by a second-stage
#'       pooled SFA. The specific construction of the dependent variable is
#'       determined by \code{sfaApproach}.
#'   }
#' @param sfaApproach Character string. Applicable only when
#'   \code{metaMethod = "sfa"}. Determines how the second-stage SFA is
#'   constructed:
#'   \itemize{
#'     \item \code{"ordonnell"} (default): The LP envelope of the group frontier
#'       predicted values is re-estimated with a stochastic frontier, following
#'       O'Donnell, Rao, and Battese (2008). The second-stage SFA directly
#'       targets the global technology envelope.
#'     \item \code{"huang"}: the group-specific fitted frontier value
#'       \eqn{\ln \hat{y}^g_i} for each observation is used as the dependent
#'       variable in a pooled cross-sectional SFA
#'       (Huang, Huang, and Liu, 2014). The technology gap \eqn{U_i \ge 0} and
#'       second-stage noise \eqn{V_i} are estimated directly by the SFA
#'       procedure.
#'     \item \code{"ordonnell"}: the column-wise maximum of all group-fitted
#'       frontier values (the deterministic LP envelope) is used as the dependent
#'       variable in the second-stage SFA (O'Donnell, Rao, and Battese, 2008).
#'   }
#' @param selectionF A two-sided formula specifying the sample selection
#'   equation, e.g., \code{selected ~ z1 + z2}. The left-hand side must be a
#'   binary (0/1) indicator already present in \code{data}: \code{1} means the
#'   observation participates in the frontier and metafrontier; \code{0} means
#'   it is excluded (efficiency estimates will be \code{NA}). Alternatively, a
#'   named list of formulas, one per group level, may be supplied to allow
#'   group-specific selection equations. Required when
#'   \code{groupType = "sfaselectioncross"}; ignored otherwise.
#' @param lcmClasses Integer. Number of latent classes to be estimated per group
#'   when \code{groupType = "sfalcmcross"}. Must be between \code{2} and
#'   \code{5} (default \code{2}). The optimal number of classes can be selected
#'   based on information criteria (see \code{\link[sfaR]{ic}}).
#' @param whichStart Integer. Strategy for obtaining starting values in the
#'   latent class model (\code{groupType = "sfalcmcross"}):
#'   \itemize{
#'     \item \code{1}: starting values are obtained from the method of moments.
#'     \item \code{2} (default): the model is initialised by first solving a
#'       homoscedastic pooled cross-sectional SFA using the algorithm specified
#'       by \code{initAlg} for at most \code{initIter} iterations.
#'   }
#' @param initAlg Character string. Optimisation algorithm used during the
#'   initialisation of the latent class model when \code{whichStart = 2}.
#'   Only algorithms from the \code{maxLik} package are supported:
#'   \itemize{
#'     \item \code{"nm"} (default): Nelder-Mead (see
#'       \code{\link[maxLik]{maxNM}}).
#'     \item \code{"bfgs"}: Broyden-Fletcher-Goldfarb-Shanno (see
#'       \code{\link[maxLik]{maxBFGS}}).
#'     \item \code{"bhhh"}: Berndt-Hall-Hall-Hausman (see
#'       \code{\link[maxLik]{maxBHHH}}).
#'     \item \code{"nr"}: Newton-Raphson (see \code{\link[maxLik]{maxNR}}).
#'     \item \code{"cg"}: Conjugate Gradient (see
#'       \code{\link[maxLik]{maxCG}}).
#'     \item \code{"sann"}: Simulated Annealing (see
#'       \code{\link[maxLik]{maxSANN}}).
#'   }
#' @param initIter Integer. Maximum number of iterations for the initialisation
#'   algorithm when \code{whichStart = 2} and
#'   \code{groupType = "sfalcmcross"}. Default \code{100}.
#' @param lType Character string. Specifies how the likelihood is evaluated for
#'   the selection model (\code{groupType = "sfaselectioncross"}). Five options
#'   are available:
#'   \itemize{
#'     \item \code{"ghermite"} (default): Gauss-Hermite quadrature (see
#'       \code{\link[fastGHQuad]{gaussHermiteData}}).
#'     \item \code{"kronrod"}: Gauss-Kronrod quadrature (see
#'       \code{\link[stats]{integrate}}).
#'     \item \code{"hcubature"}: adaptive integration over hypercubes (see
#'       \code{\link[cubature]{hcubature}}).
#'     \item \code{"pcubature"}: p-adaptive cubature (see
#'       \code{\link[cubature]{pcubature}}).
#'     \item \code{"msl"}: maximum simulated likelihood (controlled by
#'       \code{simType}, \code{Nsim}, \code{prime}, \code{burn},
#'       \code{antithetics}, and \code{seed}).
#'   }
#' @param Nsub Integer. Number of quadrature nodes or integration subdivisions
#'   when \code{lType} is \code{"ghermite"}, \code{"kronrod"},
#'   \code{"hcubature"}, or \code{"pcubature"}. Applicable only when
#'   \code{groupType = "sfaselectioncross"}. Default \code{100}.
#' @param uBound Numeric. Upper bound for the numerical integration of the
#'   inefficiency component when \code{lType} is \code{"kronrod"},
#'   \code{"hcubature"}, or \code{"pcubature"}. For Gauss-Hermite the bound is
#'   automatically infinite. Applicable only when
#'   \code{groupType = "sfaselectioncross"}. Default \code{Inf}.
#' @param intol Numeric. Integration tolerance for the quadrature approaches
#'   \code{"kronrod"}, \code{"hcubature"}, and \code{"pcubature"}. Applicable
#'   only when \code{groupType = "sfaselectioncross"}. Default \code{1e-6}.
#' @param method Character string. Optimisation algorithm for the main ML/MSL
#'   estimation of each group-level frontier model. Default \code{"bfgs"}.
#'   Eleven algorithms are available:
#'   \itemize{
#'     \item \code{"bfgs"}: Broyden-Fletcher-Goldfarb-Shanno (see
#'       \code{\link[maxLik]{maxBFGS}}).
#'     \item \code{"bhhh"}: Berndt-Hall-Hall-Hausman (see
#'       \code{\link[maxLik]{maxBHHH}}).
#'     \item \code{"nr"}: Newton-Raphson (see \code{\link[maxLik]{maxNR}}).
#'     \item \code{"nm"}: Nelder-Mead (see \code{\link[maxLik]{maxNM}}).
#'     \item \code{"cg"}: Conjugate Gradient (see
#'       \code{\link[maxLik]{maxCG}}).
#'     \item \code{"sann"}: Simulated Annealing (see
#'       \code{\link[maxLik]{maxSANN}}).
#'     \item \code{"ucminf"}: quasi-Newton optimisation with BFGS updating of
#'       the inverse Hessian and soft line search (see
#'       \code{\link[ucminf]{ucminf}}).
#'     \item \code{"mla"}: Marquardt-Levenberg algorithm (see
#'       \code{\link[marqLevAlg]{mla}}).
#'     \item \code{"sr1"}: Symmetric Rank 1 trust-region method (see
#'       \code{\link[trustOptim]{trust.optim}}).
#'     \item \code{"sparse"}: trust-region method with sparse Hessian (see
#'       \code{\link[trustOptim]{trust.optim}}).
#'     \item \code{"nlminb"}: PORT routines optimisation (see
#'       \code{\link[stats]{nlminb}}).
#'   }
#' @param hessianType Integer. Specifies which Hessian is returned for the
#'   group-level frontier estimation. The accepted values match those of the
#'   underlying \code{sfaR} function for each \code{groupType}:
#'   \itemize{
#'     \item For \code{groupType = "sfacross"}: if \code{1} (default), the
#'       analytic Hessian is returned; if \code{2}, the BHHH Hessian
#'       \eqn{\mathbf{G}'\mathbf{G}} is estimated.
#'     \item For \code{groupType = "sfalcmcross"}: if \code{1} (default),
#'       the analytic Hessian is returned; if \code{2}, the BHHH Hessian is
#'       estimated.
#'     \item For \code{groupType = "sfaselectioncross"}: if \code{1}, the
#'       analytic Hessian is returned; if \code{2} (default), the BHHH
#'       Hessian \eqn{\mathbf{G}'\mathbf{G}} is estimated. The BHHH default
#'       reflects the two-step nature of the selection estimator.
#'   }
#'   When \code{NULL} (the package default), each group-level model uses the
#'   natural default of the corresponding \code{sfaR} function, ensuring that
#'   standard errors computed by \code{smfa} are identical to those
#'   from a standalone \code{sfaR} call on the same group subset.
#' @param simType Character string. Simulation method for maximum simulated
#'   likelihood (MSL). Applicable to \code{groupType = "sfacross"} when
#'   \code{udist} is \code{"gamma"}, \code{"lognormal"}, or \code{"weibull"},
#'   and to \code{groupType = "sfaselectioncross"} when \code{lType = "msl"}:
#'   \itemize{
#'     \item \code{"halton"} (default): Halton quasi-random sequences.
#'     \item \code{"ghalton"}: Generalised-Halton sequences.
#'     \item \code{"sobol"}: Sobol low-discrepancy sequences.
#'     \item \code{"uniform"}: pseudo-random uniform draws.
#'   }
#' @param Nsim Integer. Number of simulation draws for MSL. Default \code{100}.
#' @param prime Integer. Prime number used to construct Halton or
#'   Generalised-Halton sequences. Default \code{2}.
#' @param burn Integer. Number of leading draws discarded from the Halton
#'   sequence to reduce serial correlation. Default \code{10}.
#' @param antithetics Logical. If \code{TRUE}, antithetic draws are added: the
#'   first \code{Nsim/2} draws are taken, and the remaining \code{Nsim/2} are
#'   \eqn{1 - \text{draw}}. Default \code{FALSE}.
#' @param seed Integer. Random seed for simulation draws, ensuring
#'   reproducibility of MSL estimates. Default \code{12345}.
#' @param itermax Integer. Maximum number of iterations for the main
#'   optimisation. Default \code{2000}. For \code{method = "sann"}, it is
#'   recommended to increase this substantially (e.g., \code{itermax = 20000}).
#' @param printInfo Logical. If \code{TRUE}, optimisation progress is printed
#'   during estimation of each group-level model. Default \code{FALSE}.
#' @param tol Numeric. Convergence tolerance. The algorithm is considered
#'   converged when the change in the log-likelihood between successive
#'   iterations is smaller than \code{tol} in absolute value. Default
#'   \code{1e-12}.
#' @param gradtol Numeric. Gradient convergence tolerance. The algorithm is
#'   considered converged when the Euclidean norm of the gradient is smaller
#'   than \code{gradtol}. Default \code{1e-6}.
#' @param stepmax Numeric. Maximum step length used by the \code{"ucminf"}
#'   algorithm. Default \code{0.1}.
#' @param qac Character string. Quadratic Approximation Correction for the
#'   \code{"bhhh"} and \code{"nr"} algorithms when the Hessian is not negative
#'   definite:
#'   \itemize{
#'     \item \code{"marquardt"} (default): step length is decreased while
#'       also shifting closer to the gradient direction.
#'     \item \code{"stephalving"}: step length is halved, preserving the
#'       current direction.
#'   }
#'   See \code{\link[maxLik]{maxBHHH}} and \code{\link[maxLik]{maxNR}} for
#'   details.
#' @param x An object of class \code{"smfa"}, as returned by
#'   \code{smfa}, for use with the \code{print} method.
#' @param ... Additional arguments passed through to the second-stage SFA
#'   call when \code{metaMethod = "sfa"}.
#'
#' @return \code{smfa} returns an object of class
#'   \code{"smfa"}, which is a list containing:
#'   \item{call}{The matched call.}
#'   \item{groupModels}{A named list of fitted group-level frontier objects,
#'     one per technology group. Each element is of class \code{"sfacross"},
#'     \code{"sfaselectioncross"}, or \code{"sfalcmcross"}, depending on
#'     \code{groupType}.}
#'   \item{metaSfaObj}{The fitted metafrontier object. For
#'     \code{metaMethod = "sfa"}, an object of class \code{"sfacross"}
#'     from the second-stage SFA. The dependent variable column in
#'     \code{metaSfaObj$dataTable} is named according to the approach used:
#'     \code{"lp_envelope"} when \code{sfaApproach = "ordonnell"} (the
#'     column-wise maximum of all group-evaluated frontier values is the
#'     dependent variable) and \code{"group_fitted_values"} when
#'     \code{sfaApproach = "huang"} (each observation's own-group fitted
#'     frontier value is the dependent variable). For \code{metaMethod = "lp"}
#'     or \code{"qp"}, a list containing the optimisation result and the
#'     estimated envelope coefficients.}
#'   \item{metaRes}{Estimated metafrontier coefficients (with standard errors,
#'     z-values, and p-values for \code{metaMethod = "sfa"}, or the plain
#'     coefficient vector for deterministic envelopes).}
#'   \item{formula}{The \code{formula} supplied to the call.}
#'   \item{metaMethod}{The metafrontier estimation method used.}
#'   \item{sfaApproach}{The second-stage SFA approach; \code{NA} when
#'     \code{metaMethod} is not \code{"sfa"}.}
#'   \item{groupType}{The type of group-level frontier model estimated.}
#'   \item{group}{The name of the grouping variable.}
#'   \item{groups}{Character vector of unique group labels.}
#'   \item{S}{The frontier orientation (\code{1} or \code{-1}).}
#'   \item{dataTable}{The data used in estimation, augmented with
#'     \code{.mf_yhat_group} (group-specific fitted frontier values) and
#'     \code{.mf_yhat_meta} (metafrontier fitted values).}
#'   \item{lcmNoGroup}{Logical. \code{TRUE} when \code{groupType = "sfalcmcross"}
#'     and \code{group} was not supplied.}
#'   \item{lcmObj}{When \code{lcmNoGroup = TRUE}, the pooled
#'     \code{sfalcmcross} object.}
#'
#' @details
#' \subsection{Standard stochastic frontier (\code{groupType = "sfacross"})}{
#'   The stochastic frontier model is defined as:
#'   \deqn{y_i = \alpha + \mathbf{x}_i'\bm{\beta} + v_i - Su_i}
#'   where \eqn{y} is the output (cost, revenue, or profit), \eqn{\mathbf{x}}
#'   is the vector of frontier regressors, \eqn{u_i \ge 0} is the one-sided
#'   inefficiency term with variance \eqn{\sigma^2_u}, and \eqn{v_i} is the
#'   symmetric noise term with variance \eqn{\sigma^2_v}.
#'
#'   Estimation is by ML for all distributions except \code{"gamma"},
#'   \code{"lognormal"}, and \code{"weibull"}, for which MSL is used with
#'   Halton, Generalised-Halton, Sobol, or uniform draws. Antithetic draws are
#'   available for the uniform case.
#'
#'   To account for heteroscedasticity, the variances are modelled as
#'   \eqn{\sigma^2_u = \exp(\bm{\delta}'\mathbf{Z}_u)} and
#'   \eqn{\sigma^2_v = \exp(\bm{\phi}'\mathbf{Z}_v)}. For the truncated normal
#'   distribution, heterogeneity in the pre-truncation mean is modelled as
#'   \eqn{\mu_i = \bm{\omega}'\mathbf{Z}_{\mu}}. The scaling property (Wang and
#'   Schmidt, 2002) can also be imposed for the truncated normal.
#' }
#'
#' \subsection{Sample selection frontier (\code{groupType = "sfaselectioncross"})}{
#'   This model extends the Heckman (1979) selection framework to the
#'   stochastic frontier setting (Greene, 2010; Dakpo \emph{et al.}, 2021).
#'   The selection and frontier equations are:
#'   \deqn{y_{1i}^* = \mathbf{Z}_{si}'\bm{\gamma} + w_i, \quad
#'     w_i \sim \mathcal{N}(0,1)}
#'   \deqn{y_{2i}^* = \mathbf{x}_i'\bm{\beta} + v_i - Su_i}
#'   where \eqn{y_{1i} = \mathbf{1}(y_{1i}^* > 0)} is the binary selection
#'   indicator and \eqn{y_{2i} = y_{2i}^*} is observed only when
#'   \eqn{y_{1i} = 1}. Selection bias arises from
#'   \eqn{\rho = \mathrm{Corr}(w_i, v_i) \ne 0}. Only selected observations
#'   enter the frontier and metafrontier estimation; efficiency estimates for
#'   non-selected observations are \code{NA}.
#' }
#'
#' \subsection{Latent class frontier (\code{groupType = "sfalcmcross"})}{
#'   The latent class model (Orea and Kumbhakar, 2004) estimates a finite
#'   mixture of \eqn{J} frontier models:
#'   \deqn{y_i = \alpha_j + \mathbf{x}_i'\bm{\beta}_j + v_{i|j} - Su_{i|j}}
#'   The prior class probability follows a logit specification:
#'   \deqn{\pi(i,j) = \frac{\exp(\bm{\theta}_j'\mathbf{Z}_{hi})}
#'     {\sum_{m=1}^{J}\exp(\bm{\theta}_m'\mathbf{Z}_{hi})}}
#'   Class assignment is based on the maximum posterior probability computed via
#'   Bayes' rule. When \code{group} is omitted, a single pooled model is
#'   estimated and class assignments serve as technology groups.
#' }
#'
#' \subsection{Metafrontier estimation}{
#'   The global metafrontier \eqn{f(x_i, \bm{\beta}^*)} envelopes all
#'   group frontiers. With LP (Battese \emph{et al.}, 2004),
#'   \eqn{\bm{\beta}^*} minimises
#'   \eqn{\sum_i |\ln \hat{f}(x_i, \bm{\beta}^*) - \ln \hat{f}(x_i,
#'   \hat{\bm{\beta}}_{(g)})|}
#'   subject to \eqn{\ln \hat{f}(x_i, \bm{\beta}^*) \ge \ln \hat{f}(x_i,
#'   \hat{\bm{\beta}}_{(g)})}. QP minimises the squared analogue. The
#'   stochastic approaches (Huang \emph{et al.}, 2014; O'Donnell \emph{et al.},
#'   2008) treat the technology gap \eqn{U_i} as a one-sided error in a
#'   second-stage SFA. Group and metafrontier efficiencies are:
#'   \deqn{TE_i^g = \exp(-u_i^g), \quad
#'     MTR_i = \exp(-U_i), \quad
#'     TE_i^* = TE_i^g \times MTR_i}
#'   Both Jondrow \emph{et al.} (1982) and Battese and Coelli (1988) estimators
#'   are provided for each measure. See \code{\link{efficiencies}} for details.
#' }
#'
#' @references
#' Aigner, D. J., Lovell, C. A. K., and Schmidt, P. 1977. Formulation and
#' estimation of stochastic frontier production function models.
#' \emph{Journal of Econometrics}, \bold{6}(1), 21--37.
#' \doi{10.1016/0304-4076(77)90052-5}
#'
#' Battese, G. E., and Coelli, T. J. 1988. Prediction of firm-level technical
#' efficiencies with a generalized frontier production function and panel data.
#' \emph{Journal of Econometrics}, \bold{38}(3), 387--399.
#' \doi{10.1016/0304-4076(88)90053-X}
#'
#' Battese, G. E., Rao, D. S. P., and O'Donnell, C. J. 2004. A metafrontier
#' production function for estimation of technical efficiencies and technology
#' gaps for firms operating under different technologies.
#' \emph{Journal of Productivity Analysis}, \bold{21}(1), 91--103.
#' \doi{10.1023/B:PROD.0000012454.06094.29}
#'
#' Greene, W. 2003. Simulated likelihood estimation of the normal-gamma
#' stochastic frontier function. \emph{Journal of Productivity Analysis},
#' \bold{19}(2-3), 179--190.
#' \doi{10.1023/A:1022853416499}
#'
#' Greene, W. 2010. A stochastic frontier model with correction for sample
#' selection. \emph{Journal of Productivity Analysis}, \bold{34}(1), 15--24.
#' \doi{10.1007/s11123-009-0159-1}
#'
#' Hajargasht, G. 2015. Stochastic frontiers with a Rayleigh distribution.
#' \emph{Journal of Productivity Analysis}, \bold{44}(2), 199--208.
#' \doi{10.1007/s11123-014-0417-8}
#'
#' Heckman, J. J. 1979. Sample selection bias as a specification error.
#' \emph{Econometrica}, \bold{47}(1), 153--161.
#' \doi{10.2307/1912352}
#'
#' Huang, C. J., Huang, T.-H., and Liu, N.-H. 2014. A new approach to
#' estimating the metafrontier production function based on a stochastic
#' frontier framework. \emph{Journal of Productivity Analysis},
#' \bold{42}(3), 241--254. \doi{10.1007/s11123-014-0402-2}
#'
#' Jondrow, J., Lovell, C. A. K., Materov, I. S., and Schmidt, P. 1982. On
#' the estimation of technical inefficiency in the stochastic frontier
#' production function model. \emph{Journal of Econometrics},
#' \bold{19}(2-3), 233--238.
#' \doi{10.1016/0304-4076(82)90004-5}
#'
#' Li, Q. 1996. Estimating a stochastic production frontier when the adjusted
#' error is symmetric. \emph{Economics Letters}, \bold{52}(3), 221--228.
#' \doi{10.1016/S0165-1765(96)00857-9}
#'
#' Meeusen, W., and van den Broeck, J. 1977. Efficiency estimation from
#' Cobb-Douglas production functions with composed error.
#' \emph{International Economic Review}, \bold{18}(2), 435--444.
#' \doi{10.2307/2525757}
#'
#' Migon, H. S., and Medici, E. 2001. Bayesian inference for generalised
#' exponential models. Working paper, Universidade Federal do Rio de Janeiro.
#'
#' Nguyen, N. B. 2010. Estimation of technical efficiency in stochastic
#' frontier analysis. PhD thesis, Bowling Green State University.
#'
#' O'Donnell, C. J., Rao, D. S. P., and Battese, G. E. 2008. Metafrontier
#' frameworks for the study of firm-level efficiencies and technology ratios.
#' \emph{Empirical Economics}, \bold{34}(2), 231--255.
#' \doi{10.1007/s00181-007-0119-4}
#'
#' Orea, L., and Kumbhakar, S. C. 2004. Efficiency measurement using a latent
#' class stochastic frontier model. \emph{Empirical Economics}, \bold{29}(1),
#' 169--183. \doi{10.1007/s00181-003-0184-2}
#'
#' Dakpo, K. H., Jeanneaux, P., and Latruffe, L. 2016. Modelling
#' pollution-generating technologies in performance benchmarking: Recent
#' developments, limits and future prospects in the nonparametric framework.
#' \emph{European Journal of Operational Research}, \bold{250}(2), 347--359.
#' \doi{10.1016/j.ejor.2015.07.024}
#'
#' Papadopoulos, A. 2015. The half-normal specification for the two-tier
#' stochastic frontier model. \emph{Journal of Productivity Analysis},
#' \bold{43}(2), 225--230. \doi{10.1007/s11123-014-0389-8}
#'
#' Stevenson, R. E. 1980. Likelihood functions for generalised stochastic
#' frontier estimation. \emph{Journal of Econometrics}, \bold{13}(1), 57--66.
#' \doi{10.1016/0304-4076(80)90042-1}
#'
#' Dakpo, K. H., Latruffe, L., Desjeux, Y., and Jeanneaux, P. 2021. Latent
#' class modelling for a robust assessment of productivity: Application to
#' French grazing livestock farms. \emph{Journal of Agricultural Economics},
#' \bold{72}(3), 760--781. \doi{10.1111/1477-9552.12422}
#'
#' Dakpo, K. H., Latruffe, L., Desjeux, Y., and Jeanneaux, P. 2022. Modeling
#' heterogeneous technologies in the presence of sample selection: The case of
#' dairy farms and the adoption of agri-environmental schemes in France.
#' \emph{Agricultural Economics}, \bold{53}(3), 422--438.
#' \doi{10.1111/agec.12683}
#'
#' Tsionas, E. G. 2007. Efficiency measurement with the Weibull stochastic
#' frontier. \emph{Oxford Bulletin of Economics and Statistics},
#' \bold{69}(5), 693--706. \doi{10.1111/j.1468-0084.2007.00475.x}
#'
#' Wang, H.-J. 2012. Stochastic frontier models. In \emph{A Companion to
#' Theoretical Econometrics}, ed. B. H. Baltagi, Blackwell, Oxford.
#'
#' Wang, H.-J., and Schmidt, P. 2002. One-step and two-step estimation of the
#' effects of exogenous variables on technical efficiency levels.
#' \emph{Journal of Productivity Analysis}, \bold{18}(2), 129--144.
#' \doi{10.1023/A:1016565719882}
#'
#' Dakpo, K. H., Desjeux, Y., and Latruffe, L. 2023. sfaR: Stochastic Frontier
#' Analysis using R. R package version 1.0.1.
#' \url{https://CRAN.R-project.org/package=sfaR}
#'
#' @seealso \code{\link[sfaR]{sfacross}}, \code{\link[sfaR]{sfaselectioncross}},
#'   \code{\link[sfaR]{sfalcmcross}}, \code{\link{efficiencies}},
#'   \code{\link{summary.smfa}}, \code{\link[sfaR]{ic}}
#'
#' @keywords models optimize metafrontier
#'
#' @examples
#' ###########################################################################
#' ## -------- SECTION 1: Standard SFA Group Frontier ----------------------##
#' ## Using the rice production dataset (ricephil) from Battese et al.      ##
#' ## Groups are formed based on farm area terciles (small/medium/large).   ##
#' ###########################################################################
#'
#' data("ricephil", package = "sfaR")
#' ricephil$group <- cut(ricephil$AREA,
#'   breaks = quantile(ricephil$AREA, probs = c(0, 1 / 3, 2 / 3, 1), na.rm = TRUE),
#'   labels = c("small", "medium", "large"),
#'   include.lowest = TRUE
#' )
#'
#' ## 1a. sfacross groups + LP metafrontier
#' ##     Deterministic envelope via linear programming (Battese et al., 2004).
#' meta_sfacross_lp <- smfa(
#'   formula    = log(PROD) ~ log(AREA) + log(LABOR) + log(NPK),
#'   data       = ricephil,
#'   group      = "group",
#'   S          = 1,
#'   udist      = "hnormal",
#'   groupType  = "sfacross",
#'   metaMethod = "lp"
#' )
#' summary(meta_sfacross_lp)
#' # Retrieve individual efficiency and metatechnology ratio estimates:
#' ef_lp <- efficiencies(meta_sfacross_lp)
#' head(ef_lp)
#'
#' ## 1b. sfacross groups + QP metafrontier
#' ##     Deterministic envelope via quadratic programming.
#' meta_sfacross_qp <- smfa(
#'   formula    = log(PROD) ~ log(AREA) + log(LABOR) + log(NPK),
#'   data       = ricephil,
#'   group      = "group",
#'   S          = 1,
#'   udist      = "hnormal",
#'   groupType  = "sfacross",
#'   metaMethod = "qp"
#' )
#' summary(meta_sfacross_qp)
#'
#' \donttest{
#' ## 1c. sfacross groups + Two-stage SFA metafrontier (Huang et al., 2014)
#' ##     The group-specific fitted frontier values serve as the dependent
#' ##     variable in the second-stage SFA, yielding a stochastic technology gap.
#' meta_sfacross_huang <- smfa(
#'   formula     = log(PROD) ~ log(AREA) + log(LABOR) + log(NPK),
#'   data        = ricephil,
#'   group       = "group",
#'   S           = 1,
#'   udist       = "hnormal",
#'   groupType   = "sfacross",
#'   metaMethod  = "sfa",
#'   sfaApproach = "huang"
#' )
#' summary(meta_sfacross_huang)
#' ef_huang <- efficiencies(meta_sfacross_huang)
#' head(ef_huang)
#'
#' ## 1d. sfacross groups + O'Donnell et al. (2008) stochastic metafrontier
#' ##     The LP deterministic envelope is used as the second-stage dependent
#' ##     variable: the metafrontier is estimated stochastically around the
#' ##     envelope.
#' meta_sfacross_odonnell <- smfa(
#'   formula     = log(PROD) ~ log(AREA) + log(LABOR) + log(NPK),
#'   data        = ricephil,
#'   group       = "group",
#'   S           = 1,
#'   udist       = "hnormal",
#'   groupType   = "sfacross",
#'   metaMethod  = "sfa",
#'   sfaApproach = "ordonnell"
#' )
#' summary(meta_sfacross_odonnell)
#' }
#'
#' ###########################################################################
#' ## -------- SECTION 2: Latent Class (LCM) Group Frontier ---------------##
#' ## No observed group variable: a pooled sfalcmcross model assigns       ##
#' ## observations to 2 latent technology classes; these classes become the ##
#' ## technology groups for the metafrontier.                               ##
#' ###########################################################################
#'
#' data("utility", package = "sfaR")
#'
#' ## 2a. sfalcmcross (pooled, 2 classes) + LP metafrontier
#' meta_lcm_lp <- smfa(
#'   formula    = log(tc / wf) ~ log(y) + log(wl / wf) + log(wk / wf),
#'   data       = utility,
#'   S          = -1,
#'   groupType  = "sfalcmcross",
#'   lcmClasses = 2,
#'   metaMethod = "lp"
#' )
#' summary(meta_lcm_lp)
#' ef_lcm_lp <- efficiencies(meta_lcm_lp)
#' head(ef_lcm_lp)
#'
#' \donttest{
#' ## 2b. sfalcmcross (pooled, 2 classes) + QP metafrontier
#' meta_lcm_qp <- smfa(
#'   formula    = log(tc / wf) ~ log(y) + log(wl / wf) + log(wk / wf),
#'   data       = utility,
#'   S          = -1,
#'   groupType  = "sfalcmcross",
#'   lcmClasses = 2,
#'   metaMethod = "qp"
#' )
#' summary(meta_lcm_qp)
#'
#' ## 2c. sfalcmcross (pooled, 2 classes) + Two-stage SFA metafrontier
#' ##     (Huang et al., 2014)
#' meta_lcm_huang <- smfa(
#'   formula     = log(tc / wf) ~ log(y) + log(wl / wf) + log(wk / wf),
#'   data        = utility,
#'   S           = -1,
#'   groupType   = "sfalcmcross",
#'   lcmClasses  = 2,
#'   metaMethod  = "sfa",
#'   sfaApproach = "huang"
#' )
#' summary(meta_lcm_huang)
#' ef_lcm_huang <- efficiencies(meta_lcm_huang)
#' head(ef_lcm_huang)
#'
#' ## 2d. sfalcmcross (pooled, 2 classes) + O'Donnell et al. (2008)
#' meta_lcm_odonnell <- smfa(
#'   formula     = log(tc / wf) ~ log(y) + log(wl / wf) + log(wk / wf),
#'   data        = utility,
#'   S           = -1,
#'   groupType   = "sfalcmcross",
#'   lcmClasses  = 2,
#'   metaMethod  = "sfa",
#'   sfaApproach = "ordonnell"
#' )
#' summary(meta_lcm_odonnell)
#' }
#'
#' ###########################################################################
#' ## -------- SECTION 3: Sample Selection SFA Group Frontier -------------##
#' ###########################################################################
#'
#' ## 3a. Small toy example for automatic testing (< 5s)
#' N <- 100
#' set.seed(12345)
#' z1 <- rnorm(N); v1 <- rnorm(N); g <- rnorm(N)
#' ds <- z1 + v1; d <- ifelse(ds > 0, 1, 0)
#' group <- ifelse(g > 0, 1, 0)
#' x1 <- rnorm(N); y <- x1 + rnorm(N) - abs(rnorm(N))
#' dat <- data.frame(y = y, x1 = x1, z1 = z1, d = d, group = group)
#'
#' meta_toy <- smfa(
#'   formula    = y ~ x1,
#'   selectionF = d ~ z1,
#'   data       = dat,
#'   group      = "group",
#'   groupType  = "sfaselectioncross",
#'   lType      = "ghermite",
#'   Nsub       = 10,
#'   itermax    = 100,
#'   metaMethod = "lp"
#' )
#' summary(meta_toy)
#'
#' \donttest{
#' ## 3b. More complex selection models
#' ## Simulated dataset with a Heckman selection mechanism.
#'
#' N <- 2000
#' set.seed(12345)
#' z1 <- rnorm(N); z2 <- rnorm(N); v1 <- rnorm(N); v2 <- rnorm(N); g <- rnorm(N)
#' e1 <- v1; e2 <- 0.7071 * (v1 + v2)
#' ds <- z1 + z2 + e1; d <- ifelse(ds > 0, 1, 0)
#' group <- ifelse(g > 0, 1, 0)
#' u <- abs(rnorm(N)); x1 <- rnorm(N); x2 <- rnorm(N)
#' y <- x1 + x2 + e2 - u
#' dat <- data.frame(y = y, x1 = x1, x2 = x2, z1 = z1, z2 = z2, d = d, group = group)
#'
#' meta_sel_lp <- smfa(
#'   formula    = y ~ x1 + x2,
#'   selectionF = d ~ z1 + z2,
#'   data       = dat,
#'   group      = "group",
#'   S          = 1L,
#'   udist      = "hnormal",
#'   groupType  = "sfaselectioncross",
#'   modelType  = "greene10",
#'   lType      = "kronrod",
#'   Nsub       = 100,
#'   metaMethod = "lp"
#' )
#' summary(meta_sel_lp)
#' }


#'
#' @importFrom sfaR sfacross sfalcmcross sfaselectioncross
#' @importFrom stats as.formula lm model.frame model.matrix model.response na.pass pnorm printCoefmat setNames terms update
#' @export
smfa <- function(
  formula,
  muhet,
  uhet,
  vhet,
  thet,
  logDepVar = TRUE,
  data,
  subset,
  weights,
  wscale = TRUE,
  group = NULL,
  S = 1L,
  udist = "hnormal",
  start = NULL,
  scaling = FALSE,
  modelType = "greene10",
  groupType = "sfacross",
  metaMethod = "lp",
  sfaApproach = "ordonnell",
  selectionF = NULL,
  lcmClasses = 2L,
  whichStart = 2L,
  initAlg = "nm",
  initIter = 100L,
  lType = "ghermite",
  Nsub = 100L,
  uBound = Inf,
  intol = 1e-6,
  method = "bfgs",
  hessianType = NULL,
  simType = "halton",
  Nsim = 100L,
  prime = 2L,
  burn = 10L,
  antithetics = FALSE,
  seed = 12345L,
  itermax = 2000L,
  printInfo = FALSE,
  tol = 1e-12,
  gradtol = 1e-06,
  stepmax = 0.1,
  qac = "marquardt",
  ...
) {
  cl <- match.call()

  # ---------- Reject dollar-sign formula notation ----------
  # Using df$var inside a formula (e.g. log(ricephil$PROD) ~ log(ricephil$AREA))
  # bypasses the 'data=' argument when data is subset per group, causing
  # model.frame to use the full dataset and producing length mismatches.
  .check_no_dollar <- function(fml, nm) {
    if (!missing(fml) && !is.null(fml)) {
      txt <- deparse(fml)
      if (any(grepl("$", txt, fixed = TRUE))) {
        stop(
          "Do not use '$' notation in '",
          nm,
          "'. ",
          "Use bare variable names with 'data = <your data frame>' instead.\n",
          "  Bad : log(ricephil$PROD) ~ log(ricephil$AREA)\n",
          "  Good: log(PROD) ~ log(AREA)  with  data = ricephil",
          call. = FALSE
        )
      }
    }
  }
  .check_no_dollar(formula, "formula")
  if (!missing(muhet)) {
    .check_no_dollar(muhet, "muhet")
  }
  if (!missing(uhet)) {
    .check_no_dollar(uhet, "uhet")
  }
  if (!missing(vhet)) {
    .check_no_dollar(vhet, "vhet")
  }
  if (!missing(thet)) {
    .check_no_dollar(thet, "thet")
  }
  if (!is.null(selectionF) && inherits(selectionF, "formula")) {
    .check_no_dollar(selectionF, "selectionF")
  }

  # ---------- Validate groupType ----------
  groupType <- tolower(groupType)
  if (!(groupType %in% c("sfacross", "sfaselectioncross", "sfalcmcross"))) {
    stop(
      "argument 'groupType' must be 'sfacross', 'sfaselectioncross', or 'sfalcmcross'",
      call. = FALSE
    )
  }

  # ---------- Validate metaMethod ----------
  metaMethod <- tolower(metaMethod)
  if (!(metaMethod %in% c("lp", "qp", "sfa"))) {
    stop("argument 'metaMethod' must be 'lp', 'qp', or 'sfa'", call. = FALSE)
  }

  # ---------- Validate sfaApproach ----------
  sfaApproach <- tolower(sfaApproach)
  if (!(sfaApproach %in% c("ordonnell", "huang"))) {
    stop("argument 'sfaApproach' must be 'ordonnell' or 'huang'", call. = FALSE)
  }

  # ---------- Validate / normalise group ----------
  # For sfalcmcross the group variable is OPTIONAL: if omitted, a pooled
  # sfalcmcross is fitted on all data and latent classes serve as groups.
  lcmNoGroup <- (groupType == "sfalcmcross" && is.null(group))

  if (!lcmNoGroup) {
    if (is.null(group) || !is.character(group) || length(group) != 1) {
      stop(
        "argument 'group' must be a single character string naming a column in 'data'",
        call. = FALSE
      )
    }
    if (!group %in% names(data)) {
      stop("'group' variable '", group, "' not found in 'data'", call. = FALSE)
    }
  }

  # Apply subset if provided
  if (!missing(subset)) {
    data <- data[subset, , drop = FALSE]
  }

  # ---- LCM NO-GROUP early path ----
  # Fit a single pooled sfalcmcross; use posterior class assignments as groups.
  if (lcmNoGroup) {
    return(.smfa_lcm_nogroup(
      formula = formula,
      data = data,
      S = S,
      udist = udist,
      start = start,
      lcmClasses = lcmClasses,
      whichStart = whichStart,
      initAlg = initAlg,
      initIter = initIter,
      metaMethod = metaMethod,
      sfaApproach = sfaApproach,
      method = method,
      hessianType = hessianType,
      itermax = itermax,
      printInfo = printInfo,
      tol = tol,
      gradtol = gradtol,
      stepmax = stepmax,
      qac = qac,
      logDepVar = logDepVar,
      wscale = wscale,
      muhet_missing = missing(muhet),
      uhet_missing = missing(uhet),
      vhet_missing = missing(vhet),
      thet_missing = missing(thet),
      uhet = if (!missing(uhet)) uhet else NULL,
      vhet = if (!missing(vhet)) vhet else NULL,
      thet = if (!missing(thet)) thet else NULL,
      weights = if (!missing(weights)) weights else NULL,
      cl = cl,
      ...
    ))
  }

  # Ensure group is a factor (normal grouped path)
  data[[group]] <- as.factor(data[[group]])
  groupLevels <- levels(data[[group]])
  nGroups <- length(groupLevels)
  if (nGroups < 2) {
    stop(
      "at least 2 groups are required for metafrontier analysis",
      call. = FALSE
    )
  }

  # ---------- Validate S ----------
  if (length(S) != 1 || !(S %in% c(-1L, 1L))) {
    stop("argument 'S' must equal either 1 or -1", call. = FALSE)
  }
  typeSfa <- if (S == 1L) {
    "Stochastic Production/Profit Frontier, e = v - u"
  } else {
    "Stochastic Cost Frontier, e = v + u"
  }

  # ---------- selectionF validation for sfaselectioncross ----------
  if (groupType == "sfaselectioncross") {
    if (is.null(selectionF)) {
      stop(
        "'selectionF' must be provided when groupType = 'sfaselectioncross'",
        call. = FALSE
      )
    }
    # selectionF must be a two-sided formula whose LHS is the selection indicator,
    # e.g. selected ~ EDYRS + AGE  - consistent with sfaselectioncross's own API.
    # The LHS variable name is used to extract the selectDum column.
    .validate_selF <- function(fml, gname) {
      if (!inherits(fml, "formula")) {
        stop(
          "'selectionF' for group '",
          gname,
          "' must be a formula",
          call. = FALSE
        )
      }
      lhs_vars <- all.vars(update(fml, . ~ 0))
      if (length(lhs_vars) < 1L) {
        stop(
          "'selectionF' must be a two-sided formula with the selection indicator ",
          "as the LHS, e.g. selected ~ EDYRS + AGE",
          call. = FALSE
        )
      }
    }
    if (inherits(selectionF, "formula")) {
      .validate_selF(selectionF, "all groups")
      selecFList <- setNames(rep(list(selectionF), nGroups), groupLevels)
    } else if (is.list(selectionF)) {
      missing_g <- setdiff(groupLevels, names(selectionF))
      if (length(missing_g) > 0) {
        stop(
          "'selectionF' list is missing entries for groups: ",
          paste(missing_g, collapse = ", "),
          call. = FALSE
        )
      }
      selecFList <- selectionF[groupLevels]
      lapply(seq_along(selecFList), function(k) {
        .validate_selF(selecFList[[k]], names(selecFList)[k])
      })
    } else {
      stop(
        "'selectionF' must be a two-sided formula (e.g. selected ~ z1 + z2) ",
        "or a named list of such formulas",
        call. = FALSE
      )
    }
  }

  # ---------- Build sfaR model arguments ----------
  # Common arguments accepted by all three sfaR functions
  sfaBaseArgs <- list(
    logDepVar = logDepVar,
    S = S,
    udist = udist,
    start = start,
    method = method,
    itermax = itermax,
    printInfo = printInfo,
    tol = tol,
    gradtol = gradtol,
    stepmax = stepmax,
    qac = qac
  )
  # hessianType: only include if the user explicitly passed it;
  # otherwise each underlying sfaR function uses its own natural default:
  #   sfacross / sfalcmcross -> hessianType = 1L (analytic Hessian)
  #   sfaselectioncross      -> hessianType = 2L (BHHH / outer-product)
  # Forcing a single default here causes SE differences vs. standalone sfaR calls.
  if (!is.null(hessianType)) {
    sfaBaseArgs$hessianType <- hessianType
  }
  # Add weights only if provided (all three accept it as optional)
  if (!missing(weights)) sfaBaseArgs$weights <- weights
  sfaBaseArgs$wscale <- wscale

  if (groupType == "sfacross") {
    # sfacross-specific: formula, scaling, muhet, uhet, vhet, simType/Nsim/prime/burn/antithetics/seed
    sfaBaseArgs$formula <- formula
    sfaBaseArgs$scaling <- scaling
    sfaBaseArgs$simType <- simType
    sfaBaseArgs$Nsim <- Nsim
    sfaBaseArgs$prime <- prime
    sfaBaseArgs$burn <- burn
    sfaBaseArgs$antithetics <- antithetics
    sfaBaseArgs$seed <- seed
    if (!missing(muhet)) sfaBaseArgs$muhet <- muhet
    if (!missing(uhet)) sfaBaseArgs$uhet <- uhet
    if (!missing(vhet)) sfaBaseArgs$vhet <- vhet
  } else if (groupType == "sfalcmcross") {
    # sfalcmcross-specific: formula, lcmClasses, whichStart, initAlg, initIter, uhet, vhet, thet
    # sfalcmcross does NOT accept: seed, simType, Nsim, prime, burn, antithetics, scaling, muhet
    sfaBaseArgs$formula <- formula
    sfaBaseArgs$lcmClasses <- lcmClasses
    sfaBaseArgs$whichStart <- whichStart
    sfaBaseArgs$initAlg <- initAlg
    sfaBaseArgs$initIter <- initIter
    if (!missing(uhet)) sfaBaseArgs$uhet <- uhet
    if (!missing(vhet)) sfaBaseArgs$vhet <- vhet
    if (!missing(thet)) sfaBaseArgs$thet <- thet
  } else if (groupType == "sfaselectioncross") {
    # sfaselectioncross-specific: uses frontierF (NOT formula), selectionF, modelType,
    # lType, Nsub, uBound, intol, simType, Nsim, prime, burn, antithetics, seed
    # Does NOT accept: formula, scaling, muhet, thet, whichStart, initAlg, initIter, lcmClasses
    sfaBaseArgs$frontierF <- formula
    sfaBaseArgs$modelType <- modelType
    sfaBaseArgs$lType <- lType
    sfaBaseArgs$Nsub <- Nsub
    sfaBaseArgs$uBound <- uBound
    sfaBaseArgs$intol <- intol
    sfaBaseArgs$simType <- simType
    sfaBaseArgs$Nsim <- Nsim
    sfaBaseArgs$prime <- prime
    sfaBaseArgs$burn <- burn
    sfaBaseArgs$antithetics <- antithetics
    sfaBaseArgs$seed <- seed
    if (!missing(uhet)) sfaBaseArgs$uhet <- uhet
    if (!missing(vhet)) sfaBaseArgs$vhet <- vhet
  }

  # ---------- Step 1: Fit group-specific frontier models ----------
  if (printInfo) {
    cat(
      "Estimating group-specific stochastic frontiers (",
      groupType,
      ") ...\n",
      sep = ""
    )
  }
  groupModels <- vector("list", nGroups)
  names(groupModels) <- groupLevels

  for (g in groupLevels) {
    if (printInfo) cat("  Group:", g, "\n")
    subData <- data[data[[group]] == g, , drop = FALSE]
    if (nrow(subData) == 0) {
      stop("Group '", g, "' has zero observations", call. = FALSE)
    }
    grpArgs <- c(sfaBaseArgs, list(data = subData))

    if (groupType == "sfaselectioncross") {
      grpArgs$selectionF <- selecFList[[g]]
    }

    estimFun <- switch(groupType,
      sfacross = sfacross,
      sfaselectioncross = sfaselectioncross,
      sfalcmcross = sfalcmcross
    )

    groupModels[[g]] <- tryCatch(
      do.call(estimFun, grpArgs),
      error = function(e) {
        stop(
          "Estimation failed for group '",
          g,
          "': ",
          e$message,
          call. = FALSE
        )
      }
    )
  }
  if (printInfo) cat("Group frontiers estimated.\n")

  # ---------- Collect full dataset and fitted values ----------
  dataFull <- data
  dataFull$.mf_rowid <- seq_len(nrow(dataFull))
  dataFull$.mf_group <- dataFull[[group]]
  dataFull$.mf_yhat_group <- NA_real_
  dataFull$.mf_logL_OBS <- NA_real_

  # Selection mask (only relevant for sfaselectioncross)
  if (groupType == "sfaselectioncross") {
    dataFull$.mf_selected <- NA_integer_
  }

  for (g in groupLevels) {
    idx <- which(dataFull[[group]] == g)
    dt_g <- groupModels[[g]]$dataTable

    if (groupType == "sfaselectioncross") {
      # Use the LHS of selectionF to identify the selection indicator - exactly
      # as chalfnormeff_ss does via all.vars(object$selectionF)[1]
      selColName <- all.vars(selecFList[[g]])[1]
      selDum <- dt_g[[selColName]]
      if (is.null(selDum)) {
        stop(
          "Selection indicator column '",
          selColName,
          "' not found in ",
          "dataTable for group '",
          g,
          "'",
          call. = FALSE
        )
      }
      dataFull$.mf_selected[idx] <- as.integer(selDum)
      sel_idx <- idx[selDum == 1]
      dataFull$.mf_yhat_group[sel_idx] <- dt_g$mlFitted[selDum == 1]
      dataFull$.mf_logL_OBS[sel_idx] <- dt_g$logL_OBS[selDum == 1]
    } else if (groupType == "sfalcmcross") {
      best_fit <- extract_lcm_fitted(groupModels[[g]])
      dataFull$.mf_yhat_group[idx] <- best_fit
      dataFull$.mf_logL_OBS[idx] <- dt_g$logL_OBS
    } else {
      # sfacross
      dataFull$.mf_yhat_group[idx] <- dt_g$mlFitted
      dataFull$.mf_logL_OBS[idx] <- dt_g$logL_OBS
    }
  }

  # ---------- Build Xvar matrix for meta-stage ----------
  # Use formula from one of the group models
  if (groupType == "sfaselectioncross") {
    firstModel <- groupModels[[1]]
    formulaFull <- firstModel$frontierF
    # Only use selected obs for metafrontier
    metaIdx <- which(!is.na(dataFull$.mf_yhat_group))
    dataMeta <- dataFull[metaIdx, , drop = FALSE]
    mcFull <- model.frame(formulaFull, data = dataMeta, na.action = na.pass)
    validMeta <- rowSums(is.na(mcFull)) == 0
    Xvar_full <- model.matrix(terms(formulaFull, rhs = 1), mcFull)[
      validMeta, ,
      drop = FALSE
    ]
  } else {
    firstModel <- groupModels[[1]]
    formulaFull <- firstModel$formula
    mcFull <- model.frame(formulaFull, data = dataFull, na.action = na.pass)
    validFull <- rowSums(
      is.na(mcFull) | Reduce("|", lapply(mcFull, is.infinite))
    ) ==
      0
    Xvar_full <- model.matrix(terms(formulaFull, rhs = 1), mcFull)[
      validFull, ,
      drop = FALSE
    ]
    metaIdx <- seq_len(nrow(dataFull))[validFull]
  }

  nXvar <- ncol(Xvar_full)

  # ---------- Build N x G matrix of group betas evaluated at all obs ----------
  # (needed for LP, QP, and ordonnell SFA; not needed for Huang)
  if (
    metaMethod %in%
      c("lp", "qp") ||
      (metaMethod == "sfa" && sfaApproach == "ordonnell")
  ) {
    groupFrontierAll <- matrix(NA_real_, nrow = length(metaIdx), ncol = nGroups)
    colnames(groupFrontierAll) <- groupLevels

    for (g in groupLevels) {
      if (groupType == "sfalcmcross") {
        # For LCM: for obs in group g, use their best-posterior-class fitted
        # value directly from dataTable (already computed by sfalcmcross).
        # For obs NOT in group g, evaluate group g's class-1 beta at their X.
        lcm_nXvar <- groupModels[[g]]$nXvar
        lcm_nuZU <- groupModels[[g]]$nuZUvar
        lcm_nvZV <- groupModels[[g]]$nvZVvar
        stride <- lcm_nXvar + lcm_nuZU + lcm_nvZV
        dt_g <- groupModels[[g]]$dataTable

        # Positions of obs in group g within metaIdx
        grp_pos_in_meta <- which(dataFull[[group]][metaIdx] == g)
        non_pos_in_meta <- which(dataFull[[group]][metaIdx] != g)

        # Own-group obs: use best-class fitted value from dataTable
        # extract_lcm_fitted() returns N_g-length vector ordered as dt_g rows
        if (length(grp_pos_in_meta) > 0) {
          best_fit_g <- extract_lcm_fitted(groupModels[[g]])
          groupFrontierAll[grp_pos_in_meta, g] <- best_fit_g
        }

        # Non-own-group obs: evaluate class-1 beta from group g at their X
        if (length(non_pos_in_meta) > 0) {
          beta1_g <- groupModels[[g]]$mlParam[seq_len(lcm_nXvar)]
          groupFrontierAll[non_pos_in_meta, g] <-
            as.numeric(Xvar_full[non_pos_in_meta, , drop = FALSE] %*% beta1_g)
        }
      } else if (groupType == "sfaselectioncross") {
        # frontier beta: mlParam[1:nXvar_F] as in sfaselectioncross.R
        betaG <- groupModels[[g]]$mlParam[seq_len(groupModels[[g]]$nXvar)]
        groupFrontierAll[, g] <- as.numeric(Xvar_full %*% betaG)
      } else {
        # sfacross: mlParam[1:nXvar]
        betaG <- groupModels[[g]]$mlParam[seq_len(nXvar)]
        groupFrontierAll[, g] <- as.numeric(Xvar_full %*% betaG)
      }
    }
  }

  # ---------- Step 2: Estimate metafrontier ----------
  if (printInfo) {
    cat(
      "Estimating metafrontier using method:",
      mfauxdist(metaMethod, sfaApproach),
      "\n"
    )
  }

  metaFrontierParam <- NULL
  metaFrontierVcov <- NULL
  metaSfaObj <- NULL
  yhat_meta_local <- NULL # meta fitted values on metaIdx rows

  if (metaMethod == "lp") {
    yhat_meta_local <- mf_lp(groupFrontierAll)
    metaFrontierParam <- NULL
    metaFrontierVcov <- NULL
  } else if (metaMethod == "qp") {
    qpRes <- mf_qp(Xvar = Xvar_full, groupFrontierMat = groupFrontierAll)
    metaFrontierParam <- qpRes$beta
    names(metaFrontierParam) <- colnames(Xvar_full)
    metaFrontierVcov <- qpRes$vcov
    yhat_meta_local <- qpRes$yhat
  } else {
    # sfa
    if (sfaApproach == "huang") {
      yhat_group_meta <- dataFull$.mf_yhat_group[metaIdx]
      sfaMetaRes <- mf_huang(
        Xvar = Xvar_full,
        yhat_group = yhat_group_meta,
        S = S,
        method = method,
        udist = udist,
        itermax = itermax,
        tol = tol,
        gradtol = gradtol,
        stepmax = stepmax,
        qac = qac,
        ...
      )
    } else {
      sfaMetaRes <- mf_sfa_ordonnell(
        Xvar = Xvar_full,
        groupFrontierMat = groupFrontierAll,
        S = S,
        method = method,
        udist = udist,
        itermax = itermax,
        tol = tol,
        gradtol = gradtol,
        stepmax = stepmax,
        qac = qac,
        ...
      )
    }
    metaFrontierParam <- sfaMetaRes$beta
    names(metaFrontierParam) <- colnames(Xvar_full)
    metaFrontierVcov <- sfaMetaRes$vcov
    metaSfaObj <- sfaMetaRes$sfaObj
    yhat_meta_local <- sfaMetaRes$yhat
  }

  # Store meta fitted values in full data table
  dataFull$.mf_yhat_meta <- NA_real_
  dataFull$.mf_yhat_meta[metaIdx] <- yhat_meta_local
  dataFull$.mf_gap <- NA_real_
  dataFull$.mf_gap[metaIdx] <-
    S * (dataFull$.mf_yhat_meta[metaIdx] - dataFull$.mf_yhat_group[metaIdx])

  # ---------- Compute total log-likelihood and nParm ----------
  groupLogLiks <- sapply(groupModels, function(m) m$mlLoglik)
  groupNParms <- sapply(groupModels, function(m) m$nParm)
  totalLogLik <- sum(groupLogLiks)
  totalNParm <- sum(groupNParms)

  if (metaMethod == "sfa" && !is.null(metaSfaObj)) {
    totalLogLik <- totalLogLik + metaSfaObj$mlLoglik
    totalNParm <- totalNParm + metaSfaObj$nParm
  } else if (metaMethod == "qp") {
    totalNParm <- totalNParm + length(metaFrontierParam)
  }

  # ---------- Assemble return object ----------
  mlDate <- format(Sys.time(), "Model was estimated on : %b %a %d, %Y at %H:%M")

  returnObj <- list()
  returnObj$call <- cl
  returnObj$formula <- formula
  returnObj$group <- group
  returnObj$groups <- groupLevels
  returnObj$nGroups <- nGroups
  returnObj$S <- S
  returnObj$typeSfa <- typeSfa
  returnObj$udist <- udist
  returnObj$groupType <- groupType
  returnObj$metaMethod <- metaMethod
  returnObj$sfaApproach <- sfaApproach
  returnObj$lcmNoGroup <- FALSE
  returnObj$logDepVar <- logDepVar
  returnObj$Nobs <- nrow(dataFull)
  returnObj$NobsMeta <- length(metaIdx)
  returnObj$nXvar <- nXvar
  returnObj$groupModels <- groupModels
  if (exists("groupFrontierAll")) {
    returnObj$groupFrontierAll <- groupFrontierAll
  }
  returnObj$metaFrontierParam <- metaFrontierParam
  returnObj$metaFrontierVcov <- metaFrontierVcov
  returnObj$metaSfaObj <- metaSfaObj
  returnObj$mlLoglik <- totalLogLik
  returnObj$nParm <- totalNParm
  returnObj$dataTable <- dataFull
  returnObj$mlDate <- mlDate

  class(returnObj) <- "smfa"
  return(returnObj)
}

# print for smfa ----------
#' @rdname smfa
#' @export
print.smfa <- function(x, ...) {
  cat("Call:\n")
  cat(deparse(x$call), "\n\n")
  cat("Stochastic Metafrontier Analysis\n")
  grp_appr <- switch(x$groupType,
    sfacross = "Stochastic Frontier Analysis",
    sfaselectioncross = "Sample Selection Stochastic Frontier Analysis",
    sfalcmcross = "Latent Class Stochastic Frontier Analysis",
    x$groupType
  )
  cat("Group approach   :", grp_appr, "\n")
  cat("Group estimator  :", x$groupType, "\n")
  if (isTRUE(x$lcmNoGroup)) {
    cat("  (Pooled LCM - latent classes used as groups)\n")
  }
  cat("Metafrontier method:", mfauxdist(x$metaMethod, x$sfaApproach), "\n")
  cat("Groups (", x$nGroups, "):", paste(x$groups, collapse = ", "), "\n")
  cat("Total observations:", x$Nobs)
  if (!is.null(x$NobsMeta) && x$NobsMeta < x$Nobs) {
    cat(" (", x$NobsMeta, "in metafrontier)", sep = "")
  }
  cat("\n")
  cat("Distribution:", x$udist, "\n")
  cat(x$typeSfa, "\n\n")
  if (!is.null(x$metaFrontierParam)) {
    cat("Metafrontier coefficients:\n")
    print.default(format(x$metaFrontierParam), print.gap = 2, quote = FALSE)
  } else {
    cat("Metafrontier: deterministic envelope (LP) - no estimated parameters\n")
  }
  if (isTRUE(x$lcmNoGroup)) {
    cat("\nLog-likelihood (pooled LCM):", round(x$mlLoglik, 4), "\n")
  } else {
    cat("\nGroup log-likelihoods:\n")
    for (g in x$groups) {
      cat(sprintf("  %-20s: %.4f\n", g, x$groupModels[[g]]$mlLoglik))
    }
    cat(sprintf("  %-20s: %.4f\n", "Total", x$mlLoglik))
  }
  invisible(x)
}

# ---------------------------------------------------------------------------
# Internal helper: pooled LCM metafrontier (no explicit group variable)
# ---------------------------------------------------------------------------
.smfa_lcm_nogroup <- function(
  formula,
  data,
  S,
  udist,
  lcmClasses,
  whichStart,
  initAlg,
  initIter,
  metaMethod,
  sfaApproach,
  method,
  hessianType,
  itermax,
  printInfo,
  tol,
  gradtol,
  stepmax,
  qac,
  logDepVar,
  muhet_missing,
  uhet_missing,
  vhet_missing,
  thet_missing,
  uhet = NULL,
  vhet = NULL,
  thet = NULL,
  cl,
  ...
) {
  typeSfa <- if (S == 1L) {
    "Stochastic Production/Profit Frontier, e = v - u"
  } else {
    "Stochastic Cost Frontier, e = v + u"
  }

  # ---- Step 1: Fit single pooled sfalcmcross ----
  if (printInfo) {
    cat(
      "Fitting pooled sfalcmcross (",
      lcmClasses,
      " classes) on all data ...\n",
      sep = ""
    )
  }
  lcmArgs <- list(
    formula    = formula,
    data       = data,
    S          = S,
    udist      = udist,
    logDepVar  = logDepVar,
    lcmClasses = lcmClasses,
    whichStart = whichStart,
    initAlg    = initAlg,
    initIter   = initIter,
    method     = method,
    itermax    = itermax,
    printInfo  = printInfo,
    tol        = tol,
    gradtol    = gradtol,
    stepmax    = stepmax,
    qac        = qac
  )
  # hessianType: only include if the user explicitly passed it;
  # otherwise sfalcmcross uses its own natural default (1 = analytic Hessian).
  if (!is.null(hessianType)) {
    lcmArgs$hessianType <- hessianType
  }
  if (!uhet_missing && !is.null(uhet)) {
    lcmArgs$uhet <- uhet
  }
  if (!vhet_missing && !is.null(vhet)) {
    lcmArgs$vhet <- vhet
  }
  if (!thet_missing && !is.null(thet)) {
    lcmArgs$thet <- thet
  }

  lcmObj <- tryCatch(
    do.call(sfalcmcross, lcmArgs),
    error = function(e) stop("sfalcmcross failed: ", e$message, call. = FALSE)
  )
  if (printInfo) cat("Pooled LCM estimated.\n")

  # ---- Step 2: Extract class assignments and TEs ----
  effLcm <- efficiencies(lcmObj)
  Group_c <- effLcm$Group_c # integer 1..nc per obs
  teGroup_BC <- effLcm$teBC_c # best-class BC efficiency

  nc <- lcmClasses
  groupNames <- paste0("Class_", seq_len(nc))
  nObs <- nrow(data)

  # Build .mf_group column (factor with class labels)
  mf_group <- factor(paste0("Class_", Group_c), levels = groupNames)

  # ---- Step 3: Build Xvar matrix for meta-stage ----
  mcFull <- model.frame(formula, data = data, na.action = na.pass)
  validFull <- rowSums(
    is.na(mcFull) |
      Reduce("|", lapply(mcFull, is.infinite))
  ) ==
    0
  Xvar_full <- model.matrix(terms(formula, rhs = 1), mcFull)[
    validFull, ,
    drop = FALSE
  ]
  metaIdx <- seq_len(nObs)[validFull]
  nXvar <- ncol(Xvar_full)

  # ---- Step 4: Extract class-k betas and build groupFrontierAll ----
  # sfalcmcross mlParam layout: [beta_1 ... beta_nc | Zu_1 ... Zu_nc | Zv_1 ... Zv_nc | theta]
  nX <- lcmObj$nXvar
  nU <- lcmObj$nuZUvar
  nV <- lcmObj$nvZVvar

  # Best-class fitted values (y-hat for each obs using its assigned class frontier)
  # Use mlFitted_ck columns if available, otherwise reconstruct.
  dt_lcm <- lcmObj$dataTable
  yhat_group <- rep(NA_real_, nObs)

  # Try to use mlFitted_ck columns directly
  fit_cols <- paste0("mlFitted_c", seq_len(nc))
  has_fit <- all(fit_cols %in% names(dt_lcm))
  if (has_fit) {
    for (k in seq_len(nc)) {
      idx_k <- which(Group_c == k)
      if (length(idx_k) > 0) {
        yhat_group[idx_k] <- dt_lcm[[fit_cols[k]]][idx_k]
      }
    }
  } else {
    # Reconstruct from class betas
    for (k in seq_len(nc)) {
      # Interleaved layout: [beta_1, Zu_1, Zv_1, beta_2, Zu_2, Zv_2, ...]
      s_idx <- (k - 1) * (nX + nU + nV) + 1
      beta_k <- lcmObj$mlParam[s_idx:(s_idx + nX - 1)]
      yhat_group <- yhat_group # keep NA for invalid
      idx_k <- which(mf_group[validFull] == groupNames[k])
      if (length(idx_k) > 0) {
        yhat_group[metaIdx[idx_k]] <- as.numeric(Xvar_full[idx_k, ] %*% beta_k)
      }
    }
  }

  # groupFrontierAll: N_meta x nc matrix - class k beta evaluated at all obs
  if (
    metaMethod %in%
      c("lp", "qp") ||
      (metaMethod == "sfa" && sfaApproach == "ordonnell")
  ) {
    groupFrontierAll <- matrix(
      NA_real_,
      nrow = length(metaIdx),
      ncol = nc,
      dimnames = list(NULL, groupNames)
    )
    for (k in seq_len(nc)) {
      s_idx <- (k - 1) * (nX + nU + nV) + 1
      beta_k <- lcmObj$mlParam[s_idx:(s_idx + nX - 1)]
      groupFrontierAll[, k] <- as.numeric(Xvar_full %*% beta_k)
    }
  }

  # ---- Step 5: Estimate metafrontier ----
  if (printInfo) {
    cat(
      "Estimating metafrontier using method:",
      mfauxdist(metaMethod, sfaApproach),
      "\n"
    )
  }
  metaFrontierParam <- NULL
  metaFrontierVcov <- NULL
  metaSfaObj <- NULL
  yhat_meta_local <- NULL

  if (metaMethod == "lp") {
    yhat_meta_local <- mf_lp(groupFrontierAll)
  } else if (metaMethod == "qp") {
    qpRes <- mf_qp(Xvar = Xvar_full, groupFrontierMat = groupFrontierAll)
    metaFrontierParam <- setNames(qpRes$beta, colnames(Xvar_full))
    metaFrontierVcov <- qpRes$vcov
    yhat_meta_local <- qpRes$yhat
  } else {
    # sfa
    if (sfaApproach == "huang") {
      sfaMetaRes <- mf_huang(
        Xvar = Xvar_full,
        yhat_group = yhat_group[metaIdx],
        S = S,
        method = method,
        udist = udist,
        itermax = itermax,
        tol = tol,
        gradtol = gradtol,
        stepmax = stepmax,
        qac = qac,
        ...
      )
    } else {
      sfaMetaRes <- mf_sfa_ordonnell(
        Xvar = Xvar_full,
        groupFrontierMat = groupFrontierAll,
        S = S,
        method = method,
        udist = udist,
        itermax = itermax,
        tol = tol,
        gradtol = gradtol,
        stepmax = stepmax,
        qac = qac,
        ...
      )
    }
    metaFrontierParam <- setNames(sfaMetaRes$beta, colnames(Xvar_full))
    metaFrontierVcov <- sfaMetaRes$vcov
    metaSfaObj <- sfaMetaRes$sfaObj
    yhat_meta_local <- sfaMetaRes$yhat
  }

  # ---- Step 6: Assemble dataTable ----
  dataFull <- data
  dataFull$.mf_rowid <- seq_len(nObs)
  dataFull$.mf_group <- mf_group # Class_1 / Class_2 / ...
  dataFull$.mf_yhat_group <- yhat_group
  dataFull$.mf_yhat_meta <- NA_real_
  dataFull$.mf_yhat_meta[metaIdx] <- yhat_meta_local
  dataFull$.mf_gap <- NA_real_
  dataFull$.mf_gap[metaIdx] <-
    S * (dataFull$.mf_yhat_meta[metaIdx] - dataFull$.mf_yhat_group[metaIdx])
  dataFull$.mf_logL_OBS <- dt_lcm$logL_OBS

  # ---- Step 7: Assemble return object ----
  mlDate <- format(Sys.time(), "Model was estimated on : %b %a %d, %Y at %H:%M")
  totalNParm <- lcmObj$nParm
  totalLogLik <- lcmObj$mlLoglik
  if (metaMethod == "sfa" && !is.null(metaSfaObj)) {
    totalLogLik <- totalLogLik + metaSfaObj$mlLoglik
    totalNParm <- totalNParm + metaSfaObj$nParm
  } else if (metaMethod == "qp") {
    totalNParm <- totalNParm + length(metaFrontierParam)
  }

  obj <- list(
    call = cl,
    formula = formula,
    group = ".mf_group",
    groups = groupNames,
    nGroups = nc,
    S = S,
    typeSfa = typeSfa,
    udist = udist,
    groupType = "sfalcmcross",
    metaMethod = metaMethod,
    sfaApproach = sfaApproach,
    lcmNoGroup = TRUE,
    lcmObj = lcmObj, # single pooled LCM model
    groupModels = setNames(
      # mirror same object per class for compatibility
      rep(list(lcmObj), nc),
      groupNames
    ),
    logDepVar = logDepVar,
    Nobs = nObs,
    NobsMeta = length(metaIdx),
    nXvar = nXvar,
    metaFrontierParam = metaFrontierParam,
    metaFrontierVcov = metaFrontierVcov,
    metaSfaObj = metaSfaObj,
    mlLoglik = totalLogLik,
    nParm = totalNParm,
    dataTable = dataFull,
    mlDate = mlDate
  )
  if (exists("groupFrontierAll")) {
    obj$groupFrontierAll <- groupFrontierAll
  }
  class(obj) <- "smfa"
  return(obj)
}
