/*
  Copyright 2025 Equinor ASA

  This file is part of the Open Porous Media project (OPM).

  OPM is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  OPM is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with OPM.  If not, see <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include <opm/input/eclipse/Schedule/Group/GuideRateModel.hpp>
#include <opm/simulators/wells/GroupConstraintCalculator.hpp>
#include <opm/material/fluidsystems/BlackOilDefaultFluidSystemIndices.hpp>

#include <tuple>  // for std::tie
#include <fmt/format.h>

namespace Opm {

// ----------------------------------------------------
// Constructor for the GroupConstraintCalculator class
// ----------------------------------------------------
template<class Scalar, class IndexTraits>
GroupConstraintCalculator<Scalar, IndexTraits>::
GroupConstraintCalculator(
    const BlackoilWellModelGeneric<Scalar, IndexTraits>& well_model,
    const GroupStateHelperType& group_state_helper
) :
    well_model_{well_model},
    group_state_helper_{group_state_helper},
    well_state_{group_state_helper.wellState()},
    group_state_{group_state_helper.groupState()},
    schedule_{group_state_helper.schedule()},
    summary_state_{group_state_helper.summaryState()},
    phase_usage_{group_state_helper.phaseUsage()},
    guide_rate_{group_state_helper.guideRate()},
    report_step_idx_{group_state_helper.reportStepIdx()},
    resv_coeffs_inj_(group_state_helper.phaseUsage().numPhases, 0.0)
{
    std::tie(this->fipnum_, this->pvtreg_) = this->well_model_.getGroupFipnumAndPvtreg();
    // Get the reservoir coefficients for injection
    // NOTE: The production reservoir coefficients depend on the group production rates, so we
    //  cannot precompute them here.
    this->well_model_.calcInjResvCoeff(this->fipnum_, this->pvtreg_, this->resv_coeffs_inj_);
}

template<class Scalar, class IndexTraits>
std::optional<typename GroupConstraintCalculator<Scalar, IndexTraits>::InjectionConstraintInfo>
GroupConstraintCalculator<Scalar, IndexTraits>::
groupInjectionTarget(const Group& group, ReservoirCoupling::Phase injection_phase)
{
    GeneralCalculator general_calculator{*this, group, injection_phase};
    auto target_info = general_calculator.calculateGroupConstraint();
    if (!target_info) {
        // No constraints were calculated, return an empty optional.
        return std::nullopt;
    }
    return std::make_optional<InjectionConstraintInfo>(
        InjectionConstraintInfo{
            target_info->constraint,
            std::get<Group::InjectionCMode>(target_info->cmode)
        }
    );
}

template<class Scalar, class IndexTraits>
std::optional<typename GroupConstraintCalculator<Scalar, IndexTraits>::ProductionConstraintInfo>
GroupConstraintCalculator<Scalar, IndexTraits>::
groupProductionTarget(const Group& group) {
    GeneralCalculator general_calculator{*this, group};
    auto target_info = general_calculator.calculateGroupConstraint();
    if (!target_info) {
        // No constraints were calculated, return an empty optional.
        return std::nullopt;
    }
    return std::make_optional<ProductionConstraintInfo>(
        ProductionConstraintInfo{
            target_info->constraint,
            std::get<Group::ProductionCMode>(target_info->cmode)
        }
    );
}

template<class Scalar, class IndexTraits>
std::optional<typename GroupConstraintCalculator<Scalar, IndexTraits>::ProductionConstraintResult>
GroupConstraintCalculator<Scalar, IndexTraits>::
groupProductionConstraints(const Group& group)
{
    // First, compute the active target using the existing method.
    auto active_target = this->groupProductionTarget(group);
    if (!active_target) {
        return std::nullopt;
    }

    ProductionConstraintResult constraints;
    constraints.active_target = active_target->constraint;
    constraints.active_cmode = active_target->cmode;

    // Store the active target in the corresponding limit field as well.
    switch (constraints.active_cmode) {
    case Group::ProductionCMode::ORAT:
        constraints.oil_limit = constraints.active_target; break;
    case Group::ProductionCMode::WRAT:
        constraints.water_limit = constraints.active_target; break;
    case Group::ProductionCMode::GRAT:
        constraints.gas_limit = constraints.active_target; break;
    case Group::ProductionCMode::LRAT:
        constraints.liquid_limit = constraints.active_target; break;
    case Group::ProductionCMode::RESV:
        constraints.resv_limit = constraints.active_target; break;
    default:
        break;
    }

    // For each non-active rate type, compute the effective limit
    // by running the hierarchy traversal with an explicit cmode.
    static const std::array<Group::ProductionCMode, 5> all_modes = {
        Group::ProductionCMode::ORAT,
        Group::ProductionCMode::WRAT,
        Group::ProductionCMode::GRAT,
        Group::ProductionCMode::LRAT,
        Group::ProductionCMode::RESV
    };

    for (const auto mode : all_modes) {
        if (mode == constraints.active_cmode) {
            continue; // Already computed as the active target
        }
        // Check if any group in the hierarchy has a limit for this rate type.
        // We do this by creating a GeneralCalculator with the explicit cmode.
        // The recursion will walk up until it finds a group with has_control(mode).
        GeneralCalculator general_calculator{*this, group, mode};
        auto limit_info = general_calculator.calculateGroupConstraint();
        if (limit_info) {
            switch (mode) {
            case Group::ProductionCMode::ORAT:
                constraints.oil_limit = limit_info->constraint; break;
            case Group::ProductionCMode::WRAT:
                constraints.water_limit = limit_info->constraint; break;
            case Group::ProductionCMode::GRAT:
                constraints.gas_limit = limit_info->constraint; break;
            case Group::ProductionCMode::LRAT:
                constraints.liquid_limit = limit_info->constraint; break;
            case Group::ProductionCMode::RESV:
                constraints.resv_limit = limit_info->constraint; break;
            default:
                break;
            }
        }
    }

    return constraints;
}

// ----------------------------------------------------
// Constructor for inner class GeneralCalculator
// ----------------------------------------------------

template<class Scalar, class IndexTraits>
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
GeneralCalculator(
    GroupConstraintCalculator<Scalar, IndexTraits>& parent_calculator,
    const Group& original_group,
    std::optional<ReservoirCoupling::Phase> injection_phase  // Only used for injectors
) : GeneralCalculator(
    parent_calculator,
    original_group,
    injection_phase,
    /*production_cmode=*/std::nullopt,
    /*constraint_type=*/ConstraintType::Target
) {}

