!! Pseudo-salt verification tracer for the ocean path.
module rdb_ocean_pseudo_salt
   !! Pseudo-salt (Shao 2016): a passive tracer seeded to salinity's
   !! initial condition and given exactly the operators salinity
   !! receives that the registry does NOT deliver automatically — the
   !! surface salt flux and the KPP/EPBL non-local counter-gradient
   !! `gamma_s` (both mirrored in-place in the kernels that own them,
   !! `rdb_ocean_surface_flux` / `rdb_ocean_vmix` — this module adds no
   !! new dyn-step call site).  Every other operator (advection, ALE
   !! remap, vertical/horizontal diffusion, vertical exchange, halo,
   !! sponge, OBC) already rides the generic tracer registry, so
   !! pseudo-salt receives it "for free" the moment it is registered.
   !!
   !! The deviation `D = pseudo_salt - S` is a direct, measured proxy
   !! for how far the passive-tracer transport path has drifted from
   !! the active-tracer (salinity) path — an architectural claim turned
   !! into a number.  Reference: Shao, A. (2016), MOM6
   !! `pseudo_salt_tracer.F90`; the idea is standard practice in
   !! offline/online transport verification and carries no separate
   !! citation in MOM6 either.
   !!
   !! Excluded by design (fail-loud at configure, `rdb_config`
   !! `validate_config`) because Roundabout has no mirror for them and an
   !! un-mirrored salinity source would make `D` measure that source
   !! instead of the passive-path error: SSS piston restoring
   !! (`&ocean_restore_nml enable_restore_salt`) and sea-ice frazil /
   !! basal salt exchange (`&ocean_ice_nml enable`).
#ifdef LFORTRAN_PASSING
   use rdb_constants, only: wp, H_VANISHED
#else
   use rdb_constants, only: wp, H_VANISHED, NZ_STACK_MAX
#endif
   use rdb_grid, only: hgrid_t
   use rdb_multilayer_state, only: multilayer_state_t
   use pic_logger, only: global_logger
   use rdb_error_ring, only: error_ring_push
   implicit none
   private

#ifdef LFORTRAN_PASSING
   integer, parameter :: NZ_STACK_MAX = 64
      !! LFortran 0.64 workaround: module-local copy of the rdb_constants value
      !! (an imported parameter used as an explicit-shape dummy bound inside a
      !! PURE call becomes an impure getter under LFortran). Keep in sync (=64).
#endif

   public :: ocean_pseudo_salt_register
   public :: ocean_pseudo_salt_seed
   public :: ocean_pseudo_salt_deviation
   public :: pseudo_salt_conflicts_restore
   public :: pseudo_salt_conflicts_ice
   public :: pseudo_salt_needs_thermo_warning

