package com.ruoyi.generator.service.front;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.GenTable;
import com.ruoyi.generator.domain.GenTableColumn;
import com.ruoyi.generator.domain.front.FrontProject;
import com.ruoyi.generator.domain.front.FrontProjectColumn;
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
import com.ruoyi.generator.domain.front.FrontProjectTable;
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.AppMenuDesign;
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
import com.ruoyi.generator.domain.front.dto.FrontendPageDesignInitResponse;
import com.ruoyi.generator.domain.front.dto.FrontendPageDesignRequest;
import com.ruoyi.generator.domain.front.dto.FrontendPageDesignResponse;
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
import com.ruoyi.generator.mapper.front.FrontProjectPageDesignMapper;
import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;

@Service
public class FrontendPageDesignService
{
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final String MENU_DIRECTORY_PAGE_TYPE = "menu_directory";
    private static final String HOME_PAGE_TYPE = "business_block";
    private static final String HOME_PAGE_NAME = "\u4e3b\u9875";
    private static final String FRONTEND_HOME_PAGE_CODE = "home";
    private static final String ADMIN_HOME_PAGE_CODE = "admin_home";
    private static final Set<String> PAGE_TYPES = new HashSet<String>(
            Arrays.asList("list", "detail", "form", "current_user_list", "business_block", MENU_DIRECTORY_PAGE_TYPE));
    private static final Set<String> REGION_TYPES = new HashSet<String>(
            Arrays.asList("query", "toolbar", "table", "dialogForm", "detail", "form", "cardList", "actions"));
    private static final Set<String> BUILTIN_ACTIONS = new HashSet<String>(
            Arrays.asList("create", "batchDelete", "view", "edit", "delete", "export"));
    private static final Map<String, Set<String>> LAYOUT_CANVASES_BY_PAGE_TYPE = layoutCanvasesByPageType();
    private static final String FRONTEND_SCOPE = "frontend";
    private static final String ADMIN_SCOPE = "admin";

    @Autowired
    private IFrontProjectService frontProjectService;
    @Autowired
    private FrontProjectPageDesignMapper pageDesignMapper;
    @Autowired
    private FrontProjectTableMapper frontProjectTableMapper;
    @Autowired
    private FrontProjectColumnMapper frontProjectColumnMapper;
    @Autowired
    private BusinessBlockRegistryService businessBlockRegistryService;

    public List<FrontendPageDesignResponse> listPageDesigns(Long userId, Long projectId)
    {
        return listPageDesigns(userId, projectId, FRONTEND_SCOPE);
    }

    public List<FrontendPageDesignResponse> listPageDesigns(Long userId, Long projectId, String scope)
    {
        frontProjectService.getProject(userId, projectId);
        String normalizedScope = normalizeScope(scope);
        return toScopedResponses(pageDesignMapper.selectDesignsByProjectIdAndScope(projectId, normalizedScope), normalizedScope);
    }

    public FrontendPageDesignResponse getPageDesign(Long userId, Long projectId, Long designId)
    {
        return getPageDesign(userId, projectId, designId, FRONTEND_SCOPE);
    }

    public FrontendPageDesignResponse getPageDesign(Long userId, Long projectId, Long designId, String scope)
    {
        frontProjectService.getProject(userId, projectId);
        return FrontendPageDesignResponse.from(requireDesign(projectId, designId, normalizeScope(scope)));
    }

    @Transactional
    public FrontendPageDesignInitResponse initializePageDesigns(Long userId, Long projectId)
    {
        return initializePageDesigns(userId, projectId, FRONTEND_SCOPE);
    }

    @Transactional
    public FrontendPageDesignInitResponse initializePageDesigns(Long userId, Long projectId, String scope)
    {
        String normalizedScope = normalizeScope(scope);
        if (ADMIN_SCOPE.equals(normalizedScope))
        {
            return initializeAdminPageDesigns(userId, projectId);
        }
        return initializeFrontendPageDesigns(userId, projectId);
    }

    private FrontendPageDesignInitResponse initializeFrontendPageDesigns(Long userId, Long projectId)
    {
        FrontProject project = frontProjectService.getProject(userId, projectId);
        AppBlueprintDesign blueprint = readAppBlueprint(project.getAppBlueprint());
        if (blueprint.getFrontendPages() == null || blueprint.getFrontendPages().isEmpty())
        {
            throw new ServiceException("请先生成并确认前台页面蓝图");
        }

        List<FrontProjectPageDesign> existing = safeDesigns(pageDesignMapper.selectDesignsByProjectIdAndScope(projectId, FRONTEND_SCOPE));
        Map<String, FrontProjectPageDesign> existingByPageCode = new LinkedHashMap<String, FrontProjectPageDesign>();
        for (FrontProjectPageDesign design : existing)
        {
            if (shouldExposeDesign(design, FRONTEND_SCOPE) && StringUtils.isNotEmpty(design.getPageCode()))
            {
                existingByPageCode.put(design.getPageCode(), design);
            }
        }

        Map<String, TableBundle> tablesByName = loadTables(projectId);
        List<FrontProjectPageDesign> merged = new ArrayList<FrontProjectPageDesign>();
        Set<String> blueprintPageCodes = new LinkedHashSet<String>();
        FrontProjectPageDesign homeDesign = getOrCreateHomeDesign(projectId, userId, FRONTEND_SCOPE, existingByPageCode);
        merged.add(homeDesign);
        blueprintPageCodes.add(homeDesign.getPageCode());
        for (AppPageDesign page : blueprint.getFrontendPages())
        {
            if (page == null || StringUtils.isEmpty(page.getCode()) || isAdminLikePage(page))
            {
                continue;
            }
            blueprintPageCodes.add(page.getCode());
            if (isHomePageCode(page.getCode(), FRONTEND_SCOPE))
            {
                continue;
            }
            FrontProjectPageDesign design = existingByPageCode.get(page.getCode());
            if (design == null)
            {
                design = newDefaultDesign(projectId, userId, FRONTEND_SCOPE, page, tablesByName.get(page.getTableName()));
                pageDesignMapper.insertFrontProjectPageDesign(design);
            }
            merged.add(design);
        }

        List<FrontendPageDesignResponse> detached = new ArrayList<FrontendPageDesignResponse>();
        for (FrontProjectPageDesign design : existing)
        {
            if (shouldExposeDesign(design, FRONTEND_SCOPE) && !blueprintPageCodes.contains(design.getPageCode()))
            {
                detached.add(FrontendPageDesignResponse.from(design));
            }
        }

        FrontendPageDesignInitResponse response = new FrontendPageDesignInitResponse();
        response.setDesigns(toResponses(merged));
        response.setDetachedDesigns(detached);
        return response;
    }