template<class Scalar, class IndexTraits>
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
GeneralCalculator(
    GroupConstraintCalculator<Scalar, IndexTraits>& parent_calculator,
    const Group& original_group,
    Group::ProductionCMode production_cmode
) : GeneralCalculator(
    parent_calculator,
    original_group,
    /*injection_phase=*/std::nullopt,
    /*production_cmode=*/production_cmode,
    /*constraint_type=*/ConstraintType::Limit
) {}

// Private constructor for the GeneralCalculator class
template<class Scalar, class IndexTraits>
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
GeneralCalculator(
    GroupConstraintCalculator<Scalar, IndexTraits>& parent_calculator,
    const Group& original_group,
    std::optional<ReservoirCoupling::Phase> injection_phase,  // Only used for injectors
    std::optional<Group::ProductionCMode> production_cmode,
    ConstraintType constraint_type  // Target or Limit ?
) :
    parent_calculator_{parent_calculator},
    original_group_{original_group},
    injection_phase_{injection_phase},
    constraint_type_{constraint_type},
    resv_coeffs_prod_(this->phaseUsage().numPhases, 0.0)
{
    if (constraint_type == ConstraintType::Limit) {
        assert(production_cmode.has_value());
        this->control_mode_ = ControlMode{*production_cmode};
    }
    else {
        if (injection_phase.has_value()) {
            this->control_mode_ = ControlMode{this->groupState().injection_control(original_group.name(), this->injectionPhase_())};
        }
        else {
            this->control_mode_ = ControlMode{this->groupState().production_control(original_group.name())};
        }
    }
    this->wellModel().calcResvCoeff(
        this->fipnum(),
        this->pvtreg(),
        this->groupState().production_rates(original_group.name()),
        this->resv_coeffs_prod_
    );
}

// -------------------------------------------------------
// Public methods for the GeneralCalculator class
// -------------------------------------------------------

