/**
 * Copyright (c) 2010-2018 by the respective copyright holders.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.ui.habot.notification.internal;

import static org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import javax.ws.rs.core.Response;

import org.apache.commons.io.IOUtils;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.interfaces.ECPrivateKey;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.eclipse.smarthome.config.core.ConfigConstants;
import org.jose4j.lang.JoseException;
import org.openhab.ui.habot.notification.internal.webpush.Notification;
import org.openhab.ui.habot.notification.internal.webpush.PushService;
import org.openhab.ui.habot.notification.internal.webpush.Subscription;
import org.openhab.ui.habot.notification.internal.webpush.Utils;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.io.BaseEncoding;

/**
 * Handles the web push notifications.
 *
 * @author Yannick Schaus
 */
@Component(service = NotificationService.class, immediate = true)
public class NotificationService {

    private final Logger logger = LoggerFactory.getLogger(NotificationService.class);

    private static final String VAPID_KEYS_FILE_NAME = "habot" + File.separator + "vapid_keys";
    private static final String SUBJECT_NAME = "[email protected]";

    private SubscriptionProvider subscriptionProvider;
    private String publicVAPIDKey;
    private String privateVAPIDKey;

    private PushService pushService = null;

    public NotificationService() {
        // Add BouncyCastle as an algorithm provider
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }

    /**
     * Gets the public VAPID key
     *
     * @return the string representation of the public key
     */
    public String getVAPIDPublicKey() {
        loadVAPIDKeys();
        return publicVAPIDKey;
    }

    /**
     * Store a new subscription
     *
     * @param subscription the subscription to store
     */
    public void addSubscription(Subscription subscription) {
        if (this.subscriptionProvider.get(subscription.keys) == null) {
            this.subscriptionProvider.add(subscription);
        }
    }

    /**
     * Broadcast a web push notification to all subscriptions
     *
     * @param payload the payload to push
     * @throws GeneralSecurityException
     */
    public void broadcastNotification(String payload) throws GeneralSecurityException {
        for (Subscription subscription : this.subscriptionProvider.getAll()) {
            sendNotification(subscription, payload);
        }
    }

    /**
     * Sends a web push notification to a specified subscription
     *
     * @param subscription the subscription to send the notification to
     * @param payload the payload to push
     * @return the {@link Future} for the {@link Response} to the push server
     * @throws GeneralSecurityException
     */
    public Future<Response> sendNotification(Subscription subscription, String payload)
            throws GeneralSecurityException {
        getPushService();

        Notification notification = new Notification(subscription, payload);
        try {
            return this.pushService.send(notification);
        } catch (IOException | JoseException | ExecutionException | InterruptedException e) {
            logger.error("Unable to send the notification to {}: {}",
                    this.subscriptionProvider.keyToString(subscription.keys), e.toString());
            return null;
        }
    }

    @Reference(policy = ReferencePolicy.DYNAMIC)
    protected void setSubscriptionProvider(SubscriptionProvider subscriptionProvider) {
        this.subscriptionProvider = subscriptionProvider;
    }

    protected void unsetSubscriptionProvider(SubscriptionProvider subscriptionProvider) {
        this.subscriptionProvider = null;
    }

    /**
     * Generate an EC keypair on the prime256v1 curve and save them to a file for later usage.
     *
     * Some code borrowed from
     * <a href=
     * "https://github.com/web-push-libs/webpush-java/blob/master/src/main/java/nl/martijndwars/webpush/cli/handlers/GenerateKeyHandler.java">webpush-java</a>.
     *
     * @author Martijn Dwars
     *
     * @throws InvalidAlgorithmParameterException
     * @throws NoSuchProviderException
     * @throws NoSuchAlgorithmException
     * @throws IOException
     * @throws FileNotFoundException
     */
    private void generateVAPIDKeyPair() throws InvalidAlgorithmParameterException, NoSuchProviderException,
            NoSuchAlgorithmException, FileNotFoundException, IOException {
        ECNamedCurveParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec(Utils.CURVE);

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(Utils.ALGORITHM, PROVIDER_NAME);
        keyPairGenerator.initialize(parameterSpec);

        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        byte[] publicKey = Utils.savePublicKey((ECPublicKey) keyPair.getPublic());
        byte[] privateKey = Utils.savePrivateKey((ECPrivateKey) keyPair.getPrivate());

        List<String> encodedKeys = new ArrayList<String>();
        encodedKeys.add(BaseEncoding.base64Url().encode(publicKey));
        encodedKeys.add(BaseEncoding.base64Url().encode(privateKey));

        // write the public key, then the private key in encoded form on separate lines in the file
        File file = new File(ConfigConstants.getUserDataFolder() + File.separator + VAPID_KEYS_FILE_NAME);
        file.getParentFile().mkdirs();
        IOUtils.writeLines(encodedKeys, System.lineSeparator(), new FileOutputStream(file));

        this.publicVAPIDKey = encodedKeys.get(0);
        this.privateVAPIDKey = encodedKeys.get(1);
    }

    /**
     * Loads the VAPID keypair from the file, or generate them if they don't exist.
     */
    private void loadVAPIDKeys() {
        try {
            List<String> encodedKeys = IOUtils.readLines(
                    new FileInputStream(ConfigConstants.getUserDataFolder() + File.separator + VAPID_KEYS_FILE_NAME));
            this.publicVAPIDKey = encodedKeys.get(0);
            this.privateVAPIDKey = encodedKeys.get(1);
        } catch (IOException e) {
            try {
                generateVAPIDKeyPair();
            } catch (InvalidAlgorithmParameterException | NoSuchProviderException | NoSuchAlgorithmException
                    | IOException e1) {
                RuntimeException ex = new RuntimeException("Cannot get the VAPID keypair for push notifications");
                ex.initCause(e1);
                throw ex;
            }
        }
    }

    private PushService getPushService() throws GeneralSecurityException {
        if (this.pushService == null) {
            loadVAPIDKeys();
            this.pushService = new PushService(this.publicVAPIDKey, this.privateVAPIDKey, SUBJECT_NAME);
        }
        return this.pushService;
    }

}