package com.ruoyi.generator.service;

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.alibaba.fastjson2.JSON;
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.GeneratedBusinessBlockFile;
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockConfigField;
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockDefinition;
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockInstance;
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockTemplateFile;
import com.ruoyi.generator.domain.front.dto.block.ChartMetricConfig;
import com.ruoyi.generator.domain.front.dto.block.ChartQueryParameter;
import com.ruoyi.generator.domain.front.dto.block.ChartQueryRenderModel;
import com.ruoyi.generator.domain.front.dto.block.PageBusinessBlockLayout;
import com.ruoyi.generator.service.front.BusinessBlockRegistryService;
import com.ruoyi.generator.util.BusinessActionRenderSupport;
import com.ruoyi.generator.util.ChartBlockRenderSupport;
import com.ruoyi.generator.util.VelocityInitializer;
import com.ruoyi.generator.util.VelocityUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BusinessBlockGenerationService
{
    private static final String PAGE_TEMPLATE_RESOURCE = "business-blocks/page/business-block-page.vue.vm";
    private static final String FRONTEND_PAGE_TEMPLATE_RESOURCE = "qing/index.vue.vm";
    private static final String BUSINESS_BLOCK_CATEGORY_PREFIX = "business-block:";
    private static final String PAGE_DESIGN_CATEGORY_PREFIX = "page-design:";
    private static final Pattern STATUS_VALUE_PATTERN = Pattern.compile(
            "([0-9]+)\\s*[=:：]?\\s*([^A-Za-z0-9_\\s,，;；()（）]+)");

    @Autowired
    private BusinessBlockRegistryService businessBlockRegistryService;

    @Autowired
    private ChartBlockRenderSupport chartBlockRenderSupport;

    public List<GeneratedBusinessBlockFile> listFiles(GenProject project, String templateType)
    {
        if (project == null || StringUtils.isEmpty(project.getPageDesigns()))
        {
            return Collections.emptyList();
        }
        List<GeneratedBusinessBlockFile> files = new ArrayList<GeneratedBusinessBlockFile>();
        Set<String> emittedSharedOutputs = new HashSet<String>();
        for (FrontProjectPageDesign pageDesign : project.getPageDesigns())
        {
            if (pageDesign == null)
            {
                continue;
            }
            boolean adminScope = "admin".equals(pageDesign.getPageScope());
            if ("frontend".equals(templateType) && adminScope)
            {
                continue;
            }
            if ("admin_frontend".equals(templateType) && !adminScope)
            {
                continue;
            }
            int index = 1;
            if (isBusinessBlockPage(pageDesign))
            {
                PageBusinessBlockLayout layout = registry().parseLayout(pageDesign.getLayoutJson());
                if (BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS.equals(layout.getCanvas()))
                {
                    if ("frontend".equals(templateType) || "admin_frontend".equals(templateType))
                    {
                        files.add(pageFile(pageDesign, templateType));
                    }
                    index = appendBlockFiles(files, emittedSharedOutputs, project, pageDesign,
                            safeBlocks(layout), templateType, index);
                }
            }
            else if ("frontend".equals(templateType) && isFrontendDesignedPage(pageDesign))
            {
                files.add(frontendPageFile(pageDesign));
            }
            appendBlockFiles(files, emittedSharedOutputs, project, pageDesign,
                    embeddedBlocks(pageDesign), templateType, index);
        }
        return files;
    }

    private int appendBlockFiles(List<GeneratedBusinessBlockFile> files, Set<String> emittedSharedOutputs,
            GenProject project,
            FrontProjectPageDesign pageDesign, List<BusinessBlockInstance> instances, String templateType, int startIndex)
    {
        int index = startIndex;
        for (BusinessBlockInstance instance : instances)
        {
            if (instance == null || StringUtils.isEmpty(instance.getBlockCode()))
            {
                continue;
            }
            BusinessBlockDefinition definition = registry().findDefinition(instance.getBlockCode());
            if (StringUtils.isEmpty(definition.getTemplates()))
            {
                continue;
            }
            if ("admin_frontend".equals(templateType) && isChartDefinition(definition))
            {
                addSharedChartRuntime(files, emittedSharedOutputs);
            }
            for (BusinessBlockTemplateFile templateFile : definition.getTemplates())
            {
                if (!templateMatches(templateType, templateFile))
                {
                    continue;
                }
                files.add(blockFile(project, pageDesign, definition, instance, templateFile, index));
            }
            index++;
        }
        return index;
    }

    private void addSharedChartRuntime(List<GeneratedBusinessBlockFile> files, Set<String> emittedSharedOutputs)
    {
        String outputPath = "src/utils/chartRuntime.js";
        if (!emittedSharedOutputs.add(outputPath))
        {
            return;
        }
        GeneratedBusinessBlockFile file = new GeneratedBusinessBlockFile();
        file.setCategory("business-block:admin_frontend:shared:chartRuntime");
        file.setTemplateType("admin_frontend");
        file.setOutputPath(outputPath);
        file.setTemplateResource("business-blocks/chart/templates/frontend/chartRuntime.js.vm");
        file.setTemplateKey("chartRuntime");
        files.add(file);
    }

    public GeneratedBusinessBlockFile findFile(GenProject project, String templateType, String keyword)
    {
        if (StringUtils.isEmpty(keyword))
        {
            return null;
        }
        String normalizedKeyword = normalizePath(keyword);
        for (GeneratedBusinessBlockFile file : listFiles(project, templateType))
        {
            if (StringUtils.equals(keyword, file.getCategory())
                    || StringUtils.equals(normalizedKeyword, normalizePath(file.getOutputPath()))
                    || StringUtils.equals(normalizedKeyword, fileName(file.getOutputPath())))
            {
                return file;
            }
        }
        return null;
    }

    public String renderFile(GenProject project, GeneratedBusinessBlockFile file)
    {
        if (file == null)
        {
            throw new ServiceException("涓氬姟鍧楁枃浠朵笉瀛樺湪");
        }
        if ("frontendPage".equals(file.getTemplateKey()))
        {
            return renderFrontendPageDesignFile(project, file);
        }
        try
        {
            VelocityInitializer.initVelocity();
            VelocityContext context = VelocityUtils.prepareContextProject(project);
            putFileContext(context, project, file);
            Template template = Velocity.getTemplate(file.getTemplateResource(), "UTF-8");
            StringWriter writer = new StringWriter();
            template.merge(context, writer);
            return writer.toString();
        }
        catch (RuntimeException e)
        {
            throw new ServiceException("渲染业务块模板失败: " + file.getCategory() + " - "
                    + e.getClass().getSimpleName() + ": " + StringUtils.defaultString(e.getMessage()));
        }
    }

    public boolean isBusinessBlockCategory(String category)
    {
        return StringUtils.isNotEmpty(category) && category.startsWith(BUSINESS_BLOCK_CATEGORY_PREFIX);
    }

    private String renderFrontendPageDesignFile(GenProject project, GeneratedBusinessBlockFile file)
    {
        FrontProjectPageDesign pageDesign = file.getPageDesign();
        GenTable table = findTable(project, pageDesign == null ? null : pageDesign.getTableName());
        if (table == null)
        {
            throw new ServiceException("页面设计绑定的数据表不存在: " + file.getPageCode());
        }
        if (project != null)
        {
            table.setPackageName(project.getPackageName());
        }
        if (StringUtils.isEmpty(table.getModuleName()))
        {
            table.setModuleName("app");
        }
        if (StringUtils.isEmpty(table.getBusinessName()))
        {
            table.setBusinessName(StringUtils.defaultIfEmpty(StringUtils.uncapitalize(table.getClassName()),
                    StringUtils.defaultString(table.getTableName())));
        }
        if (StringUtils.isEmpty(table.getFunctionName()))
        {
            table.setFunctionName(StringUtils.defaultIfEmpty(table.getTableComment(), table.getClassName()));
        }
        VelocityInitializer.initVelocity();
        VelocityContext context = VelocityUtils.prepareContext(table);
        VelocityUtils.putPageDesignVelocityContext(context, project, table, "frontend", file.getPageCode());
        putEmbeddedBlockVelocityContext(context, project, pageDesign);
        putPageDesignBusinessActions(context, project);
        Template template = Velocity.getTemplate(file.getTemplateResource(), "UTF-8");
        StringWriter writer = new StringWriter();
        template.merge(context, writer);
        return writer.toString();
    }

    private void putPageDesignBusinessActions(VelocityContext context, GenProject project)
    {
        List<BusinessActionDesign> candidates = project == null || StringUtils.isEmpty(project.getBusinessActions())
                ? Collections.<BusinessActionDesign>emptyList() : project.getBusinessActions();
        FrontProjectPageDesign currentPageDesign = context.get("currentPageDesign") instanceof FrontProjectPageDesign
                ? (FrontProjectPageDesign) context.get("currentPageDesign") : null;
        List<BusinessActionDesign> toolbarBusinessActions = filterPageDesignBusinessActions(
                candidates, context.get("pageDesignToolbarActionCodes"), currentPageDesign, "primary",
                project == null ? null : project.getTables());
        List<BusinessActionDesign> rowBusinessActions = filterPageDesignBusinessActions(
                candidates, context.get("pageDesignRowActionCodes"), currentPageDesign, "text",
                project == null ? null : project.getTables());
        List<BusinessActionDesign> allActions = 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", allActions);
        context.put("hasPageDesignBusinessActions", StringUtils.isNotEmpty(allActions));
    }

    @SuppressWarnings("unchecked")
    private List<BusinessActionDesign> filterPageDesignBusinessActions(List<BusinessActionDesign> candidates,
            Object actionCodesObject, FrontProjectPageDesign pageDesign, String fallbackButtonType,
            List<GenTable> projectTables)
    {
        if (StringUtils.isEmpty(candidates) || !(actionCodesObject instanceof List))
        {
            return Collections.emptyList();
        }
        List<String> requestedCodes = new ArrayList<String>();
        for (Object actionCode : (List<Object>) actionCodesObject)
        {
            String code = StringUtils.defaultString(String.valueOf(actionCode));
            if (code.startsWith("business:"))
            {
                requestedCodes.add(code.substring("business:".length()));
            }
        }
        if (requestedCodes.isEmpty())
        {
            return Collections.emptyList();
        }
        List<BusinessActionDesign> result = new ArrayList<BusinessActionDesign>();
        Set<String> addedCodes = new HashSet<String>();
        for (String requestedCode : requestedCodes)
        {
            for (BusinessActionDesign candidate : candidates)
            {
                if (candidate != null && StringUtils.equals(requestedCode, candidate.getCode())
                        && addedCodes.add(candidate.getCode()))
                {
                    result.add(applyBusinessButtonConfig(candidate, pageDesign, fallbackButtonType, projectTables));
                }
            }
        }
        return result;
    }

    private BusinessActionDesign applyBusinessButtonConfig(BusinessActionDesign source, FrontProjectPageDesign pageDesign,
            String fallbackButtonType, List<GenTable> projectTables)
    {
        BusinessActionDesign action = copyBusinessAction(source);
        Map<String, Object> config = businessButtonConfig(pageDesign, action.getCode());
        action.setButtonLabel(stringConfig(config, "label", action.getName()));
        action.setButtonType(stringConfig(config, "type", fallbackButtonType));
        action.setButtonIcon(stringConfig(config, "icon", "el-icon-position"));
        action.setConfirmMessage(stringConfig(config, "confirmMessage", ""));
        action.setSuccessMessage(stringConfig(config, "successMessage",
                StringUtils.defaultIfEmpty(action.getName(), action.getCode()) + "成功"));
        action.setRefreshTarget(stringConfig(config, "refreshTarget", "list"));
        BusinessActionRenderSupport.enrich(action, projectTables);
        return action;
    }

    private BusinessActionDesign copyBusinessAction(BusinessActionDesign source)
    {
        BusinessActionDesign copy = new BusinessActionDesign();
        copy.setCode(source.getCode());
        copy.setName(source.getName());
        copy.setOwnerTable(source.getOwnerTable());
        copy.setMethod(source.getMethod());
        copy.setPath(source.getPath());
        copy.setTransaction(source.getTransaction());
        copy.setRequestFields(source.getRequestFields());
        copy.setRules(source.getRules());
        copy.setRuleChecks(source.getRuleChecks());
        copy.setEffects(source.getEffects());
        copy.setUiBindings(source.getUiBindings());
        copy.setCurrentUserFields(source.getCurrentUserFields());
        copy.setInputFields(source.getInputFields());
        return copy;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> businessButtonConfig(FrontProjectPageDesign pageDesign, String actionCode)
    {
        if (pageDesign == null || pageDesign.getLayout() == null || StringUtils.isEmpty(actionCode))
        {
            return Collections.emptyMap();
        }
        Object configs = pageDesign.getLayout().get("businessButtons");
        if (!(configs instanceof Map))
        {
            return Collections.emptyMap();
        }
        Object config = ((Map<String, Object>) configs).get(actionCode);
        return config instanceof Map ? (Map<String, Object>) config : Collections.<String, Object>emptyMap();
    }

    private String stringConfig(Map<String, Object> config, String key, String fallback)
    {
        if (config == null)
        {
            return fallback;
        }
        Object value = config.get(key);
        return value == null || StringUtils.isEmpty(String.valueOf(value)) ? fallback : String.valueOf(value);
    }

    private List<BusinessActionDesign> mergeBusinessActions(List<BusinessActionDesign> first,
            List<BusinessActionDesign> second)
    {
        List<BusinessActionDesign> result = new ArrayList<BusinessActionDesign>();
        if (first != null)
        {
            result.addAll(first);
        }
        if (second != null)
        {
            for (BusinessActionDesign action : second)
            {
                if (action != null && !result.contains(action))
                {
                    result.add(action);
                }
            }
        }
        return result;
    }

    private GeneratedBusinessBlockFile pageFile(FrontProjectPageDesign pageDesign)
    {
        return pageFile(pageDesign, "admin".equals(pageDesign == null ? null : pageDesign.getPageScope())
                ? "admin_frontend" : "frontend");
    }

    private GeneratedBusinessBlockFile pageFile(FrontProjectPageDesign pageDesign, String templateType)
    {
        GeneratedBusinessBlockFile file = new GeneratedBusinessBlockFile();
        String pageCode = safePageCode(pageDesign);
        file.setCategory(BUSINESS_BLOCK_CATEGORY_PREFIX + templateType + ":" + pageCode + ":page");
        file.setTemplateType(templateType);
        file.setOutputPath("src/views/pages/" + pageCode + "/index.vue");
        file.setTemplateResource(PAGE_TEMPLATE_RESOURCE);
        file.setPageCode(pageCode);
        file.setTemplateKey("page");
        file.setPageDesign(pageDesign);
        return file;
    }

    private GeneratedBusinessBlockFile frontendPageFile(FrontProjectPageDesign pageDesign)
    {
        GeneratedBusinessBlockFile file = new GeneratedBusinessBlockFile();
        String pageCode = safePageCode(pageDesign);
        file.setCategory(PAGE_DESIGN_CATEGORY_PREFIX + "frontend:" + pageCode + ":page");
        file.setTemplateType("frontend");
        file.setOutputPath("src/views/pages/" + pageCode + "/index.vue");
        file.setTemplateResource(FRONTEND_PAGE_TEMPLATE_RESOURCE);
        file.setPageCode(pageCode);
        file.setTemplateKey("frontendPage");
        file.setPageDesign(pageDesign);
        return file;
    }

    private GeneratedBusinessBlockFile blockFile(GenProject project, FrontProjectPageDesign pageDesign,
            BusinessBlockDefinition definition, BusinessBlockInstance instance, BusinessBlockTemplateFile templateFile,
            int index)
    {
        String pageCode = safePageCode(pageDesign);
        String blockCode = StringUtils.defaultString(instance.getBlockCode());
        String instanceCode = instanceCode(instance, blockCode, index);
        String instanceClassName = toPascalCase(instanceCode) + "Block";
        String fileTemplateType = templateType(templateFile);
        GeneratedBusinessBlockFile file = new GeneratedBusinessBlockFile();
        file.setCategory(BUSINESS_BLOCK_CATEGORY_PREFIX + fileTemplateType + ":" + pageCode + ":" + instanceCode
                + ":" + StringUtils.defaultString(templateFile.getKey()));
        file.setTemplateType(fileTemplateType);
        file.setOutputPath(resolveOutputPath(project, pageCode, blockCode, instanceCode, instanceClassName,
                templateFile.getOutputPath()));
        file.setTemplateResource(templateResource(blockCode, templateFile.getTemplate()));
        file.setPageCode(pageCode);
        file.setBlockCode(blockCode);
        file.setInstanceCode(instanceCode);
        file.setInstanceClassName(instanceClassName);
        file.setTemplateKey(templateFile.getKey());
        file.setPageDesign(pageDesign);
        file.setDefinition(definition);
        file.setInstance(instance);
        file.setTemplateFile(templateFile);
        return file;
    }

    private void putFileContext(VelocityContext context, GenProject project, GeneratedBusinessBlockFile file)
    {
        FrontProjectPageDesign pageDesign = file.getPageDesign();
        context.put("pageDesign", pageDesign);
        context.put("pageCode", file.getPageCode());
        context.put("pageName", pageDesign == null ? "" : StringUtils.defaultString(pageDesign.getPageName()));
        context.put("routePath", pageDesign == null ? "" : StringUtils.defaultString(pageDesign.getRoutePath()));
        context.put("pageComponentName", toPascalCase(file.getPageCode()));
        context.put("businessBlocks", pageBlocks(project, pageDesign));

        BusinessBlockInstance instance = file.getInstance();
        if (instance == null)
        {
            return;
        }
        Map<String, String> config = instance.getConfig() == null
                ? Collections.<String, String>emptyMap() : instance.getConfig();
        Map<String, String> renderConfig = renderConfig(project, file.getDefinition(), config);
        context.put("definition", file.getDefinition());
        context.put("instance", instance);
        context.put("config", renderConfig);
        context.put("rawConfig", config);
        context.put("blockCode", file.getBlockCode());
        context.put("instanceCode", file.getInstanceCode());
        context.put("InstanceClassName", file.getInstanceClassName());
        context.put("instanceClassName", StringUtils.uncapitalize(file.getInstanceClassName()));
        context.put("apiBasePath", "/business-blocks/" + file.getInstanceCode());
        context.put("apiModulePath", "@/api/business-blocks/" + file.getInstanceCode());
        if (isChartDefinition(file.getDefinition()))
        {
            ChartQueryRenderModel chartQuery = chartRenderSupport().build(
                    requireTable(project, instance.getDataset().getTable()), file.getDefinition(), instance);
            context.put("chartQuery", chartQuery);
            context.put("chartType", file.getDefinition().getChartType());
            context.put("chartDataset", instance.getDataset());
            context.put("chartDisplay", instance.getDisplay());
            context.put("chartDisplayJson", javascriptJson(instance.getDisplay() == null
                    ? Collections.<String, Object>emptyMap() : instance.getDisplay()));
            context.put("chartInstanceNameJson", javascriptJson(
                    StringUtils.defaultIfEmpty(instance.getName(), file.getDefinition().getName())));
            context.put("chartMetrics", chartMetricRenderModels(chartQuery.getMetrics()));
            context.put("chartDateStartLiteral", ChartQueryParameter.toJavaLiteral(
                    chartQuery.getDateRange() == null ? null : chartQuery.getDateRange().getStart()));
            context.put("chartDateEndLiteral", ChartQueryParameter.toJavaLiteral(
                    chartQuery.getDateRange() == null ? null : chartQuery.getDateRange().getEnd()));
            context.put("apiBasePath", "/admin/business-blocks/" + file.getInstanceCode());
        }
        context.put("cartPkColumn", pkColumnName(project, renderConfig.get("cartTable")));
        context.put("noticePkColumn", pkColumnName(project, renderConfig.get("noticeTable")));
        context.put("carouselPkColumn", pkColumnName(project, renderConfig.get("carouselTable")));
        context.put("orderPkColumn", pkColumnName(project, renderConfig.get("orderTable")));
        context.put("orderItemPkColumn", pkColumnName(project, renderConfig.get("orderItemTable")));
        context.put("productPkColumn", pkColumnName(project, renderConfig.get("productTable")));
        context.put("masterPkColumn", pkColumnName(project, renderConfig.get("masterTable")));
        context.put("detailPkColumn", pkColumnName(project, renderConfig.get("detailTable")));
        context.put("hasProductImage", StringUtils.isNotEmpty(renderConfig.get("productImage")));
        context.put("hasStock", StringUtils.isNotEmpty(renderConfig.get("stock")));
        context.put("hasNoticeStatus", StringUtils.isNotEmpty(renderConfig.get("status")));
        context.put("noticeStatusActiveValue", activeStatusValue(project, renderConfig.get("noticeTable"),
                renderConfig.get("status"), "0"));
        context.put("hasNoticePublishTime", StringUtils.isNotEmpty(renderConfig.get("publishTime")));
        context.put("hasCarouselLink", StringUtils.isNotEmpty(renderConfig.get("link")));
        context.put("hasCarouselSort", StringUtils.isNotEmpty(renderConfig.get("sort")));
        context.put("hasCarouselStatus", StringUtils.isNotEmpty(renderConfig.get("status")));
        context.put("hasOrderCreateTime", StringUtils.isNotEmpty(renderConfig.get("orderCreateTime")));
        context.put("hasOrderProductImage", StringUtils.isNotEmpty(renderConfig.get("productImage")));
        context.put("hasMasterDescription", StringUtils.isNotEmpty(renderConfig.get("masterDescription")));
        context.put("hasDetailValue", StringUtils.isNotEmpty(renderConfig.get("detailValue")));
        context.put("hasDetailTime", StringUtils.isNotEmpty(renderConfig.get("detailTime")));
        for (Map.Entry<String, String> entry : renderConfig.entrySet())
        {
            context.put(entry.getKey(), entry.getValue());
        }
    }

    private Map<String, String> renderConfig(GenProject project, BusinessBlockDefinition definition,
            Map<String, String> config)
    {
        Map<String, String> renderConfig = new LinkedHashMap<String, String>();
        if (config != null)
        {
            renderConfig.putAll(config);
        }
        List<BusinessBlockConfigField> fields = definition == null || definition.getConfigs() == null
                ? Collections.<BusinessBlockConfigField>emptyList() : definition.getConfigs();
        for (BusinessBlockConfigField field : fields)
        {
            if (field == null || !"field".equals(field.getType()))
            {
                continue;
            }
            String value = config == null ? null : config.get(field.getKey());
            if (StringUtils.isEmpty(value))
            {
                continue;
            }
            String tableName = config == null ? null : config.get(field.getTable());
            renderConfig.put(field.getKey(), columnName(project, tableName, value));
        }
        return renderConfig;
    }

    private String javascriptJson(Object value)
    {
        return JSON.toJSONString(value)
                .replace("<", "\\u003c")
                .replace(">", "\\u003e")
                .replace("&", "\\u0026");
    }

    private List<Map<String, String>> chartMetricRenderModels(List<ChartMetricConfig> metrics)
    {
        List<Map<String, String>> result = new ArrayList<Map<String, String>>();
        for (ChartMetricConfig metric : metrics == null
                ? Collections.<ChartMetricConfig>emptyList() : metrics)
        {
            Map<String, String> item = new LinkedHashMap<String, String>();
            item.put("alias", metric.getAlias());
            item.put("aliasLiteral", ChartQueryParameter.toJavaLiteral(metric.getAlias()));
            item.put("labelLiteral", ChartQueryParameter.toJavaLiteral(
                    StringUtils.defaultIfEmpty(metric.getLabel(), metric.getAlias())));
            result.add(item);
        }
        return result;
    }

    public void putEmbeddedBlockVelocityContext(VelocityContext context, GenProject project,
            FrontProjectPageDesign pageDesign)
    {
        List<Map<String, Object>> topBlocks = embeddedPageBlocks(project, pageDesign, "top");
        List<Map<String, Object>> bottomBlocks = embeddedPageBlocks(project, pageDesign, "bottom");
        List<Map<String, Object>> allBlocks = new ArrayList<Map<String, Object>>();
        allBlocks.addAll(topBlocks);
        allBlocks.addAll(bottomBlocks);
        context.put("pageDesignTopBusinessBlocks", topBlocks);
        context.put("pageDesignBottomBusinessBlocks", bottomBlocks);
        context.put("pageDesignEmbeddedBusinessBlocks", allBlocks);
        context.put("hasPageDesignTopBusinessBlocks", StringUtils.isNotEmpty(topBlocks));
        context.put("hasPageDesignBottomBusinessBlocks", StringUtils.isNotEmpty(bottomBlocks));
        context.put("hasPageDesignEmbeddedBusinessBlocks", StringUtils.isNotEmpty(allBlocks));
    }

    private List<Map<String, Object>> pageBlocks(GenProject project, FrontProjectPageDesign pageDesign)
    {
        if (pageDesign == null)
        {
            return Collections.emptyList();
        }
        PageBusinessBlockLayout layout = registry().parseLayout(pageDesign.getLayoutJson());
        GeneratedBusinessBlockFile pageFile = pageFile(pageDesign);
        return blockViewModels(project, pageDesign, safeBlocks(layout), parentPath(pageFile.getOutputPath()), false);
    }

    private List<Map<String, Object>> embeddedPageBlocks(GenProject project, FrontProjectPageDesign pageDesign,
            String zone)
    {
        Map<String, List<BusinessBlockInstance>> zones = registry().parseEmbeddedBlockZones(
                pageDesign == null ? null : pageDesign.getLayoutJson());
        List<BusinessBlockInstance> instances = zones.get(zone);
        return blockViewModels(project, pageDesign, instances, null, true);
    }

    private List<Map<String, Object>> blockViewModels(GenProject project, FrontProjectPageDesign pageDesign,
            List<BusinessBlockInstance> instances, String pageDir, boolean useVueAlias)
    {
        List<Map<String, Object>> blocks = new ArrayList<Map<String, Object>>();
        int index = 1;
        for (BusinessBlockInstance instance : instances == null
                ? Collections.<BusinessBlockInstance>emptyList() : instances)
        {
            if (instance == null || StringUtils.isEmpty(instance.getBlockCode()))
            {
                continue;
            }
            BusinessBlockDefinition definition = registry().findDefinition(instance.getBlockCode());
            BusinessBlockTemplateFile templateFile = frontendComponentTemplate(definition);
            if (templateFile == null)
            {
                continue;
            }
            GeneratedBusinessBlockFile blockFile = blockFile(project, pageDesign, definition, instance, templateFile, index);
            Map<String, Object> block = new LinkedHashMap<String, Object>();
            block.put("componentName", blockFile.getInstanceClassName());
            block.put("relativeImportPath", useVueAlias
                    ? vueAliasImportPath(blockFile.getOutputPath())
                    : relativeImportPath(pageDir, blockFile.getOutputPath()));
            block.put("instanceCode", blockFile.getInstanceCode());
            block.put("blockCode", blockFile.getBlockCode());
            block.put("name", StringUtils.defaultIfEmpty(instance.getName(), definition.getName()));
            block.put("span", instance.getSpan() == null ? Integer.valueOf(12) : instance.getSpan());
            blocks.add(block);
            index++;
        }
        return blocks;
    }

    private BusinessBlockTemplateFile frontendComponentTemplate(BusinessBlockDefinition definition)
    {
        if (definition == null || StringUtils.isEmpty(definition.getTemplates()))
        {
            return null;
        }
        for (BusinessBlockTemplateFile templateFile : definition.getTemplates())
        {
            if ("frontendComponent".equals(templateFile.getKey()))
            {
                return templateFile;
            }
        }
        for (BusinessBlockTemplateFile templateFile : definition.getTemplates())
        {
            if ("frontend".equals(templateType(templateFile)) && normalizePath(templateFile.getOutputPath()).endsWith(".vue"))
            {
                return templateFile;
            }
        }
        return null;
    }

    private String resolveOutputPath(GenProject project, String pageCode, String blockCode, String instanceCode,
            String instanceClassName, String outputPath)
    {
        String path = normalizePath(outputPath);
        String packageName = project == null ? "" : StringUtils.defaultString(project.getPackageName());
        path = path.replace("${pageCode}", pageCode);
        path = path.replace("${blockCode}", blockCode);
        path = path.replace("${instanceCode}", instanceCode);
        path = path.replace("${InstanceClassName}", instanceClassName);
        path = path.replace("${instanceClassName}", StringUtils.uncapitalize(instanceClassName));
        path = path.replace("${packageName}", packageName);
        path = path.replace("${packagePath}", packageName.replace('.', '/'));
        while (path.startsWith("/"))
        {
            path = path.substring(1);
        }
        return path;
    }

    private String templateType(BusinessBlockTemplateFile templateFile)
    {
        String template = normalizePath(templateFile == null ? null : templateFile.getTemplate());
        String outputPath = normalizePath(templateFile == null ? null : templateFile.getOutputPath());
        if (template.contains("/frontend/") || outputPath.startsWith("src/views/") || outputPath.startsWith("src/api/"))
        {
            return "frontend";
        }
        return "backend";
    }

    private boolean templateMatches(String requestedType, BusinessBlockTemplateFile templateFile)
    {
        String fileType = templateType(templateFile);
        if ("admin_frontend".equals(requestedType))
        {
            return "frontend".equals(fileType);
        }
        return StringUtils.equals(requestedType, fileType);
    }

    private String templateResource(String blockCode, String template)
    {
        String normalized = normalizePath(template);
        if (normalized.startsWith("/"))
        {
            return normalized.substring(1);
        }
        return "business-blocks/" + blockCode + "/" + normalized;
    }

    private boolean isChartDefinition(BusinessBlockDefinition definition)
    {
        return definition != null && "chart".equals(definition.getKind());
    }

    private boolean isFrontendBusinessBlockPage(FrontProjectPageDesign pageDesign)
    {
        if (pageDesign == null || "admin".equals(pageDesign.getPageScope()))
        {
            return false;
        }
        return isBusinessBlockPage(pageDesign);
    }

    private boolean isBusinessBlockPage(FrontProjectPageDesign pageDesign)
    {
        if (pageDesign == null)
        {
            return false;
        }
        if ("business_block".equals(pageDesign.getPageType()))
        {
            return true;
        }
        return StringUtils.isNotEmpty(pageDesign.getLayoutJson())
                && pageDesign.getLayoutJson().contains(BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS);
    }

    private boolean isFrontendDesignedPage(FrontProjectPageDesign pageDesign)
    {
        return pageDesign != null && !"admin".equals(pageDesign.getPageScope())
                && StringUtils.isNotEmpty(pageDesign.getPageCode())
                && StringUtils.isNotEmpty(pageDesign.getTableName());
    }

    private List<BusinessBlockInstance> safeBlocks(PageBusinessBlockLayout layout)
    {
        return layout == null || layout.getBlocks() == null
                ? Collections.<BusinessBlockInstance>emptyList() : layout.getBlocks();
    }

    private List<BusinessBlockInstance> embeddedBlocks(FrontProjectPageDesign pageDesign)
    {
        Map<String, List<BusinessBlockInstance>> zones = registry().parseEmbeddedBlockZones(
                pageDesign == null ? null : pageDesign.getLayoutJson());
        List<BusinessBlockInstance> blocks = new ArrayList<BusinessBlockInstance>();
        for (List<BusinessBlockInstance> zoneBlocks : zones.values())
        {
            if (zoneBlocks != null)
            {
                blocks.addAll(zoneBlocks);
            }
        }
        return blocks;
    }

    private String safePageCode(FrontProjectPageDesign pageDesign)
    {
        return sanitizeIdentifier(pageDesign == null ? "business_page" : pageDesign.getPageCode(), "business_page");
    }

    private String instanceCode(BusinessBlockInstance instance, String blockCode, int index)
    {
        String raw = instance == null ? null : instance.getId();
        if (StringUtils.isEmpty(raw))
        {
            raw = StringUtils.defaultIfEmpty(blockCode, "block") + "_" + index;
        }
        return sanitizeIdentifier(raw, "block_" + index);
    }

    private String sanitizeIdentifier(String value, String fallback)
    {
        String normalized = StringUtils.defaultString(value).replaceAll("[^A-Za-z0-9_-]", "_");
        return StringUtils.isEmpty(normalized) ? fallback : normalized;
    }

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

    private String pkColumnName(GenProject project, String tableName)
    {
        GenTable table = findTable(project, tableName);
        if (table == null)
        {
            return "id";
        }
        if (table.getPkColumn() != null && StringUtils.isNotEmpty(table.getPkColumn().getColumnName()))
        {
            return table.getPkColumn().getColumnName();
        }
        if (StringUtils.isNotEmpty(table.getColumns()))
        {
            for (GenTableColumn column : table.getColumns())
            {
                if (column != null && "1".equals(column.getIsPk()) && StringUtils.isNotEmpty(column.getColumnName()))
                {
                    return column.getColumnName();
                }
            }
            for (GenTableColumn column : table.getColumns())
            {
                if (column != null && "id".equals(column.getColumnName()))
                {
                    return "id";
                }
            }
        }
        return "id";
    }

    private String columnName(GenProject project, String tableName, String fieldName)
    {
        GenTable table = findTable(project, tableName);
        if (table == null || StringUtils.isEmpty(table.getColumns()) || StringUtils.isEmpty(fieldName))
        {
            return fieldName;
        }
        for (GenTableColumn column : table.getColumns())
        {
            if (column == null)
            {
                continue;
            }
            if (StringUtils.equals(fieldName, column.getColumnName())
                    || StringUtils.equals(fieldName, column.getJavaField()))
            {
                return StringUtils.defaultIfEmpty(column.getColumnName(), fieldName);
            }
        }
        return fieldName;
    }

    private String activeStatusValue(GenProject project, String tableName, String statusColumnName, String defaultValue)
    {
        GenTableColumn column = findColumn(project, tableName, statusColumnName);
        if (column == null || StringUtils.isEmpty(column.getColumnComment()))
        {
            return defaultValue;
        }
        Matcher matcher = STATUS_VALUE_PATTERN.matcher(column.getColumnComment());
        while (matcher.find())
        {
            String value = matcher.group(1);
            String label = matcher.group(2);
            if (isActiveStatusLabel(label) && !isInactiveStatusLabel(label))
            {
                return value;
            }
        }
        return defaultValue;
    }

    private boolean isActiveStatusLabel(String label)
    {
        return StringUtils.containsAny(label, "发布", "启用", "上架", "正常", "可用", "有效", "显示");
    }

    private boolean isInactiveStatusLabel(String label)
    {
        return StringUtils.containsAny(label, "未发布", "草稿", "下架", "关闭", "禁用", "停用", "暂停", "删除", "失效",
                "不显示");
    }

    private GenTableColumn findColumn(GenProject project, String tableName, String columnName)
    {
        GenTable table = findTable(project, tableName);
        if (table == null || StringUtils.isEmpty(table.getColumns()) || StringUtils.isEmpty(columnName))
        {
            return null;
        }
        for (GenTableColumn column : table.getColumns())
        {
            if (column == null)
            {
                continue;
            }
            if (StringUtils.equals(columnName, column.getColumnName())
                    || StringUtils.equals(columnName, column.getJavaField()))
            {
                return column;
            }
        }
        return null;
    }

    private GenTable findTable(GenProject project, String tableName)
    {
        if (project == null || StringUtils.isEmpty(project.getTables()) || StringUtils.isEmpty(tableName))
        {
            return null;
        }
        for (GenTable table : project.getTables())
        {
            if (table != null && StringUtils.equals(tableName, table.getTableName()))
            {
                return table;
            }
        }
        return null;
    }

    private GenTable requireTable(GenProject project, String tableName)
    {
        GenTable table = findTable(project, tableName);
        if (table == null)
        {
            throw new ServiceException("图表数据表不存在：" + StringUtils.defaultString(tableName));
        }
        return table;
    }

    private String relativeImportPath(String fromDir, String targetPath)
    {
        String[] from = normalizePath(fromDir).split("/");
        String[] target = normalizePath(targetPath).split("/");
        int common = 0;
        while (common < from.length && common < target.length && from[common].equals(target[common]))
        {
            common++;
        }
        StringBuilder result = new StringBuilder();
        for (int i = common; i < from.length; i++)
        {
            if (from[i].length() > 0)
            {
                result.append("../");
            }
        }
        for (int i = common; i < target.length; i++)
        {
            if (target[i].length() == 0)
            {
                continue;
            }
            if (result.length() > 0 && !result.toString().endsWith("/"))
            {
                result.append("/");
            }
            result.append(target[i]);
        }
        if (result.length() == 0)
        {
            return "./";
        }
        if (!result.toString().startsWith("."))
        {
            result.insert(0, "./");
        }
        return result.toString();
    }

    private String vueAliasImportPath(String targetPath)
    {
        String normalized = normalizePath(targetPath);
        int index = normalized.indexOf("src/");
        if (index >= 0)
        {
            normalized = normalized.substring(index + 4);
        }
        return normalized.startsWith("@/") ? normalized : "@/" + normalized;
    }

    private String parentPath(String path)
    {
        String normalized = normalizePath(path);
        int index = normalized.lastIndexOf('/');
        return index < 0 ? "" : normalized.substring(0, index);
    }

    private String fileName(String path)
    {
        String normalized = normalizePath(path);
        int index = normalized.lastIndexOf('/');
        return index < 0 ? normalized : normalized.substring(index + 1);
    }

    private String normalizePath(String path)
    {
        return StringUtils.defaultString(path).replace('\\', '/').replaceAll("/{2,}", "/");
    }

    private BusinessBlockRegistryService registry()
    {
        if (businessBlockRegistryService == null)
        {
            businessBlockRegistryService = new BusinessBlockRegistryService();
        }
        return businessBlockRegistryService;
    }

    private ChartBlockRenderSupport chartRenderSupport()
    {
        if (chartBlockRenderSupport == null)
        {
            chartBlockRenderSupport = new ChartBlockRenderSupport();
        }
        return chartBlockRenderSupport;
    }
}
