package com.uas.search.console.b2b.service.impl;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.fastjson.JSONObject;
import com.uas.search.b2b.exception.SearchException;
import com.uas.search.b2b.model.PageParams;
import com.uas.search.b2b.model.SPage;
import com.uas.search.b2b.service.SearchService.Table_name;
import com.uas.search.console.b2b.DruidDBConfiguration;
import com.uas.search.console.b2b.LuceneProperties;
import com.uas.search.console.b2b.jms.AQListener;
import com.uas.search.console.b2b.jms.LuceneQueueMessage;
import com.uas.search.console.b2b.jms.LuceneQueueMessageDao;
import com.uas.search.console.b2b.jms.QueueMessageParser;
import com.uas.search.console.b2b.model.PageInfo;
import com.uas.search.console.b2b.model.ParsedQueueMessage;
import com.uas.search.console.b2b.service.IndexService;
import com.uas.search.console.b2b.support.DownloadHelper;
import com.uas.search.console.b2b.support.DownloadService;
import com.uas.search.console.b2b.support.IndexSearcherManager;
import com.uas.search.console.b2b.support.IndexWriterManager;
import com.uas.search.console.b2b.util.ClassAndTableNameUtils;
import com.uas.search.console.b2b.util.FileUtils;
import com.uas.search.console.b2b.util.ObjectToDocumentUtils;
import com.uas.search.console.b2b.util.SearchConstants.DataSourceQualifier;
import com.uas.search.console.b2b.util.SearchUtils;
import com.uas.search.util.ArrayUtils;
import com.uas.search.util.CollectionUtils;
import com.uas.search.util.StringUtils;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;

import java.io.*;
import java.sql.SQLRecoverableException;
import java.util.*;

/**
 * 创建索引等
 * 
 * @author sunyj
 * @since 2016年11月9日 上午11:20:51
 */
@Service
public class IndexServiceImpl implements IndexService {

	@Autowired
	private AQListener aqListener;

	@Autowired
	private QueueMessageParser queueMessageParser;

	@Autowired
	private LuceneProperties luceneProperties;

	@Autowired
	private DruidDBConfiguration druidDBConfiguration;

	private IndexSearcherManager indexSearcherManager = IndexSearcherManager.getInstance();

	private Logger logger = LoggerFactory.getLogger(IndexServiceImpl.class);

	/**
	 * 从数据库获取数据时的分页大小
	 */
	public static final int PAGE_SIZE = 1000;

	/**
	 * 单个文件存储的最大数据数目，需是PAGE_SIZE的整数倍
	 */
	public static final int SINGLE_FILE_MAX_SIZE = PAGE_SIZE;

	/**
	 * 是否正在创建索引
	 */
	private boolean creatingIndex = false;

	private IndexWriter indexWriter;

	private static IndexWriterManager indexWriterManager = new IndexWriterManager();

	@Override
	public Long createIndexes(List<Table_name> tableNames, Boolean fromFiles, Boolean ifStartListener) {
		if (creatingIndex) {
			throw new SearchException("已存在线程在创建索引，不可重复请求");
		}
		creatingIndex = true;
		// 如果索引实时更新处于开启状态，需要关闭（以免两者同时操作索引出现问题）
		aqListener.stop(null);

		if (CollectionUtils.isEmpty(tableNames)) {
			tableNames = ClassAndTableNameUtils.getTableNames();
		}
		Long startTime = new Date().getTime();
		try {
			// 创建各个表的索引
			for (Table_name tableName : tableNames) {
				// 只有明确指定不从本地文件读取数据时，才从数据库获取数据建立索引
				if (fromFiles != null && !fromFiles) {
					createIndexes(ClassAndTableNameUtils.toClass(tableName));
				} else {
					createIndexesFromFiles(ClassAndTableNameUtils.toClass(tableName));
				}
				indexSearcherManager.flushCache(tableName, indexWriter, null);
			}
		} catch (Throwable e) {
			// 防止SQLRecoverableException导致应用终止
			throw new SearchException(e).setDetailedMessage(e);
		} finally {
			creatingIndex = false;
			if (ifStartListener == null || ifStartListener.booleanValue()) {
				aqListener.start(null, null);
			}
		}
		Long endTime = new Date().getTime();
		logger.info(String.format("索引创建成功， 共用时间%.2fs\n", (endTime - startTime) / 1000.0));
		return endTime - startTime;
	}

