package com.ruoyi.generator.service;

import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.GenProject;
import com.ruoyi.generator.domain.GenTable;
import com.ruoyi.generator.domain.GenTableColumn;
import com.ruoyi.generator.domain.SysCodeSnippet;
import com.ruoyi.generator.domain.SysProjectModule;
import com.ruoyi.generator.domain.SysProjectStructure;
import com.ruoyi.generator.domain.SysProjectTemplate;
import com.ruoyi.generator.domain.Template;
import com.ruoyi.generator.domain.TemplateFile;
import com.ruoyi.generator.domain.GeneratedBusinessBlockFile;
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionUiBindingDesign;
import com.ruoyi.generator.mapper.GenProjectMapper;
import com.ruoyi.generator.mapper.GenTableMapper;
import com.ruoyi.generator.util.VelocityInitializer;
import com.ruoyi.generator.util.VelocityUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
public class GenProjectServiceImpl implements IGenProjectService {
    private static final String SQL_TEMPLATE_TYPE = "sql";
    private static final String SQL_CATEGORY = "sql";
    private static final Long RUNNABLE_BACKEND_TEMPLATE_ID = 9201L;
    private static final Long RUNNABLE_ADMIN_FRONTEND_TEMPLATE_ID = 9202L;
    private static final Long RUNNABLE_FRONTEND_TEMPLATE_ID = 9203L;
    private static final String BACKEND_CONTROLLER_TEMPLATE_FILE = "controller.java.vm";
    private static final String BUNDLED_BACKEND_CONTROLLER_TEMPLATE = "qing/controller.java.vm";
    private static final String FRONTEND_INDEX_TEMPLATE_FILE = "frontend-index.vue.vm";
    private static final String BUNDLED_FRONTEND_INDEX_TEMPLATE = "qing/index.vue.vm";

    @Autowired
    private GenProjectMapper genProjectMapper;
    @Autowired
    private GenTableMapper genTableMapper;
    @Autowired
    private ITemplateService templateService;
    @Autowired
    private ITemplateFileService templateFileService;
    @Autowired
    private ISysCodeSnippetService codeSnippetService;
    @Autowired
    private ISysProjectModuleService sysProjectModuleService;
    @Autowired
    private ISysProjectTemplateService sysProjectTemplateService;
    @Autowired
    private ISysProjectStructureService sysProjectStructureService;
    @Autowired
    private BusinessBlockGenerationService businessBlockGenerationService;

    @Override
    public GenProject selectGenProjectById(Long projectId) {
        return genProjectMapper.selectGenProjectById(projectId);
    }

    @Override
    public List<GenProject> selectGenProjectList(GenProject genProject) {
        return genProjectMapper.selectGenProjectList(genProject);
    }

    @Override
    public int insertGenProject(GenProject genProject) {
        return genProjectMapper.insertGenProject(genProject);
    }

    @Override
    public int updateGenProject(GenProject genProject) {
        return genProjectMapper.updateGenProject(genProject);
    }

    @Override
    public int deleteGenProjectById(Long projectId) {
        return genProjectMapper.deleteGenProjectById(projectId);
    }

    @Override
    public int deleteGenProjectByIds(Long[] projectIds) {
        return genProjectMapper.deleteGenProjectByIds(projectIds);
    }

    @Override
    public List<Map<String, Object>> getProjectStructure(Long projectId, String type) {
        GenProject project = getRequiredProject(projectId);
        project.setTables(genTableMapper.selectGenTablesByProjectId(projectId));
        return getProjectStructure(project, type);
    }

    @Override
    public List<Map<String, Object>> getProjectStructure(GenProject project, String type) {
        if (project == null) {
            throw new ServiceException("项目信息不能为空");
        }
        List<GenTable> projectTables = project.getTables();
        if (projectTables == null) {
            projectTables = new ArrayList<GenTable>();
            project.setTables(projectTables);
        }
        if (isSqlType(type)) {
            return getSqlProjectStructure(project);
        }

        List<Map<String, Object>> structure = new ArrayList<Map<String, Object>>();
        SysProjectTemplate projectTemplate = resolveProjectTemplate(project, type);
        List<SysProjectStructure> structureNodes = selectStructureNodes(project, projectTemplate);

        Map<Long, SysProjectStructure> sourceNodeMap = new HashMap<Long, SysProjectStructure>();
        for (SysProjectStructure node : structureNodes) {
            sourceNodeMap.put(node.getNodeId(), node);
        }

        Map<String, Map<String, Object>> nodeMap = new LinkedHashMap<String, Map<String, Object>>();
        Map<String, Map<String, Object>> mergedFolderMap = new LinkedHashMap<String, Map<String, Object>>();
        for (SysProjectStructure node : structureNodes) {
            if (isTableScopedNode(node)) {
                for (GenTable table : projectTables) {
                    Map<String, Object> tableNodeData = createStructureNodeData(node, project, table);
                    tableNodeData = mergeTableScopedFolderNode(mergedFolderMap, node, project, table, sourceNodeMap, tableNodeData);
                    nodeMap.put(getNodeKey(node.getNodeId(), table.getTableId()), tableNodeData);
                }
            } else {
                Map<String, Object> nodeData = createStructureNodeData(node, project, null);
                nodeMap.put(getNodeKey(node.getNodeId(), null), nodeData);
            }
        }

        for (SysProjectStructure node : structureNodes) {
            if (isTableScopedNode(node)) {
                for (GenTable table : projectTables) {
                    Map<String, Object> nodeData = nodeMap.get(getNodeKey(node.getNodeId(), table.getTableId()));
                    addNodeToParent(structure, nodeData, node, nodeMap, sourceNodeMap, table);
                }
            } else {
                Map<String, Object> nodeData = nodeMap.get(getNodeKey(node.getNodeId(), null));
                addNodeToParent(structure, nodeData, node, nodeMap, sourceNodeMap, null);
            }
        }
        appendBusinessBlockFiles(structure, project, type);
        return structure;
    }

