/**
 * scan_production_formulas.js
 *
 * 生产公式扫描工具 — 连接 MongoDB 读取 forms 集合，遍历所有公式字段，
 * 调用 mapFormula 检测未兼容的公式语法，输出 JSON 报告。
 *
 * 用法：
 *   FORMULA_SCAN_MONGO_URL=mongodb://127.0.0.1:27017/mydb \
 *   SCAN_LABEL=prod \
 *   node main/default/test/scan_production_formulas.js
 *
 * 环境变量：
 *   FORMULA_SCAN_MONGO_URL  优先使用此连接串
 *   MONGO_URL               回退连接串
 *   SCAN_LABEL              可选，用于报告文件命名标识
 */

'use strict';

const path = require('path');
const fs = require('fs');
const { mapFormula, getSafeCode, getTableFieldMap } = require('../utils/formula-compat');

// ─── 已知问题清单（经人工审核确认不需要代码修正） ─────────────────────────────
// key = "formId::fieldCode"，value = { category, resolution }
// category: KNOWN_LIMITATION — mapFormula 功能边界 | BAD_SOURCE_DATA — 源表单数据本身有误
const KNOWN_ISSUES = {
    '451766252f8838253eab81f8::所属信息站': {
        category: 'KNOWN_LIMITATION',
        resolution: 'judgeIndex 为老版本工作流自定义函数，不在 mapFormula 支持范围内（仅支持 sum/average/count/max/min/numToRMB），amis 无等价函数。历史版本，无需修正。',
    },
    '9THeSusZyo9E3pe5p::表名': {
        category: 'KNOWN_LIMITATION',
        resolution: '文本模板公式（字段引用+文字混排无运算符），mapFormula 尚无此转换路径。{YYYY} 为日期模板 token。历史版本，无需修正。',
    },
    '6d9fe8a132c96a664ca23808::发放补贴金额（元）': {
        category: 'BAD_SOURCE_DATA',
        resolution: '原始公式 {发放补贴数量}*{} 中 {} 为空引用，表单作者漏填字段名（推测应为 {发放补贴单价（元）}）。源数据错误，非代码问题。',
    },
    'b9e7465f159b6640bbe49caa::概算、估算金额（元、大写）': {
        category: 'BAD_SOURCE_DATA',
        resolution: '原始公式 numToRMB(概算、估算金额{}) 花括号位置错误，正确写法应为 numToRMB({概算、估算金额})。源数据错误，非代码问题。',
    },
};

// ─── 工具函数 ────────────────────────────────────────────────────────────────

/**
 * 脱敏 MongoDB 连接串：隐藏密码和数据库名称中的敏感部分。
 * 例：mongodb://user:pass@host:27017/db_prod → mongodb://user:***@host:27017/***_prod
 */
function redactMongoUrl(url) {
    if (!url) return url;
    // 隐藏密码
    let redacted = url.replace(/:([^/@]+)@/, ':***@');
    // 隐藏数据库名（保留最后一个下划线后缀，便于识别环境）
    redacted = redacted.replace(/(\/[^/?]+)(\?|$)/, (m, dbPart, suffix) => {
        const dbName = dbPart.slice(1);
        const underscoreIdx = dbName.lastIndexOf('_');
        const masked = underscoreIdx > -1
            ? '***' + dbName.slice(underscoreIdx)
            : '***';
        return '/' + masked + suffix;
    });
    return redacted;
}

/**
 * 返回今日日期的各部分，供两种格式化函数共用。
 * @returns {{ y: string, m: string, day: string }}
 */
function todayParts() {
    const d = new Date();
    return {
        y: String(d.getFullYear()),
        m: String(d.getMonth() + 1).padStart(2, '0'),
        day: String(d.getDate()).padStart(2, '0'),
    };
}

/**
 * 今日日期字符串 YYYYMMDD
 */
function todayStr() {
    const { y, m, day } = todayParts();
    return `${y}${m}${day}`;
}

