/*
  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/>.
*/
#ifndef OPM_GROUPSTATEHELPER_HEADER_INCLUDED
#define OPM_GROUPSTATEHELPER_HEADER_INCLUDED

#include <opm/simulators/wells/rescoup/RescoupProxy.hpp>

#include <opm/common/TimingMacros.hpp>
#include <opm/input/eclipse/Schedule/ResCoup/GrupSlav.hpp>
#include <opm/input/eclipse/EclipseState/Grid/FieldPropsManager.hpp>
#include <opm/input/eclipse/Schedule/Group/GPMaint.hpp>
#include <opm/input/eclipse/Schedule/Group/GSatProd.hpp>
#include <opm/input/eclipse/Schedule/Group/GuideRate.hpp>
#include <opm/input/eclipse/Schedule/Schedule.hpp>
#include <opm/input/eclipse/Schedule/ScheduleState.hpp>
#include <opm/input/eclipse/Schedule/SummaryState.hpp>
#include <opm/material/fluidsystems/PhaseUsageInfo.hpp>
#include <opm/simulators/utils/DeferredLogger.hpp>
#include <opm/simulators/utils/gatherDeferredLogger.hpp>
#include <opm/simulators/utils/DeferredLoggingErrorHelpers.hpp>
#include <opm/simulators/wells/GroupState.hpp>
#include <opm/simulators/wells/VFPProdProperties.hpp>
#include <opm/simulators/wells/WellState.hpp>

#include <opm/simulators/utils/ParallelCommunication.hpp>

#include <algorithm>
#include <map>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <vector>

namespace Opm
{

template <typename Scalar, typename IndexTraits>
class GroupStateHelper
{
public:
    // RAII guard for temporarily setting wellstate pointer
    class WellStateGuard
    {
    public:
        WellStateGuard(GroupStateHelper& groupStateHelper, WellState<Scalar, IndexTraits>& well_state)
            : groupStateHelper_ {groupStateHelper}
            , previous_state_ptr_ {groupStateHelper_.well_state_}
        {
            // Set the new state directly
            groupStateHelper_.well_state_ = &well_state;
        }

        ~WellStateGuard()
        {
            // Restore the previous state
            groupStateHelper_.well_state_ = previous_state_ptr_;
        }

        // Delete copy and move operations
        WellStateGuard(const WellStateGuard&) = delete;
        WellStateGuard& operator=(const WellStateGuard&) = delete;
        WellStateGuard(WellStateGuard&&) = delete;
        WellStateGuard& operator=(WellStateGuard&&) = delete;

    private:
        GroupStateHelper& groupStateHelper_;
        const WellState<Scalar, IndexTraits>* previous_state_ptr_;
    };

    // RAII guard for temporarily setting groupstate pointer
    class GroupStateGuard
    {
    public:
        GroupStateGuard(GroupStateHelper& group_state_helper, GroupState<Scalar>& group_state)
            : group_state_helper_ {group_state_helper}
            , previous_state_ptr_ {group_state_helper.group_state_}
        {
            // Set the new state directly
            group_state_helper_.group_state_ = &group_state;
        }

        ~GroupStateGuard()
        {
            // Restore the previous state
            group_state_helper_.group_state_ = previous_state_ptr_;
        }

        // Delete copy and move operations
        GroupStateGuard(const GroupStateGuard&) = delete;
        GroupStateGuard& operator=(const GroupStateGuard&) = delete;
        GroupStateGuard(GroupStateGuard&&) = delete;
        GroupStateGuard& operator=(GroupStateGuard&&) = delete;

    private:
        GroupStateHelper& group_state_helper_;
        GroupState<Scalar>* previous_state_ptr_ {nullptr};
    };

    /// @brief RAII guard that owns a DeferredLogger and auto-gathers on destruction
    ///
    /// @details This class provides a complete lifecycle for deferred logging in parallel
    /// simulations. It owns a DeferredLogger instance and automatically handles:
    /// - Pushing the logger onto the stack (saving the previous logger)
    /// - On destruction: gathering messages across MPI ranks, logging on rank 0
    ///   (if terminal_output enabled), and restoring the previous logger
    ///
    /// Usage:
    ///   auto guard = groupStateHelper.pushLogger();
    ///   // Use groupStateHelper.deferredLogger() to log messages
    ///   // On scope exit: gather, log (if terminal), restore previous logger
    class ScopedLoggerGuard
    {
    public:
        /// @brief Constructor for scoped logger guard
        /// @param helper Reference to the GroupStateHelper
        /// @param do_mpi_gather If true, gather messages across MPI ranks on destruction.
        ///        Set to false when called from contexts where MPI synchronization is not
        ///        possible (e.g., NLDD domain-local operations).
        explicit ScopedLoggerGuard(const GroupStateHelper& helper, bool do_mpi_gather = true)
            : helper_(&helper)
            , previous_(helper.deferred_logger_)
            , do_mpi_gather_(do_mpi_gather)
        {
            helper_->deferred_logger_ = &logger_;
        }

