package servers;

import blockchain.data.BCS;
import communication.CommLayer;
import communication.overlays.clique.Clique;
import proto.types.client;
import das.ab.ABService;
import das.bbc.BBC;
import das.bbc.OBBC;
import das.ms.Membership;
import das.wrb.WRB;
import io.grpc.Server;
import io.grpc.netty.NettyServerBuilder;

import utils.cache.TxCache;
import utils.DBUtils;
import utils.DiskUtils;
import proto.types.transaction.*;
import proto.types.block.*;
import utils.config.yaml.ServerPublicDetails;

import java.io.IOException;

import static java.lang.String.format;

public class Top {

    private final static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(Top.class);
    private static CommLayer comm;
    private static int n;
    private static int f;
    private static ToyBaseServer[] toys;
    private static int id;
    private static int workers;
    private static String type;
    private static Server txsServer;
//    private ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
//    private EventLoopGroup gnio = new NioEventLoopGroup(2);


    public Top(int id, int n, int f, int workers, int tmo,
               int maxTx, ServerPublicDetails[] cluster,
               String abConfig, String type, String serverCrt, String serverPrivKey, String caRoot) {
        Top.n = n;
        Top.f = f;
        Top.workers = workers;
        Top.toys = new ToyBaseServer[workers];
        Top.id = id;
        new BCS(id, n, f, workers);
        initProtocols(cluster, tmo, serverCrt,
                serverPrivKey, caRoot,  abConfig);

        Top.type = type;
        // TODO: Apply to more types
        if (type.equals("r")) {
            for (int i = 0 ; i < workers ; i++) {
                toys[i] = new ToyServer(id, i, n, f, maxTx, comm);
            }
        }
        if (type.equals("b")) {
            for (int i = 0 ; i < workers ; i++) {
                toys[i] = new ByzToyServer(id, i, n, f, maxTx, comm);
            }
        }
        if (type.equals("a")) {
            for (int i = 0 ; i < workers ; i++) {
                toys[i] = new AsyncToyServer(id, i, n, f, maxTx, comm);
            }
        }
        logger.info(format("Initiated TOP: [id=%d; n=%d; f=%d; workers=%d]", id, n, f, workers));

    }

    private static void initProtocols(ServerPublicDetails[] cluster, int tmo,
                               String serverCrt, String serverPrivKey, String caRoot, String abConfigHome) {
        comm = new Clique(id, workers, Top.n, cluster);
        logger.info(format("[%d] has initiated communication layer", id));
        new ABService(id, n, f, abConfigHome);
        logger.info(format("[%d] has initiated ab service", id));
        new BBC(id, n, f, n - f);
        logger.info(format("[%d] has initiated BBC", id));
        new OBBC(id, n, f, workers, n - f, cluster, comm, caRoot, serverCrt, serverPrivKey);
        logger.info(format("[%d] has initiated OBBC service", id));
        new WRB(id, workers, n, f, tmo, cluster, serverCrt, serverPrivKey, caRoot);
        logger.info(format("[%d] has initiated WRB", id));
        new Membership(n);
        logger.info(format("[%d] has initiated Membership", id));
        new TxCache(0, workers);
        logger.info(format("[%d] has initiated Cache utils", id));
        new DBUtils(workers);
        logger.info(format("[%d] has initiated DB Utils", id));

    }

    public static void reconfigure() {
        comm.reconfigure();
        for (int i = 0 ; i < workers ; i++) {
            toys[i].reconfigure();
        }
    }


    public static void start() {
        logger.info("Starting OBBC");
        OBBC.start();
        logger.info("Starting wrb");
        WRB.start();
        logger.info("Joining to communication layer");
        comm.join();
        logger.info("Starting AB");
        ABService.start();
        if (Membership.start(id) == -1) {
            // Temporarily! Here we assume that everyone still correct
            logger.error("Error while trying to connect");
            shutdown();
            System.exit(0);
        }
        logger.info("Everything is up and good");

    }

