package org.getmonero.i2p.zero;

import net.i2p.data.router.RouterAddress;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.CommSystemFacade;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
import net.i2p.router.transport.FIFOBandwidthRefiller;
import net.i2p.router.transport.TransportUtil;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Date;
import java.util.HashMap;
import java.util.Properties;

public class RouterWrapper {

  private Router router;
  private boolean started = false;
  private Properties routerProperties;
  private TunnelControl tunnelControl;
  private File i2PConfigDir;
  private File i2PBaseDir;

  public RouterWrapper(Properties p) {
    this.routerProperties = p;

    p.put("i2p.dir.base", System.getProperty("user.home") + File.separator + ".i2p-zero" + File.separator + "base");
    p.put("i2p.dir.config", System.getProperty("user.home") + File.separator + ".i2p-zero" + File.separator + "config");

    i2PConfigDir = new File(routerProperties.getProperty("i2p.dir.config"));
    if(!i2PConfigDir.exists()) i2PConfigDir.mkdirs();

    i2PBaseDir = new File(routerProperties.getProperty("i2p.dir.base"));
    if(!i2PBaseDir.exists()) {
      i2PBaseDir.mkdirs();
      copyFolderRecursively(Path.of(routerProperties.getProperty("i2p.dir.base.template")), i2PBaseDir.toPath());
    }
  }

  public Router getRouter() {
    return router;
  }