template<class Scalar, class IndexTraits>
std::optional<typename GroupConstraintCalculator<Scalar, IndexTraits>::ConstraintInfo>
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
calculateGroupConstraint()
{
    // TODO: For now this is adapted to the reservoir coupling implementation where "group" below will
    //   correspond to a master group. In the future we might want to port the logic to include
    //   e.g. checkGroupConstraintsProd() and checkGroupConstraintsInj() in GroupStateHelper.cpp.
    const auto& group = this->original_group_;  // The bottom group we want to calculate the target for.
    // For injection, check if the group has injection control defined for this phase.
    // If not, there is no target to calculate.
    if (this->isInjectionConstraint()) {
        if (!group.hasInjectionControl(this->injectionPhase_())) {
            return std::nullopt;
        }
    }
    if (group.is_field() || !this->parentGroupControlAvailable_(group)) {
        return this->getGroupConstraintNoGuideRate(group);
    }
    assert(this->parentGroupControlAvailable_(group));
    if (!this->hasGuideRate(group)) {
        if (this->hasHigherLevelControlOrNoLimit(group)) {
            // Parent control is available, but no guide rate is defined. This is illegal for a master group
            // under higher level control or limit.
            OPM_DEFLOG_THROW(
                std::runtime_error,
                fmt::format("No guide rate defined for master group {}", group.name()),
                this->deferredLogger());
        }
        // A master group with:
        //   - individual (not FLD or NONE) control,
        //   - parent control available,
        //   - but no guide rate
        // this could be considered an error, but we can also fall back to use its own target.
        return this->getGroupConstraintNoGuideRate(group);
    }
    const auto efficiency_factor = group.getGroupEfficiencyFactor();
    auto constraint = this->calculateGroupConstraintRecursive_(this->parentGroup(group), efficiency_factor);
    if (constraint.has_value()) {
        // TODO: We could probably switch the group to FLD control mode now. However, this call is coming
        //    from beginTimeStep() in BlackoilWellModel_impl.hpp, but the regular higher constraints
        //    checks (for all groups, not just the master group) will first be done during the assemble() step,
        //    at which point the group should be switched to FLD control mode if necessary.
        return constraint;
    }
    // If a higher level constraint was not found or not violated, use the group's own constraint.
    return this->getGroupConstraintNoGuideRate(group);
}

template<class Scalar, class IndexTraits>
typename GroupConstraintCalculator<Scalar, IndexTraits>::GeneralCalculator::TargetCalculatorType
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
getInjectionTargetCalculator(const Group& /*group*/)
{
    return InjectionTargetCalculator{
        this->groupStateHelper(),
        this->injectionPhase_()
    };
}

template<class Scalar, class IndexTraits>
typename GroupConstraintCalculator<Scalar, IndexTraits>::GeneralCalculator::TargetCalculatorType
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
getProductionTargetCalculator(const Group& group) const
{
    return TargetCalculator{
        this->groupStateHelper(),
        this->resvCoeffsProd(),
        group
    };
}

template<class Scalar, class IndexTraits>
typename GroupConstraintCalculator<Scalar, IndexTraits>::GeneralCalculator::TargetCalculatorType
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
getTargetCalculator(const Group& group)
{
    if (this->isInjectionConstraint()) {
        return this->getInjectionTargetCalculator(group);
    }
    else {
        return this->getProductionTargetCalculator(group);
    }
}

// Get constraint info for a particular group, not considering parent control or guide rates.
template<class Scalar, class IndexTraits>
std::optional<typename GroupConstraintCalculator<Scalar, IndexTraits>::ConstraintInfo>
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
getGroupConstraintNoGuideRate(const Group& group)
{
    // Efficiency factor is not applied here, since it only applies to child rates, not to the group's
    // own target.
    // NOTE: If a master group is under individual control and has a RESV rate target, it should be
    //   transferred into a RESV target for the slave group. But it is not obvious how to transform it
    //   since the master and slave groups in general have different PVT properties.
    // TODO: For now we assume the RESV rate target is in slave reservoir units, and just send it as is
    //   to the slave group. Alternatively, we could also throw an error here for that case
    if (this->isInjectionConstraint()) {
        const auto& control_mode = this->groupState().injection_control(group.name(), this->injectionPhase_());
        return ConstraintInfo{
            this->groupStateHelper().getInjectionGroupTarget(group, this->injectionPhase_(), this->resvCoeffsInj()),
            control_mode
        };
    }
    assert(this->isProductionConstraint());
    const auto cmode = this->getProdCmode(group);
    if (cmode.has_value()) {
        // For ConstraintType::Target: cmode comes from the group state (the group's active
        // production control). If the group is under FLD or NONE control, it has no own target
        // — return std::nullopt so the caller can use a higher-level distributed target instead.
        if (*cmode == Group::ProductionCMode::FLD || *cmode == Group::ProductionCMode::NONE) {
            return std::nullopt;
        }
        return ConstraintInfo{
            this->groupStateHelper().getProductionGroupTargetForMode(group, *cmode),
            *cmode
        };
    }
    // No limit found for this rate type.
    assert(this->constraintType() == ConstraintType::Limit);
    return std::nullopt;
}