	/**
	 * 创建clazz实体对应表的索引
	 * 
	 * @param clazz
	 * @return 实际创建的索引的数目
	 * @throws IOException
	 * @throws InterruptedException
	 */
	private <T> Long createIndexes(Class<T> clazz) throws IOException, InterruptedException {
		Long startTime = new Date().getTime();
		Table_name tableName = ClassAndTableNameUtils.toTableName(clazz);
		logger.info("正在创建" + tableName.value() + "索引...");
		JpaRepository<T, Long> dao = ClassAndTableNameUtils.getDao(clazz);

		// 实际创建的索引数目（可能会有部分数据有问题，不会创建索引）
		long count = 0;
		// 数据库中数据的总数目
		long totalElements = 0L;
		try {
			deleteIndexes(tableName);
			// 分页获取数据以创建索引
			PageParams pageParams = new PageParams();
			pageParams.setPage(1);
			pageParams.setSize(PAGE_SIZE);
			PageInfo pageInfo = new PageInfo(pageParams);
			Page<T> pageResult = dao.findAll(pageInfo);

			totalElements = pageResult.getTotalElements();
			logger.info("发现数据：" + totalElements + "条");
			// 已翻页的数据数目
			Long size = 0L;
			// 用于记录上次提交索引时的创建进度
			double recordProgress = 0;

			while (totalElements > size) {
				List<T> content = pageResult.getContent();
				for (T element : content) {
					Document document = ObjectToDocumentUtils.toDocument(element);
					if (document != null) {
						indexWriter.addDocument(document);
						count++;
					}
				}
				size += content.size();

				// 器件索引的创建进度（百分比）
				double indexProgress = size * 100.0 / totalElements;
				logger.info(String.format("Created...................%.2f%%", indexProgress));
				// 每创建5%，提交一次，避免内存耗尽，发生OutOfMemoryError
				if (indexProgress - recordProgress > 5) {
					indexWriter.commit();
					recordProgress = indexProgress;
				}

				pageParams.setPage(pageParams.getPage() + 1);
				pageInfo = new PageInfo(pageParams);
				pageResult = dao.findAll(pageInfo);
			}
			indexWriter.commit();
		} finally {
			indexWriterManager.release(tableName);
		}

		Long endTime = new Date().getTime();
		logger.info(String.format("创建%s索引：%s条，问题数据：%s条，耗时%.2fs\n", tableName.value(), count, (totalElements - count),
				(endTime - startTime) / 1000.0));
		return count;
	}

	/**
	 * 创建clazz实体对应表的索引
	 * 
	 * @param clazz
	 * @return 实际创建的索引的数目
	 * @throws InterruptedException
	 * @throws IOException
	 */
	private <T> Long createIndexesFromFiles(Class<T> clazz) throws InterruptedException, IOException {
		Table_name tableName = ClassAndTableNameUtils.toTableName(clazz);
		logger.info("正在创建" + tableName.value() + "索引...");
		// 数据的总数目
		Long totalElements = 0L;
		// 实际创建的索引数目
		Long count = 0L;
		try {
			// 从本地路径读取器件数据
			File file = new File(SearchUtils.getDataPath(tableName));
			if (!file.exists() || ArrayUtils.isEmpty(file.listFiles())) {
				logger.error("创建索引失败，原因：" + tableName.value() + "数据文件不存在！\n");
				return 0L;
			}
			deleteIndexes(tableName);
			File[] files = file.listFiles();
			// 将要创建的索引总数目约为：文件数目*单个文件的行数
			long totalSize = files.length * SINGLE_FILE_MAX_SIZE;
			for (File f : files) {
				logger.info("读取文件: " + f.getName());
				BufferedReader bufferedReader = new BufferedReader(new FileReader(f));
				String line = null;
				while (!StringUtils.isEmpty(line = bufferedReader.readLine())) {
					totalElements++;
					T element = JSONObject.parseObject(line, clazz);
					Document document = ObjectToDocumentUtils.toDocument(element);
					if (document != null) {
						count++;
						// 每创建10000条，打印一次进度
						if (count % 10000 == 0) {
							logger.info(String.format(tableName.value() + " indexed...................%.2f%%",
									count * 100.0 / totalSize));
						}
						indexWriter.addDocument(document);
					}
				}
				indexWriter.commit();
				bufferedReader.close();
			}
		} finally {
			indexWriterManager.release(tableName);
		}
		logger.info(tableName.value() + "数据总数：" + totalElements + "条，创建索引：" + count + "条，问题数据："
				+ (totalElements - count) + "条\n");
		return count;
	}

