package de.dhbwstuttgart.server;

import com.fasterxml.jackson.core.JsonProcessingException;
import de.dhbwstuttgart.core.ConsoleInterface;
import de.dhbwstuttgart.server.packet.IClientToServerPacket;
import de.dhbwstuttgart.server.packet.IPacket;
import de.dhbwstuttgart.server.packet.IServerToClientPacket;
import de.dhbwstuttgart.server.packet.PacketContainer;
import de.dhbwstuttgart.server.packet.UnifyRequestPacket;
import de.dhbwstuttgart.server.packet.UnifyResultPacket;
import de.dhbwstuttgart.typeinference.constraints.ConstraintSet;
import de.dhbwstuttgart.typeinference.constraints.Pair;
import de.dhbwstuttgart.typeinference.result.ResultSet;
import de.dhbwstuttgart.typeinference.unify.UnifyContext;
import de.dhbwstuttgart.typeinference.unify.model.FiniteClosure;
import de.dhbwstuttgart.typeinference.unify.model.UnifyPair;
import de.dhbwstuttgart.util.Logger;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.enums.ReadyState;
import org.java_websocket.handshake.ServerHandshake;


/**
 * The Client-side of the websocket
 */
public class SocketClient extends WebSocketClient {

  public static Logger logger = new Logger("SocketClient");

  /**
   * The singleton object
   */
  private static SocketClient socketClient = null;

  /**
   * List of futures that are still waiting to be fulfilled
   */
  private final Map<String, SocketFuture<?>> responseFutures = new HashMap<>();

  private SocketClient(String url) {
    super(
      URI.create(url), // target url
      //SocketServer.perMessageDeflateDraft, // enable compression
      Map.of( // headers
        "packetProtocolVersion", SocketServer.packetProtocolVersion
      )
    );

    // make sure the url is in a valid format
    final String regex = "^wss?://(\\w+(\\.\\w+)?)*:(\\d+)$";
    final Matcher matcher = Pattern.compile(regex).matcher(url);
    if (!matcher.find()) {
      throw new RuntimeException("Provided string \"" + url + "\" is not a valid server URL! Use pattern ws(s?)://<host.name>:<port>");
    }

    try {
      // wait for the connection to be set up
      this.connectBlocking();
      // make sure the connection has been established successfully
      if (this.getReadyState() != ReadyState.OPEN) {
        throw new RuntimeException("WebSocket Client could not connect to remote host at " + this.uri);
      }
    } catch (InterruptedException exception) {
      throw new RuntimeException(exception);
    }

    // add a shutdown hook to close the connection when the process ends or is stopped by a SIGINT signal
    Runtime.getRuntime().addShutdownHook(new Thread(this::close));
  }

  private SocketClient(String host, int port, boolean secure) throws InterruptedException {
    this(String.format("%s://%s:%d/", secure ? "wss" : "ws", host, port));
  }

  /**
   * Singleton access method, creates one if none is available
   *
   * @return The one and only socketClient
   */
  private static SocketClient initializeClient() {
    if (socketClient == null) {
      socketClient = new SocketClient(ConsoleInterface.unifyServerUrl.get());
    }
    return socketClient;
  }

  /**
   * Send a packet to the server (connection will be created, if none is found) and return a future
   * for the response packet
   */
  synchronized public static <T extends IServerToClientPacket> SocketFuture<T> execute(IClientToServerPacket<T> packet) {
    SocketClient client = initializeClient();

    /*
     * Create a future that will be associated with the packet and eventually completed
     */
    SocketFuture<T> future = packet.getFuture();
    if (!future.isDone()) {
      client.responseFutures.put(future.futureId, future);
    }

    /*
     * Establish connection, if not already done.
     * Serialize the packet and send it to the server.
     * Return the future to be handled by the caller.
     */
    try {
      String json = PacketContainer.serialize(packet);
      client.send(json);
    } catch (Exception exception) {
      logger.exception(exception);
      throw new RuntimeException("Exception occurred in server connection: ", exception);
    }

    return future;
  }

  /**
   * Shortcut for waiting and retrieving the response immediately
   *
   * @param packet The packet to send
   * @param <T>    The type of response packet to await
   * @return The response packet, once it is received
   */
  public static <T extends IServerToClientPacket> T executeAndGet(IClientToServerPacket<T> packet) {
    return SocketClient.execute(packet).get();
  }

  /**
   * Specific client-side implementations to handle incoming packets
   */
  protected void handleReceivedPacket(IPacket packet) {
    if (!(packet instanceof IServerToClientPacket serverToClientPacket)) {
      System.err.println("Received package of invalid type + " + packet.getClass().getName());
      this.close();
      return;
    }

    serverToClientPacket.onHandle(this.getConnection(), this);
  }

  /**
   * Complete a registered future, so it can be handled by whoever executed the creator task
   *
   * @param id      The associated id for this future
   * @param trigger The object triggering the completion
   */
  public void completeResponseFuture(String id, IServerToClientPacket trigger) {
    SocketFuture<?> future = this.responseFutures.remove(id);
    if (future == null) return;
    if (!future.accept(trigger)) {
      throw new RuntimeException("Packet " + trigger.getClass().getName() + " tried to complete future, but was not allowed to");
    }
  }

  public static void closeIfOpen() {
    if (socketClient != null && socketClient.isOpen()) {
      socketClient.close();
    }
  }

  @Override
  public void onOpen(ServerHandshake handshakedata) {
    logger.success("Connected to server with status " + handshakedata.getHttpStatus());
  }

  @Override
  public void onMessage(String message) {
    // logger.info("received: " + message);
    IPacket packet = PacketContainer.deserialize(message);
    this.handleReceivedPacket(packet);
  }

  @Override
  public void onClose(int code, String reason, boolean remote) {
    logger.info(
      "Disconnected from server " +
        "with code " + code + " " +
        (reason.isEmpty() ? "" : "and reason " + reason + " ") +
        "(closed by remote: " + remote + ")"
    );

    if (!this.responseFutures.isEmpty()) {
      throw new RuntimeException("Server closed before all required tasks were answered");
    }
  }

  @Override
  public void onError(Exception e) {
    logger.exception(e);
    throw new RuntimeException(e);
  }

}
