#!/usr/bin/env node
const fs = require('fs');
const path = require('path');

// 尝试加载xlsx模块，如果不存在则提供友好的错误信息
// 注意：xlsx模块支持读取.xls格式文件
let xlsx;
try {
	xlsx = require('xlsx');
} catch (e) {
	console.error('Error: xlsx module not found. Please install it with: npm install xlsx');
	console.error('Note: This script only supports .xls format files, not .xlsx');
	process.exit(1);
}

// Usage:
//   node gen_pojo_from_xls_json.js <sourceJsonDir> <sourceXlsDir> <packageName> <outputDir> [Tables...] [--ignore=a,b,c]
// Tables can be names with or without .json/.xls, case-insensitive. Multiple can be space- or comma-separated.
// Note: Only .xls format is supported for Excel files, not .xlsx
const [,, jsonDir, xlsDir, packageName, outputDir, ...rest] = process.argv;

if (!jsonDir || !xlsDir || !packageName || !outputDir) {
	console.log('Usage: node gen_pojo_from_xls_json.js <sourceJsonDir> <sourceXlsDir> <packageName> <outputDir> [Tables...] [--ignore=a,b,c]');
	process.exit(1);
}

function normalizeName(n) {
	return String(n).toLowerCase().replace(/\.[^.]+$/, '');
}

function parseArgs(args) {
	const whitelist = [];
	const blacklist = [];
	for (let i = 0; i < args.length; i++) {
		const a = String(args[i]);
		if (a.startsWith('--ignore=')) {
			const v = a.substring('--ignore='.length);
			v.split(',').map(s => s.trim()).filter(Boolean).forEach(x => blacklist.push(normalizeName(x)));
			continue;
		}
		if (a === '--ignore' && i + 1 < args.length) {
			const v = String(args[++i]);
			v.split(',').map(s => s.trim()).filter(Boolean).forEach(x => blacklist.push(normalizeName(x)));
			continue;
		}
		whitelist.push(normalizeName(a));
	}
	return { whitelist: new Set(whitelist.filter(Boolean)), blacklist: new Set(blacklist) };
}

const { whitelist: whiteSet, blacklist: blackSet } = parseArgs(rest);

function listJsonFiles(dir) {
	return fs.readdirSync(dir)
		.filter(name => name.toLowerCase().endsWith('.json'))
		.map(name => path.join(dir, name));
}

function readJson(filePath) {
	let content = fs.readFileSync(filePath, 'utf8');
	if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
	return JSON.parse(content);
}

function pickSample(root) {
	if (Array.isArray(root)) return root.length > 0 ? root[0] : null;
	if (root && typeof root === 'object') {
		const keys = Object.keys(root);
		return keys.length > 0 ? root[keys[0]] : null;
	}
	return null;
}

function analyzeAllSamples(root) {
	if (Array.isArray(root)) {
		return root; // 分析所有样本
	}
	if (root && typeof root === 'object') {
		const keys = Object.keys(root);
		return keys.map(k => root[k]); // 分析所有样本
	}
	return [];
}

function upperFirst(str) {
	if (!str) return str;
	return str.charAt(0).toUpperCase() + str.slice(1);
}

// Convert filename like M_item.json -> MItem
function toClassName(fileName) {
	const base = fileName.replace(/\.[^.]+$/, '');
	return base.split(/[_\-\s]+/).filter(Boolean).map(upperFirst).join('');
}

function isInteger(num) {
	return typeof num === 'number' && Number.isInteger(num);
}

function isLong(num) {
	return typeof num === 'number' && Number.isInteger(num) && (num > 2147483647 || num < -2147483648);
}

function isFloat(num) {
	return typeof num === 'number' && !Number.isInteger(num);
}

// 检查字符串是否包含小数点（用于识别BigDecimal类型）
function hasDecimalPoint(str) {
	return typeof str === 'string' && str.includes('.') && !isNaN(Number(str));
}

// 检查字符串是否为有效的数字格式
function isValidNumberString(str) {
	if (typeof str !== 'string') return false;
	const trimmed = str.trim();
	if (trimmed === '') return false;
	return !isNaN(Number(trimmed)) && isFinite(Number(trimmed));
}

