/*
 * 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.shardingsphere.proxy.backend.handler.distsql.ral.updatable;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import org.apache.shardingsphere.distsql.handler.engine.update.DistSQLUpdateExecutor;
import org.apache.shardingsphere.distsql.statement.ral.updatable.SetDistVariableStatement;
import org.apache.shardingsphere.infra.config.props.ConfigurationPropertyKey;
import org.apache.shardingsphere.infra.config.props.temporary.TemporaryConfigurationPropertyKey;
import org.apache.shardingsphere.infra.config.rule.RuleConfiguration;
import org.apache.shardingsphere.infra.exception.core.ShardingSpherePreconditions;
import org.apache.shardingsphere.infra.exception.kernel.syntax.InvalidVariableValueException;
import org.apache.shardingsphere.infra.exception.kernel.syntax.UnsupportedVariableException;
import org.apache.shardingsphere.infra.props.TypedPropertyKey;
import org.apache.shardingsphere.infra.props.TypedPropertyValue;
import org.apache.shardingsphere.infra.props.exception.TypedPropertyValueException;
import org.apache.shardingsphere.infra.spi.type.typed.TypedSPI;
import org.apache.shardingsphere.logging.constant.LoggingConstants;
import org.apache.shardingsphere.logging.rule.LoggingRule;
import org.apache.shardingsphere.mode.manager.ContextManager;
import org.apache.shardingsphere.mode.metadata.MetaDataContexts;
import org.apache.shardingsphere.mode.metadata.decorator.RuleConfigurationPersistDecorateEngine;
import org.slf4j.LoggerFactory;

import java.sql.SQLException;
import java.util.Collection;
import java.util.Properties;

/**
 * Set dist variable statement executor.
 */
public final class SetDistVariableExecutor implements DistSQLUpdateExecutor<SetDistVariableStatement> {
    
    @Override
    public void executeUpdate(final SetDistVariableStatement sqlStatement, final ContextManager contextManager) throws SQLException {
        ShardingSpherePreconditions.checkState(getEnumType(sqlStatement.getName()) instanceof TypedPropertyKey, () -> new UnsupportedVariableException(sqlStatement.getName()));
        handleConfigurationProperty(contextManager, (TypedPropertyKey) getEnumType(sqlStatement.getName()), sqlStatement.getValue());
    }
    
    private Enum<?> getEnumType(final String name) {
        try {
            return ConfigurationPropertyKey.valueOf(name.toUpperCase());
        } catch (final IllegalArgumentException ex) {
            try {
                return TemporaryConfigurationPropertyKey.valueOf(name.toUpperCase());
            } catch (final IllegalArgumentException exception) {
                throw new UnsupportedVariableException(name);
            }
        }
    }
    
    private void handleConfigurationProperty(final ContextManager contextManager, final TypedPropertyKey propertyKey, final String value) throws SQLException {
        MetaDataContexts metaDataContexts = contextManager.getMetaDataContexts();
        Properties props = new Properties();
        props.putAll(metaDataContexts.getMetaData().getProps().getProps());
        props.putAll(metaDataContexts.getMetaData().getTemporaryProps().getProps());
        props.put(propertyKey.getKey(), getValue(propertyKey, value));
        contextManager.getPersistServiceFacade().getMetaDataManagerPersistService().alterProperties(props);
        refreshRootLogger(props);
        syncSQLShowToLoggingRule(propertyKey, metaDataContexts, value, contextManager);
        syncSQLSimpleToLoggingRule(propertyKey, metaDataContexts, value, contextManager);
    }
    
    private Object getValue(final TypedPropertyKey propertyKey, final String value) {
        try {
            Object propertyValue = new TypedPropertyValue(propertyKey, value).getValue();
            if (Enum.class.isAssignableFrom(propertyKey.getType())) {
                return propertyValue.toString();
            }
            return TypedSPI.class.isAssignableFrom(propertyKey.getType()) ? ((TypedSPI) propertyValue).getType().toString() : propertyValue;
        } catch (final TypedPropertyValueException ignored) {
            throw new InvalidVariableValueException(value);
        }
    }
    
    private void refreshRootLogger(final Properties props) {
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
        renewRootLoggerLevel(rootLogger, props);
    }
    
    private void renewRootLoggerLevel(final Logger rootLogger, final Properties props) {
        rootLogger.setLevel(Level.valueOf(props.getOrDefault(ConfigurationPropertyKey.SYSTEM_LOG_LEVEL.getKey(), ConfigurationPropertyKey.SYSTEM_LOG_LEVEL.getDefaultValue()).toString()));
    }
    
    private void syncSQLShowToLoggingRule(final TypedPropertyKey propertyKey, final MetaDataContexts metaDataContexts, final String value, final ContextManager contextManager) {
        if (LoggingConstants.SQL_SHOW.equalsIgnoreCase(propertyKey.getKey())) {
            metaDataContexts.getMetaData().getGlobalRuleMetaData().findSingleRule(LoggingRule.class).flatMap(LoggingRule::getSQLLogger).ifPresent(option -> {
                option.getProps().setProperty(LoggingConstants.SQL_LOG_ENABLE, value);
                decorateGlobalRuleConfiguration(contextManager);
            });
        }
    }
    
    private void syncSQLSimpleToLoggingRule(final TypedPropertyKey propertyKey, final MetaDataContexts metaDataContexts, final String value, final ContextManager contextManager) {
        if (LoggingConstants.SQL_SIMPLE.equalsIgnoreCase(propertyKey.getKey())) {
            metaDataContexts.getMetaData().getGlobalRuleMetaData().findSingleRule(LoggingRule.class).flatMap(LoggingRule::getSQLLogger).ifPresent(optional -> {
                optional.getProps().setProperty(LoggingConstants.SQL_LOG_SIMPLE, value);
                decorateGlobalRuleConfiguration(contextManager);
            });
        }
    }
    
    private void decorateGlobalRuleConfiguration(final ContextManager contextManager) {
        RuleConfigurationPersistDecorateEngine ruleConfigPersistDecorateEngine = new RuleConfigurationPersistDecorateEngine(contextManager.getComputeNodeInstanceContext());
        Collection<RuleConfiguration> globalRuleConfigs = ruleConfigPersistDecorateEngine.decorate(contextManager.getMetaDataContexts().getMetaData().getGlobalRuleMetaData().getConfigurations());
        contextManager.getPersistServiceFacade().getMetaDataPersistService().getGlobalRuleService().persist(globalRuleConfigs);
    }
    
    @Override
    public Class<SetDistVariableStatement> getType() {
        return SetDistVariableStatement.class;
    }
}