        ~ScopedLoggerGuard()
        {
            if (helper_) {
                if (do_mpi_gather_) {
                    // 1. Gather messages across MPI ranks
                    DeferredLogger global = gatherDeferredLogger(logger_, helper_->comm());

                    // 2. Log on rank 0 (if terminal_output enabled)
                    if (helper_->terminalOutput()) {
                        global.logMessages();
                    }
                } else {
                    // Just log locally without MPI gather
                    if (helper_->terminalOutput()) {
                        logger_.logMessages();
                    }
                }

                // 3. Restore previous logger
                helper_->deferred_logger_ = previous_;
            }
        }

        // Delete copy operations and move assignment
        ScopedLoggerGuard(const ScopedLoggerGuard&) = delete;
        ScopedLoggerGuard& operator=(const ScopedLoggerGuard&) = delete;
        ScopedLoggerGuard& operator=(ScopedLoggerGuard&&) = delete;

        // Move constructor required for pushLogger() return-by-value (must be
        // available even if elided by RVO)
        ScopedLoggerGuard(ScopedLoggerGuard&& other) noexcept
            : helper_(other.helper_)
            , logger_(std::move(other.logger_))
            , previous_(other.previous_)
            , do_mpi_gather_(other.do_mpi_gather_)
        {
            // Update the helper's pointer to our moved logger
            if (helper_) {
                helper_->deferred_logger_ = &logger_;
            }
            other.helper_ = nullptr;
            other.previous_ = nullptr;
        }

    private:
        // Pointer (not reference) to allow nulling in move constructor
        const GroupStateHelper* helper_{nullptr};
        DeferredLogger logger_;            // Owned logger
        DeferredLogger* previous_{nullptr}; // For restore
        bool do_mpi_gather_{true};         // Whether to gather messages across MPI ranks
    };

    using GroupTarget = typename SingleWellState<Scalar, IndexTraits>::GroupTarget;

    GroupStateHelper(WellState<Scalar, IndexTraits>& well_state,
                    GroupState<Scalar>& group_state,
                    const Schedule& schedule,
                    const SummaryState& summary_state,
                    const GuideRate& guide_rate,
                    const PhaseUsageInfo<IndexTraits>& phase_usage_info,
                    const Parallel::Communication& comm,
                    bool terminal_output);

    void accumulateGroupEfficiencyFactor(const Group& group, Scalar& factor) const;

    std::pair<bool, Scalar> checkGroupConstraintsInj(const std::string& name,
                                                     const std::string& parent,
                                                     const Group& group,
                                                     const Scalar* rates,
                                                     const Phase injection_phase,
                                                     const Scalar efficiency_factor,
                                                     const std::vector<Scalar>& resv_coeff,
                                                     const bool check_guide_rate) const;

    std::pair<bool, Scalar> checkGroupConstraintsProd(const std::string& name,
                                                      const std::string& parent,
                                                      const Group& group,
                                                      const Scalar* rates,
                                                      const Scalar efficiency_factor,
                                                      const std::vector<Scalar>& resv_coeff,
                                                      const bool check_guide_rate) const;

    std::pair<Group::ProductionCMode, Scalar>
    checkGroupProductionConstraints(const Group& group) const;

    const Parallel::Communication& comm() const { return this->comm_; }

    /// @brief Get the deferred logger
    /// @throws std::logic_error if no logger has been set via pushLogger()
    DeferredLogger& deferredLogger() const
    {
        if (this->deferred_logger_ == nullptr) {
            throw std::logic_error("DeferredLogger not set. Call pushLogger() first.");
        }
        return *this->deferred_logger_;
    }

    std::vector<Scalar> getGroupRatesAvailableForHigherLevelControl(const Group& group, const bool is_injector) const;

    Scalar getInjectionGroupTarget(const Group& group,
                                   const Phase& injection_phase,
                                   const std::vector<Scalar>& resv_coeff) const;