function inferJavaType(value) {
	if (value === null || value === undefined) return 'String';
	if (typeof value === 'number') {
		if (isLong(value)) return 'Long';
		if (isInteger(value)) return 'Integer';
		if (isFloat(value)) return 'BigDecimal';
		return 'BigDecimal';
	}
	if (typeof value === 'boolean') return 'Boolean';
	if (typeof value === 'string') return 'String';
	if (Array.isArray(value)) {
		if (value.length === 0) return 'List<Object>';
		return `List<${inferJavaType(value[0])}>`;
	}
	if (typeof value === 'object') return 'Object';
	return 'String';
}

function inferJavaTypeFromSamples(samples, fieldName) {
	if (samples.length === 0) return 'String';
	
	// 统计所有样本的类型
	const typeStats = {
		null: 0,
		undefined: 0,
		number: 0,
		boolean: 0,
		string: 0,
		array: 0,
		object: 0
	};
	
	const numberValues = [];
	const stringValues = [];
	const arrayValues = [];
	
	for (const sample of samples) {
		if (sample === null) {
			typeStats.null++;
		} else if (sample === undefined) {
			typeStats.undefined++;
		} else if (typeof sample === 'number') {
			typeStats.number++;
			numberValues.push(sample);
		} else if (typeof sample === 'boolean') {
			typeStats.boolean++;
		} else if (typeof sample === 'string') {
			typeStats.string++;
			stringValues.push(sample);
		} else if (Array.isArray(sample)) {
			typeStats.array++;
			arrayValues.push(sample);
		} else if (typeof sample === 'object') {
			typeStats.object++;
		}
	}
	
	// 找出主要类型
	const total = samples.length;
	const maxType = Object.keys(typeStats).reduce((a, b) => typeStats[a] > typeStats[b] ? a : b);
	const maxRatio = typeStats[maxType] / total;
	
	// 如果主要类型占比超过80%，使用该类型
	if (maxRatio >= 0.8) {
		switch (maxType) {
			case 'number':
				if (numberValues.length === 0) return 'Integer';
				// 检查是否需要Long类型
				const hasLong = numberValues.some(n => isLong(n));
				const hasFloat = numberValues.some(n => isFloat(n));
				if (hasLong) return 'Long';
				if (hasFloat) return 'BigDecimal';
				return 'Integer';
			case 'boolean':
				return 'Boolean';
			case 'string':
				// 检查字符串是否都是数字格式
				const allNumeric = stringValues.every(s => isValidNumberString(s));
				if (allNumeric && stringValues.length > 0) {
					const allNumbers = stringValues.map(s => Number(s));
					const hasLong = allNumbers.some(n => isLong(n));
					const hasFloat = allNumbers.some(n => isFloat(n));
					// 检查原始字符串是否包含小数点
					const hasDecimalPointInStrings = stringValues.some(s => hasDecimalPoint(s));
					if (hasLong) return 'Long';
					if (hasFloat || hasDecimalPointInStrings) return 'BigDecimal';
					return 'Integer';
				}
				return 'String';
			case 'array':
				if (arrayValues.length === 0) return 'List<Object>';
				// 分析数组元素类型
				const allArrayElements = [];
				for (const arr of arrayValues) {
					allArrayElements.push(...arr.slice(0, 3)); // 取前3个元素
				}
				if (allArrayElements.length === 0) return 'List<Object>';
				const elementType = inferJavaTypeFromSamples(allArrayElements, fieldName + '_element');
				return `List<${elementType}>`;
			case 'object':
				return 'Object';
			default:
				return 'String';
		}
	}
	
	// 混合类型，优先选择数字类型（可能是字符串形式的数字）
	if (typeStats.number > 0 && typeStats.string > 0) {
		// 检查字符串是否都是有效数字
		const allNumeric = stringValues.every(s => isValidNumberString(s));
		if (allNumeric) {
			const allNumbers = [...numberValues, ...stringValues.map(s => Number(s))];
			const hasLong = allNumbers.some(n => isLong(n));
			const hasFloat = allNumbers.some(n => isFloat(n));
			// 检查原始字符串是否包含小数点
			const hasDecimalPointInStrings = stringValues.some(s => hasDecimalPoint(s));
			if (hasLong) return 'Long';
			if (hasFloat || hasDecimalPointInStrings) return 'BigDecimal';
			return 'Integer';
		}
	}
	
	// 如果只有字符串类型，检查是否都是数字格式
	if (typeStats.string > 0 && typeStats.number === 0) {
		const allNumeric = stringValues.every(s => isValidNumberString(s));
		if (allNumeric && stringValues.length > 0) {
			const allNumbers = stringValues.map(s => Number(s));
			const hasLong = allNumbers.some(n => isLong(n));
			const hasFloat = allNumbers.some(n => isFloat(n));
			// 检查原始字符串是否包含小数点
			const hasDecimalPointInStrings = stringValues.some(s => hasDecimalPoint(s));
			if (hasLong) return 'Long';
			if (hasFloat || hasDecimalPointInStrings) return 'BigDecimal';
			return 'Integer';
		}
	}
	
	// 默认返回String
	return 'String';
}

