/**
 * Copyright (c) 2010-2019 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.extensionservice.marketplace.internal;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.config.core.ConfigurableService;
import org.eclipse.smarthome.core.events.Event;
import org.eclipse.smarthome.core.events.EventPublisher;
import org.eclipse.smarthome.core.extension.Extension;
import org.eclipse.smarthome.core.extension.ExtensionEventFactory;
import org.eclipse.smarthome.core.extension.ExtensionService;
import org.eclipse.smarthome.core.extension.ExtensionType;
import org.openhab.extensionservice.marketplace.MarketplaceExtension;
import org.openhab.extensionservice.marketplace.MarketplaceExtensionHandler;
import org.openhab.extensionservice.marketplace.MarketplaceHandlerException;
import org.openhab.extensionservice.marketplace.internal.model.Node;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is an {@link ExtensionService}, which accesses the Eclipse IoT Marketplace and makes its content available as
 * extensions.
 *
 * @author Kai Kreuzer - Initial contribution and API
 *
 */
@Component(configurationPid = "org.eclipse.smarthome.marketplace", immediate = true, property = {
        Constants.SERVICE_PID + "=org.eclipse.smarthome.marketplace", //
        ConfigurableService.SERVICE_PROPERTY_CATEGORY + "=system", //
        ConfigurableService.SERVICE_PROPERTY_DESCRIPTION_URI + "=system:marketplace", //
        ConfigurableService.SERVICE_PROPERTY_LABEL + "=Marketplace" //
})
public class MarketplaceExtensionService implements ExtensionService {
    /**
     * Enumeration of supported extension package types plus associated attributes.
     */
    private enum PackageType {
        BINDING("binding", MarketplaceExtension.EXT_TYPE_BINDING, "bindings", "Bindings"),
        RULE_TEMPLATE("rule_template", MarketplaceExtension.EXT_TYPE_RULE_TEMPLATE, "ruletemplates", "Rule Templates"),
        VOICE("voice", MarketplaceExtension.EXT_TYPE_VOICE, "voice", "Voice");

        /**
         * Constant used in marketplace nodes.
         */
        final String typeName;

        /**
         * MarketplaceExtension.EXT_TYPE_ symbolic name.
         */
        final String extType;

        /**
         * Key used in config file for setting visibility property.
         */
        final String configKey;

        /**
         * Label to display on Paper UI tab.
         */
        final String label;

        private PackageType(String typeName, String extType, String configKey, String label) {
            this.typeName = typeName;
            this.extType = extType;
            this.configKey = configKey;
            this.label = label;
        }
    }

    private static final String MARKETPLACE_HOST = "marketplace.eclipse.org";
    private static final Pattern EXTENSION_ID_PATTERN = Pattern.compile(".*?mpc_install=([^&]+?)(&.*)?");

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

    // increased visibility for unit tests
    MarketplaceProxy proxy;
    private EventPublisher eventPublisher;
    private final Pattern labelPattern = Pattern.compile("<.*>"); // checks for the existence of any xml element
    private final Pattern descriptionPattern = Pattern.compile("<(javascript|div|font)"); // checks for the existence of
                                                                                          // some
    // invalid elements

    // configured package type inclusion settings, keyed by package typeName
    private Map<String, Boolean> packageTypeInclusions = new HashMap<>();

    private int maturityLevel = 1;
    private final Set<MarketplaceExtensionHandler> extensionHandlers = new HashSet<>();

    @Activate
    protected void activate(Map<String, Object> config) {
        this.proxy = new MarketplaceProxy();
        modified(config);
    }

    @Deactivate
    protected void deactivate() {
        this.proxy.dispose();
        this.proxy = null;
    }

    @Modified
    protected void modified(Map<String, Object> config) {
        for (PackageType packageType : PackageType.values()) {
            Object inclusionCfg = config.get(packageType.configKey);
            if (inclusionCfg != null) {
                packageTypeInclusions.put(packageType.typeName,
                        inclusionCfg.toString().equals(Boolean.TRUE.toString()));
            }
        }
        Object cfgMaturityLevel = config.get("maturity");
        if (cfgMaturityLevel != null) {
            try {
                this.maturityLevel = Integer.valueOf(cfgMaturityLevel.toString());
            } catch (NumberFormatException e) {
                logger.warn("Ignoring invalid value '{}' for configuration parameter '{}'", cfgMaturityLevel.toString(),
                        "maturity");
            }
        }
    }

    @Reference
    protected void setEventPublisher(EventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    protected void unsetEventPublisher(EventPublisher eventPublisher) {
        this.eventPublisher = null;
    }

    @Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC)
    protected void addExtensionHandler(MarketplaceExtensionHandler handler) {
        this.extensionHandlers.add(handler);
    }

    protected void removeExtensionHandler(MarketplaceExtensionHandler handler) {
        this.extensionHandlers.remove(handler);
    }

    @Override
    public List<Extension> getExtensions(Locale locale) {
        List<Node> nodes = proxy.getNodes();
        List<Extension> exts = new ArrayList<>(nodes.size());
        for (Node node : nodes) {
            if (node.id == null) {
                // workaround for bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=512493
                continue;
            }
            if (toMaturityLevel(node.status) < this.maturityLevel) {
                continue;
            }
            if (!packageTypeInclusions.getOrDefault(node.packagetypes, true)) {
                continue;
            }

            MarketplaceExtension ext = convertToExtension(node);
            if (ext != null) {
                if (setInstalledFlag(ext)) {
                    exts.add(ext);
                }
            }
        }
        return exts;
    }

