/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.brooklyn.entity.cloudfoundry.webapp;


import org.apache.brooklyn.entity.cloudfoundry.PaasEntityCloudFoundryDriver;
import org.apache.brooklyn.entity.cloudfoundry.services.CloudFoundryService;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.location.cloudfoundry.CloudFoundryPaasLocation;
import org.apache.brooklyn.util.http.HttpTool;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.Strings;
import org.cloudfoundry.client.lib.domain.CloudApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.HttpURLConnection;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;


public abstract class PaasWebAppCloudFoundryDriver extends PaasEntityCloudFoundryDriver
        implements PaasWebAppDriver {

    public static final Logger log = LoggerFactory.getLogger(PaasWebAppCloudFoundryDriver.class);

    private String applicationUrl;
    private String applicationName;

    public PaasWebAppCloudFoundryDriver(CloudFoundryWebAppImpl entity,
                                        CloudFoundryPaasLocation location) {
        super(entity, location);
    }

    @Override
    protected void init() {
        super.init();
        initApplicationParameters();
    }

    private void initApplicationParameters() {
        if(!Strings.isBlank(getEntity().getConfig(CloudFoundryWebApp.APPLICATION_NAME))){
            applicationName = getEntity().getConfig(CloudFoundryWebApp.APPLICATION_NAME);
        } else {
            applicationName = "cf-app-" + Identifiers.makeRandomId(8);
        }
        applicationUrl = getEntity().getConfig(CloudFoundryWebApp.APPLICATION_URL);
    }

    @Override
    public CloudFoundryWebAppImpl getEntity() {
        return (CloudFoundryWebAppImpl) super.getEntity();
    }

    protected String getApplicationUrl(){
        return applicationUrl;
    }

    protected String getApplicationName(){
        return applicationName;
    }

    public abstract String getBuildpack();

    @Override
    public boolean isRunning() {
        try {
            CloudApplication app = getClient().getApplication(applicationName);
            return (app != null)
                    && app.getState().equals(CloudApplication.AppState.STARTED)
                    && isApplicationDomainAvailable();
        } catch (Exception e) {
            return false;
        }
    }

    private boolean isApplicationDomainAvailable(){
        boolean result;
        try {
            result = HttpTool.getHttpStatusCode(getDomainUri()) == HttpURLConnection.HTTP_OK ;
        } catch (Exception e) {
            result = false;
        }
        return result;
    }

    @Override
    public  int getInstancesNumber(){
        CloudApplication app = getClient().getApplication(getApplicationName());
        return app.getInstances();
    }

    @Override
    public  int getDisk(){
        CloudApplication app = getClient().getApplication(getApplicationName());
        return app.getDiskQuota();
    }

    @Override
    public  int getMemory(){
        CloudApplication app = getClient().getApplication(getApplicationName());
        return app.getMemory();
    }

    @Override
    public void start() {
        super.start();

        preDeploy();
        deploy();
        preLaunch();
        launch();
        postLaunch();
    }

    public void preDeploy() {}

    public abstract void deploy();

    public void preLaunch() {
        manageServices();
        
        configureEnv();
    }

    private void manageServices() {
        List<Entity> config = getEntity().getConfig(CloudFoundryWebApp.NAMED_SERVICES);
        if (config != null) {
            for (Entity serviceEntityId : config) {
                manageService(serviceEntityId);
            }
        }
    }

    /*TODO RENAME Method. It could be represent that the service is bound and the service
     operation is called*/
    private void manageService(Entity rawEntity){

        CloudFoundryService cloudFoundryService;
        if (rawEntity instanceof CloudFoundryService){

            cloudFoundryService = (CloudFoundryService) rawEntity;
        
            String serviceName = cloudFoundryService
                    .getConfig(CloudFoundryService.SERVICE_INSTANCE_NAME);


            if (!Strings.isEmpty(serviceName)){

                Entities.waitForServiceUp(cloudFoundryService,
                        cloudFoundryService.getConfig(BrooklynConfigKeys.START_TIMEOUT));

                bindingServiceToEntity(serviceName);
                setCredentialsOnService(cloudFoundryService);
                cloudFoundryService.operation(getEntity());
            } else {
                log.error("Trying to get service instance name from {}, but getting null",
                        cloudFoundryService);
            }
        } else {
            log.error("The service entity {} is not available from the application {}",
                    new Object[]{rawEntity, getEntity()});

            throw new NoSuchElementException("No entity matching id " + rawEntity.getId() +
                    " in Management Context "+getEntity().getManagementContext()+
                    " during entity service binding "+getEntity().getId());
        }
    }

    private void bindingServiceToEntity(String serviceId) {
        getClient().bindService(applicationName, serviceId);
        log.info("The service {} was bound correctly to the application {}",
                new Object[]{serviceId, applicationName});

        updateVariableEnvironmentSensors();
        updateBoundServiceSensor(serviceId);
    }

    private void updateVariableEnvironmentSensors(){
        Map<String, Object> env = getClient().getApplicationEnvironment(applicationName);
        JsonObject envTree = new Gson().toJsonTree(env).getAsJsonObject();
        getEntity().setAttribute(CloudFoundryWebApp.VCAP_SERVICES,
                envTree.getAsJsonObject("system_env_json")
                        .getAsJsonObject("VCAP_SERVICES").toString());
    }

    protected void setCredentialsOnService(CloudFoundryService service) {
        service.setBindingCredentialsFromApp(getEntity());
    }

    //TODO it ma be renamed to updateBoundServicesSensor
    protected void updateBoundServiceSensor(String serviceId){
        if(serviceIsBoundToCloudApplication(serviceId)){
            List<String> currentBoundServices = getEntity()
                    .getAttribute(CloudFoundryWebApp.BOUND_SERVICES);
            currentBoundServices.add(serviceId);
            getEntity().setAttribute(CloudFoundryWebApp.BOUND_SERVICES, currentBoundServices);
        }
    }

    /**
     * Return if a service is bound to the cloud application. Checks the running application in
     * the cloud.
     * @param serviceId
     * @return
     */
    public boolean serviceIsBoundToCloudApplication(String serviceId){
        return getClient().getApplication(applicationName)
                .getServices().contains(serviceId);
    }

    public void launch() {
        getClient().startApplication(applicationName);
    }

    public void postLaunch() {
        //TODO: we should use TASK for avoid wait methods.
        getEntity().waitForEntityStart();

        CloudApplication application = getClient().getApplication(applicationName);
        String domainUri = getDomainUri();
        getEntity().setAttribute(Attributes.MAIN_URI, URI.create(domainUri));
        getEntity().setAttribute(CloudFoundryWebApp.ROOT_URL, domainUri);
    }

    protected String getDomainUri(){
        String domainUri=null;
        CloudApplication application = getClient().getApplication(applicationName);
        if(application != null){
             domainUri = "https://"+application.getUris().get(0);
        }
        return domainUri;
    }

    @Override
    public void restart() {
        // TODO: complete
    }

    @Override
    public void stop() {
        getClient().stopApplication(applicationName);
        deleteApplication();
    }

    @Override
    public void deleteApplication() {
        getClient().deleteApplication(applicationName);
    }

    protected String inferApplicationDomainUri(String name) {
        String defaultDomainName = getClient().getDefaultDomain().getName();
        return name + "-domain." + defaultDomainName;
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public void setEnv(String key, String value) {
        CloudApplication app = getClient().getApplication(applicationName);

        // app.setEnv() replaces the entire set of variables, so we need to add it externally.
        Map envAsMap = app.getEnvAsMap(); 
        envAsMap.put(key, value);
        app.setEnv(envAsMap);
        getClient().updateApplicationEnv(applicationName, envAsMap);
    }

    protected void configureEnv() {
        //TODO a sensor with the custom-environment variables?
        setEnv(getEntity().getConfig(CloudFoundryWebApp.ENV));
    }

    @SuppressWarnings("unchecked")
    void setEnv(Map<String, String> envs) {
        CloudApplication app = getClient().getApplication(applicationName);

        // app.setEnv() replaces the entire set of variables, so we need to add it externally.
        Map oldEnv = app.getEnvAsMap();
        oldEnv.putAll(envs);
        getClient().updateApplicationEnv(applicationName, oldEnv);
    }

    @Override
    public void changeInstancesNumber(int instancesNumber){
        getClient().updateApplicationInstances(
                getApplicationName(), instancesNumber);
    }

    @Override
    public void updateApplicationDiskQuota(int diskQuota){
        getClient().updateApplicationDiskQuota(getApplicationName(), diskQuota);
    }

    @Override
    public void updateApplicationMemory(int memory){
        getClient().updateApplicationMemory(getApplicationName(), memory);
    }


}