contains

   subroutine ocean_pseudo_salt_register(ms, grid)
      !! Setup-time entry point: registers the "pseudo_salt" passive
      !! tracer (name/units/long_name copied verbatim from Shao 2016)
      !! and records its slot on `ms%idx_pseudo_salt`.  MUST be called
      !! after `ms%init` and before `enter_data` — and, on the ocean
      !! path, before `ocean_bc_state_init` sizes `bc%n_tracers` (the
      !! `register_passive_tracer` contract).  `error stop`s on
      !! refusal (mirrors `rdb_state.F90`'s treatment of a fatal setup
      !! misconfiguration — a caller that reaches here has already
      !! decided to register, so a silent no-op would be worse than a
      !! loud abort).
      type(multilayer_state_t), intent(inout) :: ms
      type(hgrid_t), intent(in) :: grid

      integer :: idx

      call ms%register_passive_tracer(grid, "pseudo_salt", "psu", &
                                      "Pseudo salt passive tracer", idx)
      if (idx <= 0) then
         call error_ring_push("ocean_pseudo_salt_register: "// &
                              "register_passive_tracer refused (registry "// &
                              "not initialised, or already locked by enter_data)")
         call global_logger%error("ocean_pseudo_salt_register: "// &
                                  "register_passive_tracer refused (registry "// &
                                  "not initialised, or already locked by enter_data)")
         error stop "ocean_pseudo_salt_register: registration refused"
      end if
      ms%idx_pseudo_salt = idx
   end subroutine ocean_pseudo_salt_register

   pure subroutine ocean_pseudo_salt_seed(ms)
      !! Seed `hTr(idx_pseudo_salt) = hTr(idx_salinity)` over the FULL
      !! array shape (nx_total, ny_total, nz_ml) — ghosts included,
      !! matching MOM6's `isd:ied, jsd:jed` seed (§4 of the plan). A
      !! halved ghost band would make the deviation diagnostic non-zero
      !! at the very first halo exchange, before any real transport has
      !! run. Self-gates on either index being unregistered. Must run
      !! AFTER every write to salinity's initial condition (analytical
      !! IC, then any z-file overlay) and BEFORE `ocean_state_enter_data`.
      type(multilayer_state_t), intent(inout) :: ms

      if (ms%idx_pseudo_salt <= 0 .or. ms%idx_salinity <= 0) return
      ms%tracers(ms%idx_pseudo_salt)%hTr = ms%tracers(ms%idx_salinity)%hTr
   end subroutine ocean_pseudo_salt_seed

   pure subroutine ocean_pseudo_salt_deviation(h_layer, hTr_ps, hTr_s, buf, nx, ny, nz, &
                                               missing)
      !! Diagnostic helper: `D(i,j,k) = hTr_ps/h - hTr_s/h`, i.e. the
      !! pseudo-salt concentration minus the salinity concentration.
      !!
      !! A vanished layer (`rdb_vl_is_live` false — the ONE predicate, see
      !! `src/core/ocean/README.md`, "The vanished-layer content rule")
      !! writes `missing`, which the caller passes as the IEEE NaN sentinel
      !! `fill_tracer_impl` uses for the two concentrations this is the
      !! difference of.  It used to write `0` below a private `1e-6 m`
      !! floor: a deviation of zero is the one value this diagnostic
      !! exists to report as "the two transport paths agree", so a filler
      !! read as a perfect score, and layers between `1e-6 m` and
      !! `H_VANISHED` were divided through at all.
      integer, intent(in)     :: nx, ny, nz
      real(wp), intent(in)    :: h_layer(nx, ny, nz)
      real(wp), intent(in)    :: hTr_ps(nx, ny, nz)
      real(wp), intent(in)    :: hTr_s(nx, ny, nz)
      real(wp), intent(inout) :: buf(nx, ny, nz)
      real(wp), intent(in)    :: missing
         !! Value written on a vanished layer (the diagnostics' NaN).
      integer :: i, j, k
      real(wp) :: h

      do concurrent(k=1:nz, j=1:ny, i=1:nx)
         h = h_layer(i, j, k)
         if (rdb_vl_is_live(h)) then
            buf(i, j, k) = rdb_vl_conc(hTr_ps(i, j, k), h) - rdb_vl_conc(hTr_s(i, j, k), h)
         else
            buf(i, j, k) = missing
         end if
      end do
   end subroutine ocean_pseudo_salt_deviation

   pure function pseudo_salt_conflicts_restore(enable_ps, enable_restore_salt) result(conflict)
      !! `.true.` iff pseudo-salt is enabled alongside SSS piston
      !! restoring — an un-mirrored salinity source
      !! (`rdb_ocean_surface_flux.F90` restore branch) that would make
      !! the deviation diagnostic measure the restoring term instead of
      !! the passive-transport-path error. Drives a configure-time
      !! fail-loud abort.
      logical, intent(in) :: enable_ps
      logical, intent(in) :: enable_restore_salt
      logical :: conflict
      conflict = enable_ps .and. enable_restore_salt
   end function pseudo_salt_conflicts_restore

   pure function pseudo_salt_conflicts_ice(enable_ps, enable_ice) result(conflict)
      !! `.true.` iff pseudo-salt is enabled alongside the sea-ice
      !! model — frazil / basal salt exchange are un-mirrored salinity
      !! sources (`rdb_ice_frazil.F90`, `rdb_ice_frazil_uptake.F90`,
      !! `rdb_ice_basal_flux.F90`). Drives a configure-time fail-loud
      !! abort.
      logical, intent(in) :: enable_ps
      logical, intent(in) :: enable_ice
      logical :: conflict
      conflict = enable_ps .and. enable_ice
   end function pseudo_salt_conflicts_ice

   pure function pseudo_salt_needs_thermo_warning(enable_ps, enable_thermodynamics) &
      result(warn)
      !! `.true.` iff pseudo-salt is enabled with thermodynamics off.
      !! NOT a hard error — pseudo-salt still measures pure transport,
      !! a legitimate use — but the caller should warn: without
      !! thermodynamics, the tracer never receives the surface salt
      !! flux / KPP nonlocal mirrors, so `D` degenerates to a pure
      !! advection/diffusion probe.
      logical, intent(in) :: enable_ps
      logical, intent(in) :: enable_thermodynamics
      logical :: warn
      warn = enable_ps .and. .not. enable_thermodynamics
   end function pseudo_salt_needs_thermo_warning

#include "rdb_vanished_layer.inc"

end module rdb_ocean_pseudo_salt
