#' GMM estimator combining moment-corrected and phase-function equations
#'
#' Computes Generalized Method of Moments (GMM) estimators by combining
#' moment-corrected estimating equations and phase-function estimating
#' equations. The GMM weighting matrix is estimated via a cluster bootstrap
#' that accounts for uncertainty in estimating the measurement error
#' covariance.
#'
#' @param W Numeric array of dimension \code{n x p x J} containing replicate
#'   measurements of contaminated covariates.
#' @param y Numeric vector of length \code{n} containing the response.
#' @param Zmat Numeric matrix of dimension \code{n x q} containing error-free
#'   covariates.
#' @param B Integer specifying the number of bootstrap replicates.
#' @param t_grid_length Integer specifying the length of optimal t for
#' computational intergration.
#'
#'
#' @return A list with components:
#' \describe{
#'   \item{est}{Numeric matrix of GMM estimates; one column per weight choice.}
#'   \item{CV_boot}{Array of bootstrap covariance matrices used in GMM.}
#' }
#'
#'@keywords internal
computing_GMM_estimator <- function(
    W, y, Zmat,
    weight_method = c("uniform", "minimax", "quasi-likelihood"),
    B = 100,
    t_grid_length = 1000
) {

  ## Detect replicate observations

  has_rep <- apply(
    W[, 1, , drop = FALSE],
    1,
    function(x) sum(!is.na(x)) > 1
  )

  ind_rep    <- which(has_rep)
  ind_nonrep <- which(!has_rep)


  # number of obs having replicates
  n_rep <- sum(has_rep)

  if (n_rep == 0) {

    warning("No replicate information detected. Returning uniform weights.")

  }

  # weight for estimation
  weight_method <- match.arg(weight_method)


  Wbar <- apply(W, c(1, 2), mean, na.rm = TRUE)

  n <- length(y)
  p <- ncol(Wbar)
  J <- dim(W)[3]
  gamma <- log(n)

  ## Original weight
  # w_origin = estimating_weight(
  #   W,
  #   method = weight_method,
  #   gamma = gamma
  # )[, 1]

  w_origin = suppressWarnings(
    estimating_weight(
      W,
      method = weight_method,
      gamma = gamma
    )
  )[, 1]

  t_star <- find_t_star(y, n)
  t <- seq(0.001 * t_star, t_star, length.out = t_grid_length)


  ## Original Sy, Cy
  tvec_origin <- outer(y, t)
  S_y <- colSums(sin(tvec_origin))
  C_y <- colSums(cos(tvec_origin))


  ## Estimate Sigma_U
  sigma_U_bar <- estimating_SigmaU(W)[[1]]


  ## Choose initial beta

  b_in <- choose_beta_init(
      y             = y,
      Wbar          = Wbar,
      Zmat          = Zmat,
      S_y           = S_y,
      C_y           = C_y,
      t             = t,
      estWeight     = w_origin,
      estSigmaU_bar = sigma_U_bar
    )

  q <- length(b_in)

  ## Storage for bootstrap estimating equations

  g0 <- estim_eq_correctedAndPhase(
    Wbar, Zmat, y,
    S_y, C_y,
    b_in,
    w_origin,
    sigma_U_bar,
    t
  )

  E <- matrix(NA_real_, nrow = B, ncol = length(g0))

  # E <- matrix(NA_real_, nrow = B, ncol = 2*q)

  ## Bootstrap stage: estimate Omega

  for (b in seq_len(B)) {

    ## Cluster bootstrap
    ind_b <- c(
      sample(ind_rep,    length(ind_rep),    replace = TRUE),
      sample(ind_nonrep, length(ind_nonrep), replace = TRUE)
    )

    ## Bootstrap data
    Wb     <- W[ind_b, , , drop = FALSE]
    Wbar_b <- apply(Wb, c(1, 2), mean, na.rm = TRUE)
    yb     <- y[ind_b]
    Zb     <- Zmat[ind_b, , drop = FALSE]

    ## Re-estimate Sigma_U
    sigma_U_bar_b <- estimating_SigmaU(Wb)[[1]]

    ## Observation weights (computed internally)
    # w_b <- estimating_weight(
    #   Wb,
    #   method = weight_method,
    #   gamma = gamma
    # )[, 1]

    w_b <- suppressWarnings(
      estimating_weight(
        Wb,
        method = weight_method,
        gamma = gamma
      )
    )[, 1]

    ## Phase-function quantities

    t_star_b <- find_t_star(yb, n)
    t_b <- seq(0.001 * t_star_b, t_star_b, length.out = 1000)

    tvec <- outer(yb, t_b)

    S_y_b <- colSums(sin(tvec))
    C_y_b <- colSums(cos(tvec))

    ## Evaluate estimating equations

    E[b, ] <- estim_eq_correctedAndPhase(
      Wbar_b, Zb, yb,
      S_y_b, C_y_b,
      b_in,
      w_b,
      sigma_U_bar_b,
      t_b
    )
  }

  ## GMM stage

  Omega_hat <- cov(E)

  ## CASE 1: < 2 replicates -> QUADRATIC estimator
  if (n_rep < 2) {

    warning(
      "Fewer than two observations with replicates detected. ",
      "Using quadratic (identity-weight) estimator."
    )

    w_uniform <- rep(1 / n, n)

    est = b_in

    ## Jacobian
    P_moment <- compute_P1_moment(
      Wbar,
      Zmat,
      apply(sigma_U_bar, c(2, 3), mean, na.rm = TRUE)
    )

    P_phase <- compute_P1_phase(
      b = est,
      S_y = S_y,
      C_y = C_y,
      Wbar = Wbar,
      Zmat = Zmat,
      t = t,
      estWeight = w_uniform
    )

    P <- rbind(P_moment, P_phase)

    ## Sandwich variance (identity-weight GMM)
    PtP_inv <- solve(t(P) %*% P)
    var_est <- PtP_inv %*% t(P) %*% Omega_hat %*% P %*% PtP_inv

    return(list(
      est     = est,
      var_est = var_est,
      method  = "quadratic (identity-weight)"
    ))
  }


  ## CASE 2: ≥ 2 replicates -> TWO-STEP GMM

  obj <- function(b) {
    g <- estim_eq_correctedAndPhase(
      Wbar, Zmat, y,
      S_y, C_y,
      b,
      w_origin,
      sigma_U_bar,
      t
    )
    as.numeric(t(g) %*% solve(Omega_hat) %*% g)
  }

  est <- optim(b_in, obj, method = "Nelder-Mead", control = list(maxit = 10000))$par

  P_moment <- compute_P1_moment(
    Wbar,
    Zmat,
    apply(sigma_U_bar, c(2,3), mean, na.rm = TRUE)
  )

  P_phase <- compute_P1_phase(
    b = est,
    S_y = S_y,
    C_y = C_y,
    Wbar = Wbar,
    Zmat = Zmat,
    t = t,
    estWeight = w_origin
  )

  P <- rbind(
    P_moment,
    P_phase
  )

  Omega_inv <- solve(Omega_hat)

  var_est <- solve(
    t(P) %*% Omega_inv %*% P
  )


  return(list(
    est     = est,
    var_est = var_est,
    method  = "Generalised Method of Moments"
  ))
}
