!! Bottom drag for the ocean dynamical core.  Applies a momentum
!! sink at the bed-adjacent LIVE layer of each face — `ms%k_bot_u/v`,
!! the first layer live on both sides counting up from the bed (bed at
!! k=1, surface at k=nz).  `k_bot ≡ 1` on every coordinate without a
!! static bed filler, so this is the historical `k = 1` row there; under
!! `z_fixed` the layers below it are inert fillers the vdiff solve cuts
!! out of the column, and a drag on `k = 1` would never reach water.
!! Two variants supported via a tag:
!!
!!   BDRAG_LINEAR    — Rayleigh-style: du/dt = -r * u, where `r`
!!                     has units of 1/s.
!!   BDRAG_QUADRATIC — log-layer drag: du/dt = -C_d * |U_bot| *
!!                     u / h_bot.  Closure for the unresolved
!!                     bottom-boundary-layer drag.  C_d ~ 2.5e-3
!!                     is the standard MOM6 / ROMS default.
!!
!! Only the bottom layer feels the drag — the kernel writes into
!! its own tendency buffer (`du_drag`, `dv_drag`) and the apply
!! step adds `dt * tendency` to `u_face_x_layer` / `v_face_y_layer`,
!! matching the additive-tendency pattern of Coriolis, PGF, and hvisc.
module rdb_ocean_bottom_drag
   use rdb_constants, only: wp
   use rdb_grid, only: hgrid_t
   use rdb_multilayer_state, only: multilayer_state_t
   use rdb_ocean_metrics, only: ocean_metrics_t
   use rdb_scratch_3d, only: scratch_3d_buffer_t, &
                             scratch_3d_buffer_enter_data_impl, &
                             scratch_3d_buffer_exit_data_impl
   use, intrinsic :: iso_fortran_env, only: int64
   use rdb_mem_report, only: arr_bytes
   implicit none
   private

   public :: ocean_bottom_drag_t
   public :: ocean_bottom_drag_compute_tendencies
   public :: ocean_bottom_drag_apply_tendencies
   public :: ocean_channel_drag_compute_tendencies
   public :: ocean_channel_drag_apply_tendencies
   public :: parse_bdrag_variant
   public :: bdrag_variant_is_implemented

   real(wp), parameter :: SIDE_H_VANISH = 1.5e-4_wp
      !! Layer-thickness threshold below which a cross-stream neighbour
      !! layer counts as "blocked by sloping bathymetry" for the
      !! channel-drag perimeter fraction (matches the `H_VANISHED`
      !! dynamic-vanish role used elsewhere in the ocean core).

   integer, parameter, public :: BDRAG_LINEAR = 1
      !! Linear Rayleigh drag.  Use for analytic / regression tests
      !! where energy decay rate must be predictable.
   integer, parameter, public :: BDRAG_QUADRATIC = 2
      !! Log-layer / quadratic drag.  Production default.
   integer, parameter, public :: BDRAG_INVALID = -1
      !! Sentinel returned by `parse_bdrag_variant` for an unrecognised
      !! string (PR-6 fail-loud).  Linear (`τ = ρ·r·u`) and quadratic
      !! (`τ = ρ·C_d·|u|·u`) have different coefficient dimensions and
      !! energy-decay laws, so a typo must abort rather than silently
      !! pick one.

   type :: ocean_bottom_drag_t
      logical :: is_init = .false.
         !! True between `init` and `destroy`.
      integer :: variant = BDRAG_QUADRATIC
         !! Active drag variant.
      real(wp) :: r_linear = 0.0_wp
         !! Linear Rayleigh coefficient (1/s).  Zero disables the
         !! linear branch even if the variant tag selects it.
      real(wp) :: c_drag = 0.0_wp
         !! Quadratic-drag coefficient (dimensionless).  Zero
         !! disables the quadratic branch.  MOM6 / ROMS default
         !! is 2.5e-3.
      real(wp) :: h_min = 1.0e-3_wp
         !! Floor on the bottom-layer thickness inside the
         !! `u / h_bot` division — keeps the kernel finite when
         !! the bottom layer pinches out under ZSTAR_FULL.
      real(wp) :: hbbl = 0.0_wp
         !! Bottom-boundary-layer thickness (m) over which the drag
         !! is distributed (MOM6 HBBL).  When zero (default), the
         !! drag is applied to the bed layer only — historical
         !! behaviour, bit-identical to prior production.  Positive
         !! values activate the distributed-drag form: the stress
         !! is spread across the bottom `hbbl` metres so very thin
         !! bed layers don't see all the damping at once.
      real(wp) :: drag_bg_vel = 0.0_wp
         !! Background velocity floor (m/s) for the quadratic and
         !! distributed forms (MOM6 DRAG_BG_VEL).  The effective
         !! bottom speed used in the stress formula is
         !! `max(drag_bg_vel, |u_bbl|)` so laminar-bottom cells
         !! still see damping.  MOM6 production default 0.1 m/s.
         !! Zero (default) disables the floor.
      real(wp) :: bbl_thick_min = 0.0_wp
         !! Minimum effective BBL thickness (m) used in the
         !! `stress / h_bbl` denominator (MOM6 BBL_THICK_MIN).
         !! Guards against `hbbl - sum(h_bottom_layers)` going to
         !! zero when the bottom layers themselves are very thin.
         !! Zero (default) leaves no floor (falls back to `h_min`).
      real(wp) :: bed_factor = 1.0_wp
         !! Multiplier on the bed-layer (`k = k_bot`) drag tendency only —
         !! layers above it are unchanged.  Lets the bed get
         !! `bed_factor · r` while the surface keeps the nominal `r`
         !! (and via HBBL distribution typically gets none).  Default
         !! `1.0` keeps the historical drag bit-identical.  Driver
         !! writes from `cfg%ocean%bdrag%bed_factor` at init.  See
         !! `compute_distributed_drag` for the kernel-side application.
      logical :: implicit = .false.
         !! Backward-Euler (implicit) bottom drag when `.true.`:
         !! `u^{n+1} = u/(1 + dt·λ)`, unconditionally stable for any
         !! bottom-layer thickness (matches MOM6's implicit bottom-BC
         !! drag).  Default `.false.` = explicit forward-Euler tendency,
         !! bit-identical to the historical path but conditionally
         !! unstable on thin shelf bottom layers (`dt·λ > 1`).  Driver
         !! writes from `cfg%ocean%bdrag%implicit` at init.

      logical :: channel_drag = .false.
         !! Enable the per-layer lateral side-wall (channel) Rayleigh
         !! drag (MOM6 CHANNEL_DRAG analogue).  Default `.false.` ⇒ the
         !! channel-drag kernels are no-ops (bit-identical).  Driver
         !! writes from `cfg%ocean%bdrag%channel_drag` at init.
      real(wp) :: cdrag_side = 0.0_wp
         !! Side-wall drag coefficient (dimensionless).  Zero (default)
         !! ⇒ zero side-drag rate even when `channel_drag = .true.`.

      type(scratch_3d_buffer_t) :: du_drag
         !! Drag tendency at east faces, shape (nx+1, ny, nz).
         !! In the bed-only mode only `k = k_bot_u` (the face's first live
         !! layer, `1` off `z_fixed`) carries a non-zero value.
      type(scratch_3d_buffer_t) :: dv_drag
         !! Drag tendency at north faces, shape (nx, ny+1, nz).

      logical :: implicit_fold = .false.
         !! When `.true.`, also fill the bed-layer (`k = k_bot`) Rayleigh RATE
         !! fields `lambda_bot_u/v` (1/s) in `compute_tendencies` so the
         !! vdiff solver can fold the drag into its tridiagonal diagonal
         !! (`&ocean_vdiff_nml implicit_drag`).  The explicit `du_drag`
         !! tendency is then NOT applied by the driver (gated off) to avoid
         !! double-counting.  Default `.false.` ⇒ the rate fields stay zero
         !! and the explicit path is unchanged (bit-identical).  Driver sets
         !! this from `cfg%ocean%vdiff%implicit_drag` at configure.

      real(wp), allocatable :: lambda_bot_u(:, :)
         !! Bed-layer (`k = k_bot_u`) bottom-drag Rayleigh RATE λ (1/s) at
         !! east faces, shape (nx+1, ny).  `λ = c_d·|U_bbl|/h_kb` (quadratic) or
         !! `r` (linear), `|U|` frozen at uⁿ.  Consumed by `vdiff_apply_
         !! momentum` as the `+dt·λ` diagonal add when `implicit_drag`.
         !! Zero unless `implicit_fold = .true.`.
      real(wp), allocatable :: lambda_bot_v(:, :)
         !! Bed-layer bottom-drag Rayleigh rate λ (1/s) at north faces,
         !! shape (nx, ny+1).

      type(scratch_3d_buffer_t) :: lambda_side_u
         !! Per-layer side-drag Rayleigh RATE (1/s) at east faces,
         !! shape (nx+1, ny, nz).  Stored (not a tendency) so the apply
         !! can use the implicit form `u/(1+dt*lambda)`.  Zero where no
         !! lateral perimeter is blocked (all-wet / flat-bottom).
      type(scratch_3d_buffer_t) :: lambda_side_v
         !! Per-layer side-drag Rayleigh rate (1/s) at north faces,
         !! shape (nx, ny+1, nz).
   contains
      procedure, non_overridable :: init => ocean_bdrag_init
      procedure, non_overridable :: destroy => ocean_bdrag_destroy
      procedure, non_overridable :: enter_data => ocean_bdrag_enter_data
      procedure, non_overridable :: exit_data => ocean_bdrag_exit_data
      procedure, non_overridable :: bytes => ocean_bottom_drag_bytes
   end type ocean_bottom_drag_t