// This method is only used for production constraints.
// For this->constraintType() == ConstraintType::Limit, there is a possibility that the group
// does not have a limit, in that case, we return std::nullopt.
template<class Scalar, class IndexTraits>
std::optional<Group::ProductionCMode>
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
getProdCmode(const Group& group) const
{
    assert(this->isProductionConstraint());
    if (this->constraintType() == ConstraintType::Limit) {
        auto cmode = std::get<Group::ProductionCMode>(this->control_mode_);
        if (group.has_control(cmode)) {
            return cmode;
        }
        return std::nullopt;
    }
    else {
        return this->groupState().production_control(group.name());
    }
}

template<class Scalar, class IndexTraits>
Group::ProductionCMode
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
getProdCmode() const
{
    assert(this->isProductionConstraint());
    return std::get<Group::ProductionCMode>(this->control_mode_);
}

template<class Scalar, class IndexTraits>
bool
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
hasHigherLevelControlOrNoLimit(const Group& group)
{
    // Check if the group (constrained to production or injection) has control FLD or NONE.
    // For example, a pure injection group will have production control NONE.
    const auto& name = group.name();
    if (this->isInjectionConstraint()) {
        return this->groupState().has_field_or_none_control(name, this->injectionPhase_());
    }
    else {
        if (this->constraintType() == ConstraintType::Limit) {
            // For constraint type Limit: if the current group does not have a limit, check
            // recursively if any parent groups has a limit.
            auto cmode = this->getProdCmode(group);
            return !cmode.has_value();
        }
        else {
            return this->groupState().has_field_or_none_control(name);
        }
    }
}


// -------------------------------------------------------
// Private methods for the GeneralCalculator class
// -------------------------------------------------------

// See comments above RescoupConstraintsCalculator::calculateMasterGroupConstraintsAndSendToSlaves() about how
// the target is calculated.
template<class Scalar, class IndexTraits>
std::optional<typename GroupConstraintCalculator<Scalar, IndexTraits>::ConstraintInfo>
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
calculateGroupConstraintRecursive_(const Group& group, const Scalar efficiency_factor)
{
    if (this->hasHigherLevelControlOrNoLimit(group)) {
        // NOTE: A pure injection group is assumed to have production control NONE and
        //   a pure production group is assumed to have injection control NONE, see
        //   GroupStateHelper.cpp::setCmodeGroup() for details.
        if (!this->parentGroupControlAvailable_(group)) {
            // If we are here, the group has control mode NONE, and either
            //   - item 8 of GCONPROD/GCONINJE is "NO" or,
            //   - group.is_field() is true.
            // NOTE: it cannot have control mode FLD, since FLD will ovverride item 8 of
            //   GCONPROD/GCONINJE, see GroupKeywordHandlers.cpp
            // TODO: We could throw an error here. Or try to fall back to use the group's own constraint.
            // Fall back to use the group's own constraint.
            return std::nullopt;
        } else {
            assert(!group.is_field());  // The field group should not have parent control.
            auto new_efficiency_factor = efficiency_factor * group.getGroupEfficiencyFactor();
            return this->calculateGroupConstraintRecursive_(this->parentGroup(group), new_efficiency_factor);
        }
    }

    if ((this->isInjectionConstraint() && !group.isInjectionGroup()) ||
            (this->isProductionConstraint() && !group.isProductionGroup()))
    {
        // Controlling group that is not of the type we are interested in.
        // This should never happen, since any group that is not an injector should have injection
        // control NONE. And any group that is not a producer should have production control NONE.
        // See GroupStateHelper.cpp::setCmodeGroup() for details.
        // .. and therefore should be caught by the hasHigherLevelControlOrNoLimit() check above.
        OPM_DEFLOG_THROW(
            std::runtime_error,
            fmt::format("Controlling group that is not of the type we are interested in: {}", group.name()),
            this->deferredLogger()
        );
    }

    // If we are here, we are at the topmost group to be visited in the recursion.
    // This is the group containing the control we will check against.

    const auto& top_group = group;
    const auto& bottom_group = this->original_group_;
    TopToBottomCalculator top_bottom_calc{*this, top_group, bottom_group, efficiency_factor};
    return top_bottom_calc.calculateGroupConstraint();
}