    private FrontendPageDesignInitResponse initializeAdminPageDesigns(Long userId, Long projectId)
    {
        FrontProject project = frontProjectService.getProject(userId, projectId);
        AppBlueprintDesign blueprint = readAppBlueprint(project.getAppBlueprint());
        Map<String, TableBundle> tablesByName = loadTables(projectId);
        if ((blueprint.getAdminMenus() == null || blueprint.getAdminMenus().isEmpty()) && tablesByName.isEmpty())
        {
            throw new ServiceException("请先生成后台菜单或数据库表");
        }

        List<FrontProjectPageDesign> existing = safeDesigns(pageDesignMapper.selectDesignsByProjectIdAndScope(projectId, ADMIN_SCOPE));
        Map<String, FrontProjectPageDesign> existingByPageCode = new LinkedHashMap<String, FrontProjectPageDesign>();
        for (FrontProjectPageDesign design : existing)
        {
            if (StringUtils.isNotEmpty(design.getPageCode()))
            {
                existingByPageCode.put(design.getPageCode(), design);
            }
        }

        Map<String, TableBundle> unmatchedTables = new LinkedHashMap<String, TableBundle>(tablesByName);
        List<FrontProjectPageDesign> merged = new ArrayList<FrontProjectPageDesign>();
        Set<String> expectedPageCodes = new LinkedHashSet<String>();
        FrontProjectPageDesign homeDesign = getOrCreateHomeDesign(projectId, userId, ADMIN_SCOPE, existingByPageCode);
        merged.add(homeDesign);
        expectedPageCodes.add(homeDesign.getPageCode());
        if (blueprint.getAdminMenus() != null)
        {
            for (AppMenuDesign menu : blueprint.getAdminMenus())
            {
                if (menu == null || StringUtils.isEmpty(menu.getCode()))
                {
                    continue;
                }
                if (isHomePageCode(menu.getCode(), ADMIN_SCOPE))
                {
                    expectedPageCodes.add(menu.getCode());
                    continue;
                }
                TableBundle table = findAdminMenuTable(menu, unmatchedTables);
                if (table == null)
                {
                    continue;
                }
                unmatchedTables.remove(table.table.getTableName());
                AppPageDesign page = adminPageFromMenu(menu, table.table);
                expectedPageCodes.add(page.getCode());
                FrontProjectPageDesign design = existingByPageCode.get(page.getCode());
                if (design == null)
                {
                    design = newDefaultDesign(projectId, userId, ADMIN_SCOPE, page, table);
                    pageDesignMapper.insertFrontProjectPageDesign(design);
                }
                merged.add(design);
            }
        }

        for (TableBundle table : unmatchedTables.values())
        {
            AppPageDesign page = adminPageFromTable(table.table);
            expectedPageCodes.add(page.getCode());
            FrontProjectPageDesign design = existingByPageCode.get(page.getCode());
            if (design == null)
            {
                design = newDefaultDesign(projectId, userId, ADMIN_SCOPE, page, table);
                pageDesignMapper.insertFrontProjectPageDesign(design);
            }
            merged.add(design);
        }

        List<FrontendPageDesignResponse> detached = new ArrayList<FrontendPageDesignResponse>();
        for (FrontProjectPageDesign design : existing)
        {
            if (!expectedPageCodes.contains(design.getPageCode()))
            {
                detached.add(FrontendPageDesignResponse.from(design));
            }
        }

        FrontendPageDesignInitResponse response = new FrontendPageDesignInitResponse();
        response.setDesigns(toResponses(merged));
        response.setDetachedDesigns(detached);
        return response;
    }

    @Transactional
    public FrontendPageDesignResponse createPageDesign(Long userId, Long projectId, FrontendPageDesignRequest request)
    {
        return createPageDesign(userId, projectId, request, FRONTEND_SCOPE);
    }

    @Transactional
    public FrontendPageDesignResponse createPageDesign(Long userId, Long projectId, FrontendPageDesignRequest request,
            String scope)
    {
        if (request == null)
        {
            throw new ServiceException("页面设计参数不能为空");
        }
        frontProjectService.getProject(userId, projectId);
        String normalizedScope = normalizeScope(scope);

        FrontProjectPageDesign create = new FrontProjectPageDesign();
        create.setProjectId(projectId);
        create.setUserId(userId);
        create.setPageScope(defaultText(request.getPageScope(), normalizedScope));
        create.setPageCode(request.getPageCode());
        create.setMenuCode(defaultText(request.getMenuCode(), request.getPageCode()));
        create.setPageName(request.getPageName());
        create.setRoutePath(request.getRoutePath());
        create.setPageType(defaultText(request.getPageType(), "list"));
        create.setTableName(request.getTableName());
        normalizeMenuDirectoryMeta(create);
        create.setStatus(defaultText(request.getStatus(), "0"));
        create.setRemark(request.getRemark());
        create.setVersion(1);

        validatePageMeta(create);
        ensureUniquePageCode(projectId, create.getPageCode(), normalizedScope);

        Map<String, TableBundle> tablesByName = loadTables(projectId);
        TableBundle table = null;
        if (StringUtils.isNotEmpty(create.getTableName()))
        {
            table = tablesByName.get(create.getTableName());
            if (table == null)
            {
                throw new ServiceException("页面绑定的数据表不存在: " + create.getTableName());
            }
        }
        String layoutJson = defaultText(request.getLayoutJson(),
                defaultLayoutJson(create.getPageType(), table, normalizedScope));
        String actionJson = defaultText(request.getActionJson(),
                isMenuDirectory(create.getPageType()) ? "{}" : defaultActionJson(table, normalizedScope));
        create.setLayoutJson(normalizeLayout(layoutJson, table, create.getPageType(), create.getPageScope(), tablesByName));
        create.setActionJson(normalizeAction(actionJson, table, create.getPageScope()));
        pageDesignMapper.insertFrontProjectPageDesign(create);
        return FrontendPageDesignResponse.from(create);
    }

