/**
 *  Copyright (C) 2008 Progress Software, Inc. All rights reserved.
 *  http://fusesource.com
 *
 *  The software in this package is published under the terms of the AGPL license
 *  a copy of which has been included with this distribution in the license.txt file.
 */
package org.fusesource.cloudmix.agent.karaf;


import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.felix.karaf.features.FeaturesService;
import org.apache.felix.karaf.admin.AdminService;
import org.apache.felix.karaf.admin.Instance;
import org.apache.felix.karaf.admin.InstanceSettings;
import org.fusesource.cloudmix.agent.Bundle;
import org.fusesource.cloudmix.agent.Feature;
import org.fusesource.cloudmix.agent.FeatureList;
import org.fusesource.cloudmix.agent.InstallerAgent;
import org.fusesource.cloudmix.common.dto.AgentDetails;
import org.fusesource.cloudmix.common.dto.ConfigurationUpdate;
import org.fusesource.cloudmix.common.dto.ProvisioningAction;
import org.fusesource.cloudmix.common.util.FileUtils;
import static org.fusesource.cloudmix.agent.karaf.ConfigAdminHelper.merge;

public class KarafAgent extends InstallerAgent {

    public static final String FEATURES_REPOSITORIES = "featuresRepositories";
    public static final String FEATURES_BOOT = "featuresBoot";

    protected static final String VM_PROP_SMX_HOME = "karaf.home";

    protected static final String AGENT_WORK_DIR = File.separator + "data" + File.separator + "cloudmix";
    protected static final String AGENT_PROPS_PATH_SUFFIX = "agent.properties";

    private static final Log LOGGER = LogFactory.getLog(KarafAgent.class);

    private static final String FEATURE_FILE_KEY = KarafAgent.class.getName() + ".featureFile";

    private static final String SMX4_CONTAINER_TYPE = "smx4";
    private static final String JBI_TYPE = "jbi";

    private static final String[] SMX4_PACKAGE_TYPES = {
        "osgi", "jbi"
    };

    private static final String JBI_URL_PREFIX = "jbi:";

    private FeaturesService featuresService;

    private AdminService adminService;

    public void setFeaturesService(FeaturesService featuresService) {
        this.featuresService = featuresService;
    }

    public void setAdminService(AdminService adminService) {
        this.adminService = adminService;
    }

    @Override
    public String getDetailsPropertyFilePath() {
        if (propertyFilePath == null) {
            propertyFilePath = defaultWorkFile(AGENT_PROPS_PATH_SUFFIX);
        }
        return propertyFilePath;
    }

    @Override
    protected boolean validateAgent() {
        return featuresService != null && getWorkDirectory() != null;
    }

    @Override
    protected void installFeatures(ProvisioningAction action, String credentials, String resource)
        throws Exception {
        LOGGER.info("Installing CloudMix feature " + resource);
        Feature feat = new Feature(action.getFeature());

        if (resource.startsWith("karaf:")) {
            String name = getInstanceName(feat.getName());
            Instance instance = adminService.createInstance(name, new InstanceSettings(0, null, null, null));
            configureInstance(instance, resource);
            instance.start(null);
        } else {
            if (resource.startsWith("scan-features:")) {
                resource = resource.substring("scan-features:".length());
            }
            int idx = resource.indexOf("!/");
            if (idx > 1) {
                String repo = resource.substring(0, idx);
                String feature = resource.substring(idx + 2);
                URI repoUri = new URI(repo);
                LOGGER.info("Adding feature repository " + repoUri);
                featuresService.addRepository(repoUri);
                LOGGER.info("Adding feature: " + feature);
                featuresService.installFeature(feature);
            }
        }
        addAgentFeature(feat);
    }

    private void configureInstance(Instance instance, String resource) throws IOException {
        File config = new File(instance.getLocation(), "etc" + File.separator
                                                       + "org.apache.felix.karaf.features.cfg");
        Map<String, String> properties = new HashMap<String, String>();
        String[] parts = resource.split(" ");
        if (parts.length > 2) {
            properties.put(FEATURES_BOOT, parts[2]);
        }
        if (parts.length > 1) {
            properties.put(FEATURES_REPOSITORIES, parts[1]);
        }
        merge(config, properties);
    }

