/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.engine;

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.List;

import javax.management.NotificationEmitter;
import javax.management.openmbean.CompositeData;

import com.codahale.metrics.Gauge;
import com.google.common.util.concurrent.AtomicDouble;

import lombok.extern.slf4j.Slf4j;

/**
 * A {@link Gauge} returning a string indicating whether memory usage is within a certain band = a PoC to see whether
 * we can detect low memory situations reliable
 *
 * NB - tends to report false low memory situations, as the standard saw tooth behaviour of the jvm's memory
 * use will trip the gauge, even though a GC will reclaim the memory.  Hard to know if this matters, but if code just
 * uses this gauge to manage caches, it's probably fine.  Maybe see about using the technique in
 * http://www.fasterj.com/articles/gcnotifs.shtml
 */
@Slf4j
public class MemoryMonitoring {


  public final float warningRatio = 0.75F;
  public final float badRatio = 0.85F;

  public final float postFullGCwarningRatio = 0.55F;
  public final float postFullGCbadRatio = 0.7F;


  public enum Level {
    OK,
    WARNING,
    BAD;

    /**
     * @return true if the gauge is reporting that memory usage is either {@link Level#WARNING} or {@link Level#BAD}
     */
    public boolean isWarningOrWorse() {
      return ordinal() >= Level.WARNING.ordinal();
    }

  }

  public MemoryMonitoring() {
    setupListening();
  }

  MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();

  List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();

  private final AtomicDouble postFullGCRatio = new AtomicDouble(0.0F);

  private class HeapInfo {
    long max;
    long used;

    HeapInfo(MemoryUsage usage) {
      this.max = usage.getMax();
      this.used = usage.getUsed();
    }

    @Override
    public String toString() {
      return String.format("Heap[max=%s, used=%s, perc=%.1f]",
          formatBytes(max),
          formatBytes(used),
          usedPercentage());
    }

    public double usedPercentage() {
      return ((double)used / max) * 100;
    }

    public double usedRatio() {
      return ((double)used / max);
    }

  }

  final void setupListening() {
    for (GarbageCollectorMXBean gcBean : gcBeans) {
      if (gcBean instanceof NotificationEmitter) {
        ((NotificationEmitter) gcBean).addNotificationListener((notification, handback) -> {
          CompositeData compositeData = (CompositeData) notification.getUserData();
          String gcAction = compositeData.get("gcAction").toString();

          if (gcAction.equals("end of major GC")) {
            HeapInfo current = new HeapInfo(memoryMXBean.getHeapMemoryUsage());
            postFullGCRatio.set(current.usedRatio());

            log.info(
                "Major GC Event - max: {}, used: {}, free: {}",
                formatBytes(current.max),
                formatBytes(current.used),
                formatBytes(current.max - current.used)
            );
          }
        }, null, null);
      }
    }
  }

  protected static String formatBytes(long numBytes) {
    double divided = Math.abs(numBytes);
    String[] units = new String[] {"bytes", "Kbytes", "Mbytes", "Gbytes"};
    int unitIdx = 0;
    while (divided > 1024 && unitIdx < units.length) {
      unitIdx++;
      divided = divided / 1024;
    }

    return String.format("%.1f%s", divided, units[unitIdx]);
  }

  public Gauge<Level> getCurrentHeapFreeLevel() {
    return () -> {
      double ratio = new HeapInfo(memoryMXBean.getHeapMemoryUsage()).usedRatio();
      if (ratio > this.badRatio) {
        return Level.BAD;
      } else if (ratio > this.warningRatio) {
        return Level.WARNING;
      } else {
        return Level.OK;
      }
    };

  }

  public Gauge<Level> getPostFullGCHeapFreeLevel() {
    return () -> {
      double ratio = this.postFullGCRatio.get();
      if (ratio > this.postFullGCbadRatio) {
        return Level.BAD;
      } else if (ratio > this.postFullGCwarningRatio) {
        return Level.WARNING;
      } else {
        return Level.OK;
      }
    };
  }
}