    @Transactional
    public FrontendPageDesignResponse updatePageDesign(Long userId, Long projectId, Long designId,
            FrontendPageDesignRequest request)
    {
        return updatePageDesign(userId, projectId, designId, request, FRONTEND_SCOPE);
    }

    @Transactional
    public FrontendPageDesignResponse updatePageDesign(Long userId, Long projectId, Long designId,
            FrontendPageDesignRequest request, String scope)
    {
        if (request == null)
        {
            throw new ServiceException("页面设计参数不能为空");
        }
        frontProjectService.getProject(userId, projectId);
        String normalizedScope = normalizeScope(scope);
        FrontProjectPageDesign current = requireDesign(projectId, designId, normalizedScope);
        int currentVersion = current.getVersion() == null ? 1 : current.getVersion().intValue();
        if (request.getVersion() != null && request.getVersion().intValue() != currentVersion)
        {
            throw new ServiceException("页面设计已被更新，请刷新后重试");
        }

        FrontProjectPageDesign update = new FrontProjectPageDesign();
        update.setDesignId(designId);
        update.setProjectId(projectId);
        update.setUserId(userId);
        update.setPageScope(defaultText(request.getPageScope(), current.getPageScope()));
        update.setPageScope(defaultText(update.getPageScope(), normalizedScope));
        update.setPageCode(defaultText(request.getPageCode(), current.getPageCode()));
        update.setMenuCode(defaultText(request.getMenuCode(), current.getMenuCode()));
        update.setPageName(defaultText(request.getPageName(), current.getPageName()));
        update.setRoutePath(defaultText(request.getRoutePath(), current.getRoutePath()));
        update.setPageType(defaultText(request.getPageType(), current.getPageType()));
        update.setTableName(defaultText(request.getTableName(), current.getTableName()));
        normalizeMenuDirectoryMeta(update);
        update.setStatus(defaultText(request.getStatus(), current.getStatus()));
        update.setRemark(defaultText(request.getRemark(), current.getRemark()));
        update.setVersion(currentVersion + 1);

        validatePageMeta(update);
        Map<String, TableBundle> tablesByName = loadTables(projectId);
        TableBundle table = null;
        if (StringUtils.isNotEmpty(update.getTableName()))
        {
            table = tablesByName.get(update.getTableName());
            if (table == null)
            {
                throw new ServiceException("页面绑定的数据表不存在: " + update.getTableName());
            }
        }
        update.setLayoutJson(normalizeLayout(defaultText(request.getLayoutJson(), current.getLayoutJson()), table,
                update.getPageType(), update.getPageScope(), tablesByName));
        update.setActionJson(normalizeAction(defaultText(request.getActionJson(), current.getActionJson()), table,
                update.getPageScope()));
        pageDesignMapper.updateFrontProjectPageDesign(update);
        return FrontendPageDesignResponse.from(update);
    }

    @Transactional
    public void deletePageDesign(Long userId, Long projectId, Long designId, String scope)
    {
        frontProjectService.getProject(userId, projectId);
        String normalizedScope = normalizeScope(scope);
        requireDesign(projectId, designId, normalizedScope);
        pageDesignMapper.deleteFrontProjectPageDesignById(designId);
    }

    private FrontProjectPageDesign requireDesign(Long projectId, Long designId)
    {
        return requireDesign(projectId, designId, FRONTEND_SCOPE);
    }

    private FrontProjectPageDesign requireDesign(Long projectId, Long designId, String scope)
    {
        FrontProjectPageDesign design = pageDesignMapper.selectFrontProjectPageDesignById(designId);
        if (design == null || design.getProjectId() == null || !design.getProjectId().equals(projectId)
                || !scopeMatches(design.getPageScope(), scope))
        {
            throw new ServiceException("页面设计不存在或无权限");
        }
        return design;
    }

    private void ensureUniquePageCode(Long projectId, String pageCode, String scope)
    {
        List<FrontProjectPageDesign> existing = safeDesigns(pageDesignMapper.selectDesignsByProjectIdAndScope(projectId, scope));
        for (FrontProjectPageDesign design : existing)
        {
            if (design != null && pageCode.equals(design.getPageCode()))
            {
                throw new ServiceException("页面编码已存在: " + pageCode);
            }
        }
    }

    private FrontProjectPageDesign newDefaultDesign(Long projectId, Long userId, String scope, AppPageDesign page, TableBundle table)
    {
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setProjectId(projectId);
        design.setUserId(userId);
        design.setPageScope(scope);
        design.setPageCode(page.getCode());
        design.setMenuCode(page.getMenuCode());
        design.setPageName(defaultText(page.getName(), page.getCode()));
        design.setRoutePath(defaultText(page.getPath(), "/" + page.getCode()));
        String pageType = defaultText(page.getPageType(), "list");
        design.setPageType(pageType);
        design.setTableName(page.getTableName());
        design.setLayoutJson(defaultLayoutJson(pageType, table, scope, page));
        design.setActionJson(defaultActionJson(table, scope));
        design.setStatus("0");
        design.setVersion(1);
        return design;
    }

    private FrontProjectPageDesign getOrCreateHomeDesign(Long projectId, Long userId, String scope,
            Map<String, FrontProjectPageDesign> existingByPageCode)
    {
        String pageCode = homePageCode(scope);
        FrontProjectPageDesign design = existingByPageCode.get(pageCode);
        if (design != null)
        {
            return design;
        }
        design = newDefaultHomeDesign(projectId, userId, scope);
        pageDesignMapper.insertFrontProjectPageDesign(design);
        existingByPageCode.put(pageCode, design);
        return design;
    }

    private FrontProjectPageDesign newDefaultHomeDesign(Long projectId, Long userId, String scope)
    {
        String normalizedScope = normalizeScope(scope);
        String pageCode = homePageCode(normalizedScope);
        FrontProjectPageDesign design = new FrontProjectPageDesign();
        design.setProjectId(projectId);
        design.setUserId(userId);
        design.setPageScope(normalizedScope);
        design.setPageCode(pageCode);
        design.setMenuCode(pageCode);
        design.setPageName(HOME_PAGE_NAME);
        design.setRoutePath(homeRoutePath(normalizedScope));
        design.setPageType(HOME_PAGE_TYPE);
        design.setTableName("");
        design.setLayoutJson(defaultHomeLayoutJson(normalizedScope, pageCode));
        design.setActionJson(emptyActionJson());
        design.setStatus("0");
        design.setVersion(1);
        return design;
    }