template<class Scalar, class IndexTraits>
bool
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
parentGroupControlAvailable_(const Group& group)
{
    if (this->isInjectionConstraint()) {
        return group.injectionGroupControlAvailable(this->injectionPhase_());
    } else {
        return group.productionGroupControlAvailable();
    }
}

template<class Scalar, class IndexTraits>
Phase
GroupConstraintCalculator<Scalar, IndexTraits>::
GeneralCalculator::
injectionPhase_() const
{
    if (this->injection_phase_.has_value()) {
        switch (this->injection_phase_.value()) {
            case ReservoirCoupling::Phase::Water:
                return Phase::WATER;
            case ReservoirCoupling::Phase::Oil:
                return Phase::OIL;
            case ReservoirCoupling::Phase::Gas:
                return Phase::GAS;
            default:
                OPM_DEFLOG_THROW(std::runtime_error, "Invalid injection phase", this->deferredLogger());
                return Phase::WATER; // Unreachable, but satisfies compiler
        }
    }
    else {
        return Phase::OIL;   // Dummy phase, not used for producers.
    }
}

// ----------------------------------------------------
// Constructor for inner class TopToBottomCalculator
// ----------------------------------------------------

template<class Scalar, class IndexTraits>
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
TopToBottomCalculator(
    GeneralCalculator& parent_calculator,
    const Group& top_group,
    const Group& bottom_group,
    Scalar chain_efficiency_factor
) :
    parent_calculator_{parent_calculator},
    top_group_{top_group},
    bottom_group_{bottom_group},
    chain_efficiency_factor_{chain_efficiency_factor}
{
    if (this->isInjectionConstraint()) {
        this->initForInjector_();
    }
    else {
        this->initForProducer_();
    }
}

// -------------------------------------------------------
// Public methods for the TopToBottomCalculator class
// -------------------------------------------------------

