/*
 * 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.brooklynnode;

import java.util.Collection;
import java.util.Map;
import java.util.concurrent.Callable;

import javax.annotation.Nullable;

import org.apache.brooklyn.api.effector.Effector;
import org.apache.brooklyn.core.effector.EffectorBody;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityDynamicType;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.feed.http.HttpFeed;
import org.apache.brooklyn.feed.http.HttpPollConfig;
import org.apache.brooklyn.util.collections.Jsonya;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.http.HttpToolResponse;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.net.Urls;
import org.apache.http.HttpStatus;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.net.MediaType;
import com.google.gson.Gson;

public class BrooklynEntityMirrorImpl extends AbstractEntity implements BrooklynEntityMirror {
    @SuppressWarnings("rawtypes")
    private class MirrorSummary implements Function<HttpToolResponse, Map> {
        @Override
        public Map apply(HttpToolResponse input) {
            Map<?, ?> entitySummary = new Gson().fromJson(input.getContentAsString(), Map.class);
            String catalogItemId = (String)entitySummary.get("catalogItemId");
            sensors().set(MIRROR_CATALOG_ITEM_ID, catalogItemId);
            return entitySummary;
        }
    }

    private HttpFeed mirror;
    

    //Passively mirror entity's state
    @Override
    protected void initEnrichers() {}

    @Override
    public void init() {
        super.init();
        connectSensorsAsync();

        //start spinning, could take some time before MIRRORED_ENTITY_URL is available for first time mirroring
        sensors().set(Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STARTING);
    }

    @Override
    public void rebind() {
        super.rebind();
        connectSensorsAsync();
    }

    protected void connectSensorsAsync() {
        Callable<Void> asyncTask = new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                //blocks until available (could be a task)
                String mirroredEntityUrl = getConfig(MIRRORED_ENTITY_URL);
                Preconditions.checkNotNull(mirroredEntityUrl, "Required config: "+MIRRORED_ENTITY_URL);

                connectSensors(mirroredEntityUrl);
                return null;
            }
        };

        DynamicTasks.queueIfPossible(
                Tasks.<Void>builder()
                    .displayName("Start entity mirror feed")
                    .body(asyncTask)
                    .build())
            .orSubmitAsync(this);
    }

    protected void connectSensors(String mirroredEntityUrl) {
        Function<HttpToolResponse, Void> mirrorSensors = new Function<HttpToolResponse,Void>() {
            @SuppressWarnings("rawtypes")
            @Override
            public Void apply(HttpToolResponse input) {
                Map sensors = new Gson().fromJson(input.getContentAsString(), Map.class);
                for (Object kv: sensors.entrySet())
                    sensors().set(Sensors.newSensor(Object.class, ""+((Map.Entry)kv).getKey()), ((Map.Entry)kv).getValue());
                sensors().set(MIRROR_STATUS, "normal");
                return null;
            }
        };

        final BrooklynEntityMirrorImpl self = this;
        mirror = HttpFeed.builder().entity(this)
            .baseUri(mirroredEntityUrl)
            .credentialsIfNotNull(getConfig(BrooklynNode.MANAGEMENT_USER), getConfig(BrooklynNode.MANAGEMENT_PASSWORD))
            .period(getConfig(POLL_PERIOD))
            .poll(HttpPollConfig.forMultiple()
                .suburl("/sensors/current-state")
                .onSuccess(mirrorSensors)
                .onFailureOrException(new Function<Object, Void>() {
                    @Override
                    public Void apply(Object input) {
                        ServiceStateLogic.updateMapSensorEntry(self, Attributes.SERVICE_PROBLEMS, "mirror-feed", "error contacting service");
                        if (input instanceof HttpToolResponse) {
                            int responseCode = ((HttpToolResponse)input).getResponseCode();
                            if (responseCode == HttpStatus.SC_NOT_FOUND) {
                                //the remote entity no longer exists
                                Entities.unmanage(self);
                            }
                        }
                        return null;
                    }
                }))
            .poll(HttpPollConfig.forSensor(MIRROR_SUMMARY).onSuccess(new MirrorSummary())).build();

        populateEffectors();
    }

    private void populateEffectors() {
        HttpToolResponse result = http().get("/effectors");
        Collection<?> cfgEffectors = new Gson().fromJson(result.getContentAsString(), Collection.class);
        Collection<Effector<String>> remoteEntityEffectors = RemoteEffectorBuilder.of(cfgEffectors);
        EntityDynamicType mutableEntityType = getMutableEntityType();
        for (Effector<String> eff : remoteEntityEffectors) {
            //remote already started
            if ("start".equals(eff.getName())) continue;
            mutableEntityType.addEffector(eff);
        }
    }

    protected void disconnectSensors() {
        if (mirror != null) mirror.stop();
    }

    @Override
    public void destroy() {
        disconnectSensors();
    }

    @Override
    public EntityHttpClient http() {
        return new EntityHttpClientImpl(this, MIRRORED_ENTITY_URL);
    }

    public static class RemoteEffector<T> extends EffectorBody<T> {
        public final String remoteEffectorName;
        public final Function<HttpToolResponse, T> resultParser;
        
        /** creates an effector implementation which POSTs to a remote effector endpoint, optionally converting
         * the byte[] response (if resultParser is null then null is returned) */
        public RemoteEffector(String remoteEffectorName, @Nullable Function<HttpToolResponse,T> resultParser) {
            this.remoteEffectorName = remoteEffectorName;
            this.resultParser = resultParser;
        }

        @Override
        public T call(ConfigBag parameters) {
            MutableMap<String, String> headers = MutableMap.of(com.google.common.net.HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
            byte[] httpBody = Jsonya.of(parameters.getAllConfig()).toString().getBytes();
            String effectorUrl = Urls.mergePaths("effectors", Urls.encode(remoteEffectorName));
            HttpToolResponse result = ((BrooklynEntityMirror)entity()).http().post(effectorUrl, headers, httpBody);
            if (resultParser!=null) return resultParser.apply(result);
            else return null;
        }
    }
}