    private String defaultHomeLayoutJson(String scope, String pageCode)
    {
        Map<String, Object> layout = new LinkedHashMap<String, Object>();
        layout.put("canvas", BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS);
        layout.put("blocks", new ArrayList<Object>());
        layout.put("navigation", defaultHomeNavigation(pageCode));
        return writeJson(layout);
    }

    private Map<String, Object> defaultHomeNavigation(String pageCode)
    {
        Map<String, Object> navigation = new LinkedHashMap<String, Object>();
        navigation.put("source", "pageDesigns");
        navigation.put("position", "top");
        navigation.put("display", "dropdown");
        navigation.put("showInMenu", true);
        navigation.put("menuLevel", "primary");
        navigation.put("menuCode", pageCode);
        navigation.put("menuName", HOME_PAGE_NAME);
        navigation.put("menuOrder", Integer.valueOf(1));
        return navigation;
    }

    private String emptyActionJson()
    {
        Map<String, Object> action = new LinkedHashMap<String, Object>();
        action.put("toolbarActions", new ArrayList<Object>());
        action.put("rowActions", new ArrayList<Object>());
        return writeJson(action);
    }

    private AppPageDesign adminPageFromMenu(AppMenuDesign menu, FrontProjectTable table)
    {
        AppPageDesign page = new AppPageDesign();
        page.setCode(menu.getCode());
        page.setMenuCode(menu.getCode());
        page.setName(defaultText(menu.getName(), defaultAdminPageName(table)));
        page.setPath(defaultText(menu.getPath(), adminRoutePath(table)));
        page.setPageType("list");
        page.setTableName(table.getTableName());
        return page;
    }

    private AppPageDesign adminPageFromTable(FrontProjectTable table)
    {
        AppPageDesign page = new AppPageDesign();
        page.setCode("admin_" + table.getTableName());
        page.setMenuCode("admin_" + table.getTableName());
        page.setName(defaultAdminPageName(table));
        page.setPath(adminRoutePath(table));
        page.setPageType("list");
        page.setTableName(table.getTableName());
        return page;
    }

    private TableBundle findAdminMenuTable(AppMenuDesign menu, Map<String, TableBundle> tablesByName)
    {
        if (menu == null || tablesByName == null || tablesByName.isEmpty())
        {
            return null;
        }
        String menuName = normalizeMatchText(menu.getName());
        String menuCode = normalizeMatchText(menu.getCode());
        String menuPath = normalizeMatchText(menu.getPath());
        for (TableBundle table : tablesByName.values())
        {
            if (table == null || table.table == null)
            {
                continue;
            }
            String adminMenuName = normalizeMatchText(table.table.getAdminMenuName());
            String tableName = normalizeMatchText(table.table.getTableName());
            String businessName = normalizeMatchText(table.table.getBusinessName());
            if (StringUtils.isNotEmpty(adminMenuName) && (adminMenuName.equals(menuName) || adminMenuName.equals(menuCode)))
            {
                return table;
            }
            if (StringUtils.isNotEmpty(tableName) && (menuPath.contains(tableName) || menuCode.contains(tableName)))
            {
                return table;
            }
            if (StringUtils.isNotEmpty(businessName) && (menuPath.contains(businessName) || menuCode.contains(businessName)))
            {
                return table;
            }
        }
        return null;
    }

    private void validatePageMeta(FrontProjectPageDesign design)
    {
        if (!FRONTEND_SCOPE.equals(design.getPageScope()) && !ADMIN_SCOPE.equals(design.getPageScope()))
        {
            throw new ServiceException("页面范围不支持: " + design.getPageScope());
        }
        if (StringUtils.isEmpty(design.getPageCode()))
        {
            throw new ServiceException("页面编码不能为空");
        }
        if (StringUtils.isEmpty(design.getPageName()))
        {
            throw new ServiceException("页面名称不能为空");
        }
        boolean menuDirectory = isMenuDirectory(design.getPageType());
        if (StringUtils.isEmpty(design.getRoutePath()) && !menuDirectory)
        {
            throw new ServiceException("页面路由不能为空");
        }
        if (!PAGE_TYPES.contains(design.getPageType()))
        {
            throw new ServiceException("页面类型不支持: " + design.getPageType());
        }
        if (StringUtils.isEmpty(design.getTableName()) && !"business_block".equals(design.getPageType()) && !menuDirectory)
        {
            throw new ServiceException("页面必须绑定数据表");
        }
    }

    private void normalizeMenuDirectoryMeta(FrontProjectPageDesign design)
    {
        if (!isMenuDirectory(design.getPageType()))
        {
            return;
        }
        design.setRoutePath("");
        design.setTableName("");
        design.setMenuCode(defaultText(design.getMenuCode(), design.getPageCode()));
    }

    private Map<String, TableBundle> loadTables(Long projectId)
    {
        Map<String, TableBundle> result = new LinkedHashMap<String, TableBundle>();
        List<FrontProjectTable> tables = frontProjectTableMapper.selectTablesByProjectId(projectId);
        if (tables == null)
        {
            return result;
        }
        for (FrontProjectTable table : tables)
        {
            if (table == null || StringUtils.isEmpty(table.getTableName()))
            {
                continue;
            }
            TableBundle bundle = new TableBundle();
            bundle.table = table;
            List<FrontProjectColumn> columns = table.getTableId() == null
                    ? new ArrayList<FrontProjectColumn>()
                    : frontProjectColumnMapper.selectColumnsByTableId(table.getTableId());
            bundle.columns = columns == null ? new ArrayList<FrontProjectColumn>() : columns;
            result.put(table.getTableName(), bundle);
        }
        return result;
    }

