/**
 * Copyright 2011-2021 Asakusa Framework Team.
 *
 * Licensed 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 com.asakusafw.yaess.core;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.asakusafw.yaess.core.util.PropertiesUtil;

/**
 * A skeletal implementation of {@link ExecutionScriptHandler}.
 * Note that this class does not implement {@link ExecutionScriptHandler},
 * so that subclasses must implement {@link HadoopScriptHandler} or {@link CommandScriptHandler}.
 * @since 0.2.3
 * @version 0.2.6
 */
public abstract class ExecutionScriptHandlerBase implements Service {

    static final YaessLogger YSLOG = new YaessCoreLogger(ExecutionScriptHandlerBase.class);

    static final Logger LOG = LoggerFactory.getLogger(ExecutionScriptHandlerBase.class);

    private volatile String prefix;

    private volatile String resourceId;

    private volatile Map<String, String> properties;

    private volatile Map<String, String> environmentVariables;

    @Override
    public final void configure(ServiceProfile<?> profile) throws InterruptedException, IOException {
        this.prefix = profile.getPrefix();
        try {
            configureResourceId(profile);
            Map<String, String> desiredProperties = getDesiredProperties(profile);
            Map<String, String> desiredEnvironmentVariables = getDesiredEnvironmentVariables(profile);
            this.properties = Collections.unmodifiableMap(desiredProperties);
            this.environmentVariables = Collections.unmodifiableMap(desiredEnvironmentVariables);
            doConfigure(profile, desiredProperties, desiredEnvironmentVariables);
        } catch (IllegalArgumentException e) {
            throw new IOException(MessageFormat.format(
                    "Failed to configure \"{0}\" ({1})",
                    profile.getPrefix(),
                    profile.getPrefix()), e);
        }
    }

    /**
     * Returns the ID of this handler.
     * @return the ID
     */
    public final String getHandlerId() {
        return prefix;
    }

    private void configureResourceId(ServiceProfile<?> profile) {
        assert profile != null;
        String override = profile.getConfiguration(ExecutionScriptHandler.KEY_RESOURCE, false, true);
        if (override == null) {
            LOG.debug("resourceId is not override in {}",
                    profile.getPrefix());
            resourceId = ExecutionScriptHandler.DEFAULT_RESOURCE_ID;
        } else {
            LOG.debug("resourceId is overriden in {}: {}",
                    profile.getPrefix(),
                    override);
            resourceId = override;
        }
    }

    private Map<String, String> getDesiredProperties(ServiceProfile<?> profile) throws IOException {
        assert profile != null;
        NavigableMap<String, String> vars = PropertiesUtil.createPrefixMap(
                profile.getConfiguration(),
                ExecutionScriptHandler.KEY_PROP_PREFIX);
        Map<String, String> resolved = new TreeMap<>();
        for (Map.Entry<String, String> entry : vars.entrySet()) {
            String key = entry.getKey();
            String unresolved = entry.getValue();
            try {
                String value = profile.getContext().getContextParameters().replace(unresolved, true);
                resolved.put(key, value);
            } catch (IllegalArgumentException e) {
                YSLOG.error(e, "E10001",
                        profile.getPrefix(),
                        ExecutionScriptHandler.KEY_PROP_PREFIX,
                        key,
                        unresolved);
                throw new IOException(MessageFormat.format(
                        "Failed to resolve a property: {0}.{1}.{2} = {3}",
                        profile.getPrefix(),
                        ExecutionScriptHandler.KEY_PROP_PREFIX,
                        key,
                        unresolved), e);
            }
        }
        LOG.debug("Desired properties for {}: {}",
                profile.getPrefix(),
                resolved);
        return resolved;
    }

