package meerkat.pollingstation;

import java.io.IOException;
import java.net.*;
import java.nio.channels.ServerSocketChannel;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.util.Enumeration;
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.Crypto;
import meerkat.protobuf.PollingStation;
import org.eclipse.jetty.server.Connector;
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 SCAN_HANDLER_ATTRIBUTE_NAME = "controller";

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

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

    /**
     * Context path for servlet
     */

    String contextPath;


    /**
     * 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;
    ByteString connectedScannerID;

    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;
                }
            }
            try {
                if (verifySignatures) {

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

                    Crypto.SignatureVerificationKey pk = data.getScannerPK();
                    connectedScannerID = verifier.computeCertificateFingerprint(pk);
                    verifier.loadVerificationCertificate(pk);

                } else {
                    // If we don't verify certs, we can still get a public key, in which case
                    // the ID will be the hash of the public key.
                    switch (data.getIdCase()) {
                        case SCANNERID:
                            connectedScannerID = data.getScannerId();
                            break;
                        case SCANNERPK:
                            connectedScannerID = verifier.computeCertificateFingerprint(data.getScannerPK());
                            break;
                        default:
                            logger.warn("Scanner connection with no ID or certificate!");
                            return false;
                    }

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

            expectedSerial = 0;
            return true;
        }

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

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

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

            if (scannedData.getSerial() != expectedSerial) {
                logger.warn("Got scan with serial {}, expecting {}", scannedData.getSerial(), expectedSerial);
            }

            expectedSerial = scannedData.getSerial() + 1;

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

            return true;
        }

    }

    ScanRequestHandler scanRequestHandler;


    public PollingStationWebScanner(int port, String contextPath) {
        this.contextPath = contextPath;
        scanRequestHandler = new ScanRequestHandler();
        callbacks = new LinkedList<>();
        verifier = new ECDSASignature();

        server = new Server(port);

        ServletContextHandler servletContextHandler = new ServletContextHandler(server, contextPath);
        servletContextHandler.setAttribute(SCAN_HANDLER_ATTRIBUTE_NAME, scanRequestHandler);

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

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

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


    String getServerURL(Server server) {

        String serverURL = server.getURI().toASCIIString();

        try {

            Connector connector = server.getConnectors()[0];

            Object transport = connector.getTransport();

            if (transport instanceof ServerSocketChannel) {
                // We can get better info about the local endpoint
                ServerSocketChannel sock = (ServerSocketChannel) transport;
                SocketAddress localAddr = null;
                localAddr = sock.getLocalAddress();
                if (localAddr instanceof InetSocketAddress) {
                    InetSocketAddress localInet = (InetSocketAddress) localAddr;
                    InetAddress hostAddr = localInet.getAddress();
                    if (hostAddr.isAnyLocalAddress()) {
                        Enumeration<NetworkInterface> n = null;
                        n = NetworkInterface.getNetworkInterfaces();
                        while (n.hasMoreElements()) {
                            NetworkInterface e = n.nextElement();
                            if (e.isLoopback() || e.isPointToPoint() || !e.isUp())
                                continue;

                            Enumeration<InetAddress> aList = e.getInetAddresses();
                            while (aList.hasMoreElements()) {
                                InetAddress a = aList.nextElement();
                                if (a instanceof Inet4Address) {
                                    serverURL =  "http://" + a.getHostAddress() + ":" + localInet.getPort()
                                            + contextPath;
                                }
                            }
                        }
                    }
                }
            }

        } catch (SocketException e) {
            // Ignore
        } catch (IOException e) {
            // Ignore
        }

        return serverURL;
    }

    @Override
    public PollingStation.ConnectionServerData start(boolean verifyScanner) throws Exception {
        this.verifyNonce = this.verifySignatures = verifyScanner;
        // Use a 40-bit nonce (should be enough, since you can't do offline verification).
        nonce = new SecureRandom().nextLong() & 0xffffffffffL;

        server.start();

        return PollingStation.ConnectionServerData.newBuilder()
                .setNonce(nonce)
                .setServerUrl(getServerURL(server))
                .build();
    }

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

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

}