    /// @brief Get the guide rate target mode for an injection phase
    /// @param injection_phase The injection phase (WATER, OIL, or GAS)
    /// @return The corresponding GuideRateModel::Target for the injection phase
    GuideRateModel::Target getInjectionGuideTargetMode(Phase injection_phase) const;

    Scalar getProductionGroupTarget(const Group& group) const;

    /// Get the production target for a specific control mode (not necessarily the active one).
    Scalar getProductionGroupTargetForMode(const Group& group,
                                           const Group::ProductionCMode cmode) const;

    /// @brief Get the guide rate target mode for a production group
    /// @param group The production group
    /// @return The GuideRateModel::Target based on the group's production control mode
    GuideRateModel::Target getProductionGuideTargetMode(const Group& group) const;

    std::pair<Scalar, Group::ProductionCMode>
    getAutoChokeGroupProductionTargetRate(const Group& bottom_group,
                                          const Group& group,
                                          const std::vector<Scalar>& resv_coeff,
                                          Scalar efficiencyFactor) const;

    GuideRate::RateVector getProductionGroupRateVector(const std::string& group_name) const;

    std::optional<GroupTarget> getWellGroupTargetInjector(const std::string& name,
                                                          const std::string& parent,
                                                          const Group& group,
                                                          const Scalar* rates,
                                                          const Phase injection_phase,
                                                          const Scalar efficiency_factor,
                                                          const std::vector<Scalar>& resv_coeff) const;

    std::optional<GroupTarget> getWellGroupTargetProducer(const std::string& name,
                                                          const std::string& parent,
                                                          const Group& group,
                                                          const Scalar* rates,
                                                          const Scalar efficiency_factor,
                                                          const std::vector<Scalar>& resv_coeff) const;

    GuideRate::RateVector getWellRateVector(const std::string& name) const;

    std::vector<std::string> groupChainTopBot(const std::string& bottom, const std::string& top) const;

    /// returns the number of wells that are actively under group control for a given group with name given
    /// by group_name
    int groupControlledWells(const std::string& group_name,
                             const std::string& always_included_child,
                             const bool is_production_group,
                             const Phase injection_phase) const;

    GroupState<Scalar>& groupState() const
    {
        return *this->group_state_;
    }

    const GuideRate& guideRate() const
    {
        return this->guide_rate_;
    }

    bool isRank0() const
    {
        return this->well_state_->isRank0();
    }

    bool isReservoirCouplingMaster() const { return rescoup_.isMaster(); }

    bool isReservoirCouplingMasterGroup(const Group& group) const { return rescoup_.isMasterGroup(group.name()); }

    bool isReservoirCouplingSlave() const { return rescoup_.isSlave(); }

    bool isReservoirCouplingSlaveGroup(const Group& group) const { return rescoup_.isSlaveGroup(group.name()); }

    constexpr int numPhases() const {
        return this->wellState().numPhases();
    }

    int phaseToActivePhaseIdx(const Phase phase) const;

    const PhaseUsageInfo<IndexTraits>& phaseUsage() const {
        return this->phase_usage_info_;
    }

    GroupStateGuard pushGroupState(GroupState<Scalar>& group_state)
    {
        return GroupStateGuard(*this, group_state);
    }

    /// @brief Push a new logger onto the stack with auto-cleanup on destruction
    ///
    /// @details Creates a new DeferredLogger and pushes it onto the logger stack.
    /// When the returned guard goes out of scope:
    /// 1. If do_mpi_gather is true: Messages are gathered across MPI ranks via gatherDeferredLogger()
    ///    and logged on rank 0 (if terminal_output is enabled)
    /// 2. If do_mpi_gather is false: Messages are logged locally without MPI gather
    /// 3. The previous logger is restored
    ///
    /// @param do_mpi_gather If true (default), gather messages across MPI ranks on destruction.
    ///        Set to false when called from contexts where MPI synchronization is not
    ///        possible (e.g., NLDD domain-local operations called at different times on different ranks).
    /// @return RAII guard that owns the logger and handles cleanup
    ScopedLoggerGuard pushLogger(bool do_mpi_gather = true) const
    {
        return ScopedLoggerGuard(*this, do_mpi_gather);
    }

    WellStateGuard pushWellState(WellState<Scalar, IndexTraits>& well_state)
    {
        return WellStateGuard(*this, well_state);
    }

    int reportStepIdx() const
    {
        return report_step_;
    }

    const Schedule& schedule() const
    {
        return this->schedule_;
    }