    @Override
    public byte[] downloadStructure(Long projectId, String type) {
        GenProject project = getRequiredProject(projectId);
        project.setTables(genTableMapper.selectGenTablesByProjectId(projectId));
        return downloadStructure(project, type);
    }

    @Override
    public byte[] downloadStructure(GenProject project, String type) {
        if (project == null) {
            throw new ServiceException("项目信息不能为空");
        }
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ZipOutputStream zip = new ZipOutputStream(outputStream);
        try {
            processStructure(getProjectStructure(project, type), zip, project, type);
            zip.close();
            return outputStream.toByteArray();
        } catch (IOException e) {
            throw new ServiceException("生成项目结构失败");
        }
    }

    private void processStructure(List<Map<String, Object>> structure, ZipOutputStream zip, GenProject project, String type) throws IOException {
        processStructureWithPath(structure, zip, project, "", type);
    }

    private void processStructureWithPath(List<Map<String, Object>> structure, ZipOutputStream zip, GenProject project, String currentPath, String type) throws IOException {
        for (Map<String, Object> node : structure) {
            String nodeType = (String) node.get("type");
            String name = (String) node.get("name");
            String category = (String) node.get("category");
            Object tableId = node.get("tableId");
            String fullPath = currentPath + name;

            if ("folder".equals(nodeType)) {
                String folderPath = fullPath + "/";
                zip.putNextEntry(new ZipEntry(folderPath));
                zip.closeEntry();

                @SuppressWarnings("unchecked")
                List<Map<String, Object>> children = (List<Map<String, Object>>) node.get("children");
                if (children != null && !children.isEmpty()) {
                    processStructureWithPath(children, zip, project, folderPath, type);
                }
            } else if ("file".equals(nodeType)) {
                String content = generateFileContent(project, category, (Long) tableId, type);
                if (content != null) {
                    zip.putNextEntry(new ZipEntry(fullPath));
                    zip.write(content.getBytes(StandardCharsets.UTF_8));
                    zip.closeEntry();
                }
            }
        }
    }

    @Override
    public Map<String, String> previewCode(Long projectId, Long tableId, String keyword, String type) {
        GenProject project = getRequiredProject(projectId);
        project.setTables(genTableMapper.selectGenTablesByProjectId(projectId));
        return previewCode(project, tableId, keyword, type);
    }

    @Override
    public Map<String, String> previewCode(GenProject project, Long tableId, String keyword, String type) {
        if (project == null) {
            throw new ServiceException("项目信息不能为空");
        }
        if (isSqlType(type)) {
            return previewSqlCode(project, tableId, keyword);
        }
        Map<String, String> dataMap = new LinkedHashMap<String, String>();
        GeneratedBusinessBlockFile businessBlockFile = findBusinessBlockFile(project, type, keyword);
        if (businessBlockFile != null) {
            dataMap.put(businessBlockFile.getOutputPath(), businessBlockService().renderFile(project, businessBlockFile));
            return dataMap;
        }
        List<SysProjectModule> modules = getProjectModules(project);

        VelocityInitializer.initVelocity();
        SysProjectTemplate projectTemplate = resolveProjectTemplate(project, type);
        List<TemplateFile> templateFileList = getRequiredTemplateFiles(projectTemplate.getTemplateId());
        for (TemplateFile templateFile : templateFileList) {
            if (matchesTemplateFile(templateFile, keyword)) {
                dataMap.put(templateFile.getFileName(), renderTemplate(project, tableId, modules, templateFile));
            }
        }
        return dataMap;
    }

    private String generateFileContent(GenProject project, String category, Long tableId, String type) {
        if (project == null) {
            throw new ServiceException("项目信息不能为空");
        }
        if (isSqlType(type) || SQL_CATEGORY.equals(category)) {
            GenTable table = getRequiredTable(project, tableId);
            return getSqlFileContent(table);
        }
        GeneratedBusinessBlockFile businessBlockFile = findBusinessBlockFile(project, type, category);
        if (businessBlockFile != null) {
            return businessBlockService().renderFile(project, businessBlockFile);
        }
        List<SysProjectModule> modules = getProjectModules(project);

        VelocityInitializer.initVelocity();
        SysProjectTemplate projectTemplate = resolveProjectTemplate(project, type);
        List<TemplateFile> templateFileList = getRequiredTemplateFiles(projectTemplate.getTemplateId());
        for (TemplateFile templateFile : templateFileList) {
            if (matchesTemplateFile(templateFile, category)) {
                try {
                    return renderTemplate(project, tableId, modules, templateFile);
                } catch (Exception e) {
                    throw new ServiceException("渲染模板失败，表名：" + tableId);
                }
            }
        }
        throw new ServiceException("未找到模板文件：" + category);
    }

    private boolean isSqlType(String type) {
        return SQL_TEMPLATE_TYPE.equals(type);
    }

    private GeneratedBusinessBlockFile findBusinessBlockFile(GenProject project, String type, String keyword) {
        return businessBlockService().findFile(project, type, keyword);
    }

    private BusinessBlockGenerationService businessBlockService() {
        if (businessBlockGenerationService == null) {
            businessBlockGenerationService = new BusinessBlockGenerationService();
        }
        return businessBlockGenerationService;
    }

