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

import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.brooklyn.entity.dns.geoscaling.GeoscalingWebClient.PROVIDE_CITY_INFO;

import java.net.URI;
import java.util.Collection;
import java.util.Set;

import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
import org.apache.brooklyn.core.location.geo.HostGeoInfo;
import org.apache.brooklyn.entity.dns.AbstractGeoDnsServiceImpl;
import org.apache.brooklyn.entity.dns.geoscaling.GeoscalingWebClient.Domain;
import org.apache.brooklyn.entity.dns.geoscaling.GeoscalingWebClient.SmartSubdomain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.http.HttpTool;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.Strings;

public class GeoscalingDnsServiceImpl extends AbstractGeoDnsServiceImpl implements GeoscalingDnsService {

    private static final Logger log = LoggerFactory.getLogger(GeoscalingDnsServiceImpl.class);

    // Must remember any desired redirection targets if they're specified before configure() has been called.
    private Set<HostGeoInfo> rememberedTargetHosts;
    private GeoscalingWebClient webClient;
    
    // These are available only after the configure() method has been invoked.
    private boolean randomizeSmartSubdomainName;
    private String username;
    private String password;
    private String primaryDomainName;
    private String smartSubdomainName;

    public GeoscalingDnsServiceImpl() {
    }

    @Override
    public void init() {
        super.init();
        
        // defaulting to randomized subdomains makes deploying multiple applications easier
        if (config().get(RANDOMIZE_SUBDOMAIN_NAME) == null) {
            config().set(RANDOMIZE_SUBDOMAIN_NAME, true);
        }

        Boolean trustAll = config().get(SSL_TRUST_ALL);
        if (Boolean.TRUE.equals(trustAll)) {
            webClient = new GeoscalingWebClient(HttpTool.httpClientBuilder().trustAll().build());
        } else {
            webClient = new GeoscalingWebClient();
        }
    }
    
    // Ensure our configure() method gets called; may be able to remove this if/when the framework detects this
    // and invokes the configure() method automatically?
    @Override
    public void onManagementStarted() {
        super.onManagementStarted();
        try {
            applyConfig();
        } catch (Exception e) {
            // don't prevent management coming up, but do mark it as on fire
            log.error("Geoscaling did not come up correctly: "+e, e);
            ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
        }
    }

    boolean isConfigured = false;

    @Override
    public synchronized void applyConfig() {        
        randomizeSmartSubdomainName = Boolean.TRUE.equals(config().get(RANDOMIZE_SUBDOMAIN_NAME));
        username = config().get(GEOSCALING_USERNAME);
        password = config().get(GEOSCALING_PASSWORD);
        primaryDomainName = config().get(GEOSCALING_PRIMARY_DOMAIN_NAME);
        smartSubdomainName = config().get(GEOSCALING_SMART_SUBDOMAIN_NAME);

        // Ensure all mandatory configuration is provided.
        checkNotNull(username, "The GeoScaling username is not specified");
        checkNotNull(password, "The GeoScaling password is not specified");
        checkNotNull(primaryDomainName, "The GeoScaling primary domain name is not specified");

        if (randomizeSmartSubdomainName) {
            // if no smart subdomain specified, but random is, use something random
            if (smartSubdomainName != null) smartSubdomainName += "-";
            else smartSubdomainName = "";
            smartSubdomainName += Identifiers.makeRandomId(8);
        }
        checkNotNull(smartSubdomainName, "The GeoScaling smart subdomain name is not specified or randomized");
        
        String fullDomain = smartSubdomainName+"."+primaryDomainName;
        log.info("GeoScaling service will configure redirection for '"+fullDomain+"' domain");
        sensors().set(GEOSCALING_ACCOUNT, username);
        sensors().set(MANAGED_DOMAIN, fullDomain);
        sensors().set(HOSTNAME, getHostname());
        
        isConfigured = true;
        
        if (rememberedTargetHosts != null) {
            reconfigureService(rememberedTargetHosts);
            rememberedTargetHosts = null;
        }
    }
    
    @Override
    public String getHostname() {
        String result = getAttribute(MANAGED_DOMAIN);
        return (Strings.isBlank(result)) ? null : result;
    }
    
    /** minimum/default TTL here is 300s = 5m */
    @Override
    public long getTimeToLiveSeconds() { return 5*60; }
    
    @Override
    public void destroy() {
        setServiceState(Lifecycle.STOPPING);
        if (!isConfigured) return;
        
        // Don't leave randomized subdomains configured on our GeoScaling account.
        if (randomizeSmartSubdomainName) {
            webClient.login(username, password);
            Domain primaryDomain = webClient.getPrimaryDomain(primaryDomainName);
            SmartSubdomain smartSubdomain = (primaryDomain != null) ? primaryDomain.getSmartSubdomain(smartSubdomainName) : null;
            if (smartSubdomain != null) {
                log.info("Deleting randomized GeoScaling smart subdomain '"+smartSubdomainName+"."+primaryDomainName+"'");
                smartSubdomain.delete();
            }
            webClient.logout();
        }
        
        super.destroy();
        
        isConfigured = false;
    }
    
    @Override
    protected void reconfigureService(Collection<HostGeoInfo> targetHosts) {
        if (!isConfigured) {
            this.rememberedTargetHosts = MutableSet.copyOf(targetHosts);
            return;
        }
        
        webClient.login(username, password);
        Domain primaryDomain = webClient.getPrimaryDomain(primaryDomainName);
        if (primaryDomain==null) 
            throw new NullPointerException(this+" got null from web client for primary domain "+primaryDomainName);
        SmartSubdomain smartSubdomain = primaryDomain.getSmartSubdomain(smartSubdomainName);
        
        if (smartSubdomain == null) {
            log.info("GeoScaling {} smart subdomain '{}.{}' does not exist, creating it now", new Object[] {this, smartSubdomainName, primaryDomainName});
            // TODO use WithMutexes to ensure this is single-entrant
            primaryDomain.createSmartSubdomain(smartSubdomainName);
            smartSubdomain = primaryDomain.getSmartSubdomain(smartSubdomainName);
        }
        
        if (smartSubdomain != null) {
            log.debug("GeoScaling {} being reconfigured to use {}", this, targetHosts);
            String script = GeoscalingScriptGenerator.generateScriptString(targetHosts);
            smartSubdomain.configure(PROVIDE_CITY_INFO, script);
            if (targetHosts.isEmpty()) {
                setServiceState(Lifecycle.CREATED);
                sensors().set(ROOT_URL, null);
                sensors().set(MAIN_URI, null);
            } else {
                setServiceState(Lifecycle.RUNNING);
                String domain = getAttribute(MANAGED_DOMAIN);
                if (!Strings.isEmpty(domain)) {
                    sensors().set(ROOT_URL, "http://"+domain+"/");
                    sensors().set(MAIN_URI, URI.create("http://"+domain+"/"));
                }
            }
        } else {
            log.warn("Failed to retrieve or create GeoScaling smart subdomain '"+smartSubdomainName+"."+primaryDomainName+
                    "', aborting attempt to configure service");
            setServiceState(Lifecycle.ON_FIRE);
        }
        
        webClient.logout();
    }

}