    private Map<String, String> getDesiredEnvironmentVariables(ServiceProfile<?> profile) throws IOException {
        assert profile != null;
        NavigableMap<String, String> vars = PropertiesUtil.createPrefixMap(
                profile.getConfiguration(),
                ExecutionScriptHandler.KEY_ENV_PREFIX);
        Map<String, String> resolved = new TreeMap<>();
        for (Map.Entry<String, String> entry : vars.entrySet()) {
            String key = entry.getKey();
            String unresolved = entry.getValue();
            try {
                String value = profile.getContext().getContextParameters().replace(unresolved, true);
                resolved.put(key, value);
            } catch (IllegalArgumentException e) {
                YSLOG.error(e, "E10001",
                        profile.getPrefix(),
                        ExecutionScriptHandler.KEY_ENV_PREFIX,
                        key,
                        unresolved);
                throw new IOException(MessageFormat.format(
                        "Failed to resolve environment variable: {0}.{1}.{2} = {3}",
                        profile.getPrefix(),
                        ExecutionScriptHandler.KEY_ENV_PREFIX,
                        key,
                        unresolved), e);
            }
        }
        LOG.debug("Desired environment variables for {}: {}",
                profile.getPrefix(),
                resolved);
        return resolved;
    }

    /**
     * Configures this handler internally (extention point).
     * @param profile the profile of this service
     * @param desiredProperties the current desired system/hadoop properties (configurable)
     * @param desiredEnvironmentVariables the current desired environment variables (configurable)
     * @throws InterruptedException if interrupted in configuration
     * @throws IOException if failed to configure this service
     */
    protected abstract void doConfigure(
            ServiceProfile<?> profile,
            Map<String, String> desiredProperties,
            Map<String, String> desiredEnvironmentVariables) throws InterruptedException, IOException;

    /**
     * Returns the ID of a resource which is used for executing this handler.
     * @param context the current execution context
     * @param script the target script (nullable)
     * @return the required resource ID
     * @throws InterruptedException if this operation is interrupted
     * @throws IOException if failed to setup the target environment
     */
    public final String getResourceId(
            ExecutionContext context,
            ExecutionScript script) throws InterruptedException, IOException {
        return resourceId;
    }

    /**
     * Returns desired system/hadoop properties to execute scripts using this handler.
     * @param context the current execution context
     * @param script the target script (nullable)
     * @return desired system or hadoop properties
     * @throws InterruptedException if this operation is interrupted
     * @throws IOException if failed to setup the target environment
     */
    public Map<String, String> getProperties(
            ExecutionContext context,
            ExecutionScript script) throws InterruptedException, IOException {
        return properties;
    }

    /**
     * Returns desired environment variables to execute scripts using this handler.
     * @param context the current execution context
     * @param script the target script (nullable)
     * @return desired environment variables
     * @throws InterruptedException if this operation is interrupted
     * @throws IOException if failed to setup the target environment
     */
    public Map<String, String> getEnvironmentVariables(
            ExecutionContext context,
            ExecutionScript script) throws InterruptedException, IOException {
        return environmentVariables;
    }

    /**
     * Setup the target environment.
     * @param monitor the progress monitor of the operation
     * @param context the current execution context
     * @throws InterruptedException if this operation is interrupted
     * @throws IOException if failed to setup the target environment
     */
    public void setUp(
            ExecutionMonitor monitor,
            ExecutionContext context) throws InterruptedException, IOException {
        monitor.open(1);
        try {
            voidSetUp(context);
        } finally {
            monitor.close();
        }
    }

    /**
     * Cleanup the target environment.
     * @param monitor the progress monitor of the operation
     * @param context the current execution context
     * @throws InterruptedException if this operation is interrupted
     * @throws IOException if failed to setup the target environment
     */
    public void cleanUp(
            ExecutionMonitor monitor,
            ExecutionContext context) throws InterruptedException, IOException {
        monitor.open(1);
        try {
            voidCleanUp(context);
        } finally {
            monitor.close();
        }
    }

    /**
     * Performs as {@link #setUp(ExecutionMonitor, ExecutionContext)} that does nothing.
     * @param context current context
     * @since 0.4.0
     */
    protected final void voidSetUp(ExecutionContext context) {
        YSLOG.info("I51001",
                context.getBatchId(),
                context.getFlowId(),
                context.getExecutionId(),
                context.getPhase(),
                getHandlerId());
    }

    /**
     * Performs as {@link #cleanUp(ExecutionMonitor, ExecutionContext)} that does nothing.
     * @param context current context
     * @since 0.4.0
     */
    protected final void voidCleanUp(ExecutionContext context) {
        YSLOG.info("I51002",
                context.getBatchId(),
                context.getFlowId(),
                context.getExecutionId(),
                context.getPhase(),
                getHandlerId());
    }
}