  public void copyFolderRecursively(Path src, Path dest)  {
    try {
      Files.walk(src).forEach(source -> {
        try { Files.copy(source, dest.resolve(src.relativize(source)), StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { throw new RuntimeException(e); }
      });
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public boolean isStarted() {
    return started;
  }

  public void start() {

    new Thread(()-> {
      if(started) return;
      started = true;

      router = new Router(routerProperties);

      new Thread(()->{
        router.setKillVMOnEnd(false);
        router.runRouter();
      }).start();

      new Thread(()->{
        try {
          while(true) {
            if(router.isAlive()) {
              break;
            }
            else {
              Thread.sleep(1000);
              System.out.println("Waiting for I2P router to start...");
            }
          }

          tunnelControl = new TunnelControl(this, i2PConfigDir, new File(i2PConfigDir, "tunnelTemp"));
          new Thread(tunnelControl).start();

        }
        catch (Exception e) {
          e.printStackTrace();
        }
      }).start();

    }).start();

  }

  public void stop() {
    if(!started) return;
    started = false;
    tunnelControl.stop();
    System.out.println("I2P router will shut down gracefully");
    router.shutdownGracefully();

    // don't wait more than 2 seconds for shutdown. If tunnels are still opening, they can pause for up to 20 seconds, which is too long
    new Thread(()->{
      try { Thread.sleep(2000); } catch (InterruptedException e) {}
      System.exit(0);
    }).start();
  }

  long lastTriggerTimestamp = 0;
  public void debouncedUpdateBandwidthLimitKBPerSec(int n) {
    long triggerTime = new Date().getTime();
    lastTriggerTimestamp = triggerTime;
    new Thread(()->{
      try { Thread.sleep(2000); } catch(InterruptedException e) {}
      if(lastTriggerTimestamp==triggerTime) {
        // nothing happened after we were triggered, so proceed
        updateBandwidthLimitKBPerSec(n);
      }
    }).start();
  }

  public void updateBandwidthLimitKBPerSec(int n) {
    routerProperties.put("i2np.inboundKBytesPerSecond", n);
    routerProperties.put("i2np.outboundKBytesPerSecond", n);

    var changes = new HashMap<String, String>();

    final int DEF_BURST_PCT = 10;
    final int DEF_BURST_TIME = 20;

    int inboundRate = n;
    int outboundRate = n;

    {
      float rate = inboundRate / 1.024f;
      float kb = DEF_BURST_TIME * rate;
      changes.put(FIFOBandwidthRefiller.PROP_INBOUND_BURST_BANDWIDTH, Integer.toString(Math.round(rate)));
      changes.put(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH_PEAK, Integer.toString(Math.round(kb)));
      rate -= Math.min(rate * DEF_BURST_PCT / 100, 50);
      changes.put(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH, Integer.toString(Math.round(rate)));
    }
    {
      float rate = outboundRate / 1.024f;
      float kb = DEF_BURST_TIME * rate;
      changes.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BURST_BANDWIDTH, Integer.toString(Math.round(rate)));
      changes.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH_PEAK, Integer.toString(Math.round(kb)));
      rate -= Math.min(rate * DEF_BURST_PCT / 100, 50);
      changes.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH, Integer.toString(Math.round(rate)));
    }

    boolean saved = router.saveConfig(changes, null);
    // this has to be after the save
    router.getContext().bandwidthLimiter().reinitialize();
    if(!saved) throw new RuntimeException("Error saving the new bandwidth limit");

  }

  public double get1sRateInKBps() {
    try { return router.getContext().bandwidthLimiter().getReceiveBps()/1024d; } catch (Exception e) { return 0; }
  }
  public double get1sRateOutKBps() {
    try { return router.getContext().bandwidthLimiter().getSendBps()/1024d; } catch (Exception e) { return 0; }
  }
  public double get5mRateInKBps() {
    try {
      return router.getContext().statManager().getRate("bw.recvRate").getRate(5*60*1000).getAverageValue()/1024d;
    }
    catch (Exception e) { return 0; }
  }
  public double get5mRateOutKBps() {
    try {
      return router.getContext().statManager().getRate("bw.sendRate").getRate(5*60*1000).getAverageValue()/1024d;
    }
    catch (Exception e) { return 0; }
  }
  public double getAvgRateInKBps() {
    try {
      return router.getContext().statManager().getRate("bw.recvRate").getLifetimeAverageValue()/1024d;
    }
    catch (Exception e) { return 0; }
  }
  public double getAvgRateOutKBps() {
    try {
      return router.getContext().statManager().getRate("bw.sendRate").getLifetimeAverageValue()/1024d;
    }
    catch (Exception e) { return 0; }
  }
  public double getTotalInMB() {
    try { return router.getContext().bandwidthLimiter().getTotalAllocatedInboundBytes()/1048576d; } catch (Exception e) { return 0; }
  }
  public double getTotalOutMB() {
    try { return router.getContext().bandwidthLimiter().getTotalAllocatedOutboundBytes()/1048576d; } catch (Exception e) { return 0; }
  }

  public TunnelControl getTunnelControl() {
    return tunnelControl;
  }

  public enum NetworkState {
    HIDDEN,
    TESTING,
    FIREWALLED,
    RUNNING,
    WARN,
    ERROR,
    CLOCKSKEW,
    VMCOMM;
  }

  public static class NetworkStateMessage {
    private NetworkState state;
    private String msg;

    NetworkStateMessage(NetworkState state, String msg) {
      setMessage(state, msg);
    }

    public void setMessage(NetworkState state, String msg) {
      this.state = state;
      this.msg = msg;
    }

    public NetworkState getState() {
      return state;
    }

    public String getMessage() {
      return msg;
    }

    @Override
    public String toString() {
      return "(" + state + "; " + msg + ')';
    }
  }

  public String _t(String s) {
    return s;
  }

  final static String PROP_I2NP_NTCP_HOSTNAME = "i2np.ntcp.hostname";
  final static String PROP_I2NP_NTCP_PORT = "i2np.ntcp.port";

  public NetworkStateMessage getReachability() {
    RouterContext _context = router.getContext();
    if (_context.commSystem().isDummy())
      return new NetworkStateMessage(NetworkState.VMCOMM, "VM Comm System");
    if (_context.router().getUptime() > 60*1000 && (!_context.router().gracefulShutdownInProgress()) &&
        !_context.clientManager().isAlive())
      return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-Client Manager I2CP Error - check logs"));  // not a router problem but the user should know
    // Warn based on actual skew from peers, not update status, so if we successfully offset
    // the clock, we don't complain.
    //if (!_context.clock().getUpdatedSuccessfully())
    long skew = _context.commSystem().getFramedAveragePeerClockSkew(33);
    // Display the actual skew, not the offset
    if (Math.abs(skew) > 30*1000)
      return new NetworkStateMessage(NetworkState.CLOCKSKEW, _t("ERR-Clock Skew"));
    if (_context.router().isHidden())
      return new NetworkStateMessage(NetworkState.HIDDEN, _t("Hidden"));
    RouterInfo routerInfo = _context.router().getRouterInfo();
    if (routerInfo == null)
      return new NetworkStateMessage(NetworkState.TESTING, _t("Testing"));

    CommSystemFacade.Status status = _context.commSystem().getStatus();
    NetworkState state = NetworkState.RUNNING;
    switch (status) {
      case OK:
      case IPV4_OK_IPV6_UNKNOWN:
      case IPV4_OK_IPV6_FIREWALLED:
      case IPV4_UNKNOWN_IPV6_OK:
      case IPV4_DISABLED_IPV6_OK:
      case IPV4_SNAT_IPV6_OK:
        RouterAddress ra = routerInfo.getTargetAddress("NTCP");
        if (ra == null)
          return new NetworkStateMessage(NetworkState.RUNNING, _t(status.toStatusString()));
        byte[] ip = ra.getIP();
        if (ip == null)
          return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-Unresolved TCP Address"));
        // TODO set IPv6 arg based on configuration?
        if (TransportUtil.isPubliclyRoutable(ip, true))
          return new NetworkStateMessage(NetworkState.RUNNING, _t(status.toStatusString()));
        return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-Private TCP Address"));

      case IPV4_SNAT_IPV6_UNKNOWN:
      case DIFFERENT:
        return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-SymmetricNAT"));

      case REJECT_UNSOLICITED:
        state = NetworkState.FIREWALLED;
      case IPV4_DISABLED_IPV6_FIREWALLED:
        if (routerInfo.getTargetAddress("NTCP") != null)
          return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled with Inbound TCP Enabled"));
        // fall through...
      case IPV4_FIREWALLED_IPV6_OK:
      case IPV4_FIREWALLED_IPV6_UNKNOWN:
        if (((FloodfillNetworkDatabaseFacade)_context.netDb()).floodfillEnabled())
          return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled and Floodfill"));
        //if (_context.router().getRouterInfo().getCapabilities().indexOf('O') >= 0)
        //    return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled and Fast"));
        return new NetworkStateMessage(state, _t(status.toStatusString()));

      case DISCONNECTED:
        return new NetworkStateMessage(NetworkState.TESTING, _t("Disconnected - check network connection"));

      case HOSED:
        return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-UDP Port In Use - Set i2np.udp.internalPort=xxxx in advanced config and restart"));

      case UNKNOWN:
        state = NetworkState.TESTING;
      case IPV4_UNKNOWN_IPV6_FIREWALLED:
      case IPV4_DISABLED_IPV6_UNKNOWN:
      default:
        ra = routerInfo.getTargetAddress("SSU");
        if (ra == null && _context.router().getUptime() > 5*60*1000) {
          if (_context.commSystem().countActivePeers() <= 0)
            return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-No Active Peers, Check Network Connection and Firewall"));
          else if (_context.getProperty(PROP_I2NP_NTCP_HOSTNAME) == null ||
              _context.getProperty(PROP_I2NP_NTCP_PORT) == null)
            return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-UDP Disabled and Inbound TCP host/port not set"));
          else
            return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled with UDP Disabled"));
        }
        return new NetworkStateMessage(state, _t(status.toStatusString()));
    }
  }



}