	/**
	 * 删除指定表的索引
	 * 
	 * @param tableName
	 *            表名
	 * @throws InterruptedException
	 * @throws IOException
	 */
	private void deleteIndexes(Table_name tableName) throws InterruptedException, IOException {
		indexWriter = indexWriterManager.get(tableName);
		logger.info("正在清理旧索引..." + tableName.value());
		indexWriter.deleteAll();
		logger.info("旧索引清理完毕..." + tableName.value());
	}

    @Override
    public Long downloadData(List<Table_name> tableNames) {
        Long startTime = new Date().getTime();
        // 参数为空，则下载所有需要建立索引的表的数据
        if (CollectionUtils.isEmpty(tableNames)) {
            tableNames = ClassAndTableNameUtils.getTableNames();
            SearchUtils.deleteDir(new File(luceneProperties.getDataDir()));
        }
        Long size = 0L;
        for (Table_name tableName : tableNames) {
            // 指定的表数目为1时才起作用
            size += downloadData(tableName, null);
        }
        Long endTime = new Date().getTime();
        logger.info(size + "条，耗时 " + (endTime - startTime) + " ms\n");
        return endTime - startTime;
    }

    @Override
    public Long downloadData(Table_name tableName, Integer startFileIndex) {
        return downloadData(ClassAndTableNameUtils.toClass(tableName), startFileIndex);
    }

	/**
	 * 下载指定实体类的数据
	 * 
	 * @param clazz
	 * @return 下载的数据的数目
	 */
	private <T> Long downloadData(Class<T> clazz, Integer startFileIndex) {
		int fileIndex = startFileIndex == null || startFileIndex < 1 ? 1 : startFileIndex;
		Long startTime = new Date().getTime();
		Table_name tableName = ClassAndTableNameUtils.toTableName(clazz);
		if (fileIndex == 1) {
			// 删除旧的文件
			FileUtils.deleteSubFiles(new File(SearchUtils.getDataPath(tableName)));
		}
		logger.info("Download... " + tableName.value());
		JpaRepository<T, Long> dao = ClassAndTableNameUtils.getDao(clazz);

		// 分页获取数据
		PageParams pageParams = new PageParams();
		pageParams.setPage((fileIndex - 1) * SINGLE_FILE_MAX_SIZE / PAGE_SIZE + 1);
		pageParams.setSize(PAGE_SIZE);
		PageInfo pageInfo = new PageInfo(pageParams);
		Page<T> pageResult = dao.findAll(pageInfo);

		// 数据库中数据的总数目
		if (pageResult.getTotalElements() < (fileIndex - 1) * SINGLE_FILE_MAX_SIZE) {
			throw new SearchException("startFileIndex不超过"
					+ ((int) Math.ceil(pageResult.getTotalElements() / (1.0 * SINGLE_FILE_MAX_SIZE))));
		}
		long totalElements = pageResult.getTotalElements() - (fileIndex - 1) * SINGLE_FILE_MAX_SIZE;
		logger.info("发现数据：" + totalElements + "条");
		// 已翻页的数据数目
		Long size = 0L;
		PrintWriter printWriter = null;
		int count = 0;
		try {
			File file = new File(SearchUtils.getDataPath(tableName));
			if (!file.exists()) {
				file.mkdirs();
			}
			printWriter = new PrintWriter(SearchUtils.getDataPath(tableName) + "/" + fileIndex + ".txt");
			while (totalElements > size) {
				// 一个文件存放SINGLE_FILE_MAX_SIZE条数据，一旦超过，写入新的文件
				if (count == SINGLE_FILE_MAX_SIZE) {
					count = 0;
					printWriter.flush();
					printWriter.close();
					fileIndex++;
					printWriter = new PrintWriter(SearchUtils.getDataPath(tableName) + "/" + fileIndex + ".txt");
				}
				List<T> content = pageResult.getContent();
				for (T element : content) {
					printWriter.println(JSONObject.toJSONString(element));
					count++;
				}
				size += content.size();
				logger.info(String.format(tableName.value() + " downloaded...................%.2f%%",
						size * 100.0 / totalElements));

				pageParams.setPage(pageParams.getPage() + 1);
				pageInfo = new PageInfo(pageParams);
				pageResult = dao.findAll(pageInfo);
			}
			printWriter.flush();
			printWriter.close();
		} catch (FileNotFoundException e) {
			logger.error("", e);
		}

		Long endTime = new Date().getTime();
		logger.info(String.format("下载数据%s条，耗时%.2fs\n ", totalElements, (endTime - startTime) / 1000.0));
		return totalElements;
	}

