# R/read.R  -----------------------------------------------------------------
# CBF Reader: Conic Benchmark Format (version 4) --> ROI OP
# ---------------------------------------------------------------------------
#
# Mathematical background
# -----------------------
# CBF encodes the conic optimisation problem
#
#   min / max   sum_{jinJ^PSD} <F_j^obj, X_j>  +  sum_{jinJ} a_j^obj x_j  + b^obj
#   s.t.        g_i = sum_{jinJ^PSD} <F_ij, X_j> + sum_{jinJ} a_ij x_j + b_i  in K_i
#               G_p = sum_{jinJ} x_j H_pj + D_p  in  PSD^{m_p x m_p}
#               x  in  K^var    (variable-domain cones)
#
# where <*,*> denotes the trace inner product for symmetric matrices.
#
# This reader handles all CBF variable and constraint types:
#
#   Scalar variables (VAR) and scalar constraints (CON), including SVECPSD
#   cones on scalar constraint rows (mapped to ROI::K_psd).
#
#   PSD matrix variables (PSDVAR): each X_j in S^{m_j x m_j}_+ is replaced
#   by svec(X_j) -- a vector of m_j*(m_j+1)/2 new scalar variables appended
#   after the VAR variables.  A K_psd(m_j) cone constraint enforces X_j >= 0.
#   Objective terms (OBJFCOORD) and scalar-constraint terms (FCOORD) are
#   translated via <F, X_j> = svec(F)' svec(X_j).
#
#   PSD matrix constraints (PSDCON): each LMI G_p = sum_j x_j H_pj + D_p in
#   PSD^{m_p x m_p} is vectorised to svec(G_p) in K_psd(m_p), yielding a
#   K_psd conic constraint on the scalar variables (HCOORD / DCOORD data).
#
# Sign convention  (critical -- see also write.R)
# -----------------------------------------------
# CBF stores   g_i  = A_i x  + b_i   in  K_i
#
# ROI stores constraints as:
#   L_constraint :  A x  dir  rhs     (e.g. A x >= rhs)
#   C_constraint :  rhs  -  A x  in  K
#
# To translate CBF -> ROI conic constraints we preserve the affine map
#   A x + b  in K  =  b - (-A) x in K
# so the ROI conic form uses
#       L_cone   = -A_cbf
#       rhs_cone =  b_cbf
# Linear constraints still use the usual RHS form with
#       rhs_lin  = -b_cbf
# ---------------------------------------------------------------------------


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

#' Read a CBF File into an ROI OP Object
#'
#' Parses a file in the Conic Benchmark Format (CBF version 4) and returns
#' an \code{\link[ROI]{OP}} object representing the encoded optimisation
#' problem.
#'
#' @details
#' Supported features
#' \itemize{
#'   \item Scalar variables (VAR) with all non-parametric cone types:
#'     \code{F.}, \code{L+.}, \code{L-.}, \code{L=.}.
#'   \item Scalar constraints (CON) with non-parametric cones:
#'     \code{F.}, \code{L+.}, \code{L-.}, \code{L=.}, \code{Q.}, \code{QR.},
#'     \code{EXP.}, \code{EXP*.}, \code{SVECPSD.}, \code{GMEANABS.},
#'     \code{GMEAN.}, and their duals.
#'   \item Parametric power cones (\code{@k:POW}, \code{@k:POW*}) via the
#'     POWCONES / POW*CONES lookup tables.
#'   \item Integer variables (INT section).
#'   \item Linear and constant-term objective coefficients
#'     (OBJACOORD, OBJBCOORD).
#'   \item PSD matrix variables (PSDVAR): each \eqn{X_j \in S^{m_j \times m_j}_+}
#'     is replaced by its symmetric vectorisation \eqn{\mathrm{svec}(X_j)} of
#'     length \eqn{m_j(m_j+1)/2}, appended as new scalar variables with a
#'     \code{K_psd(m_j)} cone constraint.  Objective terms (OBJFCOORD) and
#'     scalar-constraint terms (FCOORD) involving \eqn{X_j} are translated via
#'     the trace inner-product identity
#'     \eqn{\langle F, X_j \rangle = \mathrm{svec}(F)^{\top} \mathrm{svec}(X_j)}.
#'   \item PSD matrix constraints (PSDCON / HCOORD / DCOORD): each LMI
#'     \eqn{G_p = \sum_j x_j H_{pj} + D_p \in \mathrm{PSD}^{m_p}} is svec-ed
#'     into a \code{K_psd(m_p)} conic constraint on the scalar variables.
#' }
#'
#' Features that generate a \code{warning()} and are skipped:
#' \itemize{
#'   \item Hotstart sequences (CHANGE keyword is treated as end-of-file).
#' }
#'
#' @param file  Character string. Path to the \code{.cbf} (or \code{.CBF})
#'   file.
#' @param ...   Currently unused; present for API compatibility with
#'   \code{\link[ROI]{ROI_plugin_register_reader}}.
#'
#' @return An \code{\link[ROI]{OP}} object.
#'
#' @examples
#' ## round-trip: write a simple LP to a temp file then read it back
#' op <- ROI::OP(
#'     objective   = ROI::L_objective(c(1, 1)),
#'     constraints = ROI::L_constraint(
#'         L   = matrix(c(1, 1), nrow = 1),
#'         dir = ">=",
#'         rhs = 1
#'     )
#' )
#' f <- file.path(tempdir(), "lp.cbf")
#' write_cbf(op, f)
#' op2 <- read_cbf(f)
#'
#' @seealso \code{\link{write_cbf}}
#' @export
read_cbf <- function(file, ...) {
    stopifnot(is.character(file), length(file) == 1L, nzchar(file))

    # 1. Read and clean raw lines.
    raw <- readLines(file, warn = FALSE)
    raw <- trimws(raw)                    # strip leading/trailing whitespace
    raw <- raw[!startsWith(raw, "#")]     # CBF spec Sec.2.5: '#' = comment line

    # 2. Treat CHANGE as end-of-file (hotstart sequences not supported).
    change_pos <- match("CHANGE", raw)
    if (!is.na(change_pos)) raw <- raw[seq_len(change_pos - 1L)]

    # 3. Split the cleaned lines into keyword-indexed sections.
    secs <- .cbf_parse_sections(raw)

    # 4. Translate the section list into an ROI OP.
    .cbf_build_OP(secs)
}


