package meerkat.pollingstation;

import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.LinkedList;

import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.ByteString;
import meerkat.crypto.DigitalSignature;
import meerkat.crypto.concrete.ECDSASignature;
import meerkat.protobuf.PollingStation;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.*;

import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.server.ResourceConfig;

import meerkat.protobuf.PollingStation.ScannedData;
import meerkat.rest.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Created by Arbel on 05/05/2016.
 */

public class PollingStationWebScanner implements PollingStationScanner.PollingStationServer {
    public final static String CALLBACKS_ATTRIBUTE_NAME = "controller";

    final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * Expected serial number for next scan
     */
    long expectedSerial;


    /**
     * Should a newly-connected scanner be verified using the nonce?
     */
    boolean verifyNonce;

    /**
     * Should every scan's signature be verified?
     */
    boolean verifySignatures;


    /**
     * The nonce we use to verify a new scanner connection.
     */
    long nonce;


    /**
     *
     */
    DigitalSignature verifier;

    private final Server server;

    private final List<FutureCallback<ScannedData>> callbacks;

    public class ScanRequestHandler {
        private ScanRequestHandler() { }

        boolean handleConnection(PollingStation.ConnectionClientData data) {
            if (verifyNonce) {
                if (data.getNonce() != nonce) {

                    logger.warn("Received scanner connection with bad nonce!");
                    return false;
                }
            }

            if (verifySignatures) {
                try {
                    if (data.getIdCase() != PollingStation.ConnectionClientData.IdCase.SCANNERPK)
                        throw new CertificateException("Connection request doesn't include certificate!");

                    verifier.loadVerificationCertificate(data.getScannerPK());
                } catch (CertificateException e) {
                    logger.warn("Scanner connection with bad certificate: {}", e);
                    return false;
                }
            }

            return true;
        }

        boolean handleRequest(PollingStation.SignedScannedData data) {
            ScannedData scannedData = data.getData();
            if (verifySignatures) {
                ByteString scannerID = scannedData.getScannerId();
                if (!scannerID.equals(verifier.getSignerID())) {
                    logger.warn("Scanner ID doesn't match connection public key");
                    return false;
                }

                try {
                    verifier.initVerify(data.getScannerSig());
                    verifier.updateContent(scannedData);
                    if (!verifier.verify()) {
                        logger.warn("Bad Signature");
                        return false;
                    }

                } catch (CertificateException e) {
                    logger.warn("Certificate Exception: {}", e);
                    return false;
                } catch (InvalidKeyException e) {
                    logger.warn("InvalidKey Exception: {}", e);
                    return false;
                } catch (SignatureException e) {
                    logger.warn("Signature Exception: {}", e);
                    return false;
                }
            }

            for (FutureCallback<ScannedData> callback : callbacks) {
                callback.onSuccess(scannedData);
            }

            return true;
        }

    }

    ScanRequestHandler scanRequestHandler;


    public PollingStationWebScanner(int port, String contextPath) {

        scanRequestHandler = new ScanRequestHandler();
        callbacks = new LinkedList<>();
        verifier = new ECDSASignature();

        server = new Server(port);

        ServletContextHandler servletContextHandler = new ServletContextHandler(server, contextPath);
        servletContextHandler.setAttribute(CALLBACKS_ATTRIBUTE_NAME, callbacks);

        ResourceConfig resourceConfig = new ResourceConfig(PollingStationScannerWebApp.class);
        resourceConfig.register(ProtobufMessageBodyReader.class);
        resourceConfig.register(ProtobufMessageBodyWriter.class);

        ServletHolder servletHolder = new ServletHolder(new ServletContainer(resourceConfig));

        servletContextHandler.addServlet(servletHolder, "/*");
    }



    @Override
    public PollingStation.ConnectionServerData start(boolean verifyScanner) throws Exception {
        this.verifyNonce = this.verifySignatures = verifyScanner;
        nonce = new SecureRandom().nextLong();

        server.start();
        return null;
    }

    @Override
    public void stop() throws Exception {
        server.stop();
    }

    @Override
    public void subscribe(FutureCallback<ScannedData> scanCallback) {
        callbacks.add(scanCallback);
    }

}