    private List<Map<String, Object>> getSqlProjectStructure(GenProject project) {
        Map<String, Object> sqlFolder = new LinkedHashMap<String, Object>();
        sqlFolder.put("id", SQL_TEMPLATE_TYPE);
        sqlFolder.put("name", "sql");
        sqlFolder.put("type", "folder");
        sqlFolder.put("category", SQL_CATEGORY);
        List<Map<String, Object>> children = new ArrayList<Map<String, Object>>();
        sqlFolder.put("children", children);

        if (StringUtils.isNotEmpty(project.getTables())) {
            for (GenTable table : project.getTables()) {
                Map<String, Object> fileNode = new LinkedHashMap<String, Object>();
                String fileName = getSqlFileName(table);
                fileNode.put("id", SQL_TEMPLATE_TYPE + "-" + table.getTableId());
                fileNode.put("name", fileName);
                fileNode.put("type", "file");
                fileNode.put("category", SQL_CATEGORY);
                fileNode.put("tableId", table.getTableId());
                children.add(fileNode);
            }
        }
        return Collections.singletonList(sqlFolder);
    }

    private Map<String, String> previewSqlCode(GenProject project, Long tableId, String keyword) {
        Map<String, String> dataMap = new LinkedHashMap<String, String>();
        if (StringUtils.isEmpty(project.getTables())) {
            return dataMap;
        }
        for (GenTable table : project.getTables()) {
            String fileName = getSqlFileName(table);
            if (matchesSqlFile(table, tableId, keyword, fileName)) {
                dataMap.put(fileName, getSqlFileContent(table));
            }
        }
        return dataMap;
    }

    private boolean matchesSqlFile(GenTable table, Long tableId, String keyword, String fileName) {
        if (tableId != null && tableId != -1) {
            return tableId.equals(table.getTableId());
        }
        if (StringUtils.isEmpty(keyword) || SQL_CATEGORY.equals(keyword)) {
            return true;
        }
        String normalizedKeyword = keyword.replace('\\', '/');
        return fileName.equals(normalizedKeyword) || normalizedKeyword.endsWith("/" + fileName);
    }

    private String getSqlFileName(GenTable table) {
        String name = table == null ? "" : StringUtils.defaultString(table.getBusinessName());
        if (StringUtils.isEmpty(name) && table != null) {
            name = StringUtils.defaultString(table.getTableName());
        }
        if (StringUtils.isEmpty(name)) {
            name = "schema";
        }
        name = name.trim().replaceAll("[\\\\/:*?\"<>|]", "_").replaceAll("\\s+", "_");
        if (StringUtils.isEmpty(name)) {
            name = "schema";
        }
        return name.endsWith(".sql") ? name : name + ".sql";
    }

    private String getSqlFileContent(GenTable table) {
        String sql = table == null ? "" : StringUtils.defaultString(table.getCreateTableSql());
        if (StringUtils.isEmpty(sql)) {
            return "-- 未生成建表 SQL";
        }
        return sql.endsWith("\n") ? sql : sql + "\n";
    }

    private List<SysProjectModule> getProjectModules(GenProject project) {
        if (project.isFrontDraft()) {
            return project.getModules() == null ? new ArrayList<SysProjectModule>() : project.getModules();
        }
        SysProjectModule projectModuleFilter = new SysProjectModule();
        projectModuleFilter.setProjectId(project.getProjectId());
        return sysProjectModuleService.selectSysProjectModuleList(projectModuleFilter);
    }

    private GenProject getRequiredProject(Long projectId) {
        GenProject project = genProjectMapper.selectGenProjectById(projectId);
        if (project == null) {
            throw new ServiceException("项目不存在");
        }
        return project;
    }

    private SysProjectTemplate resolveProjectTemplate(GenProject project, String type) {
        if (!project.isFrontDraft()) {
            SysProjectTemplate projectTemplate = sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(project.getProjectId(), type);
            if (projectTemplate != null && projectTemplate.getTemplateId() != null) {
                return projectTemplate;
            }
        }

        return resolveActiveTemplate(project.getProjectId(), type);
    }

    private SysProjectTemplate resolveActiveTemplate(Long projectId, String type) {
        Template templateFilter = new Template();
        templateFilter.setTemplateType(type);
        List<Template> templates = templateService.selectTemplateList(templateFilter);
        if (StringUtils.isNotEmpty(templates)) {
            Template firstActiveTemplate = null;
            Long preferredTemplateId = preferredRunnableTemplateId(type);
            for (Template template : templates) {
                if (template != null && template.getTemplateId() != null && Long.valueOf(0L).equals(template.getTemplateStatus())) {
                    if (preferredTemplateId != null && preferredTemplateId.equals(template.getTemplateId())) {
                        return toProjectTemplate(projectId, type, template);
                    }
                    if (firstActiveTemplate == null) {
                        firstActiveTemplate = template;
                    }
                }
            }
            if (firstActiveTemplate != null) {
                return toProjectTemplate(projectId, type, firstActiveTemplate);
            }
        }
        throw new ServiceException("未配置可用模板：" + type);
    }

    private Long preferredRunnableTemplateId(String type) {
        if ("backend".equals(type)) {
            return RUNNABLE_BACKEND_TEMPLATE_ID;
        }
        if ("admin_frontend".equals(type)) {
            return RUNNABLE_ADMIN_FRONTEND_TEMPLATE_ID;
        }
        if ("frontend".equals(type)) {
            return RUNNABLE_FRONTEND_TEMPLATE_ID;
        }
        return null;
    }