/**
 * 今日日期字符串 YYYY-MM-DD（用于 JSON）
 */
function todayISODate() {
    const { y, m, day } = todayParts();
    return `${y}-${m}-${day}`;
}

// ─── 检测规则 ─────────────────────────────────────────────────────────────────

/**
 * 从原始公式中提取聚合函数引用的字段名列表。
 * 支持所有变体：sum({code}), sum{code}, sum（{code}）, sum{(code)}
 * @param {string} formula 原始公式文本
 * @returns {string[]} 去重的字段名数组
 */
function extractAggregationFieldRefs(formula) {
    const fieldNames = new Set();
    const aggFuncs = 'sum|average|count|max|min|numToRMB';

    // Standard: func({code})
    const p1 = new RegExp(`(?:${aggFuncs})\\(\\{([^}]+)\\}\\)`, 'gi');
    // Fullwidth: func（{code}）
    const p2 = new RegExp(`(?:${aggFuncs})（\\{([^}]+)\\}）`, 'gi');
    // Missing parens: func{code} or func{(code)}
    const p3 = new RegExp(`(?:${aggFuncs})\\{\\(?([^})]+?)\\)?\\}`, 'gi');

    for (const pattern of [p1, p2, p3]) {
        let m;
        while ((m = pattern.exec(formula)) !== null) {
            if (m[1]) fieldNames.add(m[1].trim());
        }
    }

    return [...fieldNames];
}

/**
 * 对单个公式字段执行所有检测规则，返回 issues 数组（可能为空）。
 *
 * @param {object} params
 * @param {string} params.formula         原始公式文本
 * @param {object} params.tableFieldMap   由 getTableFieldMap 生成的映射
 * @param {string} params.formId
 * @param {string} params.formName
 * @param {string} params.formVersionType "current" | "history"
 * @param {object} params.field           字段对象
 * @param {string} params.fieldLocation   "top" | "table:{tableCode}"
 * @param {string} params.formulaSource   "formula" | "default_value"
 * @param {string[]} params.siblingCodes  同层级其他字段 code
 * @returns {{ issues: object[], convertedResult: string|null }}
 */