    @Override
    protected void installFeature(org.fusesource.cloudmix.agent.Feature feature,
                                  List<ConfigurationUpdate> featureCfgOverrides) {

        boolean success = false;
        URI reposUri = null;
        File featuresFile = null;
        try {

            featuresFile = File.createTempFile("features_", ".xml", getWorkDirectory());
            FileWriter writer = new FileWriter(featuresFile);
            writer.write(generateSMX4FeatureDoc(feature.getFeatureList()));
            writer.close();
            LOGGER.info("Wrote features document to " + featuresFile);

            reposUri = featuresFile.toURI();
            LOGGER.info("Adding features repository " + reposUri);
            featuresService.removeRepository(reposUri);
            featuresService.addRepository(reposUri);
            super.installFeature(feature, featureCfgOverrides);

            try {
                featuresService.installFeature(feature.getName());

                // TODO: check featuresService.listInstalledFeatures()?
            } catch (Exception e) {
                LOGGER.error("Error installing feature " + feature, e);
                LOGGER.debug(e);
            }

            success = true;
            LOGGER.info("installation of feature " + feature.getName() + " successful");
            feature.getAgentProperties().put(FEATURE_FILE_KEY, featuresFile);

        } catch (Exception e) {
            LOGGER.error("Error installing feature " + feature + ", exception " + e);
            LOGGER.debug(e);
        } finally {
            if (!success) {
                LOGGER.warn("installFeature failed");
                try {
                    if (reposUri != null) {
                        featuresService.removeRepository(reposUri);
                    }
                } catch (Throwable t) {
                    LOGGER.warn("error removing repository " + reposUri, t);
                }
                if (featuresFile != null) {
                    featuresFile.delete();
                }
            }
        }
    }

    @Override
    protected void uninstallFeature(Feature feature) {
        LOGGER.info("Uninstalling CloudMix feature " + feature);

        saveFeatureLog(feature);
        
        Instance instance = adminService == null ? null : adminService.getInstance(getInstanceName(feature
            .getName()));
        if (instance != null) {
            try {
                instance.stop();
                instance.destroy();
            } catch (Exception e) {
                LOGGER.warn("Unable to stop/destroy a Karaf instance: " + e.getMessage(), e);
            }
        } else {
            String featureName = feature.getName();
            removeFeatureId(featureName);
            try {
                featuresService.uninstallFeature(featureName);
                // TODO: check featuresService.listInstalledFeatures()?
                super.uninstallFeature(feature);

                File featuresFile = (File)feature.getAgentProperties().get(FEATURE_FILE_KEY);
                if (featuresFile == null) {
                    LOGGER.error("Cannot find features file for feature " + featureName);
                } else {
                    URI reposUri = featuresFile.toURI();
                    LOGGER.info("Removing features repository " + reposUri);
                    featuresService.removeRepository(reposUri);

                    LOGGER.info("Deleting features file " + featuresFile);
                    featuresFile.delete();
                }
                feature.getAgentProperties().remove(FEATURE_FILE_KEY);
            } catch (Exception e) {
                LOGGER.error("Error uninstalling feature " + featureName + ", exception " + e);
                e.printStackTrace();
                LOGGER.debug(e);
            }
        }
    }

    private void saveFeatureLog(Feature feature) {
        try {
            File workDir = getWorkDirectory();
            LOGGER.info("Copying feature log to " + workDir.getCanonicalPath() 
                    + "/" + feature.getName());
            
            String path = System.getProperty(VM_PROP_SMX_HOME);
            if (path == null) {
                LOGGER.warn("Container home directory system property is not set,"
                        + " feature log can not be copied");
                return;
            }
            String featureName = feature.getName().replace(':', '_');
            String featureLogPath = path + "/instances/" + featureName + "/data/log/karaf.log";
            InputStream is = new FileInputStream(featureLogPath);
            File outDir = new File(workDir.getCanonicalPath(), "/" + featureName);
            outDir.mkdirs();
            String outLogPath = outDir.getCanonicalPath() + "/karaf.log";
            OutputStream os = new FileOutputStream(outLogPath);
            // likely to work slower but better than invoking OS copy commands
            FileUtils.copy(is, os, 1024 * 16);            
        } catch (FileNotFoundException e) {
            LOGGER.warn("Feature log is not available");
            e.printStackTrace();
        } catch (IOException e) {
            LOGGER.warn("Can not copy the feature log");
            e.printStackTrace();
        }
    }
    