# ---------------------------------------------------------------------------
# Section parser
# ---------------------------------------------------------------------------

# Complete ordered list of all CBF keywords (CBF spec Appendix B).
.CBF_ALL_KEYWORDS <- c(
    "VER",
    "POWCONES", "POW*CONES",
    "OBJSENSE",
    "PSDVAR", "VAR", "INT", "PSDCON", "CON",
    "OBJFCOORD", "OBJACOORD", "OBJBCOORD",
    "FCOORD", "ACOORD", "BCOORD",
    "HCOORD", "DCOORD",
    "CHANGE"
)

#' Split stripped CBF lines into a named list of section bodies.
#'
#' Each element of the returned list is named by the CBF keyword.  Its value
#' is a character vector of body lines (the spec's HEADER line, if present,
#' is always the first element of that vector).
#'
#' Empty lines between sections and leading/trailing blanks have already
#' been removed by the caller; we drop any remaining blank lines here.
#'
#' @param lines  Character vector -- trimmed, comment-stripped CBF lines.
#' @return       Named list of character vectors.
#' @keywords internal
.cbf_parse_sections <- function(lines) {
    lines  <- lines[nzchar(lines)]
    n      <- length(lines)
    if (n == 0L) stop("CBF file contains no parseable content.")

    is_kw  <- lines %in% .CBF_ALL_KEYWORDS
    kw_pos <- which(is_kw)
    if (length(kw_pos) == 0L) stop("No recognised CBF keywords found.")

    result <- list()
    for (k in seq_along(kw_pos)) {
        start <- kw_pos[k] + 1L
        end   <- if (k < length(kw_pos)) kw_pos[k + 1L] - 1L else n
        kw    <- lines[kw_pos[k]]
        body  <- if (start <= end) lines[start:end] else character(0L)
        # Overwrite on repeated keyword (CHANGE sequences already stripped).
        result[[kw]] <- body
    }
    result
}


# ---------------------------------------------------------------------------
# Main OP builder
# ---------------------------------------------------------------------------

