/*
 * Copyright 2013-present the original author or authors.
 *
 * 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
 *
 *      https://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.alibaba.cloud.nacos.configdata;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.cloud.nacos.NacosPropertiesPrefixer;
import com.alibaba.cloud.nacos.utils.StringUtils;
import org.apache.commons.logging.Log;
import org.jspecify.annotations.Nullable;

import org.springframework.boot.bootstrap.BootstrapRegistry;
import org.springframework.boot.bootstrap.ConfigurableBootstrapContext;
import org.springframework.boot.context.config.ConfigDataLocation;
import org.springframework.boot.context.config.ConfigDataLocationNotFoundException;
import org.springframework.boot.context.config.ConfigDataLocationResolver;
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
import org.springframework.boot.context.config.ConfigDataResource;
import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
import org.springframework.boot.context.config.Profiles;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.core.Ordered;

import static com.alibaba.cloud.nacos.configdata.NacosConfigDataResource.NacosItemConfig;
import static com.alibaba.cloud.nacos.constants.Constants.SPRING_CONFIG_IMPORT_PROPERTIES;

/**
 * Implementation of {@link ConfigDataLocationResolver}, load Nacos
 * {@link ConfigDataResource}.
 *
 * @author freeman
 * @since 2021.0.1.0
 */
public class NacosConfigDataLocationResolver
		implements ConfigDataLocationResolver<NacosConfigDataResource>, Ordered {
	/**
	 * Prefix for Config Server imports.
	 */
	public static final String PREFIX = "nacos:";
	private static final String GROUP = "group";

	// support params
	private static final String REFRESH_ENABLED = "refreshEnabled";
	private static final String PREFERENCE = "preference";
	private final Log log;

	public NacosConfigDataLocationResolver(DeferredLogFactory logFactory) {
		this.log = logFactory.getLog(getClass());
	}

	@Override
	public int getOrder() {
		return -1;
	}

	protected @Nullable NacosConfigProperties loadProperties(
			ConfigDataLocationResolverContext context) {
		Binder binder = context.getBinder();
		@Nullable BindHandler bindHandler = getBindHandler(context);

		NacosConfigProperties nacosConfigProperties;
		if (context.getBootstrapContext().isRegistered(NacosConfigDataLoadProperties.class)) {
			nacosConfigProperties = context.getBootstrapContext()
					.get(NacosConfigDataLoadProperties.class);
		}
		else {
			String nacosPrefix = NacosPropertiesPrefixer.getPrefix(context.getBinder());

			String nacosConfigPrefix = nacosPrefix + ".config";

			nacosConfigProperties = binder
					.bind(nacosPrefix, Bindable.of(NacosConfigDataLoadProperties.class),
							bindHandler)
					.map(properties -> binder
							.bind(nacosConfigPrefix,
									Bindable.ofInstance(properties), bindHandler)
							.orElse(properties))
					.orElseGet(() -> binder
							.bind(nacosConfigPrefix,
									Bindable.of(NacosConfigDataLoadProperties.class), bindHandler)
							.orElseGet(NacosConfigDataLoadProperties::new));
		}

		return nacosConfigProperties;
	}

	private @Nullable BindHandler getBindHandler(ConfigDataLocationResolverContext context) {
		return context.getBootstrapContext().getOrElse(BindHandler.class, null);
	}

	protected Log getLog() {
		return this.log;
	}

	@Override
	public boolean isResolvable(ConfigDataLocationResolverContext context,
			ConfigDataLocation location) {
		if (!location.hasPrefix(getPrefix())) {
			return false;
		}
		String prefix = NacosPropertiesPrefixer.getPrefix(context.getBinder());

		return context.getBinder()
				.bind(prefix + ".config.enabled", Boolean.class)
				.orElse(true);
	}

	protected String getPrefix() {
		return PREFIX;
	}

	@Override
	public List<NacosConfigDataResource> resolve(
			ConfigDataLocationResolverContext context, ConfigDataLocation location)
			throws ConfigDataLocationNotFoundException,
			ConfigDataResourceNotFoundException {
		return Collections.emptyList();
	}

	@Override
	public List<NacosConfigDataResource> resolveProfileSpecific(
			ConfigDataLocationResolverContext resolverContext,
			ConfigDataLocation location, Profiles profiles)
			throws ConfigDataLocationNotFoundException {
		NacosConfigProperties properties = loadProperties(resolverContext);
		if (properties == null) {
			throw new IllegalStateException("NacosConfigProperties could not be loaded");
		}

		ConfigurableBootstrapContext bootstrapContext = resolverContext
				.getBootstrapContext();

		bootstrapContext.registerIfAbsent(NacosConfigProperties.class,
				BootstrapRegistry.InstanceSupplier.of(properties));

		registerConfigManager(properties, bootstrapContext, resolverContext);

		return loadConfigDataResources(location, profiles, properties);
	}

	private List<NacosConfigDataResource> loadConfigDataResources(
			ConfigDataLocation location, Profiles profiles,
			NacosConfigProperties properties) {
		List<NacosConfigDataResource> result = new ArrayList<>();
		URI uri = getUri(location, properties);

		String dataIdNullable = dataIdFor(uri);
		if (dataIdNullable == null || dataIdNullable.isEmpty()) {
			throw new IllegalArgumentException("dataId must be specified");
		}
		String dataId = dataIdNullable;

		String group = groupFor(uri, properties);
		if (group == null) {
			group = "";
		}

		String suffix = suffixFor(uri, properties);
		if (suffix == null) {
			suffix = "";
		}

		String preference = preferenceFor(uri);
		if (preference == null) {
			preference = "";
		}

		NacosConfigDataResource resource = new NacosConfigDataResource(properties,
				location.isOptional(), profiles, log,
				new NacosItemConfig().setGroup(group)
						.setDataId(dataId).setSuffix(suffix)
						.setRefreshEnabled(refreshEnabledFor(uri, properties))
						.setPreference(preference));
		result.add(resource);

		return result;
	}

	private @Nullable String preferenceFor(URI uri) {
		return getQueryMap(uri).get(PREFERENCE);
	}

	private URI getUri(ConfigDataLocation location, NacosConfigProperties properties) {
		String path = location.getNonPrefixedValue(getPrefix());
		if (StringUtils.isBlank(path)) {
			path = "/";
		}
		if (!path.startsWith("/")) {
			path = "/" + path;
		}
		String uri = properties.getServerAddr() + path;
		return getUri(uri);
	}

	private void registerConfigManager(NacosConfigProperties properties,
			ConfigurableBootstrapContext bootstrapContext,
			ConfigDataLocationResolverContext resolverContext) {
		List<?> springConfigImportProperties = resolverContext.getBinder()
				.bind(SPRING_CONFIG_IMPORT_PROPERTIES, List.class).get();
		if (!springConfigImportProperties.isEmpty() && !bootstrapContext.isRegistered(NacosConfigManager.class)) {
			bootstrapContext.register(NacosConfigManager.class,
					BootstrapRegistry.InstanceSupplier.of(NacosConfigManager.getInstance(properties)));
		}
	}

	private URI getUri(String uris) {
		if (!uris.startsWith("http://") && !uris.startsWith("https://")) {
			uris = "http://" + uris;
		}
		URI uri;
		try {
			uri = new URI(uris);
		}
		catch (URISyntaxException e) {
			throw new IllegalArgumentException("illegal URI: " + uris);
		}
		return uri;
	}

	private @Nullable String groupFor(URI uri, NacosConfigProperties properties) {
		Map<String, String> queryMap = getQueryMap(uri);
		return queryMap.containsKey(GROUP) ? queryMap.get(GROUP) : properties.getGroup();
	}

	private Map<String, String> getQueryMap(URI uri) {
		String query = uri.getQuery();
		if (StringUtils.isBlank(query)) {
			return Collections.emptyMap();
		}
		Map<String, String> result = new HashMap<>(4);
		for (String entry : query.split("&")) {
			String[] kv = entry.split("=");
			if (kv.length == 2) {
				result.put(kv[0], kv[1]);
			}
		}
		return result;
	}

	private @Nullable String suffixFor(URI uri, NacosConfigProperties properties) {
		String dataId = dataIdFor(uri);
		if (dataId != null && dataId.contains(".")) {
			return dataId.substring(dataId.lastIndexOf('.') + 1);
		}
		return properties.getFileExtension();
	}

	private boolean refreshEnabledFor(URI uri, NacosConfigProperties properties) {
		Map<String, String> queryMap = getQueryMap(uri);
		return queryMap.containsKey(REFRESH_ENABLED)
				? Boolean.parseBoolean(queryMap.get(REFRESH_ENABLED))
				: properties.isRefreshEnabled();
	}

	private @Nullable String dataIdFor(URI uri) {
		String path = uri.getPath();
		// notice '/'
		if (path == null || path.length() <= 1) {
			return StringUtils.EMPTY;
		}
		String[] parts = path.substring(1).split("/");
		if (parts.length != 1) {
			throw new IllegalArgumentException("illegal dataId");
		}
		return parts[0];
	}

}
