package tech.easyflow.datacenter.utils;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.WithItem;
import net.sf.jsqlparser.util.TablesNamesFinder;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

public final class SqlSupportUtils {

    private SqlSupportUtils() {
    }

    public static ResolvedSql resolve(String sql, Collection<ManagedTable> managedTables) {
        String normalizedSql = normalizeSql(sql);
        Statement statement = parseSingleStatement(normalizedSql);
        if (!(statement instanceof Select select)) {
            throw new BusinessException("查询数据节点只支持只读 SELECT SQL");
        }
        Map<String, List<ManagedTable>> byTableName = new LinkedHashMap<>();
        Map<String, ManagedTable> byCatalogAndTable = new LinkedHashMap<>();
        for (ManagedTable managedTable : managedTables) {
            if (managedTable == null || !hasText(managedTable.getTableName())) {
                continue;
            }
            String tableKey = normalizeIdentifier(managedTable.getTableName());
            byTableName.computeIfAbsent(tableKey, key -> new ArrayList<>()).add(managedTable);
            if (hasText(managedTable.getCatalogName())) {
                byCatalogAndTable.put(
                        catalogTableKey(managedTable.getCatalogName(), managedTable.getTableName()),
                        managedTable
                );
            }
        }

        SqlTableCollector collector = new SqlTableCollector();
        List<Table> referencedTables = collector.collect(select);
        if (referencedTables.isEmpty()) {
            throw new BusinessException("SQL 必须引用至少一张已接入表");
        }
        Set<String> logicalTables = new LinkedHashSet<>();
        for (Table table : referencedTables) {
            ManagedTable managedTable = resolveManagedTable(table, byTableName, byCatalogAndTable);
            rewriteTable(table, managedTable);
            logicalTables.add(renderLogicalTable(managedTable));
        }
        return new ResolvedSql(select.toString(), new ArrayList<>(logicalTables));
    }

    private static Statement parseSingleStatement(String sql) {
        try {
            Statements statements = CCJSqlParserUtil.parseStatements(sql);
            if (statements == null || statements.getStatements() == null || statements.getStatements().isEmpty()) {
                throw new BusinessException("SQL 不能为空");
            }
            if (statements.getStatements().size() != 1) {
                throw new BusinessException("查询数据节点仅支持单条 SQL");
            }
            return statements.getStatements().get(0);
        } catch (BusinessException e) {
            throw e;
        } catch (JSQLParserException e) {
            throw new BusinessException("SQL 解析失败，请检查语法");
        }
    }

    private static ManagedTable resolveManagedTable(Table table,
                                                    Map<String, List<ManagedTable>> byTableName,
                                                    Map<String, ManagedTable> byCatalogAndTable) {
        String tableName = trimToNull(table.getName());
        if (!hasText(tableName)) {
            throw new BusinessException("SQL 包含无法识别的表名");
        }
        String catalogName = trimToNull(table.getSchemaName());
        if (hasText(catalogName)) {
            ManagedTable managedTable = byCatalogAndTable.get(catalogTableKey(catalogName, tableName));
            if (managedTable == null) {
                throw new BusinessException("SQL 引用了未接入表: " + catalogName + "." + tableName);
            }
            return managedTable;
        }
        List<ManagedTable> matches = byTableName.get(normalizeIdentifier(tableName));
        if (matches == null || matches.isEmpty()) {
            throw new BusinessException("SQL 引用了未接入表: " + tableName);
        }
        if (matches.size() > 1) {
            throw new BusinessException("SQL 引用了重名表，请使用 catalog.table: " + tableName);
        }
        return matches.get(0);
    }

    private static void rewriteTable(Table table, ManagedTable managedTable) {
        table.setName(managedTable.getPhysicalTableName());
        table.setSchemaName(trimToNull(managedTable.getCatalogName()));
    }

    private static String renderLogicalTable(ManagedTable managedTable) {
        if (!hasText(managedTable.getCatalogName())) {
            return managedTable.getTableName();
        }
        return managedTable.getCatalogName() + "." + managedTable.getTableName();
    }

    private static String normalizeSql(String sql) {
        String normalized = trimToNull(sql);
        if (!hasText(normalized)) {
            throw new BusinessException("SQL 不能为空");
        }
        return normalized;
    }

    private static String catalogTableKey(String catalogName, String tableName) {
        return normalizeIdentifier(catalogName) + "." + normalizeIdentifier(tableName);
    }

    private static String normalizeIdentifier(String value) {
        String normalized = trimToNull(value);
        if (!hasText(normalized)) {
            return "";
        }
        if ((normalized.startsWith("`") && normalized.endsWith("`"))
                || (normalized.startsWith("\"") && normalized.endsWith("\""))
                || (normalized.startsWith("[") && normalized.endsWith("]"))) {
            normalized = normalized.substring(1, normalized.length() - 1);
        }
        return normalized.trim().toLowerCase(Locale.ROOT);
    }

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

    private static String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }

    public static class ManagedTable {
        private final String catalogName;
        private final String tableName;
        private final String physicalTableName;

        public ManagedTable(String catalogName, String tableName, String physicalTableName) {
            this.catalogName = trimToNull(catalogName);
            this.tableName = trimToNull(tableName);
            this.physicalTableName = hasText(physicalTableName) ? physicalTableName.trim() : this.tableName;
        }

        public String getCatalogName() {
            return catalogName;
        }

        public String getTableName() {
            return tableName;
        }

        public String getPhysicalTableName() {
            return physicalTableName;
        }
    }

    public static class ResolvedSql {
        private final String executableSql;
        private final List<String> logicalTables;

        public ResolvedSql(String executableSql, List<String> logicalTables) {
            this.executableSql = executableSql;
            this.logicalTables = logicalTables;
        }

        public String getExecutableSql() {
            return executableSql;
        }

        public List<String> getLogicalTables() {
            return logicalTables;
        }
    }

    private static class SqlTableCollector extends TablesNamesFinder {
        private final List<Table> tables = new ArrayList<>();
        private final Set<String> withNames = new LinkedHashSet<>();

        public List<Table> collect(Statement statement) {
            tables.clear();
            withNames.clear();
            statement.accept(this);
            return new ArrayList<>(tables);
        }

        @Override
        public void visit(WithItem withItem) {
            if (withItem.getAlias() != null && hasText(withItem.getAlias().getName())) {
                withNames.add(normalizeIdentifier(withItem.getAlias().getName()));
            }
            withItem.getSelect().accept((net.sf.jsqlparser.statement.select.SelectVisitor) this);
        }

        @Override
        public void visit(Table table) {
            String tableName = normalizeIdentifier(table.getName());
            if (!withNames.contains(tableName)) {
                tables.add(table);
            }
        }
    }
}