#' Convert parsed CBF sections into an ROI OP object.
#' @keywords internal
.cbf_build_OP <- function(secs) {

    # -- VER -----------------------------------------------------------------
    # Advisory check only; we proceed regardless of version.
    if (!is.null(secs[["VER"]])) {
        ver <- suppressWarnings(as.integer(secs[["VER"]][1L]))
        if (!is.na(ver) && ver > 4L)
            warning("CBF version ", ver, " is newer than supported (4). ",
                    "Some features may be ignored.")
    }

    # -- POWCONES / POW*CONES ------------------------------------------------
    # Build lookup tables for parametric power cones.
    # Cone "@k:POW" (0-based k) refers to powcones[[k+1]].
    powcones      <- .cbf_parse_powcones(secs[["POWCONES"]])
    powstar_cones <- .cbf_parse_powcones(secs[["POW*CONES"]])

    # -- OBJSENSE -------------------------------------------------------------
    maximum <- FALSE   # default: minimisation (CBF spec Sec.B: OBJSENSE required)
    if (!is.null(secs[["OBJSENSE"]])) {
        sense_str <- toupper(trimws(secs[["OBJSENSE"]][1L]))
        maximum   <- identical(sense_str, "MAX")
    }

    # -- PSDVAR ---------------------------------------------------------------
    # CBF spec Sec.3.2: PSDVAR header = total count N; body = N lines of dim m_j.
    # Each matrix variable X_j in S^{m_j x m_j}_+ is replaced by its
    # lower-triangular symmetric vectorisation svec(X_j) of length
    # d_j = m_j*(m_j+1)/2, appended as new scalar variables after the
    # n_sca_var explicit scalar variables.
    n_psdvar     <- 0L
    psd_var_dims <- integer(0L)
    if (!is.null(secs[["PSDVAR"]])) {
        bod          <- secs[["PSDVAR"]]
        n_psdvar     <- as.integer(bod[1L])
        psd_var_dims <- as.integer(bod[seq_len(n_psdvar) + 1L])
    }
    svec_dims      <- (psd_var_dims * (psd_var_dims + 1L)) %/% 2L
    n_psd_svec     <- sum(svec_dims)
    # psd_var_starts[j] is the 0-based offset of svec(X_{j-1}) within the
    # PSD-variable block (so 1-based col in full variable vector =
    # n_sca_var + psd_var_starts[j] + k + 1, with k a 0-based svec index).
    psd_var_starts <- c(0L, cumsum(svec_dims))

    # -- VAR ------------------------------------------------------------------
    # CBF spec Sec.3.2 / Sec.B:
    #   HEADER: "n_vars  n_cone_chunks"
    #   BODY:   one "CONE_NAME  count" line per chunk
    #
    # Variables are partitioned into groups, each restricted to a conic domain.
    # Linear domains translate directly to box bounds in ROI:
    #   F.   free domain        -> lb = -Inf, ub = +Inf
    #   L+.  positive orthant   -> lb = 0,    ub = +Inf
    #   L-.  negative orthant   -> lb = -Inf, ub = 0
    #   L=.  fixpoint zero      -> lb = 0,    ub = 0
    # Non-linear VAR cones are left free at the bound level here; their
    # domain restriction is added later as explicit ROI conic constraints.
    n_sca_var <- 0L
    var_chunks <- list()
    if (!is.null(secs[["VAR"]])) {
        bod         <- secs[["VAR"]]
        hdr         <- .cbf_split_ints(bod[1L])
        n_sca_var   <- hdr[1L]
        n_var_cones <- hdr[2L]
        var_chunks  <- lapply(
            bod[seq_len(n_var_cones) + 1L],
            function(ln) {
                p <- strsplit(trimws(ln), "\\s+")[[1L]]
                list(cone = p[1L], count = as.integer(p[2L]))
            }
        )
    }

    # Build per-variable lower/upper bounds from the VAR cone partition.
    lb_vec <- rep(-Inf, n_sca_var)
    ub_vec <- rep(+Inf, n_sca_var)
    idx <- 1L
    for (vc in var_chunks) {
        rng <- idx:(idx + vc$count - 1L)
        switch(vc$cone,
            "L+" = { lb_vec[rng] <-  0;   ub_vec[rng] <- +Inf },
            "L-" = { lb_vec[rng] <- -Inf; ub_vec[rng] <-  0   },
            "L=" = { lb_vec[rng] <-  0;   ub_vec[rng] <-  0   }
            # "F" and non-linear cones: leave at (-Inf, +Inf)
        )
        idx <- idx + vc$count
    }

    # Extend bounds to cover the PSD svec variables (all free).
    n_total_var <- n_sca_var + n_psd_svec
    lb_vec      <- c(lb_vec, rep(-Inf, n_psd_svec))
    ub_vec      <- c(ub_vec, rep(+Inf, n_psd_svec))

    # ROI V_bound: li/ui are the variable indices (1-based) with explicit
    # bounds; lb/ub are the corresponding bound values.
    bounds <- if (n_total_var > 0L) {
        ROI::V_bound(
            li   = seq_len(n_total_var),
            ui   = seq_len(n_total_var),
            lb   = lb_vec,
            ub   = ub_vec,
            nobj = n_total_var
        )
    } else {
        NULL
    }

    # -- INT ------------------------------------------------------------------
    # CBF spec Sec.B: INT header = count l; body = l lines of 0-based indices.
    # CBF uses 0-based indexing; R/ROI uses 1-based.
    # PSD svec variables are always continuous.
    types <- c(rep("C", n_sca_var), rep("C", n_psd_svec))
    if (!is.null(secs[["INT"]])) {
        bod     <- secs[["INT"]]
        n_int   <- as.integer(bod[1L])
        int_idx <- as.integer(bod[seq_len(n_int) + 1L]) + 1L   # 0 -> 1-based
        types[int_idx] <- "I"
    }

    # -- PSDCON ---------------------------------------------------------------
    # CBF spec Sec.B: PSDCON header = count; body = one dim m_p per row.
    # Each PSDCON p imposes G_p = sum_j x_j H_pj + D_p in PSD^{m_p x m_p},
    # translated via svec to a K_psd conic constraint on x.
    n_psdcon          <- 0L
    psd_con_dims      <- integer(0L)
    psdcon_svec_dims  <- integer(0L)
    psdcon_svec_offsets <- integer(0L)
    n_psdcon_svec     <- 0L
    if (!is.null(secs[["PSDCON"]])) {
        bod                <- secs[["PSDCON"]]
        n_psdcon           <- as.integer(bod[1L])
        psd_con_dims       <- as.integer(bod[seq_len(n_psdcon) + 1L])
        psdcon_svec_dims   <- (psd_con_dims * (psd_con_dims + 1L)) %/% 2L
        psdcon_svec_offsets <- c(0L, cumsum(psdcon_svec_dims))
        n_psdcon_svec      <- sum(psdcon_svec_dims)
    }

    # -- CON ------------------------------------------------------------------
    # CBF spec Sec.3.2 / Sec.B:
    #   HEADER: "n_constraints  n_cone_chunks"
    #   BODY:   one "CONE_NAME  count" line per chunk
    n_sca_con  <- 0L
    con_chunks <- list()
    if (!is.null(secs[["CON"]])) {
        bod         <- secs[["CON"]]
        hdr         <- .cbf_split_ints(bod[1L])
        n_sca_con   <- hdr[1L]
        n_con_cones <- hdr[2L]
        con_chunks  <- lapply(
            bod[seq_len(n_con_cones) + 1L],
            function(ln) {
                p <- strsplit(trimws(ln), "\\s+")[[1L]]
                list(cone = p[1L], count = as.integer(p[2L]))
            }
        )
    }

    # -- OBJACOORD ------------------------------------------------------------
    # CBF spec Sec.B: HEADER = count; BODY = count lines of "var_idx  coef".
    # var_idx is 0-based in CBF; shift to 1-based for R.
    # Initialise over the full variable vector (scalar + PSD svec).
    obj_coef <- numeric(n_total_var)
    if (!is.null(secs[["OBJACOORD"]])) {
        bod <- secs[["OBJACOORD"]]
        for (ln in bod[-1L]) {
            p   <- strsplit(trimws(ln), "\\s+")[[1L]]
            j   <- as.integer(p[1L]) + 1L   # 0 -> 1-based
            val <- as.numeric(p[2L])
            obj_coef[j] <- obj_coef[j] + val
        }
    }

    # -- OBJFCOORD ------------------------------------------------------------
    # CBF spec Sec.B: HEADER = count; BODY = "j row col coef" (0-based).
    # Contribution to objective: <F_j^obj, X_j> = svec(F_j^obj)' svec(X_j).
    # svec(F)[k] = F[row,col] on diagonal; sqrt(2)*F[row,col] off-diagonal.
    if (!is.null(secs[["OBJFCOORD"]])) {
        bod <- secs[["OBJFCOORD"]]
        for (ln in bod[-1L]) {
            p    <- strsplit(trimws(ln), "\\s+")[[1L]]
            j    <- as.integer(p[1L])   # 0-based PSD var index
            row  <- as.integer(p[2L])   # 0-based matrix row
            col  <- as.integer(p[3L])   # 0-based matrix col
            coef <- as.numeric(p[4L])
            rc   <- .cbf_lt(row, col)   # normalise to lower triangle
            m_j  <- psd_var_dims[j + 1L]
            k    <- .cbf_svec_idx(rc[1L], rc[2L], m_j)
            vidx <- n_sca_var + psd_var_starts[j + 1L] + k + 1L  # 1-based
            obj_coef[vidx] <- obj_coef[vidx] +
                coef * .cbf_svec_scale(rc[1L], rc[2L])
        }
    }

    # -- OBJBCOORD ------------------------------------------------------------
    # CBF spec Sec.B: No HEADER; BODY = one line containing a REAL value.
    obj_const <- 0
    if (!is.null(secs[["OBJBCOORD"]])) {
        obj_const <- as.numeric(trimws(secs[["OBJBCOORD"]][1L]))
    }

    # -- ACOORD ---------------------------------------------------------------
    # CBF spec Sec.B: HEADER = count; BODY = "con_idx  var_idx  coef" (0-based).
    # The constraint matrix has n_total_var columns so that FCOORD entries
    # (PSD svec variable columns) can be added to the same matrix.
    ai <- integer(0L); aj <- integer(0L); av <- numeric(0L)
    if (!is.null(secs[["ACOORD"]])) {
        bod <- secs[["ACOORD"]]
        for (ln in bod[-1L]) {
            p  <- strsplit(trimws(ln), "\\s+")[[1L]]
            ai <- c(ai, as.integer(p[1L]) + 1L)   # constraint row, 0->1
            aj <- c(aj, as.integer(p[2L]) + 1L)   # variable col,  0->1
            av <- c(av, as.numeric(p[3L]))
        }
    }

    # -- FCOORD ---------------------------------------------------------------
    # CBF spec Sec.B: HEADER = count; BODY = "i j row col coef" (0-based).
    # Contribution to scalar constraint i: <F_ij, X_j> = svec(F_ij)' svec(X_j).
    if (!is.null(secs[["FCOORD"]])) {
        bod <- secs[["FCOORD"]]
        for (ln in bod[-1L]) {
            p    <- strsplit(trimws(ln), "\\s+")[[1L]]
            i    <- as.integer(p[1L]) + 1L   # 1-based constraint row
            j    <- as.integer(p[2L])         # 0-based PSD var index
            row  <- as.integer(p[3L])
            col  <- as.integer(p[4L])
            coef <- as.numeric(p[5L])
            rc   <- .cbf_lt(row, col)
            m_j  <- psd_var_dims[j + 1L]
            k    <- .cbf_svec_idx(rc[1L], rc[2L], m_j)
            vidx <- n_sca_var + psd_var_starts[j + 1L] + k + 1L  # 1-based
            ai   <- c(ai, i)
            aj   <- c(aj, vidx)
            av   <- c(av, coef * .cbf_svec_scale(rc[1L], rc[2L]))
        }
    }

    # Assemble the sparse constraint matrix (n_sca_con x n_total_var).
    A_mat <- if (length(ai) > 0L) {
        slam::simple_triplet_matrix(
            i    = ai,
            j    = aj,
            v    = av,
            nrow = n_sca_con,
            ncol = n_total_var
        )
    } else {
        slam::simple_triplet_zero_matrix(nrow = n_sca_con, ncol = n_total_var)
    }

    # -- BCOORD ---------------------------------------------------------------
    # CBF spec Sec.B: HEADER = count; BODY = "con_idx  b_i" (con_idx 0-based).
    #
    # SIGN CONVENTION:
    #   CBF encodes   A_i x + b_i  in  K_i
    #   For linear constraints we store rhs_lin = -b_i so
    #       A x dir rhs_lin
    #   reproduces the CBF affine map. For conic constraints,
    #   .cbf_build_constraints() converts to ROI's
    #       rhs_cone - L_cone x in K
    #   with L_cone = -A and rhs_cone = b = -rhs_lin.
    #
    # We store rhs_vec = -b here and let the constraint builder choose the
    # appropriate interpretation for linear vs conic chunks.
    rhs_vec <- numeric(n_sca_con)
    if (!is.null(secs[["BCOORD"]])) {
        bod <- secs[["BCOORD"]]
        for (ln in bod[-1L]) {
            p           <- strsplit(trimws(ln), "\\s+")[[1L]]
            ci          <- as.integer(p[1L]) + 1L   # 0 -> 1-based
            rhs_vec[ci] <- rhs_vec[ci] - as.numeric(p[2L])  # rhs = -b
        }
    }

    # -- HCOORD / DCOORD ------------------------------------------------------
    # CBF spec Sec.B:
    #   HCOORD: "p j row col coef"  (0-based p, j, row, col)
    #     G_p += x_j * H_pj,  H_pj[row,col] = coef
    #   DCOORD: "p row col coef"  (0-based p, row, col)
    #     G_p += D_p,  D_p[row,col] = coef
    #
    # PSDCON constraint in ROI C_constraint form:
    #   rhs_p - L_p x in K_psd(m_p)
    # where rhs_p = svec(D_p),  L_p = -svec(H_p·).
    #
    # HCOORD / DCOORD only reference scalar variables (x_j, 0-based),
    # so the constraint matrix has ncol = n_total_var with nonzeros only
    # in columns 1..n_sca_var.
    hi <- integer(0L); hj <- integer(0L); hv <- numeric(0L)
    rhs_psdcon <- numeric(n_psdcon_svec)
    if (!is.null(secs[["HCOORD"]])) {
        bod <- secs[["HCOORD"]]
        for (ln in bod[-1L]) {
            p    <- strsplit(trimws(ln), "\\s+")[[1L]]
            pp   <- as.integer(p[1L])   # 0-based PSDCON index
            j    <- as.integer(p[2L])   # 0-based scalar variable
            row  <- as.integer(p[3L])
            col  <- as.integer(p[4L])
            coef <- as.numeric(p[5L])
            rc   <- .cbf_lt(row, col)
            m_p  <- psd_con_dims[pp + 1L]
            k    <- .cbf_svec_idx(rc[1L], rc[2L], m_p)
            svec_row <- psdcon_svec_offsets[pp + 1L] + k + 1L  # 1-based
            hi   <- c(hi, svec_row)
            hj   <- c(hj, j + 1L)       # 1-based scalar variable column
            hv   <- c(hv, coef * .cbf_svec_scale(rc[1L], rc[2L]))
        }
    }
    if (!is.null(secs[["DCOORD"]])) {
        bod <- secs[["DCOORD"]]
        for (ln in bod[-1L]) {
            p    <- strsplit(trimws(ln), "\\s+")[[1L]]
            pp   <- as.integer(p[1L])
            row  <- as.integer(p[2L])
            col  <- as.integer(p[3L])
            coef <- as.numeric(p[4L])
            rc   <- .cbf_lt(row, col)
            m_p  <- psd_con_dims[pp + 1L]
            k    <- .cbf_svec_idx(rc[1L], rc[2L], m_p)
            svec_row <- psdcon_svec_offsets[pp + 1L] + k + 1L
            rhs_psdcon[svec_row] <- rhs_psdcon[svec_row] +
                coef * .cbf_svec_scale(rc[1L], rc[2L])
        }
    }

    # Build per-PSDCON K_psd conic constraints.
    psdcon_con_list <- vector("list", n_psdcon)
    if (n_psdcon > 0L) {
        H_mat <- if (length(hi) > 0L) {
            bad_i <- hi < 1L | hi > n_psdcon_svec
            bad_j <- hj < 1L | hj > n_total_var
            if (any(bad_i))
                stop("HCOORD: svec row index out of range [1,", n_psdcon_svec,
                     "]: ", paste(hi[bad_i], collapse = ", "))
            if (any(bad_j))
                stop("HCOORD: variable column index out of range [1,",
                     n_total_var, "]: ", paste(hj[bad_j], collapse = ", "))
            slam::simple_triplet_matrix(
                i    = hi,
                j    = hj,
                v    = hv,
                nrow = n_psdcon_svec,
                ncol = n_total_var
            )
        } else {
            slam::simple_triplet_zero_matrix(n_psdcon_svec, n_total_var)
        }
        for (pp in seq_len(n_psdcon)) {
            d_p       <- psdcon_svec_dims[pp]
            m_p       <- psd_con_dims[pp]
            svec_rows <- (psdcon_svec_offsets[pp] + 1L) :
                         (psdcon_svec_offsets[pp] + d_p)
            H_sub     <- H_mat[svec_rows, , drop = FALSE]
            rhs_sub   <- rhs_psdcon[svec_rows]
            psdcon_con_list[[pp]] <- ROI::C_constraint(
                L     = -H_sub,
                cones = ROI::K_psd(d_p),
                rhs   = rhs_sub
            )
        }
    }

    # Build K_psd domain constraints for each PSDVAR:
    #   svec(X_j) in K_psd(m_j)
    #   i.e.  0 - (-selector_j) x in K_psd  =>  selector_j x in K_psd.
    psdvar_con_list <- vector("list", n_psdvar)
    for (j in seq_len(n_psdvar)) {
        d_j   <- svec_dims[j]
        m_j   <- psd_var_dims[j]
        start <- n_sca_var + psd_var_starts[j] + 1L   # 1-based
        cols  <- start:(start + d_j - 1L)
        sel   <- slam::simple_triplet_matrix(
            i    = seq_len(d_j),
            j    = cols,
            v    = rep(1.0, d_j),
            nrow = d_j,
            ncol = n_total_var
        )
        psdvar_con_list[[j]] <- ROI::C_constraint(
            L     = -sel,
            cones = ROI::K_psd(d_j),
            rhs   = numeric(d_j)
        )
    }

    # -- Build the ROI objective -----------------------------------------------
    # L_objective(L): minimise/maximise  L^T x  [+ constant].
    # ROI does not accept a 'constant' argument, so we attach it as $constant.
    objective <- ROI::L_objective(L = obj_coef)
    if (obj_const != 0)
        objective$constant <- obj_const

    # -- Build the ROI constraints ---------------------------------------------
    # Walk each CON chunk; produce L_constraint (linear cones) or
    # C_constraint (non-linear cones).
    con_list <- .cbf_build_constraints(
        con_chunks, A_mat, rhs_vec, powcones, powstar_cones
    )
    var_con_list <- .cbf_build_var_constraints(
        var_chunks, n_total_var, powcones, powstar_cones
    )
    con_list <- c(con_list, var_con_list, psdvar_con_list, psdcon_con_list)

    con_obj <- if (length(con_list) == 0L) {
        ROI::NO_constraint(n_total_var)
    } else if (length(con_list) == 1L) {
        con_list[[1L]]
    } else {
        Reduce(c, con_list)
    }

    # -- Assemble and return the OP --------------------------------------------
    ROI::OP(
        objective   = objective,
        constraints = con_obj,
        types       = if (any(types == "I")) types else NULL,
        bounds      = bounds,
        maximum     = maximum
    )
}