    private SysProjectTemplate toProjectTemplate(Long projectId, String type, Template template) {
        SysProjectTemplate fallbackTemplate = new SysProjectTemplate();
        fallbackTemplate.setProjectId(projectId);
        fallbackTemplate.setTemplateId(template.getTemplateId());
        fallbackTemplate.setTemplateType(type);
        return fallbackTemplate;
    }

    private List<SysProjectStructure> selectStructureNodes(GenProject project, SysProjectTemplate projectTemplate) {
        if (project.isFrontDraft()) {
            SysProjectStructure query = new SysProjectStructure();
            query.setTemplateId(projectTemplate.getTemplateId());
            query.setStatus("0");
            return sysProjectStructureService.selectSysProjectStructureList(query);
        }
        SysProjectTemplate boundProjectTemplate = sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(
                project.getProjectId(), projectTemplate.getTemplateType());
        if (boundProjectTemplate != null && boundProjectTemplate.getTemplateId() != null) {
            return sysProjectStructureService.selectProjectStructureListByModule(project.getProjectId(), projectTemplate.getTemplateId());
        }
        SysProjectStructure query = new SysProjectStructure();
        query.setTemplateId(projectTemplate.getTemplateId());
        query.setStatus("0");
        return sysProjectStructureService.selectSysProjectStructureList(query);
    }

    private List<TemplateFile> getRequiredTemplateFiles(Long templateId) {
        List<TemplateFile> templateFileList = templateFileService.selectTemplateFilesByTemplateId(templateId);
        if (StringUtils.isEmpty(templateFileList)) {
            throw new ServiceException("模板未配置文件");
        }
        return templateFileList;
    }

    private boolean isTableScopedNode(SysProjectStructure node) {
        return node.getTableId() != null && node.getTableId() == 0;
    }

    private String getNodeKey(Long nodeId, Long tableId) {
        return tableId == null ? String.valueOf(nodeId) : nodeId + "-" + tableId;
    }

    private Map<String, Object> createStructureNodeData(SysProjectStructure node, GenProject project, GenTable table) {
        Map<String, Object> nodeData = new LinkedHashMap<String, Object>();
        nodeData.put("id", table == null ? node.getNodeId() : getNodeKey(node.getNodeId(), table.getTableId()));
        nodeData.put("name", replaceNodeName(node.getNodeName(), project, table));
        nodeData.put("type", node.getNodeType());
        nodeData.put("category", node.getCategory());
        nodeData.put("tableId", table == null ? node.getTableId() : table.getTableId());
        if ("folder".equals(node.getNodeType())) {
            nodeData.put("children", new ArrayList<Map<String, Object>>());
        }
        return nodeData;
    }

    private String replaceNodeName(String nodeName, GenProject project, GenTable table) {
        if (nodeName == null) {
            return "";
        }
        String result = nodeName;
        result = result.replace("{projectName}", StringUtils.defaultString(project.getProjectFileName()));
        result = result.replace("{projectClassName}", toProjectClassName(project.getProjectFileName()));
        String packageName = StringUtils.defaultString(project.getPackageName());
        result = result.replace("{packageName}", packageName);
        result = result.replace("{packagePath}", packageName.replace('.', '/'));
        if (table != null) {
            result = result.replace("{moduleName}", StringUtils.defaultString(table.getModuleName()));
            result = result.replace("{ClassName}", StringUtils.defaultString(table.getClassName()));
            result = result.replace("{className}", StringUtils.uncapitalize(StringUtils.defaultString(table.getClassName())));
            result = result.replace("{tableName}", StringUtils.defaultString(table.getTableName()));
            result = result.replace("{businessName}", StringUtils.defaultString(table.getBusinessName()));
        }
        return result;
    }

    private String toProjectClassName(String projectFileName) {
        String source = StringUtils.defaultString(projectFileName);
        StringBuilder result = new StringBuilder();
        boolean uppercaseNext = true;
        for (int i = 0; i < source.length(); i++) {
            char ch = source.charAt(i);
            if (!Character.isLetterOrDigit(ch)) {
                uppercaseNext = true;
                continue;
            }
            if (result.length() == 0 && !Character.isJavaIdentifierStart(ch)) {
                result.append("App");
            }
            result.append(uppercaseNext ? Character.toUpperCase(ch) : ch);
            uppercaseNext = false;
        }
        if (result.length() == 0) {
            return "GeneratedProject";
        }
        return result.toString();
    }

    private Map<String, Object> mergeTableScopedFolderNode(Map<String, Map<String, Object>> mergedFolderMap,
                                                           SysProjectStructure node, GenProject project, GenTable table,
                                                           Map<Long, SysProjectStructure> sourceNodeMap,
                                                           Map<String, Object> nodeData) {
        if (!"folder".equals(node.getNodeType()) || nodeData == null) {
            return nodeData;
        }
        String mergeKey = getTableScopedFolderMergeKey(node, project, table, sourceNodeMap);
        Map<String, Object> existingNode = mergedFolderMap.get(mergeKey);
        if (existingNode != null) {
            return existingNode;
        }
        mergedFolderMap.put(mergeKey, nodeData);
        return nodeData;
    }

