/*
 * Copyright 2025 Apollo 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
 *
 * 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.ctrip.framework.apollo.build.sql.converter;

import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

public class ApolloSqlConverterUtil {

  private static final Pattern BASE_PATTERN =
      Pattern.compile("(apolloconfigdb|apolloportaldb)-v[0-9]{3,}-v[0-9]{3,}-base.sql");

  private static final Pattern BEFORE_PATTERN =
      Pattern.compile("(apolloconfigdb|apolloportaldb)-v[0-9]{3,}-v[0-9]{3,}-before.sql");

  private static final Pattern DELTA_PATTERN =
      Pattern.compile("(apolloconfigdb|apolloportaldb)-v[0-9]{3,}-v[0-9]{3,}.sql");

  private static final Pattern AFTER_PATTERN =
      Pattern.compile("(apolloconfigdb|apolloportaldb)-v[0-9]{3,}-v[0-9]{3,}-after.sql");

  public static String getRepositoryDir() {
    ProtectionDomain protectionDomain = ApolloSqlConverter.class.getProtectionDomain();
    CodeSource codeSource = protectionDomain.getCodeSource();
    URL location = codeSource.getLocation();
    URI uri;
    try {
      uri = location.toURI();
    } catch (URISyntaxException e) {
      throw new IllegalArgumentException(e.getLocalizedMessage(), e);
    }
    Path path = Paths.get(uri);
    String unixClassPath = path.toString().replace("\\", "/");

    if (!unixClassPath.endsWith("/apollo-build-sql-converter/target/classes")) {
      throw new IllegalStateException("illegal class path: " + unixClassPath);
    }

    return ApolloSqlConverterUtil.replacePath(unixClassPath,
        "/apollo-build-sql-converter/target/classes", "");
  }

  public static void ensureDirectories(String targetFilePath) {
    Path path = Paths.get(targetFilePath);
    Path dirPath = path.getParent();
    try {
      Files.createDirectories(dirPath);
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  public static String process(SqlTemplate sqlTemplate, SqlTemplateContext context) {
    Template freemarkerTemplate = sqlTemplate.getTemplate();
    StringWriter writer = new StringWriter();
    try {
      freemarkerTemplate.process(context, writer);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    return writer.toString();
  }

  public static String replacePrefix(String origin, String prefix, String target) {
    if (!origin.startsWith(prefix)) {
      throw new IllegalArgumentException("illegal file path: " + origin);
    }
    return origin.replace(prefix, target);
  }

  public static String replacePath(String origin, String src, String target) {
    if (!origin.contains(src)) {
      throw new IllegalArgumentException("illegal file path: " + origin);
    }
    return origin.replace(src, target);
  }

  public static SqlTemplateGist getGists(String repositoryDir) {
    String gistDir = repositoryDir + "/scripts/sql/src/gist";
    String autoGeneratedDeclaration = getGist(gistDir + "/autoGeneratedDeclaration.sql");
    String h2Function = getGist(gistDir + "/h2Function.sql");
    String setupDatabase = getGist(gistDir + "/setupDatabase.sql");
    String useDatabase = getGist(gistDir + "/useDatabase.sql");
    return SqlTemplateGist.builder().autoGeneratedDeclaration(autoGeneratedDeclaration)
        .h2Function(h2Function).setupDatabase(setupDatabase).useDatabase(useDatabase).build();
  }

  private static String getGist(String gistPath) {
    StringJoiner joiner = new StringJoiner("\n", "\n", "");
    boolean accept = false;
    try (BufferedReader bufferedReader =
        Files.newBufferedReader(Paths.get(gistPath), StandardCharsets.UTF_8)) {
      for (String line = bufferedReader.readLine(); line != null; line =
          bufferedReader.readLine()) {
        if (line.contains("@@gist-start@@")) {
          accept = true;
          continue;
        }
        if (line.contains("@@gist-end@@")) {
          break;
        }
        if (accept) {
          joiner.add(line);
        }
      }
    } catch (IOException e) {
      throw new UncheckedIOException("failed to open gistPath " + e.getLocalizedMessage(), e);
    }
    return joiner.toString();
  }

  public static List<String> getSqlList(String dir, Set<String> ignoreDirs) {
    List<String> sqlList = new ArrayList<>();
    if (Files.exists(Paths.get(dir + "/apolloconfigdb.sql"))) {
      sqlList.add(dir + "/apolloconfigdb.sql");
    }
    if (Files.exists(Paths.get(dir + "/apolloportaldb.sql"))) {
      sqlList.add(dir + "/apolloportaldb.sql");
    }
    List<String> deltaSqlList = getDeltaSqlList(dir, ignoreDirs);
    sqlList.addAll(deltaSqlList);
    return sqlList;
  }

  public static List<String> getSqlList(String dir) {
    return getSqlList(dir, Collections.emptySet());
  }

  public static List<Path> list(Path dir) {
    List<Path> subPathList = new ArrayList<>();
    try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
      for (Path path : ds) {
        subPathList.add(path);
      }
    } catch (IOException e) {
      throw new UncheckedIOException("failed to open dir " + e.getLocalizedMessage(), e);
    }
    return subPathList;
  }

  public static List<Path> listSorted(Path dir, Comparator<Path> comparator) {
    List<Path> subPathList = list(dir);
    List<Path> sortedSubPathList = new ArrayList<>(subPathList);
    sortedSubPathList.sort(comparator);
    return sortedSubPathList;
  }

  public static List<Path> listSorted(Path dir) {
    return listSorted(dir, Comparator.comparing(Path::toString));
  }

  public static void deleteDir(Path dir) {
    try {
      ApolloSqlConverterUtil.deleteDirInternal(dir);
    } catch (IOException e) {
      throw new UncheckedIOException("failed to delete dir " + e.getLocalizedMessage(), e);
    }
  }

  private static void deleteDirInternal(Path dir) throws IOException {
    if (!Files.exists(dir)) {
      return;
    }
    List<Path> files = ApolloSqlConverterUtil.list(dir);
    for (Path file : files) {
      if (!Files.exists(file)) {
        continue;
      }
      if (Files.isDirectory(file)) {
        ApolloSqlConverterUtil.deleteDirInternal(file);
        Files.delete(file);
      } else {
        Files.delete(file);
      }
    }
  }

  public static Comparator<String> deltaSqlComparator() {
    return Comparator.comparing(path -> {
      String unixPath = path.replace("\\", "/");
      int lastIndex = unixPath.lastIndexOf("/");
      String fileName;
      if (lastIndex > 0) {
        fileName = unixPath.substring(lastIndex + 1);
      } else {
        fileName = unixPath;
      }
      if (!fileName.endsWith(".sql")) {
        // not sql file
        return path;
      }
      // sort: base < before < delta < after
      if (BASE_PATTERN.matcher(fileName).matches()) {
        return "00" + path;
      } else if (BEFORE_PATTERN.matcher(fileName).matches()) {
        return "30" + path;
      } else if (DELTA_PATTERN.matcher(fileName).matches()) {
        return "50" + path;
      } else if (AFTER_PATTERN.matcher(fileName).matches()) {
        return "90" + path;
      } else {
        throw new IllegalArgumentException("illegal file name: " + fileName);
      }
    });
  }

  private static List<String> getDeltaSqlList(String dir, Set<String> ignoreDirs) {
    Path dirPath = Paths.get(dir + "/delta");
    if (!Files.exists(dirPath)) {
      return Collections.emptyList();
    }
    List<Path> deltaDirList = listSorted(dirPath);
    List<String> allDeltaSqlList = new ArrayList<>();
    for (Path deltaDir : deltaDirList) {
      if (!Files.isDirectory(deltaDir)) {
        continue;
      }
      if (ignoreDirs.contains(deltaDir.toString().replace("\\", "/"))) {
        continue;
      }
      List<Path> deltaFiles =
          listSorted(deltaDir, Comparator.comparing(Path::toString, deltaSqlComparator()));
      for (Path path : deltaFiles) {
        String fileName = path.toString();
        if (fileName.endsWith(".sql")) {
          allDeltaSqlList.add(fileName.replace("\\", "/"));
        }
      }
    }

    return allDeltaSqlList;
  }


  public static List<SqlTemplate> toTemplates(List<String> srcSqlList, String srcDir,
      Configuration configuration) {
    List<SqlTemplate> templateList = new ArrayList<>(srcSqlList.size());
    for (String srcSql : srcSqlList) {
      String templateName = ApolloSqlConverterUtil.replacePrefix(srcSql, srcDir + "/", "");
      Template template;
      try {
        template = configuration.getTemplate(templateName);
      } catch (IOException e) {
        throw new UncheckedIOException(e);
      }
      templateList.add(new SqlTemplate(srcSql, template));
    }
    return templateList;
  }

  public static List<SqlStatement> toStatements(String rawText) {
    List<SqlStatement> sqlStatementList = new ArrayList<>();
    try (BufferedReader bufferedReader = new BufferedReader(new StringReader(rawText))) {
      AtomicReference<StringJoiner> rawTextJoinerRef =
          new AtomicReference<>(new StringJoiner("\n"));
      StringBuilder singleLineTextBuilder = new StringBuilder();
      List<String> textLines = new ArrayList<>();
      AtomicBoolean comment = new AtomicBoolean(false);
      for (String line = bufferedReader.readLine(); line != null; line =
          bufferedReader.readLine()) {
        if (line.startsWith("--")) {
          commentLine(line, rawTextJoinerRef, singleLineTextBuilder, textLines, comment,
              sqlStatementList);
        } else {
          noCommentLine(line, rawTextJoinerRef, singleLineTextBuilder, textLines, comment,
              sqlStatementList);
        }
      }
      if (!textLines.isEmpty()) {
        StringJoiner sqlStatementRawTextJoiner = rawTextJoinerRef.get();
        SqlStatement sqlStatement =
            createStatement(sqlStatementRawTextJoiner, singleLineTextBuilder, textLines);
        sqlStatementList.add(sqlStatement);
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    return sqlStatementList;
  }

  private static void commentLine(String line, AtomicReference<StringJoiner> rawTextJoinerRef,
      StringBuilder singleLineTextBuilder, List<String> textLines, AtomicBoolean comment,
      List<SqlStatement> sqlStatementList) {

    if (!comment.get()) {
      comment.set(true);
      if (!textLines.isEmpty()) {
        StringJoiner sqlStatementRawTextJoiner = rawTextJoinerRef.get();
        SqlStatement sqlStatement =
            createStatement(sqlStatementRawTextJoiner, singleLineTextBuilder, textLines);

        resetStatementBuffer(rawTextJoinerRef, singleLineTextBuilder, textLines);
        sqlStatementList.add(sqlStatement);
      }
    }
    StringJoiner sqlStatementRawTextJoiner = rawTextJoinerRef.get();
    // raw text
    sqlStatementRawTextJoiner.add(line);
    // single line text
    singleLineTextBuilder.append(line);
    // text lines
    textLines.add(line);
  }

  private static void noCommentLine(String line, AtomicReference<StringJoiner> rawTextJoinerRef,
      StringBuilder singleLineTextBuilder, List<String> textLines, AtomicBoolean comment,
      List<SqlStatement> sqlStatementList) {

    if (comment.get()) {
      comment.set(false);
      if (!textLines.isEmpty()) {
        StringJoiner sqlStatementRawTextJoiner = rawTextJoinerRef.get();
        SqlStatement sqlStatement =
            createStatement(sqlStatementRawTextJoiner, singleLineTextBuilder, textLines);

        resetStatementBuffer(rawTextJoinerRef, singleLineTextBuilder, textLines);
        sqlStatementList.add(sqlStatement);
      }
    }

    StringJoiner sqlStatementRawTextJoiner = rawTextJoinerRef.get();
    // ; is the end of a statement
    int indexOfSemicolon = line.indexOf(';');
    if (indexOfSemicolon == -1) {
      // raw text
      sqlStatementRawTextJoiner.add(line);
      // single line text
      singleLineTextBuilder.append(line);
      // text lines
      textLines.add(line);
    } else {
      String lineBeforeSemicolon = line.substring(0, indexOfSemicolon + 1);
      // raw text
      sqlStatementRawTextJoiner.add(lineBeforeSemicolon);
      // single line text
      singleLineTextBuilder.append(lineBeforeSemicolon);
      // text lines
      textLines.add(lineBeforeSemicolon);

      SqlStatement sqlStatement =
          createStatement(sqlStatementRawTextJoiner, singleLineTextBuilder, textLines);

      resetStatementBuffer(rawTextJoinerRef, singleLineTextBuilder, textLines);

      sqlStatementList.add(sqlStatement);
    }
  }

  private static SqlStatement createStatement(StringJoiner sqlStatementRawTextJoiner,
      StringBuilder singleLineTextBuilder, List<String> textLines) {
    return SqlStatement.builder().rawText(sqlStatementRawTextJoiner.toString())
        .singleLineText(singleLineTextBuilder.toString()).textLines(new ArrayList<>(textLines))
        .build();
  }

  private static void resetStatementBuffer(AtomicReference<StringJoiner> rawTextJoinerRef,
      StringBuilder singleLineTextBuilder, List<String> textLines) {
    // raw text
    rawTextJoinerRef.set(new StringJoiner("\n"));
    // single line text
    singleLineTextBuilder.setLength(0);
    // text lines
    textLines.clear();
  }
}