#' Build ROI conic constraints induced by nonlinear VAR cone chunks.
#' @keywords internal
.cbf_build_var_constraints <- function(var_chunks, n_total_var,
                                       powcones, powstar_cones) {
    result <- list()
    idx <- 1L

    for (vc in var_chunks) {
        cnt <- vc$count
        cone_nm <- vc$cone
        rng <- idx:(idx + cnt - 1L)
        idx <- idx + cnt

        if (cone_nm %in% c("F", "L+", "L-", "L="))
            next

        selector <- slam::simple_triplet_matrix(
            i = seq_len(cnt),
            j = rng,
            v = rep(1, cnt),
            nrow = cnt,
            ncol = n_total_var
        )
        rhs_sub <- numeric(cnt)

        if (cone_nm == "QR") {
            bridged <- .cbf_bridge_rotated_soc(selector, rhs_sub)
            selector <- bridged$L
            rhs_sub <- bridged$rhs
            cone_nm <- "Q"
            cnt <- length(rhs_sub)
        } else if (cone_nm %in% c("EXP", "EXP*")) {
            bridged <- .cbf_bridge_exp_cone(selector, rhs_sub)
            selector <- bridged$L
            rhs_sub <- bridged$rhs
        }

        roi_cone <- .cbf_cone_to_roi(cone_nm, cnt, powcones, powstar_cones)
        if (is.null(roi_cone)) {
            warning("Unsupported CBF VAR cone '", cone_nm,
                    "' -- skipping ", cnt, " variable-domain row(s).")
            next
        }

        result[[length(result) + 1L]] <- ROI::C_constraint(
            L = -selector,
            cones = roi_cone,
            rhs = -rhs_sub
        )
    }

    result
}