contains

   subroutine ocean_bdrag_init(this, grid, nz_ml)
      class(ocean_bottom_drag_t), intent(inout) :: this
      type(hgrid_t), intent(in) :: grid
      integer, intent(in), optional :: nz_ml
      integer :: nx, ny, nz

      nx = grid%nx_total
      ny = grid%ny_total
      nz = 1
      if (present(nz_ml)) nz = nz_ml

      call this%du_drag%init(nx + 1, ny, nz, "ocean_bdrag_du_drag")
      call this%dv_drag%init(nx, ny + 1, nz, "ocean_bdrag_dv_drag")
      allocate (this%lambda_bot_u(nx + 1, ny), source=0.0_wp)
      allocate (this%lambda_bot_v(nx, ny + 1), source=0.0_wp)
      call this%lambda_side_u%init(nx + 1, ny, nz, "ocean_bdrag_lambda_side_u")
      call this%lambda_side_v%init(nx, ny + 1, nz, "ocean_bdrag_lambda_side_v")
      this%is_init = .true.
   end subroutine ocean_bdrag_init

   subroutine ocean_bdrag_destroy(this)
      class(ocean_bottom_drag_t), intent(inout) :: this
      this%is_init = .false.
      call this%du_drag%destroy()
      call this%dv_drag%destroy()
      if (allocated(this%lambda_bot_u)) deallocate (this%lambda_bot_u)
      if (allocated(this%lambda_bot_v)) deallocate (this%lambda_bot_v)
      call this%lambda_side_u%destroy()
      call this%lambda_side_v%destroy()
   end subroutine ocean_bdrag_destroy

   subroutine ocean_bdrag_enter_data(this)
      class(ocean_bottom_drag_t), intent(inout) :: this
      select type (this)
      type is (ocean_bottom_drag_t)
         call ocean_bdrag_enter_data_impl(this)
      end select
   end subroutine ocean_bdrag_enter_data

   subroutine ocean_bdrag_enter_data_impl(this)
      type(ocean_bottom_drag_t), intent(inout) :: this
      call scratch_3d_buffer_enter_data_impl(this%du_drag)
      call scratch_3d_buffer_enter_data_impl(this%dv_drag)
      !$acc enter data copyin(this%lambda_bot_u, this%lambda_bot_v)
      call scratch_3d_buffer_enter_data_impl(this%lambda_side_u)
      call scratch_3d_buffer_enter_data_impl(this%lambda_side_v)
   end subroutine ocean_bdrag_enter_data_impl

   subroutine ocean_bdrag_exit_data(this)
      class(ocean_bottom_drag_t), intent(inout) :: this
      select type (this)
      type is (ocean_bottom_drag_t)
         call ocean_bdrag_exit_data_impl(this)
      end select
   end subroutine ocean_bdrag_exit_data

   subroutine ocean_bdrag_exit_data_impl(this)
      type(ocean_bottom_drag_t), intent(inout) :: this
      call scratch_3d_buffer_exit_data_impl(this%du_drag)
      call scratch_3d_buffer_exit_data_impl(this%dv_drag)
      !$acc exit data delete(this%lambda_bot_u, this%lambda_bot_v)
      call scratch_3d_buffer_exit_data_impl(this%lambda_side_u)
      call scratch_3d_buffer_exit_data_impl(this%lambda_side_v)
   end subroutine ocean_bdrag_exit_data_impl

   pure subroutine ocean_bottom_drag_compute_tendencies(grid, this, ms, dt)
      !! Fill `du_drag` / `dv_drag` with the bottom-layer drag
      !! acceleration on each face's first LIVE layer `k_bot_u/v`
      !! (`≡ 1` off `z_fixed`); every other layer gets zero in the
      !! bed-only mode (default).  When `hbbl > 0` the stress is distributed
      !! across the bottom-most `hbbl` metres — every layer with
      !! `cumulative_depth_from_bed_top ≤ hbbl` gets a proportional
      !! share of the drag tendency.
      !!
      !! `implicit=.true.` (knob, default .false.) makes the drag
      !! BACKWARD-EULER in the dragged velocity: a per-face/-layer rate
      !! `λ` (= `c_d·|U|/h` etc.) gives `u^{n+1} = u/(1+dt·λ)`, formed by
      !! the tendency `-λ·u/(1+dt·λ)` so the standalone `u += dt·du_drag`
      !! apply reproduces it exactly.  Unconditionally stable for ANY h
      !! (matches MOM6's implicit bottom-BC drag); the explicit form
      !! (default) is conditionally unstable on thin bottom layers
      !! (`λ·dt > 1`).  `dt` is unused in the explicit branch.
      !!
      !! Wall faces (i=1, i=nx+1 for u; j=1, j=ny+1 for v) get a
      !! zero tendency — they don't move under drag because they
      !! don't move at all under any kernel here.
      type(hgrid_t), intent(in) :: grid
      type(ocean_bottom_drag_t), intent(inout) :: this
      type(multilayer_state_t), intent(in) :: ms
      real(wp), intent(in) :: dt
         !! Outer-step length (s); only read when `this%implicit`.

      integer :: i, j, k, nx, ny, nz, kb
      real(wp) :: r, c_d, h_floor, u_bot, v_bot, h_face, u_at_v, v_at_u
      real(wp) :: speed_at_u, speed_at_v, lam, dt_imp
      real(wp) :: hbbl, bg_vel, bbl_min
      logical :: implicit_drag, fold

      nx = grid%nx_total
      ny = grid%ny_total
      nz = ms%nz_ml
      r = this%r_linear
      c_d = this%c_drag
      h_floor = this%h_min
      hbbl = this%hbbl
      bg_vel = this%drag_bg_vel
      bbl_min = this%bbl_thick_min
      if (bbl_min <= 0.0_wp) bbl_min = h_floor
      implicit_drag = this%implicit
      dt_imp = merge(dt, 0.0_wp, implicit_drag)  ! 0 ⇒ explicit (bit-identical)
      fold = this%implicit_fold

      ! ---- Zero every level first; only k = k_bot will get filled ----
      do concurrent(k=1:nz, j=1:ny, i=1:nx + 1)
         this%du_drag%data(i, j, k) = 0.0_wp
      end do
      do concurrent(k=1:nz, j=1:ny + 1, i=1:nx)
         this%dv_drag%data(i, j, k) = 0.0_wp
      end do
      ! Always re-zero the implicit-fold bed-rate fields so a face that
      ! left the wet stencil between steps doesn't carry a stale rate.
      do concurrent(j=1:ny, i=1:nx + 1)
         this%lambda_bot_u(i, j) = 0.0_wp
      end do
      do concurrent(j=1:ny + 1, i=1:nx)
         this%lambda_bot_v(i, j) = 0.0_wp
      end do

      ! ---- Bed-only implicit-fold rate fields (k = k_bot) ----
      ! λ is the Rayleigh RATE the vdiff diagonal consumes (`+dt·λ`): r
      ! (linear) or c_d·|U_bbl|/h_kb (quadratic, |U| frozen at uⁿ), all
      ! read on the face's first LIVE layer `kb = k_bot_u/v` — the SAME
      ! row `diffuse_velocity_columns_impl` adds it to.  Filled here from
      ! the SAME stencil that forms du_drag so there is one drag algebra.
      ! Configure forbids hbbl>0 + fold, so the bed-only 2D field is
      ! sufficient.  Zero where the face touches land (mask).
      if (fold) then
         if (this%variant == BDRAG_LINEAR .and. r > 0.0_wp) then
            do concurrent(j=1:ny, i=2:nx)
               this%lambda_bot_u(i, j) = min(ms%wet_mask(i - 1, j), ms%wet_mask(i, j))*r
            end do
            do concurrent(j=2:ny, i=1:nx)
               this%lambda_bot_v(i, j) = min(ms%wet_mask(i, j - 1), ms%wet_mask(i, j))*r
            end do
         else if (this%variant == BDRAG_QUADRATIC .and. c_d > 0.0_wp) then
            do concurrent(j=1:ny, i=2:nx) local(kb, u_bot, v_at_u, h_face, speed_at_u)
               kb = ms%k_bot_u(i, j)
               u_bot = ms%u_face_x_layer(i, j, kb)
               v_at_u = 0.25_wp*( &
                        ms%v_face_y_layer(i - 1, j, kb) + ms%v_face_y_layer(i, j, kb) + &
                        ms%v_face_y_layer(i - 1, j + 1, kb) + ms%v_face_y_layer(i, j + 1, kb))
               h_face = max(0.5_wp*(ms%h_layer(i - 1, j, kb) + ms%h_layer(i, j, kb)), h_floor)
               speed_at_u = sqrt(u_bot*u_bot + v_at_u*v_at_u)
               this%lambda_bot_u(i, j) = min(ms%wet_mask(i - 1, j), ms%wet_mask(i, j))* &
                                         c_d*speed_at_u/h_face
            end do
            do concurrent(j=2:ny, i=1:nx) local(kb, v_bot, u_at_v, h_face, speed_at_v)
               kb = ms%k_bot_v(i, j)
               v_bot = ms%v_face_y_layer(i, j, kb)
               u_at_v = 0.25_wp*( &
                        ms%u_face_x_layer(i, j - 1, kb) + ms%u_face_x_layer(i + 1, j - 1, kb) + &
                        ms%u_face_x_layer(i, j, kb) + ms%u_face_x_layer(i + 1, j, kb))
               h_face = max(0.5_wp*(ms%h_layer(i, j - 1, kb) + ms%h_layer(i, j, kb)), h_floor)
               speed_at_v = sqrt(v_bot*v_bot + u_at_v*u_at_v)
               this%lambda_bot_v(i, j) = min(ms%wet_mask(i, j - 1), ms%wet_mask(i, j))* &
                                         c_d*speed_at_v/h_face
            end do
         end if
      end if

      ! ---- HBBL-distributed branch (MOM6 LINEAR_DRAG / BBL_THICK_MIN) ----
      if (hbbl > 0.0_wp) then
         call compute_distributed_drag(this%du_drag%data, this%dv_drag%data, &
                                       ms%u_face_x_layer, ms%v_face_y_layer, &
                                       ms%h_layer, ms%wet_mask, ms%k_bot_u, ms%k_bot_v, &
                                       this%variant, r, c_d, hbbl, bg_vel, bbl_min, &
                                       this%bed_factor, dt_imp, &
                                       size(ms%u_face_x_layer, 1), size(ms%u_face_x_layer, 2), &
                                       size(ms%v_face_y_layer, 1), size(ms%v_face_y_layer, 2), &
                                       nx, ny, nz)
         return
      end if

      ! Face wet-mask: drag only fires at faces between two ocean cells.
      ! `min(wet_left, wet_right)` zeros the tendency at any face that
      ! touches land.  All-1.0 mask (analytical tests) is a no-op.

      ! ---- Linear branch ----
      ! Implicit (dt_imp>0): u^{n+1}=u/(1+dt·r) via tendency -r·u/(1+dt·r).
      if (this%variant == BDRAG_LINEAR .and. r > 0.0_wp) then
         do concurrent(j=1:ny, i=2:nx) local(kb, u_bot)
            kb = ms%k_bot_u(i, j)
            u_bot = ms%u_face_x_layer(i, j, kb)
            this%du_drag%data(i, j, kb) = &
               min(ms%wet_mask(i - 1, j), ms%wet_mask(i, j))*(-r*u_bot/(1.0_wp + dt_imp*r))
         end do
         do concurrent(j=2:ny, i=1:nx) local(kb, v_bot)
            kb = ms%k_bot_v(i, j)
            v_bot = ms%v_face_y_layer(i, j, kb)
            this%dv_drag%data(i, j, kb) = &
               min(ms%wet_mask(i, j - 1), ms%wet_mask(i, j))*(-r*v_bot/(1.0_wp + dt_imp*r))
         end do
         return
      end if

      ! ---- Quadratic branch ----
      if (this%variant == BDRAG_QUADRATIC .and. c_d > 0.0_wp) then
         ! du/dt = -C_d * |U| * u / h_bot, where |U| = sqrt(u^2 + v^2)
         ! evaluated at the same face.  For u-faces we average v from
         ! the four surrounding v-faces (standard C-grid stencil); for
         ! v-faces we average u from the four surrounding u-faces.
         do concurrent(j=1:ny, i=2:nx) &
            local(kb, u_bot, v_at_u, h_face, speed_at_u)
            kb = ms%k_bot_u(i, j)
            u_bot = ms%u_face_x_layer(i, j, kb)
            v_at_u = 0.25_wp*( &
                     ms%v_face_y_layer(i - 1, j, kb) + ms%v_face_y_layer(i, j, kb) + &
                     ms%v_face_y_layer(i - 1, j + 1, kb) + ms%v_face_y_layer(i, j + 1, kb))
            h_face = 0.5_wp*(ms%h_layer(i - 1, j, kb) + ms%h_layer(i, j, kb))
            h_face = max(h_face, h_floor)
            speed_at_u = sqrt(u_bot*u_bot + v_at_u*v_at_u)
            ! Implicit: denom h_face → h_face + dt·c_d·|U| ⇒ u/(1+dt·c_d·|U|/h).
            this%du_drag%data(i, j, kb) = &
               min(ms%wet_mask(i - 1, j), ms%wet_mask(i, j))* &
               (-c_d*speed_at_u*u_bot/(h_face + dt_imp*c_d*speed_at_u))
         end do
         do concurrent(j=2:ny, i=1:nx) &
            local(kb, v_bot, u_at_v, h_face, speed_at_v)
            kb = ms%k_bot_v(i, j)
            v_bot = ms%v_face_y_layer(i, j, kb)
            u_at_v = 0.25_wp*( &
                     ms%u_face_x_layer(i, j - 1, kb) + ms%u_face_x_layer(i + 1, j - 1, kb) + &
                     ms%u_face_x_layer(i, j, kb) + ms%u_face_x_layer(i + 1, j, kb))
            h_face = 0.5_wp*(ms%h_layer(i, j - 1, kb) + ms%h_layer(i, j, kb))
            h_face = max(h_face, h_floor)
            speed_at_v = sqrt(v_bot*v_bot + u_at_v*u_at_v)
            this%dv_drag%data(i, j, kb) = &
               min(ms%wet_mask(i, j - 1), ms%wet_mask(i, j))* &
               (-c_d*speed_at_v*v_bot/(h_face + dt_imp*c_d*speed_at_v))
         end do
      end if
   end subroutine ocean_bottom_drag_compute_tendencies

   pure subroutine compute_distributed_drag(du_drag, dv_drag, u_face, v_face, &
                                            h_layer, wet_mask, k_bot_u, k_bot_v, &
                                            variant, r, c_d, hbbl, bg_vel, bbl_min, &
                                            bed_factor, dt_imp, &
                                            nx_u, ny_u, nx_v, ny_v, nx, ny, nz)
      !! HBBL-distributed bottom drag.  Mirrors MOM6's LINEAR_DRAG
      !! and quadratic-with-HBBL formulations: the drag stress is
      !! spread across the bottom `hbbl` metres rather than dumped
      !! into the bed-most layer.
      !!
      !! Linear branch (`variant == BDRAG_LINEAR, r > 0`):
      !!   du_k/dt = -r · u_k · (h_in_bbl_k / h_face_k) · f_k
      !! Each layer that overlaps the BBL band gets a damping rate
      !! proportional to its fractional BBL coverage.  Integrating
      !! over k recovers the bulk MOM6 result `r · U_bbl`.
      !!
      !! Quadratic branch (`variant == BDRAG_QUADRATIC, c_d > 0`):
      !!   1. First pass per face: compute BBL-mean velocity
      !!      `U_bbl = Σ_k u_k · h_in_bbl_k / max(Σ_k h_in_bbl_k, bbl_min)`.
      !!   2. Effective speed `|U_eff| = max(bg_vel, |U_bbl|)`.
      !!   3. Per-layer apply:
      !!      du_k/dt = -c_d · |U_eff| · u_k · (h_in_bbl_k / h_face_k) · f_k /
      !!                 max(h_in_bbl_total, bbl_min)
      !!
      !! The band walk starts at the face's first LIVE layer counting up
      !! from the bed, `kb = k_bot_u/v` (`≡ 1` off `z_fixed` ⇒ the
      !! historical `k = 1` walk), so under `z_fixed` the inert bed
      !! fillers below it neither take a share of the drag nor count
      !! toward the band thickness.
      !!
      !! `f_k = bed_factor` for `k = kb` and `f_k = 1` otherwise — lets
      !! the bed layer carry stronger drag than the rest of the BBL
      !! while preserving HBBL-distribution shape for layers k>=2.
      !! Default `bed_factor = 1.0` ⇒ `f_k ≡ 1` ⇒ bit-identical to
      !! the pre-knob path.
      !!
      !! Flat-impl: explicit-shape dummies, no derived-type derefs in
      !! the device kernels.
      integer, intent(in) :: nx_u, ny_u, nx_v, ny_v, nx, ny, nz, variant
      real(wp), intent(in) :: r, c_d, hbbl, bg_vel, bbl_min, bed_factor
      real(wp), intent(in) :: dt_imp
         !! Implicit timestep: dt for backward-Euler drag, 0 for explicit.
      real(wp), intent(in)    :: u_face(nx_u, ny_u, nz), v_face(nx_v, ny_v, nz)
      real(wp), intent(in)    :: h_layer(nx, ny, nz)
      real(wp), intent(in)    :: wet_mask(nx, ny)
      integer, intent(in)     :: k_bot_u(nx_u, ny_u), k_bot_v(nx_v, ny_v)
         !! `ms%k_bot_u/v` — first live layer of the face, counting up.
      real(wp), intent(inout) :: du_drag(nx_u, ny_u, nz), dv_drag(nx_v, ny_v, nz)

      integer :: i, j, k, kb
      real(wp) :: cumul_h, h_face_k, h_in_bbl, mask_face, f_k
      real(wp) :: h_in_bbl_total, u_bbl_int, v_bbl_int, u_bbl, v_bbl
      real(wp) :: u_at_v, v_at_u, abs_U_eff, h_eff_denom

      ! Linear: per-layer independent — single pass walks k=kb upward,
      ! accumulating BBL thickness and applying the per-layer drag.
      ! Bed layer (k=kb) scaled by `bed_factor`; layers above unchanged.
      if (variant == BDRAG_LINEAR .and. r > 0.0_wp) then
         do concurrent(j=1:ny, i=2:nx) &
            local(k, kb, cumul_h, h_face_k, h_in_bbl, mask_face, f_k)
            mask_face = min(wet_mask(i - 1, j), wet_mask(i, j))
            kb = k_bot_u(i, j)
            cumul_h = 0.0_wp
            do k = kb, nz
               if (cumul_h >= hbbl) exit
               h_face_k = 0.5_wp*(h_layer(i - 1, j, k) + h_layer(i, j, k))
               if (h_face_k > 0.0_wp) then
                  h_in_bbl = max(0.0_wp, min(h_face_k, hbbl - cumul_h))
                  if (k == kb) then
                     f_k = bed_factor
                  else
                     f_k = 1.0_wp
                  end if
                  du_drag(i, j, k) = mask_face*(-r*u_face(i, j, k)*h_in_bbl/h_face_k)*f_k &
                                     /(1.0_wp + dt_imp*r*(h_in_bbl/h_face_k)*f_k)
                  cumul_h = cumul_h + h_face_k
               else
                  exit
               end if
            end do
         end do
         do concurrent(j=2:ny, i=1:nx) &
            local(k, kb, cumul_h, h_face_k, h_in_bbl, mask_face, f_k)
            mask_face = min(wet_mask(i, j - 1), wet_mask(i, j))
            kb = k_bot_v(i, j)
            cumul_h = 0.0_wp
            do k = kb, nz
               if (cumul_h >= hbbl) exit
               h_face_k = 0.5_wp*(h_layer(i, j - 1, k) + h_layer(i, j, k))
               if (h_face_k > 0.0_wp) then
                  h_in_bbl = max(0.0_wp, min(h_face_k, hbbl - cumul_h))
                  if (k == kb) then
                     f_k = bed_factor
                  else
                     f_k = 1.0_wp
                  end if
                  dv_drag(i, j, k) = mask_face*(-r*v_face(i, j, k)*h_in_bbl/h_face_k)*f_k &
                                     /(1.0_wp + dt_imp*r*(h_in_bbl/h_face_k)*f_k)
                  cumul_h = cumul_h + h_face_k
               else
                  exit
               end if
            end do
         end do
         return
      end if

      ! Quadratic with HBBL: need U_bbl, then apply.  Two-pass per
      ! face encoded as one DC with a sequential inner k-loop that
      ! does both passes (sum then write).
      if (variant == BDRAG_QUADRATIC .and. c_d > 0.0_wp) then
         do concurrent(j=2:ny, i=2:nx) &
            local(k, kb, cumul_h, h_face_k, h_in_bbl, mask_face, f_k, &
                  h_in_bbl_total, u_bbl_int, v_bbl_int, u_bbl, v_bbl, &
                  v_at_u, abs_U_eff, h_eff_denom)
            mask_face = min(wet_mask(i - 1, j), wet_mask(i, j))
            kb = k_bot_u(i, j)
            ! Pass 1: integrate u_face * h_in_bbl over the BBL band.
            cumul_h = 0.0_wp
            u_bbl_int = 0.0_wp
            v_bbl_int = 0.0_wp
            h_in_bbl_total = 0.0_wp
            do k = kb, nz
               if (cumul_h >= hbbl) exit
               h_face_k = 0.5_wp*(h_layer(i - 1, j, k) + h_layer(i, j, k))
               if (h_face_k <= 0.0_wp) exit
               h_in_bbl = max(0.0_wp, min(h_face_k, hbbl - cumul_h))
               u_bbl_int = u_bbl_int + u_face(i, j, k)*h_in_bbl
               v_at_u = 0.25_wp*( &
                        v_face(i - 1, j, k) + v_face(i, j, k) + &
                        v_face(i - 1, j + 1, k) + v_face(i, j + 1, k))
               v_bbl_int = v_bbl_int + v_at_u*h_in_bbl
               h_in_bbl_total = h_in_bbl_total + h_in_bbl
               cumul_h = cumul_h + h_face_k
            end do
            h_eff_denom = max(h_in_bbl_total, bbl_min)
            u_bbl = u_bbl_int/h_eff_denom
            v_bbl = v_bbl_int/h_eff_denom
            abs_U_eff = max(bg_vel, sqrt(u_bbl*u_bbl + v_bbl*v_bbl))
            ! Pass 2: apply stress per-layer.  Bed layer (k=kb) scaled
            ! by `bed_factor`; layers above unchanged.
            cumul_h = 0.0_wp
            do k = kb, nz
               if (cumul_h >= hbbl) exit
               h_face_k = 0.5_wp*(h_layer(i - 1, j, k) + h_layer(i, j, k))
               if (h_face_k <= 0.0_wp) exit
               h_in_bbl = max(0.0_wp, min(h_face_k, hbbl - cumul_h))
               if (k == kb) then
                  f_k = bed_factor
               else
                  f_k = 1.0_wp
               end if
               du_drag(i, j, k) = mask_face*( &
                                  -c_d*abs_U_eff*u_face(i, j, k)* &
                                  (h_in_bbl/h_face_k)/h_eff_denom)*f_k &
                                  /(1.0_wp + dt_imp*c_d*abs_U_eff*(h_in_bbl/h_face_k)/h_eff_denom*f_k)
               cumul_h = cumul_h + h_face_k
            end do
         end do
         do concurrent(j=2:ny, i=2:nx) &
            local(k, kb, cumul_h, h_face_k, h_in_bbl, mask_face, f_k, &
                  h_in_bbl_total, u_bbl_int, v_bbl_int, u_bbl, v_bbl, &
                  u_at_v, abs_U_eff, h_eff_denom)
            mask_face = min(wet_mask(i, j - 1), wet_mask(i, j))
            kb = k_bot_v(i, j)
            cumul_h = 0.0_wp
            u_bbl_int = 0.0_wp
            v_bbl_int = 0.0_wp
            h_in_bbl_total = 0.0_wp
            do k = kb, nz
               if (cumul_h >= hbbl) exit
               h_face_k = 0.5_wp*(h_layer(i, j - 1, k) + h_layer(i, j, k))
               if (h_face_k <= 0.0_wp) exit
               h_in_bbl = max(0.0_wp, min(h_face_k, hbbl - cumul_h))
               v_bbl_int = v_bbl_int + v_face(i, j, k)*h_in_bbl
               u_at_v = 0.25_wp*( &
                        u_face(i, j - 1, k) + u_face(i + 1, j - 1, k) + &
                        u_face(i, j, k) + u_face(i + 1, j, k))
               u_bbl_int = u_bbl_int + u_at_v*h_in_bbl
               h_in_bbl_total = h_in_bbl_total + h_in_bbl
               cumul_h = cumul_h + h_face_k
            end do
            h_eff_denom = max(h_in_bbl_total, bbl_min)
            u_bbl = u_bbl_int/h_eff_denom
            v_bbl = v_bbl_int/h_eff_denom
            abs_U_eff = max(bg_vel, sqrt(u_bbl*u_bbl + v_bbl*v_bbl))
            cumul_h = 0.0_wp
            do k = kb, nz
               if (cumul_h >= hbbl) exit
               h_face_k = 0.5_wp*(h_layer(i, j - 1, k) + h_layer(i, j, k))
               if (h_face_k <= 0.0_wp) exit
               h_in_bbl = max(0.0_wp, min(h_face_k, hbbl - cumul_h))
               if (k == kb) then
                  f_k = bed_factor
               else
                  f_k = 1.0_wp
               end if
               dv_drag(i, j, k) = mask_face*( &
                                  -c_d*abs_U_eff*v_face(i, j, k)* &
                                  (h_in_bbl/h_face_k)/h_eff_denom)*f_k &
                                  /(1.0_wp + dt_imp*c_d*abs_U_eff*(h_in_bbl/h_face_k)/h_eff_denom*f_k)
               cumul_h = cumul_h + h_face_k
            end do
         end do
      end if
   end subroutine compute_distributed_drag

   subroutine ocean_bottom_drag_apply_tendencies(this, ms, dt, no_wait)
      !! `no_wait` (optional, default .false.): when .true. the apply DC
      !! loops run on OpenACC queue 1 and the routine returns WITHOUT
      !! syncing, so the batched velocity-apply chain in `run_stage_split`
      !! `!$acc wait(1)`s ONCE.  Default ⇒ blocking (safe for the unsplit
      !! `run_stage`).  Not `pure` because of the async/wait directives.
      type(ocean_bottom_drag_t), intent(in) :: this
      type(multilayer_state_t), intent(inout) :: ms
      real(wp), intent(in) :: dt
      logical, intent(in), optional :: no_wait
      integer :: i, j, k, nx_face, ny_uface, nx_vface, ny_face, nz
      logical :: lwait

      lwait = .true.
      if (present(no_wait)) lwait = .not. no_wait

      nx_face = size(ms%u_face_x_layer, 1)
      ny_uface = size(ms%u_face_x_layer, 2)
      nx_vface = size(ms%v_face_y_layer, 1)
      ny_face = size(ms%v_face_y_layer, 2)
      nz = ms%nz_ml

      !$acc kernels async(1)
      do concurrent(k=1:nz, j=1:ny_uface, i=1:nx_face)
         ms%u_face_x_layer(i, j, k) = ms%u_face_x_layer(i, j, k) + &
                                      dt*this%du_drag%data(i, j, k)
      end do
      do concurrent(k=1:nz, j=1:ny_face, i=1:nx_vface)
         ms%v_face_y_layer(i, j, k) = ms%v_face_y_layer(i, j, k) + &
                                      dt*this%dv_drag%data(i, j, k)
      end do
      !$acc end kernels
      if (lwait) then
         !$acc wait(1)
      end if
   end subroutine ocean_bottom_drag_apply_tendencies

   ! =================================================================
   ! Channel (side-wall) drag — per-layer lateral Rayleigh drag
   ! =================================================================

   pure subroutine ocean_channel_drag_compute_tendencies(grid, metrics, this, ms)
      !! Per-layer lateral side-wall (channel) Rayleigh RATE
      !! (`lambda_side_u/v`, 1/s) for every velocity face whose
      !! cross-stream perimeter is partially blocked by land or a vanished
      !! (sloping-bathymetry) neighbour layer — fires at EVERY layer k that
      !! intersects the obstruction, not just the bed.
      !!   `f_blocked` = perimeter fraction blocked, summed over the two
      !!     flanking `wet_q` corners (each weighted 0.5), with a corner
      !!     also counted blocked when its two cross-stream cells' min
      !!     `h_layer < SIDE_H_VANISH`.
      !!   `lambda = cdrag_side·|U_face|·f_blocked/max(W, eps)`, W the
      !!     cross-stream face length (`dyCu` u-face, `dxCv` v-face).
      !! All-wet / flat-bottom ⇒ `f_blocked ≡ 0` ⇒ `lambda ≡ 0` (no-op);
      !! `channel_drag=.false.` or `cdrag_side=0` short-circuits to zero.
      type(hgrid_t), intent(in) :: grid
      type(ocean_metrics_t), intent(in) :: metrics
      type(ocean_bottom_drag_t), intent(inout) :: this
      type(multilayer_state_t), intent(in) :: ms

      integer :: nx, ny, nz

      nx = grid%nx_total
      ny = grid%ny_total
      nz = ms%nz_ml

      call compute_channel_drag_rates(this%lambda_side_u%data, this%lambda_side_v%data, &
                                      ms%u_face_x_layer, ms%v_face_y_layer, &
                                      ms%h_layer, metrics%wet_q, &
                                      metrics%dyCu, metrics%dxCv, &
                                      this%channel_drag, this%cdrag_side, &
                                      size(ms%u_face_x_layer, 1), size(ms%u_face_x_layer, 2), &
                                      size(ms%v_face_y_layer, 1), size(ms%v_face_y_layer, 2), &
                                      nx, ny, nz)
   end subroutine ocean_channel_drag_compute_tendencies

   pure subroutine compute_channel_drag_rates(lambda_u, lambda_v, u_face, v_face, &
                                              h_layer, wet_q, dyCu, dxCv, &
                                              channel_drag, cdrag_side, &
                                              nx_u, ny_u, nx_v, ny_v, nx, ny, nz)
      !! Device kernel for the per-layer side-drag Rayleigh rate.  See
      !! `ocean_channel_drag_compute_tendencies` for the derivation.
      !! `wet_q` is `(nx+1, ny+1)`; `dyCu` is `(nx+1, ny)` (u-face length
      !! normal to the zonal flow); `dxCv` is `(nx, ny+1)`.
      integer, intent(in) :: nx_u, ny_u, nx_v, ny_v, nx, ny, nz
      logical, intent(in) :: channel_drag
      real(wp), intent(in) :: cdrag_side
      real(wp), intent(in) :: u_face(nx_u, ny_u, nz), v_face(nx_v, ny_v, nz)
      real(wp), intent(in) :: h_layer(nx, ny, nz)
      real(wp), intent(in) :: wet_q(nx + 1, ny + 1)
      real(wp), intent(in) :: dyCu(nx + 1, ny), dxCv(nx, ny + 1)
      real(wp), intent(out) :: lambda_u(nx_u, ny_u, nz), lambda_v(nx_v, ny_v, nz)

      integer :: i, j, k
      real(wp) :: f_s, f_n, f_blocked, speed, width, v_at_u, u_at_v
      real(wp), parameter :: WIDTH_EPS = 1.0e-20_wp

      ! ---- Zero everywhere first (also the all-disabled short-circuit) ----
      do concurrent(k=1:nz, j=1:ny_u, i=1:nx_u)
         lambda_u(i, j, k) = 0.0_wp
      end do
      do concurrent(k=1:nz, j=1:ny_v, i=1:nx_v)
         lambda_v(i, j, k) = 0.0_wp
      end do
      if (.not. channel_drag) return
      if (cdrag_side <= 0.0_wp) return

      ! ---- u-faces: blocked by the two flanking corners (i,j),(i,j+1) ----
      ! Cross-stream cells at corner (i,j): T(i-1,j-1),T(i,j-1).
      ! Cross-stream cells at corner (i,j+1): T(i-1,j),T(i,j).
      ! Face range `j=2:ny` keeps the diagonal T reads in bounds: the
      ! south corner reads row `j-1` (>=1) and the north corner reads
      ! row `j` (<=ny); `wet_q(i,j+1)` reaches `j+1<=ny+1`, in bounds.
      ! Row `j=1` is a ghost row with no prognostic velocity, so leaving
      ! its rate at 0 is exact.
      do concurrent(k=1:nz, j=2:ny, i=2:nx) &
         local(f_s, f_n, f_blocked, speed, width, v_at_u)
         ! South corner (i,j): land OR layer vanished in T(i-1,j-1),T(i,j-1).
         f_s = 1.0_wp - wet_q(i, j)
         if (min(h_layer(i - 1, j - 1, k), h_layer(i, j - 1, k)) < SIDE_H_VANISH) then
            f_s = 1.0_wp
         end if
         ! North corner (i,j+1): land OR layer vanished in T(i-1,j),T(i,j).
         f_n = 1.0_wp - wet_q(i, j + 1)
         if (min(h_layer(i - 1, j, k), h_layer(i, j, k)) < SIDE_H_VANISH) then
            f_n = 1.0_wp
         end if
         f_blocked = 0.5_wp*f_s + 0.5_wp*f_n
         v_at_u = 0.25_wp*( &
                  v_face(i - 1, j, k) + v_face(i, j, k) + &
                  v_face(i - 1, j + 1, k) + v_face(i, j + 1, k))
         speed = sqrt(u_face(i, j, k)*u_face(i, j, k) + v_at_u*v_at_u)
         width = max(dyCu(i, j), WIDTH_EPS)
         lambda_u(i, j, k) = cdrag_side*speed*f_blocked/width
      end do

      ! ---- v-faces: blocked by the two flanking corners (i,j),(i+1,j) ----
      ! Cross-stream cells at corner (i,j): T(i-1,j-1),T(i-1,j).
      ! Cross-stream cells at corner (i+1,j): T(i,j-1),T(i,j).
      ! Face range `i=2:nx` keeps the diagonal T reads in bounds: the
      ! west corner reads column `i-1` (>=1) and the east corner reads
      ! column `i` (<=nx); `wet_q(i+1,j)` reaches `i+1<=nx+1`, in bounds.
      ! Column `i=1` is a ghost column with no prognostic velocity.
      do concurrent(k=1:nz, j=2:ny, i=2:nx) &
         local(f_s, f_n, f_blocked, speed, width, u_at_v)
         ! West corner (i,j): land OR layer vanished in T(i-1,j-1),T(i-1,j).
         f_s = 1.0_wp - wet_q(i, j)
         if (min(h_layer(i - 1, j - 1, k), h_layer(i - 1, j, k)) < SIDE_H_VANISH) then
            f_s = 1.0_wp
         end if
         ! East corner (i+1,j): land OR layer vanished in T(i,j-1),T(i,j).
         f_n = 1.0_wp - wet_q(i + 1, j)
         if (min(h_layer(i, j - 1, k), h_layer(i, j, k)) < SIDE_H_VANISH) then
            f_n = 1.0_wp
         end if
         f_blocked = 0.5_wp*f_s + 0.5_wp*f_n
         u_at_v = 0.25_wp*( &
                  u_face(i, j - 1, k) + u_face(i + 1, j - 1, k) + &
                  u_face(i, j, k) + u_face(i + 1, j, k))
         speed = sqrt(v_face(i, j, k)*v_face(i, j, k) + u_at_v*u_at_v)
         width = max(dxCv(i, j), WIDTH_EPS)
         lambda_v(i, j, k) = cdrag_side*speed*f_blocked/width
      end do
   end subroutine compute_channel_drag_rates

   subroutine ocean_channel_drag_apply_tendencies(this, ms, dt, no_wait)
      !! Apply the per-layer side drag IMPLICITLY:
      !!   `u <- u / (1 + dt·lambda_side_u)`,
      !!   `v <- v / (1 + dt·lambda_side_v)`.
      !! The implicit (backward-Euler) form is unconditionally stable on
      !! thin layers where an explicit `u - dt·lambda·u` would overshoot.
      !! `lambda ≡ 0` (default-off / all-wet / flat-bottom) ⇒ division by
      !! `1` ⇒ exact no-op.  `no_wait` semantics mirror
      !! `ocean_bottom_drag_apply_tendencies`.  Not `pure` (async/wait).
      type(ocean_bottom_drag_t), intent(in) :: this
      type(multilayer_state_t), intent(inout) :: ms
      real(wp), intent(in) :: dt
      logical, intent(in), optional :: no_wait
      integer :: i, j, k, nx_face, ny_uface, nx_vface, ny_face, nz
      logical :: lwait

      lwait = .true.
      if (present(no_wait)) lwait = .not. no_wait

      nx_face = size(ms%u_face_x_layer, 1)
      ny_uface = size(ms%u_face_x_layer, 2)
      nx_vface = size(ms%v_face_y_layer, 1)
      ny_face = size(ms%v_face_y_layer, 2)
      nz = ms%nz_ml

      !$acc kernels async(1)
      do concurrent(k=1:nz, j=1:ny_uface, i=1:nx_face)
         ms%u_face_x_layer(i, j, k) = ms%u_face_x_layer(i, j, k)/ &
                                      (1.0_wp + dt*this%lambda_side_u%data(i, j, k))
      end do
      do concurrent(k=1:nz, j=1:ny_face, i=1:nx_vface)
         ms%v_face_y_layer(i, j, k) = ms%v_face_y_layer(i, j, k)/ &
                                      (1.0_wp + dt*this%lambda_side_v%data(i, j, k))
      end do
      !$acc end kernels
      if (lwait) then
         !$acc wait(1)
      end if
   end subroutine ocean_channel_drag_apply_tendencies

   pure function parse_bdrag_variant(name) result(code)
      !! Translate a namelist string into a `BDRAG_*` code.  An
      !! unrecognised string returns `BDRAG_INVALID` (PR-6 fail-loud —
      !! a typo must not silently select the quadratic default over the
      !! linear form or vice versa; the two obey different physics).
      character(len=*), intent(in) :: name
      integer :: code
      select case (trim(adjustl(name)))
      case ("quadratic", "QUADRATIC", "cd", "CD")
         code = BDRAG_QUADRATIC
      case ("linear", "LINEAR", "rayleigh", "RAYLEIGH")
         code = BDRAG_LINEAR
      case default
         code = BDRAG_INVALID
      end select
   end function parse_bdrag_variant

   pure function bdrag_variant_is_implemented(code) result(ok)
      !! `.true.` only for a bottom-drag variant with a real kernel
      !! (`BDRAG_LINEAR` / `BDRAG_QUADRATIC`).  `BDRAG_INVALID` returns
      !! `.false.`.  The single gate `validate_config` consumes (PR-6).
      integer, intent(in) :: code
      logical :: ok
      ok = (code == BDRAG_LINEAR) .or. (code == BDRAG_QUADRATIC)
   end function bdrag_variant_is_implemented

   pure function ocean_bottom_drag_bytes(this) result(nbytes)
      !! Counted allocatable footprint of the bottom drag slot
      !! (0 when unallocated). One arr_bytes term per array — add a
      !! term here when a new allocatable joins the type.
      class(ocean_bottom_drag_t), intent(in) :: this
      integer(int64) :: nbytes
      nbytes = this%du_drag%bytes() &
               + this%dv_drag%bytes() &
               + arr_bytes(this%lambda_bot_u) &
               + arr_bytes(this%lambda_bot_v) &
               + this%lambda_side_u%bytes() &
               + this%lambda_side_v%bytes()
   end function ocean_bottom_drag_bytes

end module rdb_ocean_bottom_drag
