package nccloud.impl.gl.voucherapi;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import nc.bs.logging.Logger;
import nc.itf.gl.voucher.IVoucher;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import nc.bs.dao.BaseDAO;
import nc.bs.framework.common.InvocationInfoProxy;
import nc.bs.framework.common.NCLocator;
import nc.bs.framework.comn.NetStreamContext;
import nc.jdbc.framework.processor.MapListProcessor;
import nc.service.rest.utils.ConvertVoucherUtil;
import nc.service.rest.utils.TranslateVoucherUtil;
import nc.vo.gl.pubvoucher.VoucherVO;
import nc.vo.pub.BusinessException;
import nccloud.bs.gl.voucher.VoucherWebBO;
import nccloud.dto.gl.lightgl.VoucherMDVO;
import nc.bs.framework.server.ISecurityTokenCallback;
import nc.bs.framework.server.util.KeyUtil;
import nc.bs.framework.mx.thread.ThreadTracer;
import nccloud.itf.gl.voucherapi.IVoucherRestService;

/**
 * NCC调用NC63凭证接口Service实现类 
 * 2020-11-25
 * 
 * @version NCC2005
 */
public class VoucherRestServiceImpl implements IVoucherRestService {

	private BaseDAO dao = new BaseDAO();
	public static ExecutorService executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS,
			new ArrayBlockingQueue<Runnable>(10)); // 线程池

	@Override
	@SuppressWarnings("unchecked")
	public Map<String, Object> tallyBySingleThread(String[] pks, String date, boolean op, boolean isList)
			throws BusinessException {
		if (pks == null || pks.length == 0) {
			throw new BusinessException("凭证主键为空");
		}

		// 1.标准产品记账、反记账
		Map<String, Object> returnMap = null;
		List<VoucherMDVO> successVOs = new ArrayList<VoucherMDVO>(); // 记账或取消记账凭证VO
		synchronized (this) { // 标准产品记账、反记账操作后回写同一个表，不能并发操作
			returnMap = (new VoucherWebBO()).tallyVoucher(pks, date, op, isList);
			if (returnMap.containsKey("success")) {
				successVOs = (List<VoucherMDVO>) returnMap.get("success");
			}
		}

		// 2.记账、反记账后事件处理
		if (successVOs.size() == 0) {
			return returnMap;
		}
		String[] successKeys = successVOs.stream().map(vo -> vo.getPk_voucher()).toArray(String[]::new); // 记账或取消记账凭证主键
		IVoucher vservice = NCLocator.getInstance().lookup(IVoucher.class);
		VoucherVO[] vos = vservice.queryByPks(successKeys); // 查询出凭证VO
		if (vos == null || vos.length == 0) {
			throw new BusinessException("当前单据已被其他人删除，请刷新页面");
		}
		if (op) {
			tallyAfterEvent(Arrays.asList(vos)); // 记账后事件
		} else {
			unTallyAfterEvent(Arrays.asList(vos)); // 反记账后事件
		}
		return returnMap;
	}

	@Override
	@SuppressWarnings("unchecked")
	public Map<String, Object> tallyByMultiThread(String[] pks, String date, boolean op, boolean isList)
			throws BusinessException {
		if (pks == null || pks.length == 0) {
			throw new BusinessException("凭证主键为空");
		}

		// 1.获取当前线程上下文
		Map<String, Serializable> invocationInfo = buildInvocationMap();

		// 2.根据单据个数的大小，创建线程队列
		List<Callable<Map<String, Object>>> threadList = new ArrayList<Callable<Map<String, Object>>>(); // 线程队列
		List<List<String>> voucherGroups = groupByVoucher(Arrays.asList(pks)); // 凭证VO进行分组
		for (List<String> voucherGroup : voucherGroups) {
			PushVoucherThread thread = new PushVoucherThread(voucherGroup.toArray(new String[0]), date, op, isList); // 新增线程
			thread.setInvocationInfo(invocationInfo); // 设置线程上下文
			threadList.add(thread); // 加入线程队列
		}

		// 3.执行所有线程，合并返回结果，再返回
		Map<String, Object> result = new HashMap<String, Object>(); // 多个线程执行结果
		try {
			List<Future<Map<String, Object>>> returnData = executor.invokeAll(threadList); // 等待所有线程执行完再返回结果
			// 合并多个线程执行结果
			for (Future<Map<String, Object>> data : returnData) {
				Map<String, Object> resMap = data.get();

				Object success = resMap.get("success"); // 记账或取消记账成功的VO
				if (success != null) {
					if (result.containsKey("success")) {
						List<VoucherMDVO> succVOs = (List<VoucherMDVO>) result.get("success");
						succVOs.addAll((List<VoucherMDVO>) success);
						result.put("success", succVOs);
					} else {
						result.put("success", (List<VoucherMDVO>) success);
					}
				}

				Object warn = resMap.get("warn"); // 提示信息
				if (warn != null) {
					if (result.containsKey("warn")) {
						Map<String, Object> warnMap = (Map<String, Object>) result.get("warn");
						warnMap.putAll((Map<String, Object>) warn);
						result.put("warn", warnMap);
					} else {
						result.put("warn", (Map<String, Object>) warn);
					}
				}

				Object error = resMap.get("error"); // 错误信息
				if (error != null) {
					if (result.containsKey("error")) {
						Map<String, String> errResult = (Map<String, String>) result.get("error");
						errResult.putAll((Map<String, String>) error);
						result.put("error", errResult);
					} else {
						result.put("error", (Map<String, String>) error);
					}
				}
			}

		} catch (Exception e) {
			throw new BusinessException("调用NC接口失败：" + e.getMessage());
		}
		return result;
	}
	
	/**
	 * 凭证分组
	 * 
	 * @param pks
	 * @return
	 * @throws BusinessException
	 */
	public List<List<String>> groupByVoucher(List<String> pks) throws BusinessException {

		if (pks == null || pks.size() == 0) {
			throw new BusinessException("凭证VO为空，无法分组");
		}
		int length = pks.size(); // 凭证总数
		int count = ((length - 1) / 10) + 1; // 每组中的凭证个数
		int gorupCount = ((length - 1) / count) + 1; // 分组个数

		List<List<String>> result = new ArrayList<List<String>>(); // 分组结果
		if (gorupCount == 1) {
			result.add(pks);
			return result;
		}
		for (int i = 0; i < gorupCount; i++) {
			List<String> list = new ArrayList<String>();
			if (i == gorupCount - 1) { // 最后一组
				list.addAll(pks.subList(i * count, length));
			} else {
				list.addAll(pks.subList(i * count, (i + 1) * count));
			}
			result.add(list);
		}
		return result;
	}

	/**
	 * 记账后事件：推送凭证到NC63
	 * 
	 * @param vos
	 * @throws BusinessException
	 */
	public void tallyAfterEvent(List<VoucherVO> vos) throws BusinessException {

		// 1.凭证VO中档案主键翻译成编码
		TranslateVoucherUtil translator = new TranslateVoucherUtil();
		translator.translateKey2Code(vos);

		// 2.调用NC63接口推送凭证
		Map<String, String> config = getAPIInfo("NC-NCC-GL-001"); // 获取接口信息
		String url = config.get("url");
		String esbKey = config.get("esbkey");

		Map<String, String> result = new HashMap<String, String>(); // 接口返回数据(key:年月，value:NC63凭证号)
		try {
			String json = ConvertVoucherUtil.voucherVOToJsonString(vos); // VO转JSON字符串
			JSONObject res = sendJsonPost(url, json, esbKey);
			handleExceptions(res); // 异常处理
			JSONArray array = res.getJSONArray("data"); // 返回凭证号
			if (array == null || array.size() == 0) {
				throw new BusinessException("NC63返回的凭证号格式错误:" + res.toJSONString());
			}
			for (int i = 0; i < array.size(); i++) {
				JSONObject obj = array.getJSONObject(i);
				String year = obj.getString("year"); // 年
				String period = obj.getString("period"); // 期间（月）
				String free1 = obj.getString("free1"); // 单据类型
				String free2 = obj.getString("free2"); // 单据号
				String free3 = obj.getString("free3"); // NC63回写的凭证号
				String nccVoucherNO = obj.getString("num"); // NCC凭证号
				String key = year + "_" + period + "_" + free1 + "_" + free2 + "_" + nccVoucherNO;
				result.put(key, free3);
			}
		} catch (Exception e) {
			throw new BusinessException("调用NC接口失败：" + e.getMessage());
		}

		// 3.接口返回的NC63凭证号回写到NCC凭证表头
		for (VoucherVO vo : vos) {
			String year = vo.getYear(); // 年
			String period = vo.getPeriod(); // 期间（月）
			String free1 = vo.getFree2(); // 单据类型
			String free2 = vo.getFree3(); // 单据号
			int nccVoucherNO = vo.getNo(); // 凭证号

			String key = year + "_" + period + "_" + free1 + "_" + free2 + "_" + nccVoucherNO;
			String free3 = result.get(key); // NC63回写的凭证号
			if (free3 == null || "".equals(free3.trim())) {
				throw new BusinessException("NC回写凭证号时，返回数据错误");
			}
			updateNCVoucherNO(vo.getPk_voucher(), free3); // 回写NC63凭证号
		}
	}

	/**
	 * 反记账后调用NC接口删除凭证
	 * 
	 * @throws BusinessException
	 */
	public void unTallyAfterEvent(List<VoucherVO> vos) throws BusinessException {

		// 1.翻译核算账簿
		TranslateVoucherUtil translator = new TranslateVoucherUtil();
		Set<String> accBookSet = vos.stream().map(vo -> vo.getPk_accountingbook()).collect(Collectors.toSet());
		Map<String, String> accBookMap = translator.transAccBook(accBookSet, 2); // 只需翻译账簿
		for (VoucherVO vo : vos) {
			vo.setPk_accountingbook(accBookMap.get(vo.getPk_accountingbook())); // 翻译结果设置到VO
		}

		// 2.封装NC63删除凭证接口参数
		List<Map<String, String>> params = new ArrayList<Map<String, String>>();
		for (VoucherVO vo : vos) {
			Map<String, String> param = new HashMap<String, String>();
			param.put("free1", vo.getFree2() == null ? "" : vo.getFree2()); // 单据类型
			param.put("free2", vo.getFree3() == null ? "" : vo.getFree3()); // 单据号
			param.put("free3", vo.getFree4() == null ? "" : vo.getFree4()); // NC63凭证号
			param.put("period", vo.getPeriod() == null ? "" : vo.getPeriod()); // 期间（月份）
			param.put("year", vo.getYear() == null ? "" : vo.getYear()); // 年
			param.put("pk_accbook", vo.getPk_accountingbook()); // 账簿
			params.add(param);
		}

		// 3.调用NC63删除凭证接口
		Map<String, String> config = getAPIInfo("NC-NCC-GL-002"); // 获取接口信息
		String url = config.get("url");
		String esbKey = config.get("esbkey");

		JSONObject json = new JSONObject();
		json.put("data", params);
		try {
			JSONObject result = sendJsonPost(url, json.toJSONString(), esbKey);
			handleExceptions(result); // 异常处理
		} catch (Exception e) {
			throw new BusinessException("调用NC接口失败：" + e.getMessage());
		}

		// 4.清空NCC凭证表头"NC63凭证号"字段
		for (VoucherVO vo : vos) {
			updateNCVoucherNO(vo.getPk_voucher(), ""); // 清空NC63凭证号
		}
	}

	/**
	 * 回写NC63凭证号到NCC凭证表头； 需求是记账后回写凭证表头，原来的接口不支持审核状态的凭证update
	 * 
	 * @param pk_voucher NCC凭证主键
	 * @param voucherNO  NC63回写的凭证号
	 */
	public void updateNCVoucherNO(String pk_voucher, String voucherNO) throws BusinessException {
		String sql = "update gl_voucher set free4 = '" + voucherNO + "' where pk_voucher = '" + pk_voucher + "'";
		this.dao.executeUpdate(sql);
	}

	/**
	 * 发送post请求
	 * 
	 * @param url
	 * @param paramJson
	 * @param esbKey
	 * @throws BusinessException
	 */
	public JSONObject sendJsonPost(String url, String paramJson, String esbKey) throws Exception {
		OutputStreamWriter out = null;
		BufferedReader in = null;
		StringBuffer result = new StringBuffer();
		try {
			URL realUrl = new URL(url);
			// 打开和URL之间的连接
			URLConnection conn = realUrl.openConnection();
			// 设置通用的请求属性
			conn.setRequestProperty("accept", "*/*");
			conn.setRequestProperty("connection", "Keep-Alive");
			conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
			// 设置请求字符集 防止请求参数因字符集原因（非UTF-8）导致调用接口不一致报错（数据类型不一致）
			conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
			conn.setRequestProperty("esbkey", esbKey); // EsbKey,NC63根据EsbKey验证接口
			// 发送POST请求必须设置如下两行
			conn.setDoOutput(true);
			conn.setDoInput(true);
			// 获取URLConnection对象对应的输出流
			out = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
			// 发送请求参数
			out.write(paramJson);
			// flush输出流的缓冲
			out.flush();
			// 定义BufferedReader输入流来读取URL的响应
			in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
			String line;
			while ((line = in.readLine()) != null) {
				result.append(line);
			}
		} catch (Exception e) {
			throw e;
		} finally { // 使用finally块来关闭输出流、输入流
			try {
				if (out != null) {
					out.close();
				}
				if (in != null) {
					in.close();
				}
			} catch (IOException ex) {
				ex.printStackTrace();
			}
		}
		if (result != null && result.length() > 0) { // 转换为Json对象输出
			return JSONObject.parseObject(result.toString());
		} else {
			return null;
		}
	}

	/**
	 * 处理NC接口返回报文中的业务异常信息
	 * 
	 * @param json
	 * @throws BusinessException
	 */
	public void handleExceptions(JSONObject json) throws BusinessException {
		String code = json.get("code") == null ? "" : json.get("code").toString();
		String success = json.get("success") == null ? "" : json.get("success").toString();
		if (!"200".equals(code) || !"true".equals(success)) {
			String errorMsg = json.get("message") == null ? "未知错误" : json.get("message").toString();
			throw new BusinessException("NC63接口返回的错误信息：" + errorMsg);
		}
	}

	/**
	 * 获取当前线程上下文信息
	 * 
	 * @return
	 */
	private Map<String, Serializable> buildInvocationMap() {
		Map<String, Serializable> result = new HashMap<>();
		InvocationInfoProxy proxy = InvocationInfoProxy.getInstance();
		result.put("_CallId", proxy.getCallId());
		result.put("_BizCenterCode", proxy.getBizCenterCode());
		result.put("_BizDateTime", proxy.getBizDateTime());
		try {
			String busiAction = ThreadTracer.getInstance().getCurThreadEntry().getBusiAction();
			result.put("_BusiAction", busiAction);
		} catch (Exception e) {
			Logger.error(e);
		}
		result.put("_CallPath", proxy.getCallPath());
		result.put("_CallServer", proxy.getCallServer());
		result.put("_DeviceId", proxy.getDeviceId());
		result.put("_GroupId", proxy.getGroupId());
		result.put("_GroupNumber", proxy.getGroupNumber());
		result.put("_HyCode", proxy.getHyCode());
		result.put("_LangCode", proxy.getLangCode());
		result.put("_LogLevel", proxy.getLogLevel());
		result.put("_RunAs", proxy.getRunAs());
		result.put("_Sysid", proxy.getSysid());
		result.put("_TimeZone", proxy.getTimeZone());
		result.put("_UserCode", proxy.getUserCode());
		result.put("_UserDataSource", proxy.getUserDataSource());
		result.put("_UserId", proxy.getUserId());
		result.put("_SessionId", proxy.getProperty("sessionId"));
		byte[] tokenBytes = NetStreamContext.getToken();
		String token = tokenBytes == null ? "default" : KeyUtil.encodeToken(tokenBytes);
		result.put("_token", token);
		return result;
	}

	/**
	 * 获取接口信息
	 * 
	 * @param code
	 * @return
	 * @throws BusinessException
	 */
	@SuppressWarnings("unchecked")
	public Map<String, String> getAPIInfo(String code) throws BusinessException {

		String sql = "select e.isstart,e.url,c.esbkey from bgy_esbinfo e left join bgy_certificate c on e.certificate = "
				+ " c.pk_code where e.code = '" + code + "'";
		List<Map<String, String>> res = (List<Map<String, String>>) this.dao.executeQuery(sql, new MapListProcessor());
		if (res == null || res.size() != 1) {
			throw new BusinessException("接口信息配置有误，到接口安全授权节点维护编码为【" + code + "】的配置信息");
		}
		String url = res.get(0).get("url"); // NC63接口地址
		if (url == null || "~".equals(url.trim()) || "".equals(url.trim())) {
			throw new BusinessException("接口URL为空，到接口安全授权节点维护编码为【" + code + "】的配置信息");
		}
		Map<String, String> result = new HashMap<String, String>();
		result.put("url", url);

		String esbKey = res.get(0).get("esbkey"); // 数据库中的EsbKey
		result.put("esbkey", (esbKey == null || "".equals(esbKey)) ? "~" : esbKey);
		return result;
	}

}