    @Override
    public long multiDownloadData(List<Table_name> tableNames, Integer threads, DownloadHelper.ValidateResult validateResult) {
        // 参数为空，则下载所有需要建立索引的表的数据
        if (CollectionUtils.isEmpty(tableNames)) {
            tableNames = ClassAndTableNameUtils.getTableNames();
            SearchUtils.deleteDir(new File(luceneProperties.getDataDir()));
        }
        long result = 0L;
        for (Table_name tableName : tableNames) {
            // 指定的表数目为1时才起作用
            result += multiDownloadData(tableName, threads, null, null, validateResult);
        }
        return result;
    }

    @Override
    public long multiDownloadData(Table_name tableName, Integer threads, Integer startFileIndex, Integer endFileIndex, DownloadHelper.ValidateResult validateResult) {
        return multiDownloadData(ClassAndTableNameUtils.toClass(tableName), threads, startFileIndex, endFileIndex, validateResult);
    }

    private <T> long multiDownloadData(Class<T> clazz, Integer threads, Integer startFileIndex, Integer endFileIndex, DownloadHelper.ValidateResult validateResult) {
        threads = threads == null || threads < 1 ? 1 : threads;
        if (threads > druidDBConfiguration.getMaxActive()) {
            throw new IllegalArgumentException("线程数量不可超过 " + druidDBConfiguration.getMaxActive());
        }
        DownloadHelper<T> downloadHelper = new DownloadHelper<>(threads, startFileIndex, endFileIndex, clazz, "id", ClassAndTableNameUtils.getDao(clazz), new DownloadService<T>(), validateResult);
        return downloadHelper.getResult();
    }

	@Override
	public <T> T save(T obj) {
		if (obj != null) {
			Document document = ObjectToDocumentUtils.toDocument(obj);
			if (document != null) {
				try {
					Table_name tableName = ClassAndTableNameUtils.toTableName(obj.getClass());
					indexWriter = indexWriterManager.get(tableName);
					indexWriter.addDocument(document);
					indexSearcherManager.flushCache(tableName, indexWriter, null);
					return obj;
				} catch (IOException | InterruptedException e) {
					logger.error("", e);
				} finally {
					indexWriterManager.release(ClassAndTableNameUtils.toTableName(obj.getClass()));
				}
			} else {
				logger.info("对象转为Document时为null：" + obj);
			}
		}
		return null;
	}

	@Override
	public <T> T update(T obj) {
		if (obj != null) {
			Document document = ObjectToDocumentUtils.toDocument(obj);
			if (document != null) {
				try {
					Table_name tableName = ClassAndTableNameUtils.toTableName(obj.getClass());
					indexWriter = indexWriterManager.get(tableName);
					indexWriter.updateDocument(new Term(ClassAndTableNameUtils.getIdField(obj.getClass()),
							String.valueOf(ClassAndTableNameUtils.getId(obj))), document);
					indexSearcherManager.flushCache(tableName, indexWriter, null);
					return obj;
				} catch (IOException | InterruptedException e) {
					logger.error("", e);
				} finally {
					indexWriterManager.release(ClassAndTableNameUtils.toTableName(obj.getClass()));
				}
			} else {
				logger.info("对象转为Document时为null：" + obj);
			}
		}
		return null;
	}