    private String getTableScopedFolderMergeKey(SysProjectStructure node, GenProject project, GenTable table,
                                                Map<Long, SysProjectStructure> sourceNodeMap) {
        List<String> pathParts = new ArrayList<String>();
        SysProjectStructure current = node;
        while (current != null) {
            GenTable scopedTable = isTableScopedNode(current) ? table : null;
            pathParts.add(0, current.getNodeId() + ":" + replaceNodeName(current.getNodeName(), project, scopedTable));
            if (current.getParentId() == null || current.getParentId() == 0) {
                break;
            }
            current = sourceNodeMap.get(current.getParentId());
        }

        StringBuilder key = new StringBuilder();
        for (String part : pathParts) {
            if (key.length() > 0) {
                key.append("/");
            }
            key.append(part);
        }
        return key.toString();
    }

    private void addNodeToParent(List<Map<String, Object>> roots, Map<String, Object> nodeData,
                                 SysProjectStructure node, Map<String, Map<String, Object>> nodeMap,
                                 Map<Long, SysProjectStructure> sourceNodeMap, GenTable table) {
        if (nodeData == null) {
            return;
        }
        if (node.getParentId() == null || node.getParentId() == 0) {
            addNodeIfAbsent(roots, nodeData);
            return;
        }

        SysProjectStructure parentSource = sourceNodeMap.get(node.getParentId());
        Long parentTableId = parentSource != null && isTableScopedNode(parentSource) && table != null ? table.getTableId() : null;
        Map<String, Object> parentNode = nodeMap.get(getNodeKey(node.getParentId(), parentTableId));
        if (parentNode == null) {
            return;
        }

        @SuppressWarnings("unchecked")
        List<Map<String, Object>> children = (List<Map<String, Object>>) parentNode.get("children");
        if (children != null) {
            addNodeIfAbsent(children, nodeData);
        }
    }

    private void addNodeIfAbsent(List<Map<String, Object>> nodes, Map<String, Object> nodeData) {
        for (Map<String, Object> node : nodes) {
            if (node == nodeData) {
                return;
            }
        }
        nodes.add(nodeData);
    }

    private void appendBusinessBlockFiles(List<Map<String, Object>> structure, GenProject project, String type) {
        List<GeneratedBusinessBlockFile> files = businessBlockService().listFiles(project, type);
        if (StringUtils.isEmpty(files)) {
            return;
        }
        for (GeneratedBusinessBlockFile file : files) {
            addGeneratedFileNode(structure, file);
        }
    }