/**
 * <b> 凭证记账和反记账线程 </b> 
 * <b> 当批量记账反结账时，凭证个数较多启多线程处理 </b> 
 * 
 * 2020-12-14
 */
class PushVoucherThread implements Callable<Map<String, Object>> {

	String[] pks; // 凭证主键
	private String date; // 记账日期
	private boolean op; // 操作：true:记账，false:反记账
	private boolean isList; // 是否列表页面

	private Map<String, Serializable> invocationInfo; // 线程上下文
	private IVoucherRestService service = NCLocator.getInstance().lookup(IVoucherRestService.class); // 凭证Service

	public PushVoucherThread() {
	}

	public PushVoucherThread(String[] pks, String date, boolean op, boolean isList) {
		this.pks = pks;
		this.date = date;
		this.op = op;
		this.isList = isList;
	}

	public Map<String, Serializable> getInvocationInfo() {
		return invocationInfo;
	}

	public void setInvocationInfo(Map<String, Serializable> invocationInfo) {
		this.invocationInfo = invocationInfo;
	}

	/**
	 * 初始化线程上下文
	 * 
	 * @param map
	 */
	public void initInvocationInfo() {
		for (Map.Entry<String, Serializable> entry : this.invocationInfo.entrySet()) {
			try {
				String key = entry.getKey();
				String value = String.valueOf(entry.getValue());
				switch (key) {
				case "_UserCode":
					InvocationInfoProxy.getInstance().setUserCode(value);
					break;
				case "_BizCenterCode":
					InvocationInfoProxy.getInstance().setBizCenterCode(value);
					break;
				case "_BizDateTime":
					InvocationInfoProxy.getInstance().setBizDateTime(Long.parseLong(value));
					break;
				case "_BusiAction":
					InvocationInfoProxy.getInstance().setBusiAction(value);
					break;
				case "_CallId":
					InvocationInfoProxy.getInstance().setCallId(value);
					break;
				case "_DeviceId":
					InvocationInfoProxy.getInstance().setDeviceId(value);
					break;
				case "_GroupId":
					InvocationInfoProxy.getInstance().setGroupId(value);
					break;
				case "_GroupNumber":
					InvocationInfoProxy.getInstance().setGroupNumber(value);
					break;
				case "_HyCode":
					InvocationInfoProxy.getInstance().setHyCode(value);
					break;
				case "_LangCode":
					InvocationInfoProxy.getInstance().setLangCode(value);
					break;
				case "_LogLevel":
					InvocationInfoProxy.getInstance().setLogLevel(value);
					break;
				case "_RunAs":
					InvocationInfoProxy.getInstance().setRunAs(value);
					break;
				case "_Sysid":
					InvocationInfoProxy.getInstance().setSysid(value.getBytes()[0]);
					break;
				case "_TimeZone":
					InvocationInfoProxy.getInstance().setTimeZone(value);
					break;
				case "_UserDataSource":
					InvocationInfoProxy.getInstance().setUserDataSource(value);
					break;
				case "_UserId":
					InvocationInfoProxy.getInstance().setUserId(value);
					break;
				case "_token":
					ISecurityTokenCallback stc = NCLocator.getInstance().lookup(ISecurityTokenCallback.class);
					if (!"default".equals(value)) {
						byte[] tokenBytes = KeyUtil.decodeToken(value);
						stc.restore(tokenBytes);
					}
					break;
				case "_SessionId":
					InvocationInfoProxy.getInstance().setProperty("sessionId", value);
					break;
				default:
					break;
				}
			} catch (Throwable e) {
				Logger.error(e);
			}
		}
	}

	@Override
	public Map<String, Object> call() throws Exception {
		initInvocationInfo(); // 初始化线程上下文
		return this.service.tallyBySingleThread(this.pks, this.date, this.op, this.isList);
	}
}