    ReservoirCoupling::Proxy<Scalar>& rescoup() {
        return rescoup_;
    }

    const ReservoirCoupling::Proxy<Scalar>& rescoup() const {
        return rescoup_;
    }

    ReservoirCouplingMaster<Scalar>& reservoirCouplingMaster() {
        return rescoup_.master();
    }

    const ReservoirCouplingMaster<Scalar>& reservoirCouplingMaster() const {
        return rescoup_.master();
    }

    ReservoirCouplingSlave<Scalar>& reservoirCouplingSlave() {
        return rescoup_.slave();
    }
    const ReservoirCouplingSlave<Scalar>& reservoirCouplingSlave() const {
        return rescoup_.slave();
    }

#ifdef RESERVOIR_COUPLING_ENABLED
    void setReservoirCouplingMaster(ReservoirCouplingMaster<Scalar>* master) {
        rescoup_.setMaster(master);
    }
    void setReservoirCouplingSlave(ReservoirCouplingSlave<Scalar>* slave) {
        rescoup_.setSlave(slave);
    }
#endif

    void setCmodeGroup(const Group& group);

    template <class AverageRegionalPressureType>
    void
    setRegionAveragePressureCalculator(const Group& group,
                                       const FieldPropsManager& fp,
                                       std::map<std::string, std::unique_ptr<AverageRegionalPressureType>>&
                                           regional_average_pressure_calculator) const;

    void setReportStep(int report_step)
    {
        report_step_ = report_step;
    }

    const SummaryState& summaryState() const
    {
        return this->summary_state_;
    }

    Scalar sumSolventRates(const Group& group, const bool is_injector) const;

    Scalar sumWellResRates(const Group& group, const int phase_pos, const bool injector) const;

    Scalar sumWellSurfaceRates(const Group& group, const int phase_pos, const bool injector) const;

    Scalar sumWellPhaseRates(bool res_rates,
                             const Group& group,
                             const int phase_pos,
                             const bool injector,
                             const bool network = false) const;

    bool terminalOutput() const
    {
         return this->terminal_output_;
    }

    template <class RegionalValues>
    void updateGpMaintTargetForGroups(const Group& group,
                                      const RegionalValues& regional_values,
                                      const double dt);

    /// update the number of wells that are actively under group control for a given group with name given by
    /// group_name its main usage is to detect cases where there is no wells under group control
    int updateGroupControlledWells(const bool is_production_group,
                                   const Phase injection_phase);

    void updateGroupProductionRates(const Group& group);

    void updateGroupTargetReduction(const Group& group,
                                    const bool is_injector);

    void updateNetworkLeafNodeProductionRates();

    /// @brief Set production control to NONE for groups not targeting any well.
    ///
    /// For each group in the production controls map, checks whether any open
    /// producer well has GRUP control with that group as its target. If not,
    /// the group's production control is set to NONE. This is needed to get correct summary
    /// output for GMCTP/FMCTP, see https://github.com/OPM/opm-simulators/pull/6596 for more details.
    ///
    /// Exceptions (groups excluded from the NONE reset):
    /// - Groups specified for gas lift optimization (GLO)
    /// - RC master hierarchy groups (master groups + ancestors up to FIELD),
    ///   which actively distribute targets to slave groups
    void updateNONEProductionGroups();

    void updateREINForGroups(const Group& group, bool sum_rank);

    void updateReservoirRatesInjectionGroups(const Group& group);

#ifdef RESERVOIR_COUPLING_ENABLED
    /// @brief Update the slave's GroupState cmodes from the master's active cmodes.
    ///
    /// For each slave group with a master-imposed target, sets the GroupState
    /// production/injection control mode to match the master's cmode. This ensures
    /// that all downstream consumers (constraint checks, guide rate fractions,
    /// well equation assembly) evaluate the correct rate type.
    ///
    /// The update is skipped when the GRUPSLAV filter flag is SLAV, meaning
    /// the slave ignores the master's control for that rate type.
    void updateSlaveGroupCmodesFromMaster();
#endif

    void updateState(WellState<Scalar, IndexTraits>& well_state, GroupState<Scalar>& group_state);

    void updateSurfaceRatesInjectionGroups(const Group& group);

    void updateVREPForGroups(const Group& group);

    void updateWellRates(const Group& group,
                         const WellState<Scalar, IndexTraits>& well_state_nupcol,
                         WellState<Scalar, IndexTraits>& well_state) const;