    public String generateSMX4FeatureDoc(FeatureList fl) {
        StringBuilder sb = new StringBuilder().append("<features>\n");
        for (Feature feature : fl.getAllFeatures()) {
            sb.append("  <feature name=\"").append(feature.getName()).append("\">\n");

            for (String pn : feature.getPropertyNames()) {
                sb.append("    <config name=\"").append(pn).append("\">\n");

                Properties props = feature.getProperties(pn);
                for (Object o : props.keySet()) {
                    sb.append("      ").append(o).append(" = ").append(props.get(o)).append("\n");
                }
                sb.append("    </config>\n");
            }

            Map<String, Bundle> unorderedBundles = new HashMap<String, Bundle>();
            for (Bundle b : feature.getBundles()) {
                unorderedBundles.put(b.getUri(), b);
            }

            List<Bundle> orderedBundles = new ArrayList<Bundle>();
            for (Bundle b : feature.getBundles()) {
                doOrderBundlesBasedOnTheirInterDependencies(b, orderedBundles, unorderedBundles);
            }

            for (Bundle b : orderedBundles) {
                sb.append("    <bundle>");
                String type = b.getType();
                if (JBI_TYPE.equals(type)) {
                    sb.append(JBI_URL_PREFIX);
                }
                sb.append(b.getUri()).append("</bundle>\n");
            }

            sb.append("  </feature>\n");
        }
        sb.append("</features>\n");
        return sb.toString();
    }

    private void doOrderBundlesBasedOnTheirInterDependencies(Bundle b, List<Bundle> orderedBundles,
                                                             Map<String, Bundle> unOrderedBundles) {
        if (b == null) {
            return;
        }

        for (String depUri : b.getDepUris()) {
            doOrderBundlesBasedOnTheirInterDependencies(unOrderedBundles.get(depUri), orderedBundles,
                                                        unOrderedBundles);
        }

        String uri = b.getUri();
        if (unOrderedBundles.get(uri) != null) {
            orderedBundles.add(b);
            unOrderedBundles.remove(uri);
        }
    }

    @Override
    protected boolean installBundle(org.fusesource.cloudmix.agent.Feature feature, Bundle bundle) {
        return true;
    }

    @Override
    protected boolean uninstallBundle(Feature feature, Bundle bundle) {
        return false;
    }

    @Override
    public AgentDetails updateAgentDetails() {
        AgentDetails rc = super.updateAgentDetails();

        try {
            getClient().updateAgentDetails(getAgentId(), getAgentDetails());
        } catch (URISyntaxException e) {
            LOGGER.info("Problem updating agent information ", e);
            e.printStackTrace();
        }
        return rc;
    }

    @Override
    public void init() throws Exception {

        File dir = getWorkDirectory();
        if (dir != null) {
            if (!dir.exists()) {
                FileUtils.createDirectory(dir);
            }
        }

        if (getContainerType() == null) {
            setContainerType(SMX4_CONTAINER_TYPE);
        }
        if (getSupportPackageTypes() == null || getSupportPackageTypes().length == 0) {
            setSupportPackageTypes(SMX4_PACKAGE_TYPES);
        }

        super.init();
    }

    private String defaultWorkFile(String fileName) {

        // Default work files based off the ServiceMix data directory.

        String path = System.getProperty(VM_PROP_SMX_HOME);
        if (path == null) {
            LOGGER.error("cannot determing ServiceMix home directory");
            return null;
        }

        path += AGENT_WORK_DIR;
        if (fileName != null) {
            path += File.separator + fileName;
        }

        LOGGER.info("using work file " + path);
        return path;
    }

    public String getInstanceName(String feature) {
        return feature.replaceAll(":", "_");
    }

    @Override
    public String getBaseHref() {
        return "**unavailable**";
    }
}