const JAVA_KEYWORDS = new Set([
	'abstract','assert','boolean','break','byte','case','catch','char','class','const','continue','default','do','double','else','enum','extends','final','finally','float','for','goto','if','implements','import','instanceof','int','interface','long','native','new','package','private','protected','public','return','short','static','strictfp','super','switch','synchronized','this','throw','throws','transient','try','void','volatile','while','null','true','false'
]);

function sanitizeFieldName(name) {
	let safe = name.replace(/[^A-Za-z0-9_]/g, '_');
	if (!/^[A-Za-z_]/.test(safe)) safe = '_' + safe;
	if (JAVA_KEYWORDS.has(safe)) safe = safe + '_';
	return safe;
}

function readXlsComments(xlsFile) {
	try {
		const wb = xlsx.readFile(xlsFile);
		const sheetName = wb.SheetNames[0];
		const ws = wb.Sheets[sheetName];
		const rows = xlsx.utils.sheet_to_json(ws, { header: 1, raw: false });
		// Heuristics: within first 5 rows, pick field row as the one with max ASCII ratio and contains 'ID' or many word tokens
		const maxCheck = Math.min(5, rows.length);
		let fieldRowIdx = -1, commentRowIdx = -1;
		let bestScore = -1;
		for (let i = 0; i < maxCheck; i++) {
			const row = rows[i] || [];
			const joined = row.join(' ').trim();
			if (!joined) continue;
			const asciiCount = (joined.match(/[\x20-\x7E]/g) || []).length;
			const ratio = asciiCount / Math.max(1, joined.length);
			const wordCount = (joined.match(/[A-Za-z_][A-Za-z0-9_]*/g) || []).length;
			const score = ratio * 0.7 + Math.min(wordCount / Math.max(1, row.length), 1) * 0.3 + (joined.includes('ID') ? 0.2 : 0);
			if (score > bestScore) { bestScore = score; fieldRowIdx = i; }
		}
		// Comment row: prefer a different row with lower ASCII ratio (more likely Chinese)
		let bestCR = -1;
		for (let i = 0; i < maxCheck; i++) {
			if (i === fieldRowIdx) continue;
			const row = rows[i] || [];
			const joined = row.join(' ').trim();
			if (!joined) continue;
			const asciiCount = (joined.match(/[\x20-\x7E]/g) || []).length;
			const ratio = asciiCount / Math.max(1, joined.length);
			const score = 1 - ratio; // prefer non-ASCII
			if (score > bestCR) { bestCR = score; commentRowIdx = i; }
		}
		const fields = rows[fieldRowIdx] || [];
		const comments = rows[commentRowIdx] || [];
		const map = new Map();
		for (let i = 0; i < fields.length; i++) {
			const k = String(fields[i] || '').trim();
			if (!k) continue;
			const v = String(comments[i] || '').trim();
			if (k) map.set(k, v);
		}
		return { sheetName, comments: map };
	} catch (e) {
		return { sheetName: '', comments: new Map() };
	}
}

function findXlsForBase(xlsDir, baseName) {
	// Only try .xls format
	const xlsFile = path.join(xlsDir, baseName + '.xls');
	if (fs.existsSync(xlsFile)) return xlsFile;
	
	// Case-insensitive search fallback for .xls files only
	const files = fs.readdirSync(xlsDir).filter(n => /\.xls$/i.test(n));
	for (const n of files) {
		if (normalizeName(n) === normalizeName(baseName)) return path.join(xlsDir, n);
	}
	return null;
}

