package com.red.circle.framework.shardingsphere.driver;

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import org.apache.shardingsphere.driver.jdbc.core.driver.ShardingSphereDriverURLProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.PropertyPlaceholderHelper;

public class NacosDriverURLProvider implements ShardingSphereDriverURLProvider {

  private static final Logger LOGGER = LoggerFactory.getLogger(NacosDriverURLProvider.class);
  private static final PropertyPlaceholderHelper PLACEHOLDER_HELPER =
      new PropertyPlaceholderHelper("${", "}", ":", true);

  private static final String NACOS_TYPE = "nacos:";

  private static final String GROUP = "group";

  private static final long TIMEOUT_MILLIS = 6000L;

  @Override
  public boolean accept(String url) {
    return url != null && url.contains(NACOS_TYPE);
  }

  @Override
  public byte[] getContent(String url) {
    if (!hasText(url)) {
      throw new IllegalArgumentException("url isBlank.");
    }

    String shardingConfigFilename = getShardingConfigFilename(url);
    if (!hasText(shardingConfigFilename)) {
      throw new IllegalArgumentException("sharding config error.");
    }

    Map<String, String> queryParams = parseQuery(url);
    String group = Optional.ofNullable(queryParams.get(GROUP))
        .filter(NacosDriverURLProvider::hasText)
        .orElse("DEFAULT_GROUP");

    try {
      LOGGER.info(
          "Load sharding config from nacos. url={}, dataId={}, group={}, queryParams={}",
          url,
          shardingConfigFilename,
          group,
          queryParams);
      String content = "";
      try {
        content = loadConfig(queryParams, shardingConfigFilename, group, true);
      } catch (NacosException ex) {
        LOGGER.warn(
            "Load sharding config via nacos client failed, fallback to HTTP. dataId={}, group={}",
            shardingConfigFilename,
            group,
            ex);
      }
      if (!hasText(content)) {
        LOGGER.warn(
            "Empty sharding config via nacos client, fallback to HTTP. dataId={}, group={}",
            shardingConfigFilename,
            group);
        content = loadConfigViaHTTP(queryParams, shardingConfigFilename, group);
      }
      if (!hasText(content)) {
        throw new RuntimeException(
            "Empty Nacos sharding config: dataId=" + shardingConfigFilename + ", group=" + group);
      }
      content = resolvePlaceholders(content);
      LOGGER.info(content);
      return content.getBytes(StandardCharsets.UTF_8);
    } catch (IOException ex) {
      throw new RuntimeException(ex);
    }
  }

  private String loadConfig(Map<String, String> queryParams, String dataID, String group,
      boolean includeAuth) throws NacosException {
    ConfigService configService = NacosFactory.createConfigService(
        buildProperties(queryParams, includeAuth));
    return configService.getConfig(dataID, group, TIMEOUT_MILLIS);
  }

  private String loadConfigViaHTTP(Map<String, String> queryParams, String dataID, String group)
      throws IOException {
    String serverAddr = queryParams.get("serverAddr");
    if (!hasText(serverAddr)) {
      return "";
    }

    StringBuilder urlBuilder = new StringBuilder("http://")
        .append(serverAddr)
        .append("/nacos/v1/cs/configs?dataId=")
        .append(encode(dataID))
        .append("&group=")
        .append(encode(group));
    String namespace = queryParams.get("namespace");
    if (hasText(namespace) && !"public".equalsIgnoreCase(namespace.trim())) {
      urlBuilder.append("&tenant=").append(encode(namespace));
    }

    HttpURLConnection connection = (HttpURLConnection) new URL(urlBuilder.toString()).openConnection();
    connection.setRequestMethod("GET");
    connection.setConnectTimeout((int) TIMEOUT_MILLIS);
    connection.setReadTimeout((int) TIMEOUT_MILLIS);
    int status = connection.getResponseCode();
    if (status != HttpURLConnection.HTTP_OK) {
      LOGGER.warn(
          "Load sharding config via HTTP failed. status={}, dataId={}, group={}, url={}",
          status,
          dataID,
          group,
          urlBuilder);
      return "";
    }
    try (InputStream inputStream = connection.getInputStream()) {
      return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
    } finally {
      connection.disconnect();
    }
  }

  private Properties buildProperties(Map<String, String> queryParams, boolean includeAuth) {
    Properties properties = new Properties();
    setIfHasText(properties, "serverAddr", queryParams.get("serverAddr"));
    setIfHasText(properties, "namespace", queryParams.get("namespace"));
    if (includeAuth) {
      setIfHasText(properties, "username", queryParams.get("username"));
      setIfHasText(properties, "password", queryParams.get("password"));
    }
    return properties;
  }

  private static String encode(String value) {
    return URLEncoder.encode(value, StandardCharsets.UTF_8);
  }

  private static void setIfHasText(Properties properties, String key, String value) {
    if (hasText(value)) {
      properties.setProperty(key, value);
    }
  }

  private static boolean hasText(String value) {
    return value != null && !value.trim().isEmpty();
  }

  private Map<String, String> parseQuery(String url) {
    Map<String, String> queryParams = new HashMap<>();
    int queryIndex = url.indexOf('?');
    if (queryIndex < 0 || queryIndex + 1 >= url.length()) {
      return queryParams;
    }

    String query = url.substring(queryIndex + 1);
    for (String pair : query.split("&")) {
      if (pair.isEmpty()) {
        continue;
      }
      int separatorIndex = pair.indexOf('=');
      if (separatorIndex < 0) {
        queryParams.put(decode(pair), "");
        continue;
      }
      String key = decode(pair.substring(0, separatorIndex));
      String value = decode(pair.substring(separatorIndex + 1));
      queryParams.put(key, value);
    }
    return queryParams;
  }

  private String decode(String value) {
    return URLDecoder.decode(value, StandardCharsets.UTF_8);
  }

  private String getShardingConfigFilename(String url) {
    int nacosIndex = url.indexOf(NACOS_TYPE);
    if (nacosIndex < 0) {
      return "";
    }
    return url.substring(nacosIndex + NACOS_TYPE.length()).split("\\?")[0];
  }

  String resolvePlaceholders(String content) {
    if (!hasText(content)) {
      return content;
    }
    return PLACEHOLDER_HELPER.replacePlaceholders(content, placeholderName -> {
      String systemProperty = System.getProperty(placeholderName);
      if (hasText(systemProperty)) {
        return systemProperty;
      }
      String envValue = System.getenv(placeholderName);
      return hasText(envValue) ? envValue : null;
    });
  }
}