    private String defaultLayoutJson(String pageType, TableBundle table, String scope)
    {
        if (isMenuDirectory(pageType))
        {
            Map<String, Object> layout = new LinkedHashMap<String, Object>();
            layout.put("canvas", "menu-directory-v1");
            layout.put("navigation", defaultMenuDirectoryNavigation(null, null));
            return writeJson(layout);
        }
        if ("business_block".equals(pageType))
        {
            Map<String, Object> businessLayout = new LinkedHashMap<String, Object>();
            businessLayout.put("canvas", BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS);
            businessLayout.put("blocks", new ArrayList<Object>());
            return writeJson(businessLayout);
        }
        Map<String, Object> layout = new LinkedHashMap<String, Object>();
        layout.put("canvas", defaultLayoutCanvas(pageType, scope));
        List<Map<String, Object>> regions = new ArrayList<Map<String, Object>>();
        regions.add(region("query", "query", queryFields(table), null, null));
        regions.add(region("toolbar", "toolbar", null, toolbarActions(table, scope), null));
        regions.add(region("table", "table", listFields(table), null, rowActions(table)));
        regions.add(region("form", "dialogForm", formFields(table), null, null));
        layout.put("regions", regions);
        return writeJson(layout);
    }

    private String defaultLayoutJson(String pageType, TableBundle table, String scope, AppPageDesign page)
    {
        Map<String, Object> layout = readMap(defaultLayoutJson(pageType, table, scope), "椤甸潰甯冨眬JSON鏍煎紡閿欒");
        applyAssociatedPageNavigationDefaults(layout, pageType, page == null ? null : page.getParentPageCode());
        return writeJson(layout);
    }

    @SuppressWarnings("unchecked")
    private void applyAssociatedPageNavigationDefaults(Map<String, Object> layout, String pageType, String parentPageCode)
    {
        if (layout == null || isMenuDirectory(pageType))
        {
            return;
        }
        if (!isAssociatedPageType(pageType) && StringUtils.isEmpty(parentPageCode))
        {
            return;
        }
        Object navigationObject = layout.get("navigation");
        Map<String, Object> navigation = navigationObject instanceof Map
                ? new LinkedHashMap<String, Object>((Map<String, Object>) navigationObject)
                : new LinkedHashMap<String, Object>();
        if (isAssociatedPageType(pageType))
        {
            navigation.put("showInMenu", false);
        }
        if (StringUtils.isNotEmpty(parentPageCode))
        {
            navigation.put("parentPageCode", parentPageCode);
        }
        layout.put("navigation", navigation);
    }

    private Map<String, Object> region(String id, String type, List<String> fields,
            List<String> actions, List<String> rowActions)
    {
        Map<String, Object> region = new LinkedHashMap<String, Object>();
        region.put("id", id);
        region.put("type", type);
        if (fields != null)
        {
            region.put("fields", fields);
        }
        if (actions != null)
        {
            region.put("actions", actions);
        }
        if (rowActions != null)
        {
            region.put("rowActions", rowActions);
        }
        return region;
    }

    private String defaultActionJson(TableBundle table, String scope)
    {
        Map<String, Object> action = new LinkedHashMap<String, Object>();
        List<Map<String, Object>> toolbarActions = new ArrayList<Map<String, Object>>();
        if (table == null || !"0".equals(table.table.getAllowAdd()))
        {
            toolbarActions.add(action("create", "新增", "primary", true));
        }
        if (ADMIN_SCOPE.equals(scope))
        {
            toolbarActions.add(action("export", "导出", "default", true));
        }
        List<Map<String, Object>> rowActions = new ArrayList<Map<String, Object>>();
        rowActions.add(action("view", "查看", "default", true));
        if (table == null || !"0".equals(table.table.getAllowEdit()))
        {
            rowActions.add(action("edit", "编辑", "primary", true));
        }
        if (table == null || !"0".equals(table.table.getAllowRemove()))
        {
            rowActions.add(action("delete", "删除", "danger", true));
        }
        action.put("toolbarActions", toolbarActions);
        action.put("rowActions", rowActions);
        return writeJson(action);
    }

    private Map<String, Object> action(String code, String label, String type, boolean builtin)
    {
        Map<String, Object> action = new LinkedHashMap<String, Object>();
        action.put("code", code);
        action.put("label", label);
        action.put("type", type);
        action.put("builtin", builtin);
        return action;
    }

    private List<String> queryFields(TableBundle table)
    {
        return collectFields(table, "query");
    }

    private List<String> listFields(TableBundle table)
    {
        return collectFields(table, "list");
    }

    private List<String> formFields(TableBundle table)
    {
        return collectFields(table, "form");
    }

    private List<String> collectFields(TableBundle table, String usage)
    {
        List<String> fields = new ArrayList<String>();
        if (table == null)
        {
            return fields;
        }
        for (FrontProjectColumn column : table.columns)
        {
            String field = javaField(column);
            if (StringUtils.isEmpty(field))
            {
                continue;
            }
            if ("query".equals(usage) && !"1".equals(column.getIsQuery()))
            {
                continue;
            }
            if ("list".equals(usage) && "0".equals(column.getIsList()))
            {
                continue;
            }
            if ("form".equals(usage) && "0".equals(column.getIsInsert()) && "0".equals(column.getIsEdit()))
            {
                continue;
            }
            fields.add(field);
        }
        return fields;
    }

    private List<String> toolbarActions(TableBundle table, String scope)
    {
        List<String> actions = new ArrayList<String>();
        if (table == null || !"0".equals(table.table.getAllowAdd()))
        {
            actions.add("create");
        }
        if (ADMIN_SCOPE.equals(scope))
        {
            actions.add("export");
        }
        return actions;
    }

    private List<String> rowActions(TableBundle table)
    {
        List<String> actions = new ArrayList<String>();
        actions.add("view");
        if (table == null || !"0".equals(table.table.getAllowEdit()))
        {
            actions.add("edit");
        }
        if (table == null || !"0".equals(table.table.getAllowRemove()))
        {
            actions.add("delete");
        }
        return actions;
    }