# ---------------------------------------------------------------------------
# Constraint builder
# ---------------------------------------------------------------------------

#' Build ROI constraint objects from the parsed CON chunks.
#'
#' Iterates over the cone chunks defined in the CON section.  For each chunk
#' it extracts the corresponding rows from the full sparse constraint matrix
#' and RHS vector, then constructs the appropriate ROI constraint object.
#'
#' @section Cone chunk -> ROI constraint mapping:
#' \describe{
#'   \item{\code{L+} (positive orthant)}{
#'     \eqn{Ax + b \geq 0} \eqn{\Leftrightarrow} \eqn{Ax \geq -b = \text{rhs}}.
#'     Becomes \code{L_constraint(A, ">=", rhs)}.}
#'   \item{\code{L-} (negative orthant)}{
#'     \eqn{Ax + b \leq 0} \eqn{\Leftrightarrow} \eqn{Ax \leq \text{rhs}}.
#'     Becomes \code{L_constraint(A, "<=", rhs)}.}
#'   \item{\code{L=} (fixpoint zero)}{
#'     \eqn{Ax + b = 0}. Becomes \code{L_constraint(A, "==", rhs)}.}
#'   \item{\code{F} (free)}{No constraint; chunk is silently skipped.}
#'   \item{\code{Q}, \code{EXP}, \code{EXP*}, \code{SVECPSD},
#'     geometric/power cones}{Become \code{C_constraint(A, K, rhs)}.}
#'   \item{\code{QR}}{Is bridged to a standard SOC by the linear map
#'     \eqn{(u, v, w) \mapsto (u+v, u-v, \sqrt{2} w)}, then becomes
#'     \code{C_constraint(T A, K_soc, T rhs)}.}
#' }
#'
#' @param con_chunks      List of \code{list(cone, count)} from the CON section.
#' @param A_mat           Full sparse constraint matrix (slam STM).
#' @param rhs_vec         Full RHS vector (already negated: \eqn{-b_i}).
#' @param powcones        List of alpha vectors from the POWCONES section.
#' @param powstar_cones   List of alpha vectors from the POW*CONES section.
#' @return List of ROI constraint objects.
#' @keywords internal
.cbf_build_constraints <- function(con_chunks, A_mat, rhs_vec,
                                   powcones, powstar_cones) {
    LINEAR_CONES <- c("F", "L+", "L-", "L=")
    result    <- list()
    row_start <- 1L

    for (cc in con_chunks) {
        cnt      <- cc$count
        cone_nm  <- cc$cone
        rows     <- row_start:(row_start + cnt - 1L)
        row_start <- row_start + cnt

        # Extract the rows of A and rhs for this cone chunk.
        A_sub   <- A_mat[rows, , drop = FALSE]
        rhs_sub <- rhs_vec[rows]

        if (cone_nm == "QR") {
            bridged <- .cbf_bridge_rotated_soc(A_sub, rhs_sub)
            A_sub   <- bridged$L
            rhs_sub <- bridged$rhs
            cone_nm <- "Q"
            cnt     <- length(rhs_sub)
        } else if (cone_nm %in% c("EXP", "EXP*")) {
            bridged <- .cbf_bridge_exp_cone(A_sub, rhs_sub)
            A_sub   <- bridged$L
            rhs_sub <- bridged$rhs
        }

        if (cone_nm %in% LINEAR_CONES) {
            # -- Linear (polyhedral) cones ---------------------------------
            if (cone_nm == "F") next      # free cone: no restriction, skip

            dir_sym <- switch(cone_nm,
                "L+" = ">=",
                "L-" = "<=",
                "L=" = "=="
            )
            result <- c(result,
                list(ROI::L_constraint(
                    L   = A_sub,
                    dir = rep(dir_sym, cnt),
                    rhs = rhs_sub
                ))
            )

        } else {
            # -- Conic constraints -----------------------------------------
            roi_cone <- .cbf_cone_to_roi(cone_nm, cnt, powcones, powstar_cones)
            if (!is.null(roi_cone)) {
                result <- c(result,
                    list(ROI::C_constraint(
                        L     = -A_sub,
                        cones = roi_cone,
                        rhs   = -rhs_sub
                    ))
                )
            } else {
                warning("Unsupported CBF cone '", cone_nm,
                        "' -- skipping ", cnt, " constraint(s).")
            }
        }
    }
    result
}