    @SuppressWarnings("unchecked")
    private void addGeneratedFileNode(List<Map<String, Object>> structure, GeneratedBusinessBlockFile file) {
        if (file == null || StringUtils.isEmpty(file.getOutputPath())) {
            return;
        }
        List<Map<String, Object>> current = generatedFileRoot(structure);
        String[] parts = file.getOutputPath().replace('\\', '/').split("/");
        StringBuilder path = new StringBuilder();
        for (int i = 0; i < parts.length; i++) {
            String part = parts[i];
            if (StringUtils.isEmpty(part)) {
                continue;
            }
            if (path.length() > 0) {
                path.append("/");
            }
            path.append(part);
            boolean last = i == parts.length - 1;
            if (last) {
                if (findChildNode(current, part, "file") == null) {
                    Map<String, Object> fileNode = new LinkedHashMap<String, Object>();
                    fileNode.put("id", file.getCategory());
                    fileNode.put("name", part);
                    fileNode.put("type", "file");
                    fileNode.put("category", file.getCategory());
                    fileNode.put("tableId", null);
                    current.add(fileNode);
                }
            } else {
                Map<String, Object> folderNode = findChildNode(current, part, "folder");
                if (folderNode == null) {
                    folderNode = new LinkedHashMap<String, Object>();
                    folderNode.put("id", "business-block-folder:" + path.toString());
                    folderNode.put("name", part);
                    folderNode.put("type", "folder");
                    folderNode.put("category", null);
                    folderNode.put("tableId", null);
                    folderNode.put("children", new ArrayList<Map<String, Object>>());
                    current.add(folderNode);
                }
                current = (List<Map<String, Object>>) folderNode.get("children");
            }
        }
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> generatedFileRoot(List<Map<String, Object>> structure) {
        if (structure.size() == 1 && "folder".equals(structure.get(0).get("type"))) {
            Object children = structure.get(0).get("children");
            if (children instanceof List) {
                return (List<Map<String, Object>>) children;
            }
        }
        return structure;
    }

    private Map<String, Object> findChildNode(List<Map<String, Object>> nodes, String name, String type) {
        for (Map<String, Object> node : nodes) {
            if (StringUtils.equals(name, (String) node.get("name")) && StringUtils.equals(type, (String) node.get("type"))) {
                return node;
            }
        }
        return null;
    }

    private boolean matchesTemplateFile(TemplateFile templateFile, String templateFileName) {
        if (templateFile == null || StringUtils.isEmpty(templateFileName)) {
            return false;
        }
        return StringUtils.equals(templateFile.getFileName(), templateFileName)
                || StringUtils.equals(templateFile.getFilePath(), templateFileName);
    }

    private String renderTemplate(GenProject project, Long tableId, List<SysProjectModule> modules, TemplateFile templateFile) {
        VelocityContext context = prepareVelocityContext(project, tableId, modules, templateFile);
        StringWriter sw = new StringWriter();
        Velocity.evaluate(context, sw, "", resolveTemplateContent(templateFile));
        return sw.toString();
    }

    private String resolveTemplateContent(TemplateFile templateFile) {
        String content = StringUtils.defaultString(templateFile.getFileContent());
        if (shouldUseBundledBackendControllerTemplate(templateFile, content)) {
            String bundledContent = readClasspathTemplate(BUNDLED_BACKEND_CONTROLLER_TEMPLATE);
            if (StringUtils.isNotEmpty(bundledContent)) {
                return bundledContent;
            }
        }
        if (shouldUseBundledFrontendIndexTemplate(templateFile, content)) {
            String bundledContent = readClasspathTemplate(BUNDLED_FRONTEND_INDEX_TEMPLATE);
            if (StringUtils.isNotEmpty(bundledContent)) {
                return bundledContent;
            }
        }
        return content;
    }

    private boolean shouldUseBundledBackendControllerTemplate(TemplateFile templateFile, String content) {
        if (templateFile == null || !RUNNABLE_BACKEND_TEMPLATE_ID.equals(templateFile.getTemplateId())) {
            return false;
        }
        boolean isControllerTemplate = StringUtils.equals(BACKEND_CONTROLLER_TEMPLATE_FILE, templateFile.getFileName())
                || StringUtils.equals(BACKEND_CONTROLLER_TEMPLATE_FILE, templateFile.getFilePath());
        String templateContent = StringUtils.defaultString(content);
        return isControllerTemplate && !templateContent.contains("__currentUserOnly");
    }

    private boolean shouldUseBundledFrontendIndexTemplate(TemplateFile templateFile, String content) {
        if (templateFile == null || !RUNNABLE_FRONTEND_TEMPLATE_ID.equals(templateFile.getTemplateId())) {
            return false;
        }
        boolean isFrontendIndexTemplate = StringUtils.equals(FRONTEND_INDEX_TEMPLATE_FILE, templateFile.getFileName())
                || StringUtils.equals(FRONTEND_INDEX_TEMPLATE_FILE, templateFile.getFilePath());
        String templateContent = StringUtils.defaultString(content);
        return isFrontendIndexTemplate && (!templateContent.contains("$hasCurrentFrontendPageDesign")
                || !templateContent.contains("businessActionMap")
                || !templateContent.contains("$renderForm")
                || !templateContent.contains("handleView(row)")
                || !templateContent.contains("__currentUserOnly"));
    }

    private String readClasspathTemplate(String resourceName) {
        InputStream inputStream = GenProjectServiceImpl.class.getClassLoader().getResourceAsStream(resourceName);
        if (inputStream == null) {
            return null;
        }
        try {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[4096];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
            return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
        } catch (IOException e) {
            return null;
        } finally {
            try {
                inputStream.close();
            } catch (IOException ignored) {
            }
        }
    }

    private VelocityContext prepareVelocityContext(GenProject project, Long tableId, List<SysProjectModule> modules, TemplateFile templateFile) {
        if (tableId != null && tableId != -1) {
            GenTable table = getRequiredTable(project, tableId);
            table.setModules(modules);
            table.setPackageName(project.getPackageName());
            setSubTable(table);
            setPkColumn(table);
            VelocityContext context = VelocityUtils.prepareContext(table);
            String pageDesignScope = RUNNABLE_ADMIN_FRONTEND_TEMPLATE_ID.equals(templateFile.getTemplateId())
                    ? "admin" : "frontend";
            VelocityUtils.putPageDesignVelocityContext(context, project, table, pageDesignScope);
            if ("frontend".equals(pageDesignScope)) {
                businessBlockService().putEmbeddedBlockVelocityContext(context, project,
                        (FrontProjectPageDesign) context.get("currentPageDesign"));
            }
            putTableBusinessActions(context, project, table);
            putSnippetVariables(context, table, modules, templateFile);
            return context;
        }
        return VelocityUtils.prepareContextProject(project);
    }

    private void putTableBusinessActions(VelocityContext context, GenProject project, GenTable table) {
        List<BusinessActionDesign> tableBusinessActions = filterTableBusinessActions(project, table);
        context.put("tableBusinessActions", tableBusinessActions);
        context.put("hasTableBusinessActions", StringUtils.isNotEmpty(tableBusinessActions));
        Map<String, List<BusinessActionDesign>> slotBusinessActions = buildSlotBusinessActions(tableBusinessActions);
        context.put("slotBusinessActions", slotBusinessActions);
        context.put("hasSlotBusinessActions", !slotBusinessActions.isEmpty());
        List<BusinessActionDesign> listRowBusinessActions = filterTableSlotActions(project, table, tableBusinessActions, "list.rowActions");
        List<BusinessActionDesign> detailPrimaryBusinessActions = filterTableSlotActions(project, table, tableBusinessActions, "detail.primaryActions");
        context.put("listRowBusinessActions", listRowBusinessActions);
        context.put("hasListRowBusinessActions", StringUtils.isNotEmpty(listRowBusinessActions));
        context.put("detailPrimaryBusinessActions", detailPrimaryBusinessActions);
        context.put("hasDetailPrimaryBusinessActions", StringUtils.isNotEmpty(detailPrimaryBusinessActions));
        List<BusinessActionDesign> pageDesignCandidateActions = project == null || StringUtils.isEmpty(project.getBusinessActions())
                ? Collections.<BusinessActionDesign>emptyList()
                : project.getBusinessActions();
        List<BusinessActionDesign> toolbarBusinessActions = filterPageDesignBusinessActions(
                pageDesignCandidateActions, context.get("pageDesignToolbarActionCodes"));
        List<BusinessActionDesign> rowBusinessActions = filterPageDesignBusinessActions(
                pageDesignCandidateActions, context.get("pageDesignRowActionCodes"));
        List<BusinessActionDesign> pageDesignBusinessActions = mergeBusinessActions(toolbarBusinessActions, rowBusinessActions);
        context.put("pageDesignToolbarBusinessActions", toolbarBusinessActions);
        context.put("hasPageDesignToolbarBusinessActions", StringUtils.isNotEmpty(toolbarBusinessActions));
        context.put("pageDesignRowBusinessActions", rowBusinessActions);
        context.put("hasPageDesignRowBusinessActions", StringUtils.isNotEmpty(rowBusinessActions));
        context.put("pageDesignBusinessActions", pageDesignBusinessActions);
        context.put("hasPageDesignBusinessActions", StringUtils.isNotEmpty(pageDesignBusinessActions));
    }

    private List<BusinessActionDesign> filterTableBusinessActions(GenProject project, GenTable table) {
        if (project == null || table == null || StringUtils.isEmpty(project.getBusinessActions())) {
            return Collections.emptyList();
        }
        List<BusinessActionDesign> tableBusinessActions = new ArrayList<BusinessActionDesign>();
        for (BusinessActionDesign action : project.getBusinessActions()) {
            if (action != null && StringUtils.equals(action.getOwnerTable(), table.getTableName())) {
                tableBusinessActions.add(action);
            }
        }
        return tableBusinessActions;
    }

    private Map<String, List<BusinessActionDesign>> buildSlotBusinessActions(List<BusinessActionDesign> actions) {
        Map<String, List<BusinessActionDesign>> slotActions = new LinkedHashMap<String, List<BusinessActionDesign>>();
        if (StringUtils.isEmpty(actions)) {
            return slotActions;
        }
        for (BusinessActionDesign action : actions) {
            if (action == null || StringUtils.isEmpty(action.getUiBindings())) {
                continue;
            }
            for (BusinessActionUiBindingDesign binding : action.getUiBindings()) {
                if (binding == null || StringUtils.isEmpty(binding.getPageCode()) || StringUtils.isEmpty(binding.getSlot())) {
                    continue;
                }
                String key = binding.getPageCode() + ":" + binding.getSlot();
                List<BusinessActionDesign> grouped = slotActions.get(key);
                if (grouped == null) {
                    grouped = new ArrayList<BusinessActionDesign>();
                    slotActions.put(key, grouped);
                }
                grouped.add(action);
            }
        }
        return slotActions;
    }

    private List<BusinessActionDesign> filterPageDesignBusinessActions(List<BusinessActionDesign> tableBusinessActions,
            Object actionCodesObject) {
        if (StringUtils.isEmpty(tableBusinessActions) || !(actionCodesObject instanceof List)) {
            return Collections.emptyList();
        }
        Map<String, BusinessActionDesign> actionsByCode = new LinkedHashMap<String, BusinessActionDesign>();
        for (BusinessActionDesign action : tableBusinessActions) {
            if (action != null && StringUtils.isNotEmpty(action.getCode())) {
                actionsByCode.put(action.getCode(), action);
            }
        }
        List<BusinessActionDesign> result = new ArrayList<BusinessActionDesign>();
        for (Object codeObject : (List<?>) actionCodesObject) {
            String rawCode = StringUtils.defaultString(String.valueOf(codeObject));
            if (StringUtils.isEmpty(rawCode)) {
                continue;
            }
            boolean explicitBusinessAction = rawCode.startsWith("business:");
            String actionCode = explicitBusinessAction ? rawCode.substring("business:".length()) : rawCode;
            if (!explicitBusinessAction && isBuiltinPageAction(actionCode)) {
                continue;
            }
            BusinessActionDesign action = actionsByCode.get(actionCode);
            if (action != null && !result.contains(action)) {
                result.add(action);
            }
        }
        return result;
    }

    private List<BusinessActionDesign> mergeBusinessActions(List<BusinessActionDesign> first, List<BusinessActionDesign> second) {
        Map<String, BusinessActionDesign> merged = new LinkedHashMap<String, BusinessActionDesign>();
        for (BusinessActionDesign action : first) {
            if (action != null && StringUtils.isNotEmpty(action.getCode())) {
                merged.put(action.getCode(), action);
            }
        }
        for (BusinessActionDesign action : second) {
            if (action != null && StringUtils.isNotEmpty(action.getCode())) {
                merged.put(action.getCode(), action);
            }
        }
        return new ArrayList<BusinessActionDesign>(merged.values());
    }

    private boolean isBuiltinPageAction(String actionCode) {
        String normalized = StringUtils.defaultString(actionCode).toLowerCase();
        return "create".equals(normalized) || "add".equals(normalized)
                || "batchdelete".equals(normalized) || "batch_delete".equals(normalized)
                || "view".equals(normalized) || "detail".equals(normalized)
                || "edit".equals(normalized) || "update".equals(normalized)
                || "delete".equals(normalized) || "remove".equals(normalized);
    }

    private List<BusinessActionDesign> filterTableSlotActions(GenProject project, GenTable table,
            List<BusinessActionDesign> tableBusinessActions, String slot) {
        if (StringUtils.isEmpty(tableBusinessActions)) {
            return Collections.emptyList();
        }
        List<BusinessActionDesign> slotActions = new ArrayList<BusinessActionDesign>();
        for (BusinessActionDesign action : tableBusinessActions) {
            if (action == null || StringUtils.isEmpty(action.getUiBindings())) {
                continue;
            }
            for (BusinessActionUiBindingDesign binding : action.getUiBindings()) {
                if (binding != null && StringUtils.equals(slot, binding.getSlot())
                        && bindingMatchesTablePage(project, table, binding, slot)) {
                    slotActions.add(action);
                    break;
                }
            }
        }
        if (StringUtils.isNotEmpty(slotActions)) {
            return slotActions;
        }
        return Collections.emptyList();
    }

    private boolean bindingMatchesTablePage(GenProject project, GenTable table, BusinessActionUiBindingDesign binding, String slot) {
        if (project == null || table == null || binding == null) {
            return false;
        }
        if (StringUtils.isEmpty(project.getFrontendPages())) {
            return true;
        }
        for (AppPageDesign page : project.getFrontendPages()) {
            if (page == null || !StringUtils.equals(page.getCode(), binding.getPageCode())) {
                continue;
            }
            if (!StringUtils.equals(page.getTableName(), table.getTableName())) {
                return false;
            }
            if (slot != null && slot.startsWith("detail.")) {
                return "detail".equals(page.getPageType());
            }
            if (slot != null && slot.startsWith("list.")) {
                return "list".equals(page.getPageType()) || "current_user_list".equals(page.getPageType());
            }
            return true;
        }
        return false;
    }

    private GenTable getRequiredTable(GenProject project, Long tableId) {
        if (project != null && StringUtils.isNotEmpty(project.getTables())) {
            for (GenTable projectTable : project.getTables()) {
                if (projectTable != null && tableId.equals(projectTable.getTableId())) {
                    if (StringUtils.isEmpty(projectTable.getColumns())) {
                        throw new ServiceException("业务表未配置字段：" + projectTable.getTableName());
                    }
                    return projectTable;
                }
            }
        }
        if (project != null && project.isFrontDraft()) {
            throw new ServiceException("Table is not in front draft: " + tableId);
        }
        GenTable table = genTableMapper.selectGenTableById(tableId);
        if (table == null) {
            throw new ServiceException("业务表不存在：" + tableId);
        }
        if (StringUtils.isEmpty(table.getColumns())) {
            throw new ServiceException("业务表未配置字段：" + table.getTableName());
        }
        return table;
    }

    private void putSnippetVariables(VelocityContext context, GenTable table, List<SysProjectModule> modules, TemplateFile templateFile) {
        SysCodeSnippet codeSnippet = new SysCodeSnippet();
        codeSnippet.setStatus("0");
        List<SysCodeSnippet> codeSnippetList = codeSnippetService.selectSysCodeSnippetList(codeSnippet);
        if (StringUtils.isEmpty(codeSnippetList)) {
            return;
        }

        Map<String, StringBuilder> snippetContentMap = new LinkedHashMap<String, StringBuilder>();
        for (SysCodeSnippet snippet : codeSnippetList) {
            if (StringUtils.isNotEmpty(snippet.getInsertPoint())) {
                snippetContentMap.putIfAbsent(snippet.getInsertPoint(), new StringBuilder());
            }
        }
        for (SysCodeSnippet snippet : codeSnippetList) {
            if (matchesSnippet(snippet, table, modules, templateFile)) {
                StringBuilder content = snippetContentMap.get(snippet.getInsertPoint());
                if (content.length() > 0) {
                    content.append(System.lineSeparator());
                }
                content.append(snippet.getSnippetContent());
            }
        }
        for (Map.Entry<String, StringBuilder> entry : snippetContentMap.entrySet()) {
            context.put(entry.getKey(), entry.getValue().toString());
        }
    }

    private boolean matchesSnippet(SysCodeSnippet snippet, GenTable table, List<SysProjectModule> modules, TemplateFile templateFile) {
        return snippet != null
                && StringUtils.isNotEmpty(snippet.getInsertPoint())
                && StringUtils.isNotEmpty(snippet.getSnippetContent())
                && isSnippetEnabled(snippet)
                && matchesSnippetModule(snippet, modules)
                && matchesSnippetTable(snippet, table)
                && matchesSnippetTemplate(snippet, templateFile);
    }

    private boolean isSnippetEnabled(SysCodeSnippet snippet) {
        return (StringUtils.isEmpty(snippet.getStatus()) || "0".equals(snippet.getStatus()))
                && (StringUtils.isEmpty(snippet.getDelFlag()) || "0".equals(snippet.getDelFlag()));
    }

    private boolean matchesSnippetModule(SysCodeSnippet snippet, List<SysProjectModule> modules) {
        if (snippet.getModuleId() == null) {
            return true;
        }
        if (StringUtils.isEmpty(modules)) {
            return false;
        }
        for (SysProjectModule module : modules) {
            if (snippet.getModuleId().equals(module.getModuleId())
                    && (StringUtils.isEmpty(module.getStatus()) || "0".equals(module.getStatus()))) {
                return true;
            }
        }
        return false;
    }

    private boolean matchesSnippetTable(SysCodeSnippet snippet, GenTable table) {
        return StringUtils.isEmpty(snippet.getRelationTableName())
                || StringUtils.equals(snippet.getRelationTableName(), table.getTableName());
    }

    private boolean matchesSnippetTemplate(SysCodeSnippet snippet, TemplateFile templateFile) {
        return StringUtils.isEmpty(snippet.getTemplateFileName())
                || matchesTemplateFile(templateFile, snippet.getTemplateFileName());
    }

    private void setPkColumn(GenTable table) {
        for (GenTableColumn column : table.getColumns()) {
            if (column.isPk()) {
                table.setPkColumn(column);
                break;
            }
        }
        if (StringUtils.isNull(table.getPkColumn())) {
            table.setPkColumn(table.getColumns().get(0));
        }
    }

    public void setSubTable(GenTable table) {
        String subTableName = table.getSubTableName();
        if (StringUtils.isNotEmpty(subTableName)) {
            table.setSubTable(genTableMapper.selectGenTableByName(subTableName));
        }
    }
}