    @SuppressWarnings("unchecked")
    private String normalizeLayout(String layoutJson, TableBundle table, String pageType, String scope,
            Map<String, TableBundle> tablesByName)
    {
        if (isMenuDirectory(pageType))
        {
            return normalizeMenuDirectoryLayout(layoutJson);
        }
        if (StringUtils.isEmpty(layoutJson))
        {
            return defaultLayoutJson(pageType, table, scope);
        }
        Map<String, Object> layout = readMap(layoutJson, "页面布局JSON格式错误");
        String canvas = defaultText(stringValue(layout.get("canvas")), defaultLayoutCanvas(pageType, scope));
        if (!layoutCanvasAllowed(pageType, canvas, scope))
        {
            throw new ServiceException("页面布局不支持当前页面类型: " + canvas);
        }
        layout.put("canvas", canvas);
        if (BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS.equals(canvas))
        {
            String normalizedLayout = writeJson(layout);
            businessBlockRegistryService.validateLayout(normalizedLayout, toGenTables(tablesByName), scope);
            return normalizedLayout;
        }
        Object regionsObject = layout.get("regions");
        if (!(regionsObject instanceof List))
        {
            throw new ServiceException("页面布局必须包含 regions");
        }
        Map<String, String> aliases = fieldAliases(table);
        Set<String> regionIds = new HashSet<String>();
        for (Object regionObject : (List<Object>) regionsObject)
        {
            if (!(regionObject instanceof Map))
            {
                throw new ServiceException("页面区域格式错误");
            }
            Map<String, Object> region = (Map<String, Object>) regionObject;
            String id = stringValue(region.get("id"));
            String type = stringValue(region.get("type"));
            if (StringUtils.isEmpty(id) || !regionIds.add(id))
            {
                throw new ServiceException("页面区域ID不能为空且不能重复");
            }
            if (!REGION_TYPES.contains(type))
            {
                throw new ServiceException("页面区域类型不支持: " + type);
            }
            normalizeFieldList(region, "fields", aliases);
            normalizeSingleField(region, "titleField", aliases);
            normalizeSingleField(region, "imageField", aliases);
            normalizeActionCodeList(region, "actions", table, scope);
            normalizeActionCodeList(region, "rowActions", table, scope);
        }
        businessBlockRegistryService.validateEmbeddedBlocks(writeJson(layout), toGenTables(tablesByName), scope);
        return writeJson(layout);
    }

    @SuppressWarnings("unchecked")
    private String normalizeMenuDirectoryLayout(String layoutJson)
    {
        Map<String, Object> layout = StringUtils.isEmpty(layoutJson)
                ? new LinkedHashMap<String, Object>()
                : readMap(layoutJson, "页面布局JSON格式错误");
        Object navigationObject = layout.get("navigation");
        Map<String, Object> navigation = navigationObject instanceof Map
                ? new LinkedHashMap<String, Object>((Map<String, Object>) navigationObject)
                : new LinkedHashMap<String, Object>();
        Map<String, Object> defaults = defaultMenuDirectoryNavigation(
                stringValue(navigation.get("menuCode")),
                stringValue(navigation.get("menuName")));
        defaults.putAll(navigation);
        defaults.put("showInMenu", true);
        defaults.put("menuLevel", "primary");
        defaults.put("nodeType", "directory");
        layout.put("canvas", "menu-directory-v1");
        layout.put("navigation", defaults);
        return writeJson(layout);
    }

    private Map<String, Object> defaultMenuDirectoryNavigation(String menuCode, String menuName)
    {
        Map<String, Object> navigation = new LinkedHashMap<String, Object>();
        navigation.put("source", "pageDesigns");
        navigation.put("position", "top");
        navigation.put("display", "dropdown");
        navigation.put("showInMenu", true);
        navigation.put("menuLevel", "primary");
        navigation.put("nodeType", "directory");
        if (StringUtils.isNotEmpty(menuCode))
        {
            navigation.put("menuCode", menuCode);
        }
        if (StringUtils.isNotEmpty(menuName))
        {
            navigation.put("menuName", menuName);
        }
        return navigation;
    }

    private void normalizeSingleField(Map<String, Object> region, String key, Map<String, String> aliases)
    {
        Object value = region.get(key);
        if (value == null || StringUtils.isEmpty(stringValue(value)))
        {
            return;
        }
        String normalized = aliases.get(stringValue(value));
        if (normalized == null)
        {
            throw new ServiceException("页面字段不存在: " + value);
        }
        region.put(key, normalized);
    }

    private static Map<String, Set<String>> layoutCanvasesByPageType()
    {
        Map<String, Set<String>> result = new LinkedHashMap<String, Set<String>>();
        result.put("list", new LinkedHashSet<String>(Arrays.asList(
                "frontend-list-v1",
                "frontend-list-card-v1",
                "frontend-list-split-v1",
                "admin-list-v1",
                "admin-list-dense-v1",
                "admin-list-split-v1")));
        result.put("current_user_list", new LinkedHashSet<String>(Arrays.asList(
                "frontend-current-user-list-v1",
                "frontend-current-user-card-v1")));
        result.put("detail", new LinkedHashSet<String>(Arrays.asList(
                "frontend-detail-v1",
                "frontend-detail-media-v1",
                "admin-detail-v1")));
        result.put("form", new LinkedHashSet<String>(Arrays.asList(
                "frontend-form-v1",
                "frontend-form-group-v1",
                "admin-form-v1",
                "admin-form-group-v1")));
        result.put("business_block", new LinkedHashSet<String>(Arrays.asList(
                BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS)));
        result.put(MENU_DIRECTORY_PAGE_TYPE, new LinkedHashSet<String>(Arrays.asList("menu-directory-v1")));
        return result;
    }

    private String defaultLayoutCanvas(String pageType, String scope)
    {
        if (isMenuDirectory(pageType))
        {
            return "menu-directory-v1";
        }
        if (ADMIN_SCOPE.equals(scope))
        {
            if ("detail".equals(pageType))
            {
                return "admin-detail-v1";
            }
            if ("form".equals(pageType))
            {
                return "admin-form-v1";
            }
            return "admin-list-v1";
        }
        Set<String> canvases = LAYOUT_CANVASES_BY_PAGE_TYPE.get(defaultText(pageType, "list"));
        if (canvases == null || canvases.isEmpty())
        {
            return "frontend-list-v1";
        }
        return canvases.iterator().next();
    }