#' Reorder a CBF exponential-cone chunk to ROI's coordinate convention.
#' @keywords internal
.cbf_bridge_exp_cone <- function(A_sub, rhs_sub) {
    d <- length(rhs_sub)
    if (d != 3L)
        stop("CBF exponential cones must have dimension 3.")

    Pmat <- diag(3L)[3:1, , drop = FALSE]
    A_new <- slam::as.simple_triplet_matrix(Pmat %*% as.matrix(A_sub))
    rhs_new <- as.vector(Pmat %*% rhs_sub)

    list(L = A_new, rhs = rhs_new)
}

#' Bridge a rotated quadratic cone chunk to a standard SOC chunk.
#' @keywords internal
.cbf_bridge_rotated_soc <- function(A_sub, rhs_sub) {
    d <- length(rhs_sub)
    if (d < 2L)
        stop("CBF rotated quadratic cone (QR) must have dimension at least 2.")

    Tmat <- diag(d)
    Tmat[1L, 1L] <- 1
    Tmat[1L, 2L] <- 1
    Tmat[2L, 1L] <- 1
    Tmat[2L, 2L] <- -1
    if (d > 2L)
        diag(Tmat)[3:d] <- sqrt(2)

    A_new <- slam::as.simple_triplet_matrix(Tmat %*% as.matrix(A_sub))
    rhs_new <- as.vector(Tmat %*% rhs_sub)

    list(L = A_new, rhs = rhs_new)
}