    const WellState<Scalar, IndexTraits>& wellState() const
    {
        return *this->well_state_;
    }

    void updateWellRatesFromGroupTargetScale(const Scalar scale,
                                             const Group& group,
                                             bool is_injector,
                                             WellState<Scalar, IndexTraits>& well_state) const;

    /// Returns the name of the worst offending well and its fraction (i.e. violated_phase / preferred_phase)
    std::pair<std::optional<std::string>, Scalar>
    worstOffendingWell(const Group& group,
                       const Group::ProductionCMode& offended_control) const;

private:

    //! \brief Calculate group target by applying local rate adjustments and guide rate fractions through group hierarchy.
    //!
    //! This method encapsulates the core algorithm for calculating the portion of a top-level
    //! group target that should be assigned to a bottom group (well or sub-group). The algorithm:
    //! 1. Iterates through the group chain from top to bottom
    //! 2. At each level with a guide rate (or at the top level), subtracts local reductions (the amount of rate that is not available for the group to control)
    //! 3. At the local_reduction_level, adds back the bottom group's rate (if do_addback is true)
    //! 4. Multiplies by the guide rate fraction for each level
    //!
    //! \tparam ReductionLambda Callable type: (const std::string& group_name) -> Scalar
    //! \tparam FractionLambda Callable type: (const std::string& child) -> Scalar
    //! \param chain Group chain from control group (top) to bottom group
    //! \param orig_target Original target from top-level controlling group
    //! \param current_rate_available Current rate of the bottom group (for add-back)
    //! \param local_reduction_level Level where local reduction/add-back is applied
    //! \param is_production_group True for production, false for injection
    //! \param injection_phase Phase for injection groups (ignored for production)
    //! \param local_reduction_lambda Functor to get local reduction for a group
    //! \param local_fraction_lambda Functor to get local fraction for a child group
    //! \param do_addback Whether to perform add-back at local_reduction_level
    //! \return Target after applying reductions and fractions (before efficiency factor division)
    template<typename ReductionLambda, typename FractionLambda>
    Scalar applyReductionsAndFractions_(const std::vector<std::string>& chain,
                                        Scalar orig_target,
                                        Scalar current_rate_available,
                                        std::size_t local_reduction_level,
                                        bool is_production_group,
                                        Phase injection_phase,
                                        ReductionLambda&& local_reduction_lambda,
                                        FractionLambda&& local_fraction_lambda,
                                        bool do_addback) const;

    std::pair<Group::ProductionCMode, Scalar>
    checkProductionRateConstraint_(const Group& group,
                                   Group::ProductionCMode cmode,
                                   Group::ProductionCMode currentControl,
                                   Scalar target,
                                   Scalar current_rate) const;

    /// @brief Collect groups that provide production targets to open wells.
    ///
    /// Scans all wells on this rank: for each open producer with GRUP control,
    /// adds its group_target to the set. The result is rank-local and must be
    /// synchronized via MPI before use.
    ///
    /// @return Set of group names targeted by wells on this rank
    std::unordered_set<std::string> collectTargetedProductionGroups_() const;

    //! \brief Compute partial efficiency factor for addback calculation.
    //!
    //! The addback in constraint checking must use the partial efficiency factor
    //! (from local_reduction_level down to the entity), not the accumulated efficiency
    //! (from the entity to the control group). This helper multiplies efficiency
    //! factors from local_reduction_level+1 to the entity.
    //!
    //! \param chain The group chain from control group (top) to entity (bottom)
    //! \param local_reduction_level The level at which addback is applied
    //! \return The partial efficiency factor for addback
    Scalar computeAddbackEfficiency_(const std::vector<std::string>& chain,
                                     std::size_t local_reduction_level) const;

    std::string controlGroup_(const Group& group) const;

    GuideRate::RateVector getGuideRateVector_(const std::vector<Scalar>& rates) const;

    Scalar getInjectionGroupTargetForMode_(const Group& group,
        const Phase& injection_phase,
        const std::vector<Scalar>& resv_coeff,
        const Group::InjectionCMode cmode) const;

    //! \brief Find the local reduction level in a group chain.
    //!
    //! The local reduction level is the deepest level in the chain (starting from level 1)
    //! where a group has both a guide rate and group-controlled wells (GCW > 0).
    //! This level determines where reductions are applied and add-back occurs.
    //!
    //! \param chain Group chain from control group (top) to bottom group
    //! \param is_production_group True for production, false for injection
    //! \param injection_phase Phase for injection groups (ignored for production)
    //! \return The local reduction level (0 if no intermediate group qualifies)
    std::size_t getLocalReductionLevel_(const std::vector<std::string>& chain,
        bool is_production_group,
        Phase injection_phase) const;