    public static void shutdown() {
        ABService.shutdown();
        logger.info("shutdown AB");
        comm.leave();
        logger.info("leaving communication layer");
        WRB.shutdown();
        logger.info("shutdown WRB");
        OBBC.shutdown();
        logger.info("shutdown OBBC");
        txsServer.shutdown();
        logger.info("shutdown txsServer");
        for (int i = 0 ; i < workers ; i++) {
            toys[i].shutdown();
            logger.info(format("shutdown worker [%d]", i));
        }
        DBUtils.shutdown();
        logger.info("shutdown DBUtils");
        DiskUtils.shutdown();
        logger.info("shutdown DiskUtils");

        logger.info("ByeBye");
    }

    public static void serve() {
        try {
            for (int i = 0 ; i < workers ; i++) {
                toys[i].serve();
            }
        } catch (InterruptedException e) {
            logger.error(e);
            return;
        }

        try {
            txsServer = NettyServerBuilder
                    .forPort(9876)
//                    .executor(Executors.newFixedThreadPool(2))
//                    .bossEventLoopGroup(gnio)
//                    .workerEventLoopGroup(gnio)
                    .addService(new ClientRpcsService())
//                    .maxConcurrentCallsPerConnection(5) // TODO: Inspect this limit
                    .build()
                    .start();
            logger.info("starting client RPCs server, listening on port 9876");
        } catch (IOException e) {
            logger.error("", e);
        }
        logger.info("Start serving, everything looks good");
    }

    public static TxID addTransaction(Transaction tx) {
        int ps = toys[0].getTxPoolSize();
        int worker = 0;
        for (int i = 1 ; i < workers ; i++) {
            int cps = toys[i].getTxPoolSize();
            if (ps > cps) {
                ps = cps;
                worker = i;
            }
        }
        return toys[worker].addTransaction(tx);
    }

    public static TxID addTransaction(byte[] data, int clientID) {
       int ps = toys[0].getTxPoolSize();
       int worker = 0;
       for (int i = 1 ; i < workers ; i++) {
           int cps = toys[i].getTxPoolSize();
           if (ps > cps) {
               ps = cps;
               worker = i;
           }
       }
        return toys[worker].addTransaction(data, clientID);
    }

    public static client.TxState status(TxID tid, boolean blocking) throws InterruptedException {
        if (tid.getChannel() > workers) return client.TxState.UNKNOWN;
        return toys[tid.getChannel()].status(tid, blocking);
    }

    public static Transaction getTransaction(TxID txID, boolean blocking) throws InterruptedException {
        if (txID.getChannel() > workers) return Transaction.getDefaultInstance();
        return toys[txID.getChannel()].getTx(txID, blocking);
    }

    public static Block deliver(int index, boolean blocking) throws InterruptedException {
        int channel_num = index % workers;
        int block_num = index / workers;
        if (blocking) return toys[channel_num].deliver(block_num);
        return toys[channel_num].nonBlockingdeliver(block_num);
    }

    public static int getID() {
        return id;
    }

    public static void setByzSetting() {
        if (!type.equals("b")) {
            logger.debug("Unable to set byzantine behaviour to non byzantine node");
            return;
        }
        for (int i = 0 ; i < workers ; i++) {
            ((ByzToyServer) toys[i]).setByzSetting();
        }
    }

    public static void setAsyncParam(int maxTime) {
        if (!type.equals("a") && !type.equals("b")) {
            logger.debug("Unable to set async behaviour to non async node");
            return;
        }
        if (type.equals("a")) {
            for (int i = 0 ; i < workers ; i++) {
                ((AsyncToyServer) toys[i]).setAsyncParam(maxTime);
            }
        }

        if (type.equals("b")) {
            for (int i = 0 ; i < workers ; i++) {
                ((ByzToyServer) toys[i]).setAsyncParam(maxTime);
            }
        }

    }

    public static int poolSize() {
        int ps = 0;
        for (int i = 0 ; i < workers ; i++) {
            ps += toys[i].getTxPoolSize();
        }
        return ps;
    }

    public static int pendingSize() {
        int ps = 0;
        for (int i = 0; i < workers ; i++) {
            ps += toys[i].getPendingSize();
        }
        return ps;
    }

}
