/*******************************************************************************
 * Copyright (c) 2016 Eurotech and others.
 * 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
 *
 * Contributors:
 *     Eurotech - initial API and implementation
 *******************************************************************************/
package org.eclipse.kura.net.admin;

import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.kura.KuraErrorCode;
import org.eclipse.kura.KuraException;
import org.eclipse.kura.configuration.ComponentConfiguration;
import org.eclipse.kura.configuration.SelfConfiguringComponent;
import org.eclipse.kura.core.configuration.ComponentConfigurationImpl;
import org.eclipse.kura.core.configuration.metatype.ObjectFactory;
import org.eclipse.kura.core.configuration.metatype.Tad;
import org.eclipse.kura.core.configuration.metatype.Tocd;
import org.eclipse.kura.core.configuration.metatype.Tscalar;
import org.eclipse.kura.core.net.FirewallConfiguration;
import org.eclipse.kura.linux.net.iptables.LinuxFirewall;
import org.eclipse.kura.linux.net.iptables.LocalRule;
import org.eclipse.kura.linux.net.iptables.NATRule;
import org.eclipse.kura.linux.net.iptables.PortForwardRule;
import org.eclipse.kura.net.IP4Address;
import org.eclipse.kura.net.IPAddress;
import org.eclipse.kura.net.NetProtocol;
import org.eclipse.kura.net.NetworkPair;
import org.eclipse.kura.net.admin.event.FirewallConfigurationChangeEvent;
import org.eclipse.kura.net.firewall.FirewallAutoNatConfig;
import org.eclipse.kura.net.firewall.FirewallNatConfig;
import org.eclipse.kura.net.firewall.FirewallOpenPortConfigIP;
import org.eclipse.kura.net.firewall.FirewallOpenPortConfigIP4;
import org.eclipse.kura.net.firewall.FirewallPortForwardConfigIP;
import org.eclipse.kura.net.firewall.FirewallPortForwardConfigIP4;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.event.EventAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FirewallConfigurationServiceImpl implements FirewallConfigurationService, SelfConfiguringComponent {

    private static final Logger s_logger = LoggerFactory.getLogger(FirewallConfigurationServiceImpl.class);

    private EventAdmin m_eventAdmin;

    public void setEventAdmin(EventAdmin eventAdmin) {
        this.m_eventAdmin = eventAdmin;
    }

    public void unsetEventAdmin(EventAdmin eventAdmin) {
        this.m_eventAdmin = null;
    }

    protected void activate(ComponentContext componentContext, Map<String, Object> properties) {
        s_logger.debug("activate()");

        // we are intentionally ignoring the properties from ConfigAdmin at startup
        if (properties == null) {
            s_logger.debug("activate() :: Got null properties...");
        } else {
            for (String key : properties.keySet()) {
                s_logger.debug("activate() :: Props... {}={}", key, properties.get(key));
            }
        }
    }

    protected void deactivate(ComponentContext componentContext) {
        s_logger.debug("deactivate()");
    }

    public synchronized void updated(Map<String, Object> properties) {
        s_logger.debug("updated()");
        for (String key : properties.keySet()) {
            s_logger.debug("updated() :: Props... {}={}", key, properties.get(key));
        }
        FirewallConfiguration firewallConfiguration = new FirewallConfiguration(properties);
        try {
            setFirewallOpenPortConfiguration(firewallConfiguration.getOpenPortConfigs());
        } catch (KuraException e) {
            s_logger.error("Failed to set Firewall OPen Ports Configuration - {}", e);
        }
        try {
            setFirewallPortForwardingConfiguration(firewallConfiguration.getPortForwardConfigs());
        } catch (KuraException e) {
            s_logger.error("Failed to set Firewall Port Forwarding Configuration - {}", e);
        }
        try {
            setFirewallNatConfiguration(firewallConfiguration.getNatConfigs());
        } catch (KuraException e) {
            s_logger.error("Failed to set Firewall NAT Configuration - {}", e);
        }

        // raise the event because there was a change
        this.m_eventAdmin.postEvent(new FirewallConfigurationChangeEvent(properties));
    }

    @Override
    public FirewallConfiguration getFirewallConfiguration() throws KuraException {
        s_logger.debug("getting the firewall configuration");

        FirewallConfiguration firewallConfiguration = new FirewallConfiguration();
        LinuxFirewall firewall = LinuxFirewall.getInstance();

        Iterator<LocalRule> localRules = firewall.getLocalRules().iterator();
        while (localRules.hasNext()) {
            LocalRule localRule = localRules.next();
            if (localRule.getPortRange() != null) {
                s_logger.debug("getFirewallConfiguration() :: Adding local rule for {}", localRule.getPortRange());
                firewallConfiguration.addConfig(new FirewallOpenPortConfigIP4(localRule.getPortRange(),
                        NetProtocol.valueOf(localRule.getProtocol()), localRule.getPermittedNetwork(),
                        localRule.getPermittedInterfaceName(), localRule.getUnpermittedInterfaceName(),
                        localRule.getPermittedMAC(), localRule.getSourcePortRange()));
            } else {
                s_logger.debug("getFirewallConfiguration() :: Adding local rule for {}", localRule.getPort());
                firewallConfiguration.addConfig(new FirewallOpenPortConfigIP4(localRule.getPort(),
                        NetProtocol.valueOf(localRule.getProtocol()), localRule.getPermittedNetwork(),
                        localRule.getPermittedInterfaceName(), localRule.getUnpermittedInterfaceName(),
                        localRule.getPermittedMAC(), localRule.getSourcePortRange()));
            }
        }
        Iterator<PortForwardRule> portForwardRules = firewall.getPortForwardRules().iterator();
        while (portForwardRules.hasNext()) {
            PortForwardRule portForwardRule = portForwardRules.next();
            try {
                s_logger.debug("getFirewallConfiguration() :: Adding port forwarding - inbound iface is {}",
                        portForwardRule.getInboundIface());
                firewallConfiguration
                        .addConfig(
                                new FirewallPortForwardConfigIP4(portForwardRule.getInboundIface(),
                                        portForwardRule.getOutboundIface(),
                                        (IP4Address) IPAddress.parseHostAddress(portForwardRule.getAddress()),
                                        NetProtocol.valueOf(portForwardRule.getProtocol()), portForwardRule.getInPort(),
                                        portForwardRule.getOutPort(), portForwardRule.isMasquerade(),
                                        new NetworkPair<IP4Address>(
                                                (IP4Address) IPAddress
                                                        .parseHostAddress(portForwardRule.getPermittedNetwork()),
                                                (short) portForwardRule.getPermittedNetworkMask()),
                                        portForwardRule.getPermittedMAC(), portForwardRule.getSourcePortRange()));
            } catch (UnknownHostException e) {
                e.printStackTrace();
                throw new KuraException(KuraErrorCode.INTERNAL_ERROR, e);
            }
        }
        Iterator<NATRule> autoNatRules = firewall.getAutoNatRules().iterator();
        while (autoNatRules.hasNext()) {
            NATRule autoNatRule = autoNatRules.next();
            s_logger.debug("getFirewallConfiguration() :: Adding auto NAT rules {}", autoNatRule.getSourceInterface());
            firewallConfiguration.addConfig(new FirewallAutoNatConfig(autoNatRule.getSourceInterface(),
                    autoNatRule.getDestinationInterface(), autoNatRule.isMasquerade()));
        }

        Iterator<NATRule> natRules = firewall.getNatRules().iterator();
        while (natRules.hasNext()) {
            NATRule natRule = natRules.next();
            s_logger.debug("getFirewallConfiguration() :: Adding NAT rules {}", natRule.getSourceInterface());
            firewallConfiguration.addConfig(new FirewallNatConfig(natRule.getSourceInterface(),
                    natRule.getDestinationInterface(), natRule.getProtocol(), natRule.getSource(),
                    natRule.getDestination(), natRule.isMasquerade()));
        }
        return firewallConfiguration;
    }

    @Override
    public ComponentConfiguration getConfiguration() throws KuraException {
        s_logger.debug("getConfiguration()");
        try {
            FirewallConfiguration firewallConfiguration = getFirewallConfiguration();
            return new ComponentConfigurationImpl(PID, getDefinition(),
                    firewallConfiguration.getConfigurationProperties());
        } catch (Exception e) {
            e.printStackTrace();
            throw new KuraException(KuraErrorCode.INTERNAL_ERROR, e);
        }
    }

    @Override
    public void setFirewallOpenPortConfiguration(
            List<FirewallOpenPortConfigIP<? extends IPAddress>> firewallConfiguration) throws KuraException {
        s_logger.debug("setFirewallOpenPortConfiguration() :: Deleting local rules");
        LinuxFirewall firewall = LinuxFirewall.getInstance();
        firewall.deleteAllLocalRules();

        ArrayList<LocalRule> localRules = new ArrayList<LocalRule>();
        for (FirewallOpenPortConfigIP<? extends IPAddress> openPortEntry : firewallConfiguration) {
            if (openPortEntry.getPermittedNetwork() == null
                    || openPortEntry.getPermittedNetwork().getIpAddress() == null) {
                try {
                    openPortEntry
                            .setPermittedNetwork(new NetworkPair(IPAddress.parseHostAddress("0.0.0.0"), (short) 0));
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
            }

            try {
                LocalRule localRule = null;
                if (openPortEntry.getPortRange() != null) {
                    s_logger.debug("setFirewallOpenPortConfiguration() :: Adding local rule for: {}",
                            openPortEntry.getPortRange());
                    localRule = new LocalRule(openPortEntry.getPortRange(), openPortEntry.getProtocol().name(),
                            new NetworkPair(
                                    IPAddress.parseHostAddress(
                                            openPortEntry.getPermittedNetwork().getIpAddress().getHostAddress()),
                                    openPortEntry.getPermittedNetwork().getPrefix()),
                            openPortEntry.getPermittedInterfaceName(), openPortEntry.getUnpermittedInterfaceName(),
                            openPortEntry.getPermittedMac(), openPortEntry.getSourcePortRange());
                } else {
                    s_logger.debug("setFirewallOpenPortConfiguration() :: Adding local rule for: {}",
                            openPortEntry.getPort());
                    localRule = new LocalRule(openPortEntry.getPort(), openPortEntry.getProtocol().name(),
                            new NetworkPair(
                                    IPAddress.parseHostAddress(
                                            openPortEntry.getPermittedNetwork().getIpAddress().getHostAddress()),
                                    openPortEntry.getPermittedNetwork().getPrefix()),
                            openPortEntry.getPermittedInterfaceName(), openPortEntry.getUnpermittedInterfaceName(),
                            openPortEntry.getPermittedMac(), openPortEntry.getSourcePortRange());
                }
                localRules.add(localRule);
            } catch (Exception e) {
                s_logger.error("setFirewallOpenPortConfiguration() :: Failed to add local rule for: {} - {}",
                        openPortEntry.getPort(), e);
            }
        }

        firewall.addLocalRules(localRules);
    }

    @Override
    public void setFirewallPortForwardingConfiguration(
            List<FirewallPortForwardConfigIP<? extends IPAddress>> firewallConfiguration) throws KuraException {
        s_logger.debug("setFirewallPortForwardingConfiguration() :: Deleting port forward rules");
        LinuxFirewall firewall = LinuxFirewall.getInstance();
        firewall.deleteAllPortForwardRules();

        ArrayList<PortForwardRule> portForwardRules = new ArrayList<PortForwardRule>();
        for (FirewallPortForwardConfigIP<? extends IPAddress> portForwardEntry : firewallConfiguration) {
            s_logger.debug("setFirewallPortForwardingConfiguration() :: Adding port forward rule for: {}",
                    portForwardEntry.getInPort());

            if (portForwardEntry.getPermittedNetwork() == null
                    || portForwardEntry.getPermittedNetwork().getIpAddress() == null) {
                try {
                    portForwardEntry
                            .setPermittedNetwork(new NetworkPair(IPAddress.parseHostAddress("0.0.0.0"), (short) 0));
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
            }

            PortForwardRule portForwardRule = new PortForwardRule(portForwardEntry.getInboundInterface(),
                    portForwardEntry.getOutboundInterface(), portForwardEntry.getAddress().getHostAddress(),
                    portForwardEntry.getProtocol().name(), portForwardEntry.getInPort(), portForwardEntry.getOutPort(),
                    portForwardEntry.isMasquerade(),
                    portForwardEntry.getPermittedNetwork().getIpAddress().getHostAddress(),
                    portForwardEntry.getPermittedNetwork().getPrefix(), portForwardEntry.getPermittedMac(),
                    portForwardEntry.getSourcePortRange());
            portForwardRules.add(portForwardRule);
        }

        firewall.addPortForwardRules(portForwardRules);
    }

    @Override
    public void setFirewallNatConfiguration(List<FirewallNatConfig> natConfigs) throws KuraException {

        LinuxFirewall firewall = LinuxFirewall.getInstance();
        firewall.deleteAllNatRules();

        ArrayList<NATRule> natRules = new ArrayList<NATRule>();
        for (FirewallNatConfig natConfig : natConfigs) {
            NATRule natRule = new NATRule(natConfig.getSourceInterface(), natConfig.getDestinationInterface(),
                    natConfig.getProtocol(), natConfig.getSource(), natConfig.getDestination(),
                    natConfig.isMasquerade());
            natRules.add(natRule);
        }

        firewall.addNatRules(natRules);
    }

    private Tocd getDefinition() throws KuraException {

        ObjectFactory objectFactory = new ObjectFactory();
        Tocd tocd = objectFactory.createTocd();

        tocd.setName("FirewallConfigurationService");
        tocd.setId("org.eclipse.kura.net.admin.FirewallConfigurationService");
        tocd.setDescription("Firewall Configuration Service");

        Tad tad = objectFactory.createTad();
        tad.setId(FirewallConfiguration.OPEN_PORTS_PROP_NAME);
        tad.setName(FirewallConfiguration.OPEN_PORTS_PROP_NAME);
        tad.setType(Tscalar.STRING);
        tad.setCardinality(10000);
        tad.setRequired(true);
        tad.setDefault(FirewallConfiguration.DFLT_OPEN_PORTS_VALUE);
        tad.setDescription(NetworkAdminConfigurationMessages.getMessage(NetworkAdminConfiguration.PLATFORM_INTERFACES));
        tocd.addAD(tad);

        tad = objectFactory.createTad();
        tad.setId(FirewallConfiguration.PORT_FORWARDING_PROP_NAME);
        tad.setName(FirewallConfiguration.PORT_FORWARDING_PROP_NAME);
        tad.setType(Tscalar.STRING);
        tad.setCardinality(10000);
        tad.setRequired(true);
        tad.setDefault(FirewallConfiguration.DFLT_PORT_FORWARDING_VALUE);
        tad.setDescription(NetworkAdminConfigurationMessages.getMessage(NetworkAdminConfiguration.PLATFORM_INTERFACES));
        tocd.addAD(tad);

        tad = objectFactory.createTad();
        tad.setId(FirewallConfiguration.NAT_PROP_NAME);
        tad.setName(FirewallConfiguration.NAT_PROP_NAME);
        tad.setType(Tscalar.STRING);
        tad.setCardinality(10000);
        tad.setRequired(true);
        tad.setDefault(FirewallConfiguration.DFLT_NAT_VALUE);
        tad.setDescription(NetworkAdminConfigurationMessages.getMessage(NetworkAdminConfiguration.PLATFORM_INTERFACES));
        tocd.addAD(tad);

        return tocd;
    }
}