# ---------------------------------------------------------------------------
# CBF cone name -> ROI cone object
# ---------------------------------------------------------------------------

#' Map a CBF cone name to the corresponding ROI cone object.
#'
#' Non-parametric cones are recognised by their literal CBF names (Appendix A
#' of the spec).  Parametric cones carry an \code{@k:POW} or \code{@k:POW*}
#' prefix that encodes their 0-based index into the respective lookup table.
#'
#' @section ROI cone constructors used:
#' \describe{
#'   \item{\code{ROI::K_soc(n)}}{Second-order (Lorentz) cone, CBF \code{Q}.}
#'   \item{\code{CBF QR}}{Bridged to \code{ROI::K_soc(n)} via
#'     \eqn{(u, v, w) \mapsto (u+v, u-v, \sqrt{2} w)} before cone creation.}
#'   \item{\code{ROI::K_expp(n)}}{Primal exponential cone, CBF \code{EXP} (\eqn{n=3}).}
#'   \item{\code{ROI::K_expd(n)}}{Dual exp cone, CBF \code{EXP*}.}
#'   \item{\code{ROI::K_psd(d)}}{PSD cone; \code{d} = svec dimension
#'     \eqn{d = n(n+1)/2} where \eqn{n} is the matrix side-length.}
#'   \item{\code{ROI::K_powp(alpha)}}{Power cone; \code{alpha} from
#'     POWCONES or equal-weight vector for \code{GMEANABS}/\code{GMEAN}.}
#'   \item{\code{ROI::K_powd(alpha)}}{Dual power cone.}
#' }
#'
#' @param cone_nm         CBF cone token.
#' @param count           Cone dimension (= number of constraint rows).
#' @param powcones        Primal power-cone lookup table (list of alpha vectors).
#' @param powstar_cones   Dual power-cone lookup table.
#' @return ROI cone object, or \code{NULL} if unsupported.
#' @keywords internal
.cbf_cone_to_roi <- function(cone_nm, count, powcones, powstar_cones) {

    # -- Second-order cone family (CBF spec Sec.A.1) -------------------------
    # Q  : {(t,x) | t >= ||x||_2}   dim >= 1
    # QR : bridged to Q earlier in .cbf_build_constraints().
    if (cone_nm == "Q")  return(ROI::K_soc(count))

    # -- Exponential cone family (CBF spec Sec.A.1) --------------------------
    # EXP  : cl({(t,s,r) | s>0, t >= s*exp(r/s)})   dim = 3
    # EXP* : dual exponential cone                    dim = 3
    # ROI K_expp(n) creates n exponential cones of dim 3 each.
    if (cone_nm == "EXP")  return(ROI::K_expp(max(1L, count %/% 3L)))
    if (cone_nm == "EXP*") return(ROI::K_expd(max(1L, count %/% 3L)))

    # -- Semidefinite cone in symmetric-vector form (CBF spec Sec.A.1) -------
    # SVECPSD represents svec(PSD^{nxn}) subset of R^{n(n+1)/2}.
    # svec(X) = [X_11, sqrt(2)*X_21, X_22, sqrt(2)*X_31, ..., X_nn]
    # Inner-product invariant: <X,Y> = svec(X)' svec(Y).
    #
    # ROI's K_psd(d) takes the svec dimension d = n*(n+1)/2, not the
    # matrix side-length n.  We validate that count is a triangular number.
    if (cone_nm == "SVECPSD") {
        .cbf_svec_to_n(count)   # validates triangular number; result unused
        return(ROI::K_psd(count))
    }

    # -- Non-parametric power cone family (CBF spec Sec.A.1) ----------------
    # GMEANABS / GMEAN : geometric mean cones, dimension n = k+1.
    # Equivalent to a power cone with equal weights alpha = rep(1, k).
    if (cone_nm == "GMEANABS" || cone_nm == "GMEAN") {
        warning("GMEAN/GMEANABS cones are not supported in this ROI version; skipping.")
        return(NULL)
    }
    if (cone_nm == "GMEANABS*" || cone_nm == "GMEAN*") {
        warning("GMEAN*/GMEANABS* cones are not supported in this ROI version; skipping.")
        return(NULL)
    }

    # -- Parametric power cones (CBF spec Sec.3.1, Sec.A.2) -----------------
    # Names are "@k:POW" or "@k:POW*" where k is the 0-based index into
    # the POWCONES or POW*CONES lookup table.
    if (startsWith(cone_nm, "@") && grepl(":POW", cone_nm, fixed = TRUE)) {
        return(.cbf_parametric_cone(cone_nm, count, powcones, powstar_cones))
    }

    warning("Unsupported CBF cone '", cone_nm, "' (dim=", count, "); skipping.")
    NULL
}