    private boolean layoutCanvasAllowed(String pageType, String canvas, String scope)
    {
        if (isMenuDirectory(pageType))
        {
            return "menu-directory-v1".equals(canvas);
        }
        if ("business_block".equals(pageType))
        {
            return BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS.equals(canvas);
        }
        if (ADMIN_SCOPE.equals(scope) && canvas != null && !canvas.startsWith("admin-"))
        {
            return false;
        }
        if (FRONTEND_SCOPE.equals(scope) && canvas != null && canvas.startsWith("admin-"))
        {
            return false;
        }
        Set<String> canvases = LAYOUT_CANVASES_BY_PAGE_TYPE.get(defaultText(pageType, "list"));
        return canvases != null && canvases.contains(canvas);
    }

    @SuppressWarnings("unchecked")
    private void normalizeFieldList(Map<String, Object> region, String key, Map<String, String> aliases)
    {
        Object value = region.get(key);
        if (value == null)
        {
            return;
        }
        if (!(value instanceof List))
        {
            throw new ServiceException("页面字段配置必须是数组");
        }
        List<String> fields = new ArrayList<String>();
        for (Object item : (List<Object>) value)
        {
            String field = stringValue(item);
            String normalized = aliases.get(field);
            if (normalized == null)
            {
                throw new ServiceException("页面字段不存在: " + field);
            }
            fields.add(normalized);
        }
        region.put(key, fields);
    }

    private String normalizeAction(String actionJson, TableBundle table, String scope)
    {
        if (StringUtils.isEmpty(actionJson))
        {
            return "{\"toolbarActions\":[],\"rowActions\":[]}";
        }
        Map<String, Object> action = readMap(actionJson, "页面按钮JSON格式错误");
        normalizeActionConfigList(action, "toolbarActions", table, scope);
        normalizeActionConfigList(action, "rowActions", table, scope);
        return writeJson(action);
    }

    @SuppressWarnings("unchecked")
    private void normalizeActionCodeList(Map<String, Object> region, String key, TableBundle table, String scope)
    {
        Object value = region.get(key);
        if (value == null)
        {
            return;
        }
        if (!(value instanceof List))
        {
            throw new ServiceException("页面按钮配置必须是数组");
        }
        List<String> actions = new ArrayList<String>();
        for (Object item : (List<Object>) value)
        {
            String code = stringValue(item);
            validateActionCode(code, table, scope);
            actions.add(code);
        }
        region.put(key, actions);
    }

    @SuppressWarnings("unchecked")
    private void normalizeActionConfigList(Map<String, Object> action, String key, TableBundle table, String scope)
    {
        Object value = action.get(key);
        if (value == null)
        {
            return;
        }
        if (!(value instanceof List))
        {
            throw new ServiceException("页面按钮配置必须是数组");
        }
        for (Object item : (List<Object>) value)
        {
            String code = null;
            if (item instanceof Map)
            {
                code = stringValue(((Map<String, Object>) item).get("code"));
            }
            else
            {
                code = stringValue(item);
            }
            validateActionCode(code, table, scope);
        }
    }

    private void validateActionCode(String code, TableBundle table, String scope)
    {
        if (StringUtils.isEmpty(code))
        {
            throw new ServiceException("页面按钮编码不能为空");
        }
        if (code.startsWith("business:"))
        {
            if (code.length() == "business:".length())
            {
                throw new ServiceException("页面业务按钮编码不能为空");
            }
            return;
        }
        if (!BUILTIN_ACTIONS.contains(code))
        {
            throw new ServiceException("页面按钮不支持: " + code);
        }
        if ("create".equals(code) && table != null && "0".equals(table.table.getAllowAdd()))
        {
            throw new ServiceException("页面按钮 create 需要开启新增权限");
        }
        if ("edit".equals(code) && table != null && "0".equals(table.table.getAllowEdit()))
        {
            throw new ServiceException("页面按钮 edit 需要开启编辑权限");
        }
        if (("delete".equals(code) || "batchDelete".equals(code))
                && table != null && "0".equals(table.table.getAllowRemove()))
        {
            throw new ServiceException("页面按钮 " + code + " 需要开启删除权限");
        }
        if ("export".equals(code) && !ADMIN_SCOPE.equals(normalizeScope(scope)))
        {
            throw new ServiceException("页面按钮 export 仅支持后台页面");
        }
    }

    private Map<String, String> fieldAliases(TableBundle table)
    {
        Map<String, String> aliases = new HashMap<String, String>();
        if (table == null)
        {
            return aliases;
        }
        for (FrontProjectColumn column : table.columns)
        {
            String field = javaField(column);
            if (StringUtils.isEmpty(field))
            {
                continue;
            }
            aliases.put(field, field);
            if (StringUtils.isNotEmpty(column.getColumnName()))
            {
                aliases.put(column.getColumnName(), field);
            }
        }
        return aliases;
    }

    private List<GenTable> toGenTables(Map<String, TableBundle> tablesByName)
    {
        List<GenTable> tables = new ArrayList<GenTable>();
        if (tablesByName == null)
        {
            return tables;
        }
        for (TableBundle bundle : tablesByName.values())
        {
            if (bundle == null || bundle.table == null)
            {
                continue;
            }
            GenTable table = new GenTable();
            table.setTableId(bundle.table.getTableId());
            table.setTableName(bundle.table.getTableName());
            table.setTableComment(bundle.table.getTableComment());
            List<GenTableColumn> columns = new ArrayList<GenTableColumn>();
            for (FrontProjectColumn source : bundle.columns)
            {
                if (source == null)
                {
                    continue;
                }
                GenTableColumn column = new GenTableColumn();
                column.setColumnName(source.getColumnName());
                column.setJavaField(source.getJavaField());
                column.setColumnComment(source.getColumnComment());
                column.setJavaType(source.getJavaType());
                column.setColumnType(source.getColumnType());
                columns.add(column);
            }
            table.setColumns(columns);
            tables.add(table);
        }
        return tables;
    }

    private String javaField(FrontProjectColumn column)
    {
        if (column == null)
        {
            return null;
        }
        if (StringUtils.isNotEmpty(column.getJavaField()))
        {
            return column.getJavaField();
        }
        return toCamelCase(column.getColumnName());
    }

    private String toCamelCase(String value)
    {
        if (StringUtils.isEmpty(value))
        {
            return value;
        }
        StringBuilder builder = new StringBuilder();
        boolean upperNext = false;
        for (int i = 0; i < value.length(); i++)
        {
            char c = value.charAt(i);
            if (c == '_')
            {
                upperNext = true;
                continue;
            }
            builder.append(upperNext ? Character.toUpperCase(c) : c);
            upperNext = false;
        }
        return builder.toString();
    }