    Scalar getProductionConstraintTarget_(const Group& group,
                                          Group::ProductionCMode cmode,
                                          const Group::ProductionControls& controls) const;

    Scalar getProductionGroupTargetForMode_(const Group& group, const Group::ProductionCMode cmode) const;

    Scalar getSatelliteRate_(const Group& group,
                             const int phase_pos,
                             const bool res_rates,
                             const bool is_injector) const;

    /// Check if a production auto choke group is underperforming its target rate.
    /// Returns true if the group's current rate is below its allocated target,
    /// which means wells should be excluded from the GCW count.
    bool isAutoChokeGroupUnderperforming_(const Group& group) const;

    /// check if well/group bottom is a sub well/group of the group top
    bool isInGroupChainTopBot_(const std::string& bottom, const std::string& top) const;

    bool isSatelliteGroup_(const Group& group) const;

    Scalar satelliteInjectionRate_(const ScheduleState& sched,
                                   const Group& group,
                                   const int phase_pos,
                                   bool res_rates) const;

    Scalar satelliteProductionRate_(const ScheduleState& sched,
                                    const Group& group,
                                    const GSatProd::GSatProdGroupProp::Rate rate_comp,
                                    bool res_rates) const;

    std::optional<GSatProd::GSatProdGroupProp::Rate> selectRateComponent_(const int phase_pos) const;

    //! \brief Subtract other-phase reservoir injection rates from a base rate.
    //!
    //! For multi-phase injection groups under RESV or VREP control, the total
    //! reservoir volume target must be reduced by the reservoir volumes already
    //! injected by other phases, leaving only this phase's share.
    //!
    //! \param injection_phase The controlled injection phase
    //! \param base_reservoir_rate The total reservoir rate target before subtraction
    //! \param group_injection_reservoir_rates Per-phase reservoir injection rates
    //! \return The base rate minus other phases' reservoir injection contributions
    Scalar subtractOtherPhaseResvInjection_(
        Phase injection_phase,
        Scalar base_reservoir_rate,
        const std::vector<Scalar>& group_injection_reservoir_rates) const;

    Scalar sumProductionRateForControlMode_(const Group& group, Group::ProductionCMode cmode) const;

    int updateGroupControlledWellsRecursive_(const std::string& group_name,
                                             const bool is_production_group,
                                             const Phase injection_phase);

    void updateGroupTargetReductionRecursive_(const Group& group,
                                              const bool is_injector,
                                              std::vector<Scalar>& group_target_reduction);

   // Validate that GCONINJE top-up phases (RESV/VREP) are consistent
    // across the group hierarchy. Called once from setCmodeGroup() for FIELD.
    void validateInjectionTopupPhases_(const Group& group) const;

    // Recursive helper for validateInjectionTopupPhases_()
    void validateInjectionTopupPhasesRecursive_(const Group& group,
                                                std::optional<Phase> inherited_topup_phase,
                                                const std::string& inherited_topup_group) const;

    // --- Reservoir coupling private methods ---
#ifdef RESERVOIR_COUPLING_ENABLED
    /// @brief Convert active phase index to ReservoirCoupling::Phase enum
    /// @param phase_pos Active phase index (0, 1, or 2 in a 3-phase model)
    /// @return The corresponding ReservoirCoupling::Phase enum value
    /// @note This uses the canonical phase ordering (Oil=0, Gas=1, Water=2)
    ReservoirCoupling::Phase activePhaseIdxToRescoupPhase_(int phase_pos) const;

    /// @brief Collect all groups in the RC master group hierarchy.
    ///
    /// Starting from each master group (identified via GRUPMAST), walks up
    /// via group.parent() to FIELD, adding all groups along the path.
    /// These groups actively distribute targets to slave groups and must
    /// not have their production control reset to NONE.
    ///
    /// @return Set of group names in the master group hierarchy
    std::unordered_set<std::string> collectMasterGroupHierarchy_() const;