template<class Scalar, class IndexTraits>
std::optional<typename GroupConstraintCalculator<Scalar, IndexTraits>::ConstraintInfo>
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
calculateGroupConstraint()
{
    const auto chain = this->getGroupChainTopBot_();
    // NOTE: For reservoir coupling: we are called from beginTimeStep() in BlackoilWellModel_impl.hpp,
    //   so we do not check for guide rate violations here, compare with
    //  GroupStateHelper.cpp::checkGroupConstraintsProd() and
    //  GroupStateHelper.cpp::checkGroupConstraintsInj() since the guide rate will be checked during the
    //   assemble() step.
    const auto local_reduction_level = this->getLocalReductionLevel_(chain);
    Scalar top_level_target = this->getTopLevelTargetOrLimit_();
    Scalar bottom_group_current_rate_available = this->getBottomGroupCurrentRateAvailable_();
    // Because the bottom group (the original group) is the last of the elements,
    //   and not an ancestor, we subtract one:
    const std::size_t num_ancestors = chain.size() - 1;
    Scalar target = top_level_target;
    for (std::size_t i = 0; i < num_ancestors; ++i) {
        if ((i == 0) || this->hasGuideRate_(chain[i])) {
            // Apply local reductions only at the control level
            // (top) and for levels where we have a specified
            // group guide rate.
            if (i <= local_reduction_level) {
                target -= this->localReduction_(chain[i]);
            }
            // Add my reduction back at the level where it is included in the local reduction
            if (this->bottomGroupHasIndividualControl_() && i == local_reduction_level) {
                const Scalar addback_efficiency
                    = this->computeAddbackEfficiency_(chain, local_reduction_level);
                target += bottom_group_current_rate_available * addback_efficiency;
            }
        }
        target *= this->localFraction_(chain[i + 1]);
    }
    // Avoid negative target rates coming from too large local reductions.
    target = std::max(Scalar(0.0), target);
    // Divide by the accumulated efficiency factor along the chain from top to bottom, excluding the top group.
    // This is the full target rate that should be assigned to the bottom group, to be seen as the unscaled
    // target rate for the top group.
    const Scalar full_target = target / this->chain_efficiency_factor_;
    if (bottom_group_current_rate_available > TARGET_RATE_TOLERANCE) {  // > 1e-12
        const auto toplevel_control_mode = this->getToplevelControlMode_();
        if ((bottom_group_current_rate_available > full_target)) {
            // The bottom group is producing too much according to the top level group target,
            // so we need to scale down the target rate.
            if (this->isProducerAndRESVControl_(this->top_group_)) {
                // For RESV mode with reservoir coupling master groups, we scale the slave's
                // actual reservoir rate to get a target in slave's reservoir units.
                const Scalar slave_resv_rate = this->getSlaveGroupReservoirRate_(this->bottom_group_);
                // Scale the slave's reservoir rate by the same factor we would scale surface rates
                const Scalar scale = full_target / bottom_group_current_rate_available;
                return ConstraintInfo{scale * slave_resv_rate, toplevel_control_mode};
            }
            else {
                return ConstraintInfo{full_target, toplevel_control_mode};
            }
        }
        else if (
            (this->constraintType() == ConstraintType::Limit) ||
            ((this->constraintType() == ConstraintType::Target) && (this->hasFLDControl_(this->bottom_group_)))
        ) {
            // The bottom group is under FLD control, so we return the top level constraint as is.
            return ConstraintInfo{full_target, toplevel_control_mode};
        }
    }
    // Return the more restrictive of the top-level distributed target and the
    // bottom group's own target. This prevents the slave group from overshooting the
    // top-level budget.
    auto bottom_constraint = this->getGroupConstraintNoGuideRate_(this->bottom_group_);
    if (!bottom_constraint.has_value()) {
        // The bottom group has no own constraint (e.g. production cmode NONE or FLD).
        // Check if the bottom group has a limit defined for the top-level control mode
        // (e.g. an ORAT limit in GCONPROD even though the active cmode is NONE).
        const auto toplevel_control_mode = this->getToplevelControlMode_();
        Scalar capped_target = full_target;
        if (this->isProductionConstraint()) {
            const auto* prod_cmode = std::get_if<Group::ProductionCMode>(&toplevel_control_mode);
            if (prod_cmode && this->bottom_group_.has_control(*prod_cmode)) {
                const Scalar bottom_limit
                    = this->groupStateHelper().getProductionGroupTargetForMode(
                        this->bottom_group_, *prod_cmode);
                capped_target = std::min(full_target, bottom_limit);
            }
        }
        return ConstraintInfo{capped_target, toplevel_control_mode};
    }
    if (full_target < bottom_constraint->constraint) {
        const auto toplevel_control_mode = this->getToplevelControlMode_();
        return ConstraintInfo{full_target, toplevel_control_mode};
    }
    return bottom_constraint;
}

// -------------------------------------------------------
// Private methods for the TopToBottomCalculator class
// -------------------------------------------------------

template <class Scalar, class IndexTraits>
bool
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
bottomGroupHasIndividualControl_()
{
    return !this->hasHigherLevelControlOrNoLimit(this->bottom_group_);
}

template <class Scalar, class IndexTraits>
Scalar
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
computeAddbackEfficiency_(
    const std::vector<std::string>& chain,
    const std::size_t local_reduction_level) const
{
    // Compute partial efficiency factor from local_reduction_level down to the entity.
    // Chain is ordered [control_group, ..., local_reduction_level, ..., entity].
    // We multiply efficiency factors from index (local_reduction_level + 1) to the entity.
    // Note: The entity at chain[num_ancestors] may be a well or a group.
    // Both wells (WEFAC) and groups (GEFAC) have efficiency factors that must be included,
    // since the local_reduction rates include these efficiency factors.
    const std::size_t num_ancestors = chain.size() - 1;
    Scalar efficiency = 1.0;
    for (std::size_t jj = local_reduction_level + 1; jj <= num_ancestors; ++jj) {
        const std::string& name = chain[jj];
        if (this->schedule().hasGroup(name, this->reportStepIdx())) {
            const auto& grp = this->schedule().getGroup(name, this->reportStepIdx());
            efficiency *= grp.getGroupEfficiencyFactor();
        } else if (this->schedule().hasWell(name, this->reportStepIdx())) {
            const auto& well = this->schedule().getWell(name, this->reportStepIdx());
            efficiency *= well.getEfficiencyFactor()
                * this->wellState()[name].efficiency_scaling_factor;
        }
    }
    return efficiency;
}