    private boolean setInstalledFlag(MarketplaceExtension ext) {
        for (MarketplaceExtensionHandler handler : extensionHandlers) {
            if (handler.supports(ext)) {
                ext.setInstalled(handler.isInstalled(ext));
                return true;
            }
        }
        return false;
    }

    private MarketplaceExtension convertToExtension(Node node) {
        String extId = getExtensionId(node);

        String name = node.name;
        String desc = node.shortdescription;
        String version = StringUtils.isNotEmpty(node.version) ? node.version : "1.0";

        if (!validName(name) || !validDescription(desc)) {
            logger.debug("Ignoring node {} due to invalid content.", node.id);
            return null;
        }
        for (PackageType packageType : PackageType.values()) {
            if (packageType.typeName.equals(node.packagetypes)) {
                MarketplaceExtension ext = new MarketplaceExtension(extId, packageType.extType, name, version,
                        node.supporturl, false, desc, null, node.image, node.updateurl, node.packageformat);
                return ext;
            }
        }
        return null;
    }

    @Override
    public Extension getExtension(String id, Locale locale) {
        for (Extension extension : getExtensions(locale)) {
            if (extension.getId().equals(id)) {
                return extension;
            }
        }
        return null;
    }

    @Override
    public List<ExtensionType> getTypes(Locale locale) {
        ArrayList<ExtensionType> types = new ArrayList<>(2);
        List<Extension> exts = getExtensions(locale);
        for (PackageType packageType : PackageType.values()) {
            if (packageTypeInclusions.getOrDefault(packageType.typeName, true)) {
                for (Extension ext : exts) {
                    if (ext.getType().equals(packageType.extType)) {
                        types.add(new ExtensionType(packageType.extType, packageType.label));
                        break;
                    }
                }
            }
        }
        return Collections.unmodifiableList(types);
    }

    @Override
    public void install(String extensionId) {
        Extension ext = getExtension(extensionId, null);
        if (ext instanceof MarketplaceExtension) {
            MarketplaceExtension mpExt = (MarketplaceExtension) ext;
            for (MarketplaceExtensionHandler handler : extensionHandlers) {
                if (handler.supports(mpExt)) {
                    if (!handler.isInstalled(mpExt)) {
                        try {
                            handler.install(mpExt);
                            postInstalledEvent(extensionId);
                        } catch (MarketplaceHandlerException e) {
                            postFailureEvent(extensionId, e.getMessage());
                        }
                    } else {
                        postFailureEvent(extensionId, "Extension is already installed.");
                    }
                    return;
                }
            }
        }
        postFailureEvent(extensionId, "Extension not known.");
    }

    @Override
    public void uninstall(String extensionId) {
        Extension ext = getExtension(extensionId, null);
        if (ext instanceof MarketplaceExtension) {
            MarketplaceExtension mpExt = (MarketplaceExtension) ext;
            for (MarketplaceExtensionHandler handler : extensionHandlers) {
                if (handler.supports(mpExt)) {
                    if (handler.isInstalled(mpExt)) {
                        try {
                            handler.uninstall(mpExt);
                            postUninstalledEvent(extensionId);
                        } catch (MarketplaceHandlerException e) {
                            postFailureEvent(extensionId, e.getMessage());
                        }
                    } else {
                        postFailureEvent(extensionId, "Extension is not installed.");
                    }
                    return;
                }
            }
        }
        postFailureEvent(extensionId, "Extension not known.");
    }

    @Override
    public String getExtensionId(URI extensionURI) {
        if (extensionURI != null && extensionURI.getHost().equals(MARKETPLACE_HOST)) {
            return extractExensionId(extensionURI);
        }

        return null;
    }

    private void postInstalledEvent(String extensionId) {
        Event event = ExtensionEventFactory.createExtensionInstalledEvent(extensionId);
        eventPublisher.post(event);
    }

    private void postUninstalledEvent(String extensionId) {
        Event event = ExtensionEventFactory.createExtensionUninstalledEvent(extensionId);
        eventPublisher.post(event);
    }

    private void postFailureEvent(String extensionId, String msg) {
        Event event = ExtensionEventFactory.createExtensionFailureEvent(extensionId, msg);
        eventPublisher.post(event);
    }

    private String getExtensionId(Node node) {
        StringBuilder sb = new StringBuilder(MarketplaceExtension.EXT_PREFIX);
        boolean found = false;
        for (PackageType packageType : PackageType.values()) {
            if (packageType.typeName.equals(node.packagetypes)) {
                sb.append(packageType.extType).append("-");
                sb.append(node.id.replaceAll("[^a-zA-Z0-9_]", ""));
                return sb.toString();
            }
        }
        return null;
    }

    private int toMaturityLevel(String maturity) {
        switch (maturity) {
            case "Alpha":
                return 0;
            case "Beta":
                return 1;
            case "Production/Stable":
                return 2;
            case "Mature":
                return 3;
            default:
                logger.debug("Unknown maturity level value '{}' - using 'Alpha' instead.", maturity);
                return 0;
        }
    }

    private boolean validName(String name) {
        return !labelPattern.matcher(name).find();
    }

    private boolean validDescription(String desc) {
        return !descriptionPattern.matcher(desc).find();
    }

    private String extractExensionId(URI uri) {
        Matcher idMatcher = EXTENSION_ID_PATTERN.matcher(uri.getQuery());
        String id = null;
        if (idMatcher.matches() && idMatcher.groupCount() > 1) {
            id = idMatcher.group(1);
        }

        Optional<Node> extensionNode = getExtensionNode(id);

        return extensionNode.isPresent() ? getExtensionId(extensionNode.get()) : null;
    }

    private Optional<Node> getExtensionNode(String id) {
        return proxy.getNodes().stream().filter(node -> node != null && node.id.equals(id)).findFirst();
    }

}