    /// @brief Get the effective production limit for a group and rate type,
    /// combining master limit, slave-local target, and GRUPSLAV filter flag.
    ///
    /// If the master sent a per-rate-type limit for this group and rate type,
    /// the filter flag determines which value to use:
    /// - MAST: return master limit
    /// - BOTH: return min(master limit, slave_local_target)
    /// - SLAV: return slave_local_target
    ///
    /// @param gname Slave group name
    /// @param rate_type The production rate type to check
    /// @param slave_local_target The slave's own target from GCONPROD
    /// @return The effective limit to apply
    Scalar getEffectiveProductionLimit_(const std::string& gname,
                                        Group::ProductionCMode rate_type,
                                        Scalar slave_local_target) const;

    ReservoirCoupling::GrupSlav::FilterFlag getInjectionFilterFlag_(const std::string& group_name,
                                                                    const Phase injection_phase) const;

    ReservoirCoupling::GrupSlav::FilterFlag getProductionFilterFlag_(const std::string& group_name,
                                                                     const Group::ProductionCMode cmode) const;

    Scalar getReservoirCouplingMasterGroupRate_(const Group& group,
                                                const int phase_pos,
                                                const ReservoirCoupling::RateKind kind) const;
#endif  // RESERVOIR_COUPLING_ENABLED