template<class Scalar, class IndexTraits>
Scalar
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
getBottomGroupCurrentRateAvailable_() const
{
    const auto& group = this->bottom_group_;
    if (this->isInjectionConstraint()) {
        return std::get<InjectionTargetCalculator>(this->target_calculator_).calcModeRateFromRates(
            this->groupStateHelper().getGroupRatesAvailableForHigherLevelControl(group, /*is_injector=*/true)
        );
    }
    else {
        const auto& tcalc = std::get<TargetCalculator>(this->target_calculator_);
        return tcalc.calcModeRateFromRates(
            this->groupStateHelper().getGroupRatesAvailableForHigherLevelControl(group, /*is_injector=*/false)
        );
    }
}

template<class Scalar, class IndexTraits>
Scalar
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
getSlaveGroupReservoirRate_(const Group& group)
{
    if (!this->groupStateHelper().isReservoirCouplingMasterGroup(group)) {
        OPM_DEFLOG_THROW(std::runtime_error, "Group is not a reservoir coupling master group", this->deferredLogger());
    }
    // Sum reservoir rates for all phases from the slave
    Scalar total_resv_rate = 0.0;
    const auto& master = this->groupStateHelper().reservoirCouplingMaster();
    for (auto phase : {ReservoirCoupling::Phase::Oil, ReservoirCoupling::Phase::Gas, ReservoirCoupling::Phase::Water}) {
        total_resv_rate += master.getMasterGroupRate(group.name(), phase, ReservoirCoupling::RateKind::ProductionReservoir);
    }
    return total_resv_rate;
}

template<class Scalar, class IndexTraits>
std::vector<std::string>
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
getGroupChainTopBot_() const
{
    return this->groupStateHelper().groupChainTopBot(this->bottom_group_.name(), this->top_group_.name());
}

template<class Scalar, class IndexTraits>
std::size_t
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
getLocalReductionLevel_(const std::vector<std::string>& chain)
{
    const std::size_t num_ancestors = chain.size() - 1;
    std::size_t local_reduction_level = 0;
    for (std::size_t ii = 1; ii < num_ancestors; ++ii) {
        const int num_gr_ctrl = this->groupStateHelper().groupControlledWells(
            chain[ii],
            /*always_included_child=*/"",
            /*is_producer=*/this->isProductionConstraint(),
            /*injection_phase=*/this->injectionPhase_());
        if (this->hasGuideRate_(chain[ii]) && num_gr_ctrl > 0) {
            local_reduction_level = ii;
        }
    }
    return local_reduction_level;
}

template<class Scalar, class IndexTraits>
typename GroupConstraintCalculator<Scalar, IndexTraits>::ControlMode
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
getToplevelControlMode_() const
{
    if (this->isInjectionConstraint()) {
        return this->groupState().injection_control(this->top_group_.name(), this->injectionPhase_());
    }
    else {
        if (this->constraintType() == ConstraintType::Limit) {
            const auto cmode = this->getProdCmode(this->top_group_);
            assert(cmode.has_value());
            return ControlMode{*cmode};
        }
        return ControlMode{this->groupState().production_control(this->top_group_.name())};
    }
}

template<class Scalar, class IndexTraits>
Scalar
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
getTopLevelTargetOrLimit_()
{
    if (this->isInjectionConstraint()) {
        // NOTE: For VREP mode and (and also for multi-phase injection with RESV mode),
        //   getInjectionGroupTarget() converts from reservoir to surface rate using the master's resv_coeff.
        //   If the top level group has subordinate groups connected to slave reservoirs, and
        //   possibly also combined with subordinate well groups in the master reservoir
        //   with different PVT properties, the conversion will be inaccurate.
        // TODO: Implement a more accurate approach.
        return this->groupStateHelper().getInjectionGroupTarget(
            this->top_group_, this->injectionPhase_(), this->resvCoeffsInj());
    }
    else {
        if (this->constraintType() == ConstraintType::Limit) {
            const auto cmode = this->getProdCmode(this->top_group_);
            assert(cmode.has_value());
            return this->groupStateHelper().getProductionGroupTargetForMode(this->top_group_, *cmode);
        }
        else {
            return this->groupStateHelper().getProductionGroupTarget(this->top_group_);
        }
    }
}