function generateJavaClass(pkg, className, fieldsOrder, fieldTypes, fieldComments, classComment, jsonFileName) {
	const lines = [];
	lines.push(`package ${pkg};`);
	lines.push('');
	lines.push('import lombok.Data;');
	
	// Only add imports that are actually used
	const usedTypes = new Set();
	for (const fname of fieldsOrder) {
		const type = fieldTypes.get(fname) || 'String';
		if (type === 'List<Integer>') {
			usedTypes.add('java.util.List');
		} else if (type === 'List<String>') {
			usedTypes.add('java.util.List');
		} else if (type === 'List<Object>') {
			usedTypes.add('java.util.List');
		} else if (type === 'List<BigDecimal>') {
			usedTypes.add('java.util.List');
			usedTypes.add('java.math.BigDecimal');
		} else if (type === 'List<Long>') {
			usedTypes.add('java.util.List');
		} else if (type === 'BigDecimal') {
			usedTypes.add('java.math.BigDecimal');
		}
	}
	
	// Add only the imports that are actually used
	usedTypes.forEach(importType => {
		lines.push(`import ${importType};`);
	});
	
	if (usedTypes.size > 0) {
		lines.push('');
	}
	
	if (classComment) {
		lines.push('/**');
		lines.push(` * 配置表：${classComment}`);
		lines.push(' */');
	}
	lines.push('@Data');
	lines.push(`public class ${className} {`);
	
	// Add path field using the actual JSON filename
	lines.push('\t/** JSON 配置文件路径 */');
	lines.push('\tpublic static final String path = "' + jsonFileName + '";');
	lines.push('');
	
	// Add fields
	for (const fname of fieldsOrder) {
		const type = fieldTypes.get(fname) || 'String';
		const cmt = fieldComments.get(fname);
		const safe = sanitizeFieldName(fname);
		if (cmt) {
			lines.push(`\t/** ${cmt} */`);
		}
		if (safe !== fname) {
			lines.push(`\t@JsonProperty("${fname}")`);
		}
		lines.push(`\tpublic ${type} ${safe};`);
	}
	
	lines.push('');
	lines.push('}');
	return lines.join('\n');
}

(function main() {
	if (!fs.existsSync(jsonDir)) {
		console.error('JSON dir not found:', jsonDir);
		process.exit(2);
	}
	if (!fs.existsSync(xlsDir)) {
		console.error('XLS dir not found:', xlsDir);
		process.exit(2);
	}
	const outPkgDir = path.join(outputDir, packageName.replace(/\./g, path.sep));
	fs.mkdirSync(outPkgDir, { recursive: true });
	let files = listJsonFiles(jsonDir);
	// Apply whitelist filter if provided
	if (whiteSet.size > 0) {
		files = files.filter(f => whiteSet.has(normalizeName(path.basename(f))));
	}
	// Apply blacklist filter if provided
	if (blackSet.size > 0) {
		files = files.filter(f => !blackSet.has(normalizeName(path.basename(f))));
	}
	if (files.length === 0) {
		console.log('No JSON files to process.');
		process.exit(0);
	}
	let generated = 0;
	for (const jf of files) {
		try {
			const base = path.basename(jf).replace(/\.[^.]+$/, '');
			const jsonRoot = readJson(jf);
			const sample = pickSample(jsonRoot);
			if (!sample || typeof sample !== 'object') { console.warn('Skip (no object sample):', jf); continue; }
			
			// 分析所有样本以获得更准确的类型推断
			const allSamples = analyzeAllSamples(jsonRoot);
			if (allSamples.length === 0) { console.warn('Skip (no samples):', jf); continue; }
			
			const xlsPath = findXlsForBase(xlsDir, base);
			const { sheetName, comments } = xlsPath ? readXlsComments(xlsPath) : { sheetName: '', comments: new Map() };
			const className = toClassName(base) + 'Pojo';
			const fieldsOrder = Object.keys(sample);
			const fieldTypes = new Map();
			
			// 使用多样本分析进行类型推断
			for (const fieldName of fieldsOrder) {
				const fieldSamples = allSamples.map(s => s[fieldName]).filter(v => v !== undefined);
				const inferredType = inferJavaTypeFromSamples(fieldSamples, fieldName);
				fieldTypes.set(fieldName, inferredType);
				
				// 调试输出
				if (fieldSamples.length > 0) {
					console.log(`  ${fieldName}: ${inferredType} (samples: ${fieldSamples.length}, types: ${[...new Set(fieldSamples.map(v => typeof v))].join(', ')})`);
				}
			}
			
			const classComment = sheetName ? `${base}（${sheetName}）` : `${base}`;
			const jsonFileName = path.basename(jf);
			const content = generateJavaClass(packageName, className, fieldsOrder, fieldTypes, comments, classComment, jsonFileName);
			const outPath = path.join(outPkgDir, `${className}.java`);
			fs.writeFileSync(outPath, content, 'utf8');
			generated++;
			console.log('Generated', outPath);
		} catch (e) {
			console.warn('Failed for', jf, e.message);
		}
	}
	console.log(`Done. Generated ${generated} classes.`);
})(); 