    const WellState<Scalar, IndexTraits>* well_state_ {nullptr};
    GroupState<Scalar>* group_state_ {nullptr};
    const Schedule& schedule_;
    const SummaryState& summary_state_;
    const GuideRate& guide_rate_;
    // NOTE: The deferred logger does not change the object "meaningful" state, so it should be ok to
    //   make it mutable and store a pointer to it here.
    mutable DeferredLogger* deferred_logger_ {nullptr};
    // NOTE: The phase usage info seems to be read-only throughout the simulation, so it should be safe
    // to store a reference to it here.
    const PhaseUsageInfo<IndexTraits>& phase_usage_info_;
    const Parallel::Communication& comm_;
    bool terminal_output_ {false};
    int report_step_ {0};
    ReservoirCoupling::Proxy<Scalar> rescoup_{};
};

// -----------------------------------------------------------------------------
// Template member function implementations
// -----------------------------------------------------------------------------

// NOTE: This template member function is defined here in the header because the
//       AverageRegionalPressureType type parameter depends on derived class types that are
//       not available when GroupStateHelper.cpp is compiled. Template functions
//       must be visible at their instantiation point.
//       See GroupStateHelper.cpp for detailed rationale.
template <typename Scalar, typename IndexTraits>
template <class AverageRegionalPressureType>
void
GroupStateHelper<Scalar, IndexTraits>::setRegionAveragePressureCalculator(
    const Group& group,
    const FieldPropsManager& fp,
    std::map<std::string, std::unique_ptr<AverageRegionalPressureType>>& regional_average_pressure_calculator)
    const
{
    for (const std::string& groupName : group.groups()) {
        this->setRegionAveragePressureCalculator(this->schedule_.getGroup(groupName, this->report_step_),
                                                 fp,
                                                 regional_average_pressure_calculator);
    }
    const auto& gpm = group.gpmaint();
    if (!gpm)
        return;

    const auto& reg = gpm->region();
    if (!reg)
        return;

    if (regional_average_pressure_calculator.count(reg->first) == 0) {
        const std::string name = (reg->first.rfind("FIP", 0) == 0) ? reg->first : "FIP" + reg->first;
        const auto& fipnum = fp.get_int(name);
        regional_average_pressure_calculator[reg->first]
            = std::make_unique<AverageRegionalPressureType>(fipnum);
    }
}

// NOTE: This template member function is defined in the header because the
//       RegionalValues type parameter depends on derived class types that are
//       not available when GroupStateHelper.cpp is compiled. Template functions
//       must be visible at their instantiation point.
//       See GroupStateHelper.cpp for detailed rationale.
template <typename Scalar, typename IndexTraits>
template <class RegionalValues>
void
GroupStateHelper<Scalar, IndexTraits>::updateGpMaintTargetForGroups(const Group& group,
                                                                   const RegionalValues& regional_values,
                                                                   const double dt)
{
    OPM_TIMEFUNCTION();
    for (const std::string& group_name : group.groups()) {
        const Group& group_tmp = this->schedule_.getGroup(group_name, this->report_step_);
        this->updateGpMaintTargetForGroups(group_tmp, regional_values, dt);
    }
    const auto& gpm = group.gpmaint();
    if (!gpm)
        return;

    const auto& region = gpm->region();
    if (!region)
        return;
    if (this->isReservoirCouplingMasterGroup(group)) {
        // GPMAINT is not supported for reservoir coupling master groups since master groups do not have
        //   subordinate wells in the master reservoir, so the slaves cannot influence the master reservoir's
        //   average pressure.
        //   Even specifying GPMAINT on a group superior to the master group might not make sense, since if the
        //   superior target is distributed down to the master group with guide rate fractions, adjusting
        //   the master group's target (that is sent to the slave) could only indirectly influence the master
        //   reservoir's average pressure by affecting the guide rate fractions distributed to actual wells
        //   in the master reservoir.
        OPM_DEFLOG_THROW(
            std::runtime_error,
            "GPMAINT is not supported for reservoir coupling master groups.",
            this->deferredLogger()
        );
        return;
    }
    else if (this->isReservoirCouplingSlaveGroup(group)) {
        // GPMAINT is not supported for reservoir coupling slave groups since their targets will be overridden
        //   by the corresponding master group's target anyway.
        OPM_DEFLOG_THROW(
            std::runtime_error,
            "GPMAINT is not supported for reservoir coupling slave groups.",
            this->deferredLogger()
        );
        return;
    }
    const auto [name, number] = *region;
    const Scalar error = gpm->pressure_target() - regional_values.at(name)->pressure(number);
    Scalar current_rate = 0.0;
    const auto& pu = this->phase_usage_info_;
    bool injection = true;
    Scalar sign = 1.0;
    switch (gpm->flow_target()) {
    case GPMaint::FlowTarget::RESV_PROD: {
        current_rate = -this->groupState().injection_vrep_rate(group.name());
        injection = false;
        sign = -1.0;
        break;
    }
    case GPMaint::FlowTarget::RESV_OINJ: {
        if (pu.phaseIsActive(IndexTraits::oilPhaseIdx)) {
            const auto io = pu.canonicalToActivePhaseIdx(IndexTraits::oilPhaseIdx);
            current_rate = this->groupState().injection_reservoir_rates(group.name())[io];
        }
        break;
    }
    case GPMaint::FlowTarget::RESV_WINJ: {
        if (pu.phaseIsActive(IndexTraits::waterPhaseIdx)) {
            const auto iw = pu.canonicalToActivePhaseIdx(IndexTraits::waterPhaseIdx);
            current_rate = this->groupState().injection_reservoir_rates(group.name())[iw];
        }
        break;
    }
    case GPMaint::FlowTarget::RESV_GINJ: {
        if (pu.phaseIsActive(IndexTraits::gasPhaseIdx)) {
            const auto ig = pu.canonicalToActivePhaseIdx(IndexTraits::gasPhaseIdx);
            current_rate = this->groupState().injection_reservoir_rates(group.name())[ig];
        }
        break;
    }
    case GPMaint::FlowTarget::SURF_OINJ: {
        if (pu.phaseIsActive(IndexTraits::oilPhaseIdx)) {
            const auto io = pu.canonicalToActivePhaseIdx(IndexTraits::oilPhaseIdx);
            current_rate = this->groupState().injection_surface_rates(group.name())[io];
        }
        break;
    }
    case GPMaint::FlowTarget::SURF_WINJ: {
        if (pu.phaseIsActive(IndexTraits::waterPhaseIdx)) {
            const auto iw = pu.canonicalToActivePhaseIdx(IndexTraits::waterPhaseIdx);
            current_rate = this->groupState().injection_surface_rates(group.name())[iw];
        }
        break;
    }
    case GPMaint::FlowTarget::SURF_GINJ: {
        if (pu.phaseIsActive(IndexTraits::gasPhaseIdx)) {
            const auto ig = pu.canonicalToActivePhaseIdx(IndexTraits::gasPhaseIdx);
            current_rate = this->groupState().injection_surface_rates(group.name())[ig];
        }
        break;
    }
    default:
        throw std::invalid_argument("Invalid Flow target type in GPMAINT");
    }
    auto& gpmaint_state = this->groupState().gpmaint(group.name());
    // we only activate gpmaint if pressure is lower than the target regional pressure for injectors
    // (i.e. error > 0) and higher for producers.
    bool activate = (injection && error > 0) || (!injection && error < 0);
    Scalar rate = 0.0;
    if (activate) {
        rate = gpm->rate(gpmaint_state, current_rate, error, dt);
    } else {
        gpm->resetState(gpmaint_state);
    }
    this->groupState().update_gpmaint_target(group.name(), std::max(Scalar {0.0}, sign * rate));
}

} // namespace Opm

#endif  // OPM_GROUPSTATE_HELPER_HEADER_INCLUDED