template<class Scalar, class IndexTraits>
bool
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
hasFLDControl_(const Group& group) const
{
    if (this->isInjectionConstraint()) {
        return this->groupState().injection_control(group.name(), this->injectionPhase_()) == Group::InjectionCMode::FLD;
    }
    else {
        return this->groupState().production_control(group.name()) == Group::ProductionCMode::FLD;
    }
}

template<class Scalar, class IndexTraits>
void
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
initForInjector_()
{
    this->target_calculator_.template emplace<InjectionTargetCalculator>(
        this->groupStateHelper(),
        this->injectionPhase_()
    );
    const auto guide_target_mode = this->groupStateHelper().getInjectionGuideTargetMode(this->injectionPhase_());
    this->fraction_calculator_.emplace(
        this->schedule(),
        this->groupStateHelper(),
        this->summaryState(),
        this->reportStepIdx(),
        &this->guideRate(),
        GuideRateModel::Target{guide_target_mode},
        /*is_producer=*/false,
        this->injectionPhase_()
    );
}

template<class Scalar, class IndexTraits>
void
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
initForProducer_()
{
    if (this->constraintType() == ConstraintType::Limit) {
        const auto control_mode = this->getProdCmode();
        // Use the control mode for per-rate-type limit computation
        this->target_calculator_.template emplace<TargetCalculator>(
            this->groupStateHelper(),
            this->resvCoeffsProd(),
            control_mode
        );
    }
    else {
        this->target_calculator_.template emplace<TargetCalculator>(
            this->groupStateHelper(),
            this->resvCoeffsProd(),
            this->top_group_
        );
    }
    const auto guide_target_mode = this->groupStateHelper().getProductionGuideTargetMode(this->top_group_);
    const auto dummy_phase = Phase::OIL; // Dummy phase, not used for producers.
    this->fraction_calculator_.emplace(
        this->schedule(),
        this->groupStateHelper(),
        this->summaryState(),
        this->reportStepIdx(),
        &this->guideRate(),
        GuideRateModel::Target{guide_target_mode},
        /*is_producer=*/true,
        dummy_phase
    );
}

template<class Scalar, class IndexTraits>
bool
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
isProducerAndRESVControl_(const Group& group) const
{
    if (this->isProductionConstraint()) {
        if (this->constraintType() == ConstraintType::Limit) {
            const auto cmode = this->getProdCmode(group);
            if (!cmode.has_value()) {
                return false;
            }
            return *cmode == Group::ProductionCMode::RESV;
        }
        return this->groupState().production_control(group.name()) == Group::ProductionCMode::RESV;
    }
    else {
        return false;
    }
}

template<class Scalar, class IndexTraits>
Scalar
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
localFraction_(const std::string& group_name)
{
    const auto& always_included_child = this->bottom_group_.name();
    return this->fraction_calculator_->localFraction(group_name, always_included_child);
}

template<class Scalar, class IndexTraits>
Scalar
GroupConstraintCalculator<Scalar, IndexTraits>::
TopToBottomCalculator::
localReduction_(const std::string& group_name)
{
    if (this->isInjectionConstraint()) {
        const std::vector<Scalar>& group_target_reductions =
            this->groupState().injection_reduction_rates(group_name);
        return std::get<InjectionTargetCalculator>(this->target_calculator_).calcModeRateFromRates(
            group_target_reductions
        );
    }
    else {
        const std::vector<Scalar>& group_target_reductions =
            this->groupState().production_reduction_rates(group_name);
        return std::get<TargetCalculator>(this->target_calculator_).calcModeRateFromRates(
            group_target_reductions
        );

    }
}


template class GroupConstraintCalculator<double, BlackOilDefaultFluidSystemIndices>;

#if FLOW_INSTANTIATE_FLOAT
template class GroupConstraintCalculator<float, BlackOilDefaultFluidSystemIndices>;
#endif

} // namespace Opm