	@Override
	public <T> T delete(T obj) {
		if (obj != null) {
			try {
				Table_name tableName = ClassAndTableNameUtils.toTableName(obj.getClass());
				indexWriter = indexWriterManager.get(tableName);
				indexWriter.deleteDocuments(new Term(ClassAndTableNameUtils.getIdField(obj.getClass()),
						String.valueOf(ClassAndTableNameUtils.getId(obj))));
				indexSearcherManager.flushCache(tableName, indexWriter, null);
				return obj;
			} catch (IOException e) {
				logger.error("", e);
			} catch (InterruptedException e) {
				logger.error("", e);
			} finally {
				indexWriterManager.release(ClassAndTableNameUtils.toTableName(obj.getClass()));
			}
		}
		return null;
	}

	@Override
	public List<Object> maintainIndexes(ParsedQueueMessage parsedQueueMessage) {
		if (parsedQueueMessage == null) {
			throw new SearchException("参数不能为空：parsedQueueMessage");
		}

		Object[] objects = parsedQueueMessage.getObjects();
		if (objects == null) {
			return null;
		}
		List<Object> maintainedObjects = new ArrayList<>();
		try {
			// 新增、更新索引
			if (parsedQueueMessage.isInsert() || parsedQueueMessage.isUpdate()) {
				for (Object object : objects) {
					Object maintainedObject = update(object);
					if (maintainedObject != null) {
						maintainedObjects.add(maintainedObject);
					}
				}
			}
			// 删除索引
			else if (parsedQueueMessage.isDelete()) {
				for (Object object : objects) {
					Object maintainedObject = delete(object);
					if (maintainedObject != null) {
						maintainedObjects.add(maintainedObject);
					}
				}
			} else {
				throw new SearchException("message parsing failed!");
			}
		} catch (Throwable e) {
			throw new SearchException(e).setDetailedMessage(e);
		}
		return maintainedObjects;
	}

	@Override
	public List<Object> maintainIndexesByIds(String tableName, String ids, String method) {
		if (StringUtils.isEmpty(tableName) || StringUtils.isEmpty(ids) || StringUtils.isEmpty(method)) {
			throw new SearchException("参数不能为空：tableName，ids，method");
		}
		Map<String, Object> map = new HashMap<>();
		map.put("table", tableName);
		String[] strs = ids.split(",");
		List<Long> idList = new ArrayList<>();
		for (String str : strs) {
			idList.add(Long.parseLong(str));
		}
		map.put("ids", idList);
		map.put("method", method);
		ParsedQueueMessage parsedQueueMessage = queueMessageParser.parse(JSONObject.toJSONString(map));

		if (parsedQueueMessage == null) {
			logger.error("message parsing failed!");
			return null;
		}
		return maintainIndexes(parsedQueueMessage);
	}

	@Override
	public List<Object> maintainIndexesByData(String tableName, String data, String method) {
		if (StringUtils.isEmpty(tableName) || StringUtils.isEmpty(data) || StringUtils.isEmpty(method)) {
			throw new SearchException("参数不能为空：tableName，data，method");
		}
		// data为json格式数据，并非简单的一个字符串，通过JSONObject构造，双引号、左括号等会被特殊处理
		String message = "{\"table\":\"" + tableName + "\", \"data\":" + data + ", \"method\":\"" + method + "\"}";
		ParsedQueueMessage parsedQueueMessage = queueMessageParser.parse(message);

		if (parsedQueueMessage == null) {
			logger.error("message parsing failed!");
			return null;
		}
		return maintainIndexes(parsedQueueMessage);
	}

	@Override
	public SPage<LuceneQueueMessage> getListenDetails(DataSourceQualifier dataSourceQualifier, Integer page,
			Integer size, String searchContent) {
		try {
			DruidDataSource dataSource = DruidDBConfiguration.getDataSource(dataSourceQualifier);
			return new LuceneQueueMessageDao(dataSource).findAll(page, size, searchContent);
		} catch (SQLRecoverableException e) {
			throw new SearchException(e).setDetailedMessage(e);
		}
	}

	@Override
	public boolean dequeueLuceneQueueMessage(DataSourceQualifier dataSourceQualifier, String messageId) {
		try {
			DruidDataSource dataSource = DruidDBConfiguration.getDataSource(dataSourceQualifier);
			return new LuceneQueueMessageDao(dataSource).dequeueLuceneQueueMessage(messageId);
		} catch (SQLRecoverableException e) {
			throw new SearchException(e).setDetailedMessage(e);
		}
	}

}