    private AppBlueprintDesign readAppBlueprint(String json)
    {
        if (StringUtils.isEmpty(json))
        {
            return new AppBlueprintDesign();
        }
        try
        {
            AppBlueprintDesign blueprint = OBJECT_MAPPER.readValue(json, AppBlueprintDesign.class);
            return blueprint == null ? new AppBlueprintDesign() : blueprint;
        }
        catch (Exception e)
        {
            throw new ServiceException("前台页面蓝图JSON格式错误");
        }
    }

    private Map<String, Object> readMap(String json, String message)
    {
        try
        {
            return OBJECT_MAPPER.readValue(json, new TypeReference<Map<String, Object>>() { });
        }
        catch (Exception e)
        {
            throw new ServiceException(message);
        }
    }

    private String writeJson(Object value)
    {
        try
        {
            return OBJECT_MAPPER.writeValueAsString(value);
        }
        catch (Exception e)
        {
            throw new ServiceException("页面设计JSON序列化失败");
        }
    }

    private List<FrontProjectPageDesign> safeDesigns(List<FrontProjectPageDesign> designs)
    {
        return designs == null ? new ArrayList<FrontProjectPageDesign>() : designs;
    }

    private List<FrontendPageDesignResponse> toResponses(List<FrontProjectPageDesign> designs)
    {
        List<FrontendPageDesignResponse> responses = new ArrayList<FrontendPageDesignResponse>();
        for (FrontProjectPageDesign design : safeDesigns(designs))
        {
            responses.add(FrontendPageDesignResponse.from(design));
        }
        return responses;
    }

    private List<FrontendPageDesignResponse> toScopedResponses(List<FrontProjectPageDesign> designs, String scope)
    {
        List<FrontendPageDesignResponse> responses = new ArrayList<FrontendPageDesignResponse>();
        for (FrontProjectPageDesign design : safeDesigns(designs))
        {
            if (shouldExposeDesign(design, scope))
            {
                responses.add(FrontendPageDesignResponse.from(design));
            }
        }
        return responses;
    }

    private boolean shouldExposeDesign(FrontProjectPageDesign design, String scope)
    {
        if (design == null || !scopeMatches(design.getPageScope(), scope))
        {
            return false;
        }
        return !FRONTEND_SCOPE.equals(normalizeScope(scope)) || !isAdminLikeDesign(design);
    }

    private boolean isAdminLikeDesign(FrontProjectPageDesign design)
    {
        return isAdminRoutePath(design.getRoutePath()) || layoutCanvasStartsWith(design.getLayoutJson(), "admin-");
    }

    private boolean isAdminLikePage(AppPageDesign page)
    {
        return page != null && isAdminRoutePath(page.getPath());
    }

    private boolean layoutCanvasStartsWith(String layoutJson, String prefix)
    {
        if (StringUtils.isEmpty(layoutJson))
        {
            return false;
        }
        try
        {
            Map<String, Object> layout = OBJECT_MAPPER.readValue(layoutJson, new TypeReference<Map<String, Object>>() { });
            String canvas = stringValue(layout.get("canvas"));
            return canvas != null && canvas.startsWith(prefix);
        }
        catch (Exception e)
        {
            return false;
        }
    }

    private boolean isAdminRoutePath(String routePath)
    {
        if (StringUtils.isEmpty(routePath))
        {
            return false;
        }
        String normalized = routePath.trim().replace('\\', '/').toLowerCase(Locale.ROOT);
        return "admin".equals(normalized) || normalized.startsWith("admin/")
                || "/admin".equals(normalized) || normalized.startsWith("/admin/");
    }

    private String stringValue(Object value)
    {
        return value == null ? null : String.valueOf(value);
    }

    private String defaultText(String value, String fallback)
    {
        return StringUtils.isEmpty(value) ? fallback : value;
    }

    private boolean isMenuDirectory(String pageType)
    {
        return MENU_DIRECTORY_PAGE_TYPE.equals(pageType);
    }

    private boolean isAssociatedPageType(String pageType)
    {
        return "detail".equals(pageType);
    }

    private String normalizeScope(String scope)
    {
        return ADMIN_SCOPE.equals(scope) ? ADMIN_SCOPE : FRONTEND_SCOPE;
    }

    private String homePageCode(String scope)
    {
        return ADMIN_SCOPE.equals(normalizeScope(scope)) ? ADMIN_HOME_PAGE_CODE : FRONTEND_HOME_PAGE_CODE;
    }

    private String homeRoutePath(String scope)
    {
        return ADMIN_SCOPE.equals(normalizeScope(scope)) ? "/admin/home" : "/home";
    }

    private boolean isHomePageCode(String pageCode, String scope)
    {
        return StringUtils.isNotEmpty(pageCode) && pageCode.equals(homePageCode(scope));
    }

    private boolean scopeMatches(String designScope, String expectedScope)
    {
        String normalizedDesignScope = StringUtils.isEmpty(designScope) ? FRONTEND_SCOPE : designScope;
        return normalizeScope(expectedScope).equals(normalizedDesignScope);
    }

    private String defaultAdminPageName(FrontProjectTable table)
    {
        if (table == null)
        {
            return "后台管理";
        }
        if (StringUtils.isNotEmpty(table.getAdminMenuName()))
        {
            return table.getAdminMenuName();
        }
        if (StringUtils.isNotEmpty(table.getTableComment()))
        {
            return table.getTableComment();
        }
        return table.getTableName();
    }

    private String adminRoutePath(FrontProjectTable table)
    {
        String name = table == null ? "manage" : defaultText(table.getBusinessName(), table.getTableName());
        return "/admin/" + name.replace("_", "-");
    }

    private String normalizeMatchText(String value)
    {
        if (StringUtils.isEmpty(value))
        {
            return "";
        }
        return value.toLowerCase().replace("_", "").replace("-", "").replace("/", "").replace(" ", "");
    }

    private static class TableBundle
    {
        private FrontProjectTable table;
        private List<FrontProjectColumn> columns = new ArrayList<FrontProjectColumn>();
    }
}