#' Resolve a parametric power-cone CBF name to an ROI cone object.
#' @keywords internal
.cbf_parametric_cone <- function(cone_nm, count, powcones, powstar_cones) {
    # cone_nm is "@k:POW" or "@k:POW*"
    is_dual <- grepl(":POW*", cone_nm, fixed = TRUE)
    k       <- as.integer(sub("@(\\d+):POW.*", "\\1", cone_nm))
    table   <- if (is_dual) powstar_cones else powcones
    k1      <- k + 1L   # 1-based index into the R list

    if (k1 > length(table)) {
        warning("POWCONES table has ", length(table),
                " entries, but @", k, " was requested; skipping ", count,
                " constraint(s).")
        return(NULL)
    }
    alpha <- table[[k1]]
    # ROI K_powp/K_powd take a scalar alpha and produce a 3D power cone.
    # CBF power cones with alpha length > 1 are not directly representable.
    if (length(alpha) != 1L) {
        warning("Power cone with alpha length ", length(alpha),
                " is not supported (only scalar alpha / 3D cones); skipping.")
        return(NULL)
    }
    if (is_dual) ROI::K_powd(alpha)
    else         ROI::K_powp(alpha)
}


# ---------------------------------------------------------------------------
# POWCONES section parser
# ---------------------------------------------------------------------------

#' Parse a POWCONES (or POW*CONES) section into a list of alpha vectors.
#'
#' CBF spec Sec.B, POWCONES:
#' \preformatted{
#' POWCONES
#' N  total_params          <- header: N cones, total combined param count
#' k_0                      <- chunk header: length of alpha for cone 0
#' alpha_0[0]               <- chunk body
#' ...
#' alpha_0[k_0 - 1]
#' k_1                      <- chunk header for cone 1
#' ...
#' }
#'
#' @param body  Character vector (body lines including the header line), or
#'   \code{NULL} if the section is absent.
#' @return A list of numeric vectors (one per parametric cone).
#' @keywords internal
.cbf_parse_powcones <- function(body) {
    if (is.null(body) || length(body) == 0L) return(list())

    hdr     <- .cbf_split_ints(body[1L])
    n_cones <- hdr[1L]
    # hdr[2] = total combined parameter count; derived from chunks below.

    result <- vector("list", n_cones)
    pos    <- 2L   # current line position (1 = header already consumed)

    for (k in seq_len(n_cones)) {
        k_len       <- as.integer(body[pos])
        pos         <- pos + 1L
        alpha_vals  <- as.numeric(body[pos:(pos + k_len - 1L)])
        pos         <- pos + k_len
        result[[k]] <- alpha_vals
    }
    result
}


# ---------------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------------

#' Recover PSD matrix side-length from svec dimension.
#'
#' \code{svec(PSD^{nxn})} has dimension \eqn{d = n(n+1)/2}.
#' Solving for \eqn{n} gives \eqn{n = (-1 + \sqrt{1 + 8d}) / 2}.
#'
#' @param d  Integer. Svec (half-vectorisation) dimension.
#' @return   Integer matrix side-length \eqn{n}.
#' @keywords internal
.cbf_svec_to_n <- function(d) {
    n <- as.integer(round((-1 + sqrt(1 + 8 * d)) / 2))
    if (n * (n + 1L) / 2L != d)
        stop("SVECPSD dimension ", d,
             " is not a triangular number n*(n+1)/2.")
    n
}

#' Normalise (row, col) to lower-triangular form by swapping if row < col.
#'
#' CBF stores only one triangular half of each symmetric matrix.  Most files
#' use the lower triangle (row >= col), but some generators emit upper-
#' triangular entries.  Since X[row,col] = X[col,row] for symmetric X, we
#' always swap to the lower triangle before computing the svec index.
#'
#' @param row  0-based row index.
#' @param col  0-based column index.
#' @return     Integer vector \code{c(row, col)} with \code{row >= col}.
#' @keywords internal
.cbf_lt <- function(row, col) if (row >= col) c(row, col) else c(col, row)

#' 0-based svec index for lower-triangular (row, col) in an m x m matrix.
#'
#' CBF uses column-major lower-triangular ordering:
#'   column 0: rows 0..(m-1),  column 1: rows 1..(m-1), ...
#' The cumulative start of column j is  j*m - j*(j-1)/2,
#' so  k(row, col) = col*m - col*(col-1)/2 + (row - col).
#'
#' @param row  0-based row index (>= col).
#' @param col  0-based column index.
#' @param m    Matrix side-length.
#' @return     0-based svec index.
#' @keywords internal
.cbf_svec_idx <- function(row, col, m) {
    col * m - (col * (col - 1L)) %/% 2L + (row - col)
}

#' svec scaling factor: 1 on the diagonal, sqrt(2) off-diagonal.
#'
#' svec(X)[k] = X[i,i] for diagonal entries and sqrt(2)*X[i,j] for
#' off-diagonal entries, ensuring <X,Y> = svec(X)' svec(Y).
#'
#' @param row  0-based row index.
#' @param col  0-based column index.
#' @return     Numeric scalar: 1 or sqrt(2).
#' @keywords internal
.cbf_svec_scale <- function(row, col) if (row == col) 1.0 else sqrt(2)

#' Split a whitespace-delimited string into an integer vector.
#' @keywords internal
.cbf_split_ints <- function(x) {
    as.integer(strsplit(trimws(x), "\\s+")[[1L]])
}