function checkFormula({ formula, tableFieldMap, formId, formName, formVersionType,
    field, fieldLocation, formulaSource, siblingCodes, inSection }) {

    const fieldCode = field.code || '';
    const fieldType = field.type || '';
    const hasFieldRef = /\{[^{}]+\}/.test(formula);

    const baseInfo = {
        formId,
        formName,
        formVersionType,
        fieldCode,
        fieldType,
        fieldLocation,
        formulaSource,
        originalFormula: formula,
    };

    const issues = [];
    let convertedResult = null;

    // E4: 转换异常
    try {
        convertedResult = mapFormula(formula, tableFieldMap);
    } catch (err) {
        issues.push({
            level: 'ERROR',
            ruleId: 'E4',
            ...baseInfo,
            convertedResult: null,
            errorDetail: `mapFormula 抛出异常: ${err.message}`,
            siblingFields: siblingCodes,
        });
        return { issues, convertedResult: null };
    }

    // E1: 转换失败（返回 null 但公式含 {字段名} 引用）
    if (convertedResult === null && hasFieldRef) {
        issues.push({
            level: 'ERROR',
            ruleId: 'E1',
            ...baseInfo,
            convertedResult: null,
            errorDetail: '公式含 {字段名} 引用但 mapFormula 返回 null，无法转换',
            siblingFields: siblingCodes,
        });
        return { issues, convertedResult: null };
    }

    if (convertedResult !== null) {
        // E2: 方括号残留 — 排除 ARRAYMAP 产生的 item['...'] 属性访问语法
        const resultWithoutArrayAccess = convertedResult.replace(/\bitem\s*\[\s*'[^']*'\s*\]/g, '');
        if (/[\[\]]/.test(resultWithoutArrayAccess)) {
            issues.push({
                level: 'ERROR',
                ruleId: 'E2',
                ...baseInfo,
                convertedResult,
                errorDetail: '转换结果中残留方括号 []',
                siblingFields: siblingCodes,
            });
        }

        // E3: 花括号残留（非 ${ 的裸 { 表示字段未转换）
        // 合法结果形如 ${...}，内部不应有独立的 {fieldname} 未转换片段
        if (/(?<!\$)\{/.test(convertedResult)) {
            issues.push({
                level: 'ERROR',
                ruleId: 'E3',
                ...baseInfo,
                convertedResult,
                errorDetail: '转换结果中残留花括号 {}（非合法 amis 表达式）',
                siblingFields: siblingCodes,
            });
        }

        // E5: 聚合函数引用子表字段但转换结果不含对应 ARRAYMAP（子表识别失败）
        if (tableFieldMap && Object.keys(tableFieldMap).length > 0) {
            const aggFieldNames = extractAggregationFieldRefs(formula);
            for (const refName of aggFieldNames) {
                const tableName = tableFieldMap[refName];
                if (tableName) {
                    const safeTableCode = getSafeCode(tableName);
                    if (!convertedResult.includes(`ARRAYMAP(${safeTableCode}`)) {
                        issues.push({
                            level: 'ERROR',
                            ruleId: 'E5',
                            ...baseInfo,
                            convertedResult,
                            errorDetail: `聚合函数引用子表字段「${refName}」（所属子表「${tableName}」），但转换结果不含 ARRAYMAP(${safeTableCode}...)，子表识别失败`,
                            siblingFields: siblingCodes,
                        });
                    }
                }
            }
        }
    }

    // E6: 设计器 section 嵌套兼容性 — 检测设计器对 section 子字段传递 true 而非 tableFieldMap 的问题
    if (inSection && convertedResult && tableFieldMap && Object.keys(tableFieldMap).length > 0) {
        const designerResult = mapFormula(formula, true);
        if (designerResult !== convertedResult) {
            issues.push({
                level: 'ERROR',
                ruleId: 'E6',
                ...baseInfo,
                convertedResult,
                errorDetail: `设计器兼容性: section 子字段公式转换结果与引擎不一致。引擎正确结果="${convertedResult}", 设计器结果="${designerResult}"。原因: flow_form_design.ejs 对 section 子字段传递 true 而非 tableFieldMap`,
                siblingFields: siblingCodes,
            });
        }
    }

    // W1: 除零风险（含除法且除数是字段引用）
    if (/\/\s*\{[^{}]+\}/.test(formula)) {
        issues.push({
            level: 'WARN',
            ruleId: 'W1',
            ...baseInfo,
            convertedResult,
            errorDetail: '公式含除法且除数为字段引用，存在除零风险',
            siblingFields: siblingCodes,
        });
    }

    return { issues, convertedResult };
}

/**
 * 检测同表单内 W2 字段名冲突：不同字段 getSafeCode 后结果相同。
 * @param {string[]} codes 字段 code 列表
 * @returns {Map<string, string[]>} safeCode → 原始 code 列表（仅冲突项）
 */
function detectSafeCodeConflicts(codes) {
    const safeMap = new Map();
    for (const code of codes) {
        const safe = getSafeCode(code);
        if (!safeMap.has(safe)) safeMap.set(safe, []);
        safeMap.get(safe).push(code);
    }
    const conflicts = new Map();
    for (const [safe, originals] of safeMap) {
        if (originals.length > 1) conflicts.set(safe, originals);
    }
    return conflicts;
}

// ─── 字段遍历 ─────────────────────────────────────────────────────────────────

/**
 * 递归遍历字段列表，收集含公式的字段信息。
 *
 * @param {object[]} fields
 * @param {object}   tableFieldMap   顶层 getTableFieldMap 结果
 * @param {string}   locationPrefix  "top" 或 "table:{tableCode}"
 * @param {boolean}  inSection       是否嵌套在 section 内部
 * @returns {object[]} 每项包含 field, formula, source, location, siblingCodes, inSection
 */
function collectFormulaFields(fields, tableFieldMap, locationPrefix, inSection = false) {
    if (!Array.isArray(fields)) return [];
    const results = [];

    // 同层级字段 code（用于 siblingFields）
    const siblingCodes = fields.map(f => f.code).filter(Boolean);

    for (const field of fields) {
        // 递归处理嵌套字段（section / table）
        if (Array.isArray(field.fields)) {
            const childLocation = field.type === 'table'
                ? `table:${field.code}`
                : locationPrefix;
            const childInSection = field.type === 'section' || inSection;
            results.push(...collectFormulaFields(field.fields, tableFieldMap, childLocation, childInSection));
        }

        // formula 属性
        if (field.formula && typeof field.formula === 'string' && field.formula.trim()) {
            results.push({
                field,
                formula: field.formula.trim(),
                source: 'formula',
                location: locationPrefix,
                siblingCodes: siblingCodes.filter(c => c !== field.code),
                inSection,
            });
        }

        // default_value 含公式（含 {字段名} 引用才视为公式）
        if (field.default_value && typeof field.default_value === 'string'
            && field.default_value.trim()
            && /\{[^{}]+\}/.test(field.default_value)) {
            results.push({
                field,
                formula: field.default_value.trim(),
                source: 'default_value',
                location: locationPrefix,
                siblingCodes: siblingCodes.filter(c => c !== field.code),
                inSection,
            });
        }
    }

    return results;
}

// ─── 表单扫描 ─────────────────────────────────────────────────────────────────

/**
 * 扫描单个表单文档，返回该表单所有公式字段的检测结果。
 * @param {object} form MongoDB forms 文档
 * @returns {{ allFormulas: object[], issues: object[] }}
 */
function scanForm(form) {
    const formId = String(form._id);
    const formName = form.name || '';
    const allFormulas = [];
    const issues = [];

    // 已处理的公式去重 key：current 优先，history 中若已有 current 则跳过
    // 使用 JSON.stringify 避免字段内容含分隔符导致 key 碰撞
    const seenKeys = new Set();

    /**
     * 处理一个版本的 fields 列表
     * @param {object[]} fields
     * @param {"current"|"history"} versionType
     */
    function processVersion(fields, versionType) {
        if (!Array.isArray(fields)) return;

        const tableFieldMap = getTableFieldMap(fields);
        const formulaFields = collectFormulaFields(fields, tableFieldMap, 'top');

        for (const { field, formula, source, location, siblingCodes, inSection } of formulaFields) {
            const dedupeKey = JSON.stringify({ code: field.code, location, source, formula });

            // current 版本直接处理，history 版本若 current 已有则跳过
            if (versionType === 'history' && seenKeys.has(dedupeKey)) continue;
            seenKeys.add(dedupeKey);

            const { issues: fieldIssues, convertedResult } = checkFormula({
                formula,
                tableFieldMap,
                formId,
                formName,
                formVersionType: versionType,
                field,
                fieldLocation: location,
                formulaSource: source,
                siblingCodes,
                inSection,
            });

            const status = fieldIssues.length > 0
                ? fieldIssues[0].level  // ERROR 或 WARN
                : 'PASS';

            allFormulas.push({
                formId,
                formName,
                formVersionType: versionType,
                fieldCode: field.code || '',
                fieldLocation: location,
                formulaSource: source,
                originalFormula: formula,
                convertedResult: convertedResult !== undefined ? convertedResult : null,
                status,
            });

            issues.push(...fieldIssues);
        }
    }

    // 处理 current 版本
    if (form.current && Array.isArray(form.current.fields)) {
        processVersion(form.current.fields, 'current');
    }

    // 处理历史版本
    if (Array.isArray(form.historys)) {
        for (const history of form.historys) {
            if (Array.isArray(history.fields)) {
                processVersion(history.fields, 'history');
            }
        }
    }

    // W2: 字段名冲突检测（仅基于 current 版本顶层字段）
    if (form.current && Array.isArray(form.current.fields)) {
        const allCodes = form.current.fields.map(f => f.code).filter(Boolean);
        const conflicts = detectSafeCodeConflicts(allCodes);
        for (const [safeCode, originals] of conflicts) {
            issues.push({
                level: 'WARN',
                ruleId: 'W2',
                formId,
                formName,
                formVersionType: 'current',
                fieldCode: originals.join(' / '),
                fieldType: '',
                fieldLocation: 'top',
                formulaSource: '',
                originalFormula: '',
                convertedResult: null,
                errorDetail: `字段名冲突：${originals.join('、')} 经 getSafeCode 后均为 "${safeCode}"`,
                siblingFields: allCodes,
            });
        }
    }

    return { allFormulas, issues };
}

// ─── 主流程 ───────────────────────────────────────────────────────────────────

async function main() {
    const mongoUrl = process.env.FORMULA_SCAN_MONGO_URL || process.env.MONGO_URL;
    if (!mongoUrl) {
        console.error('错误：请设置环境变量 FORMULA_SCAN_MONGO_URL 或 MONGO_URL');
        process.exit(1);
    }

    const scanLabel = process.env.SCAN_LABEL || '';
    const dateStr = todayStr();
    const isoDate = todayISODate();

    const reportFileName = scanLabel
        ? `scan-result-${scanLabel}-${dateStr}.json`
        : `scan-result-${dateStr}.json`;
    const reportDir = path.resolve(__dirname, 'reports/formula-scan');
    const reportPath = path.join(reportDir, reportFileName);
    const reportRelPath = `main/default/test/reports/formula-scan/${reportFileName}`;

    // 确保输出目录存在
    if (!fs.existsSync(reportDir)) {
        fs.mkdirSync(reportDir, { recursive: true });
    }

    // 连接 MongoDB
    let client;
    let MongoClient;
    try {
        MongoClient = require('mongodb').MongoClient;
    } catch (e) {
        console.error('错误：未找到 mongodb 模块，请先运行 npm install mongodb');
        process.exit(1);
    }

    console.log(`正在连接 MongoDB: ${redactMongoUrl(mongoUrl)}`);
    try {
        client = new MongoClient(mongoUrl);
        await client.connect();
    } catch (err) {
        console.error(`MongoDB 连接失败: ${err.message}`);
        process.exit(1);
    }

    try {
        const dbName = mongoUrl.split('/').pop().split('?')[0];
        const db = client.db(dbName);
        const formsCollection = db.collection('forms');

        console.log('正在读取 forms 集合...');
        const forms = await formsCollection.find({}, {
            projection: { _id: 1, name: 1, 'current.fields': 1, historys: 1 }
        }).toArray();

        console.log(`读取到 ${forms.length} 个表单，开始扫描...`);

        // 汇总统计
        let totalForms = forms.length;
        let formsWithFormula = 0;
        let totalFormulaFields = 0;
        const bySource = { formula: 0, default_value: 0 };
        const byStatus = { ERROR: 0, WARN: 0, PASS: 0 };
        const byVersion = { current: 0, history: 0 };
        const errorFormIds = new Set();
        const warnFormIds = new Set();

        const allErrors = [];
        const allWarnings = [];
        const allFormulasDeduped = [];

        for (const form of forms) {
            const { allFormulas, issues } = scanForm(form);

            if (allFormulas.length > 0) {
                formsWithFormula++;
                totalFormulaFields += allFormulas.length;
                for (const f of allFormulas) {
                    bySource[f.formulaSource] = (bySource[f.formulaSource] || 0) + 1;
                    byStatus[f.status] = (byStatus[f.status] || 0) + 1;
                    byVersion[f.formVersionType] = (byVersion[f.formVersionType] || 0) + 1;
                }
                allFormulasDeduped.push(...allFormulas);
            }

            if (issues.length > 0) {
                for (const issue of issues) {
                    if (issue.level === 'ERROR') {
                        errorFormIds.add(issue.formId);
                        allErrors.push(issue);
                    } else if (issue.level === 'WARN') {
                        warnFormIds.add(issue.formId);
                        allWarnings.push(issue);
                    }
                }
            }
        }

        // 补充 W2 到 byStatus（W2 不走 allFormulas 流程，需单独计入）
        const w2Count = allWarnings.filter(i => i.ruleId === 'W2').length;
        if (w2Count > 0) {
            byStatus.WARN = (byStatus.WARN || 0) + w2Count;
        }

        // 统计 ERROR 的版本分布
        const errorsByVersion = { current: 0, history: 0 };
        for (const e of allErrors) {
            errorsByVersion[e.formVersionType] = (errorsByVersion[e.formVersionType] || 0) + 1;
        }

        // 将已知问题从 allErrors 中分离出来
        const realErrors = [];
        const knownIssuesList = [];
        for (const e of allErrors) {
            const key = `${e.formId}::${e.fieldCode}`;
            const known = KNOWN_ISSUES[key];
            if (known) {
                knownIssuesList.push({
                    ...e,
                    level: 'KNOWN_ISSUE',
                    category: known.category,
                    resolution: known.resolution,
                });
            } else {
                realErrors.push(e);
            }
        }

        // 重算 ERROR 相关统计（排除已知问题）
        const realErrorFormIds = new Set(realErrors.map(e => e.formId));
        const realErrorsByVersion = { current: 0, history: 0 };
        for (const e of realErrors) {
            realErrorsByVersion[e.formVersionType] = (realErrorsByVersion[e.formVersionType] || 0) + 1;
        }
        byStatus.ERROR = realErrors.length;
        byStatus.KNOWN_ISSUE = knownIssuesList.length;

        // 生成报告
        const report = {
            scanDate: isoDate,
            mongoUrl: redactMongoUrl(mongoUrl),
            summary: {
                totalForms,
                formsWithFormula,
                totalFormulaFields,
                bySource,
                byVersion,
                byStatus,
                errorsByVersion: realErrorsByVersion,
                errorFormCount: realErrorFormIds.size,
                warnFormCount: warnFormIds.size,
                knownIssueCount: knownIssuesList.length,
            },
            errors: realErrors,
            knownIssues: knownIssuesList,
            warnings: allWarnings,
            allFormulas: allFormulasDeduped,
        };

        fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');

        // 控制台输出简要统计
        console.log('');
        console.log('扫描完成');
        console.log(`总表单数: ${totalForms}`);
        console.log(`含公式字段的表单: ${formsWithFormula}`);
        console.log(`公式字段总数: ${totalFormulaFields}`);
        console.log(`  - formula 字段: ${bySource.formula || 0}`);
        console.log(`  - default_value 公式字段: ${bySource.default_value || 0}`);
        console.log(`  - 来自 current 版本: ${byVersion.current || 0}`);
        console.log(`  - 来自 history 版本: ${byVersion.history || 0}`);
        console.log('');
        console.log('检测结果:');
        console.log(`  ERROR: ${realErrors.length} 条（涉及 ${realErrorFormIds.size} 个表单，current: ${realErrorsByVersion.current || 0}，history: ${realErrorsByVersion.history || 0}）`);
        console.log(`  KNOWN_ISSUE: ${knownIssuesList.length} 条（经人工审核确认无需代码修正）`);
        console.log(`  WARN: ${byStatus.WARN || 0} 条（涉及 ${warnFormIds.size} 个表单）`);
        console.log(`  PASS: ${byStatus.PASS || 0} 条`);
        console.log('');
        console.log(`报告已输出到: ${reportRelPath}`);

    } finally {
        await client.close();
    }
}

main().catch((err) => {
    console.error('扫描失败:', err.message);
    process.exit(1);
});
