package com.smppw.core.reta.calc;

import com.smppw.common.pojo.dto.DateValue;
import com.smppw.core.reta.Rate;
import org.joda.time.DateTime;

import java.util.ArrayList;
import java.util.List;

public class XIRR extends Rate {

	private static final double ACCURACY = 0.000_000_01;//精确到小数后8位
	private static final int MAX_LOOP = 101;

	@Override
	public Double calc(List<DateValue> dateValues) {
		if (check(dateValues))
			return xirr(dateValues);

		return null;
	}

	private boolean check(List<DateValue> dateValues) {
		if (dateValues == null || dateValues.size() == 0) {
			return false;
		}
		//现金流必须有正有负才能算
		boolean positive = false;
		boolean negative = false;

		for (DateValue dv : dateValues) {
//			System.out.println(dv.getDate() + ":" + dv.getValue());

			if (null == dv.getDate() || null == dv.getValue())
				return false;

			if (dv.getValue() >= 0)
				positive = true;
			else
				negative = true;

			//			if (positive && negative)
			//				break;
		}

		return (positive && negative);
	}

	private Double xirr(List<DateValue> dateValues) {
		if (!check(dateValues))
			return null;

		int loop = 0;

		double minRate = 0;//下边界
		double upper = 1;//上边界

		double xirr = 0;

		List<Double> rateList = new ArrayList<>(dateValues.size());
		rateList.add(0.0);
		String firstDay = dateValues.get(0).getDate();
		for (int i = 1; i < dateValues.size(); i++) {
			double value = days(firstDay, dateValues.get(i).getDate()) / 365.0;
			rateList.add(value);
		}

		while (loop < MAX_LOOP) {

			double culminateNPV = 0.0;
			for (int i = 0; i < dateValues.size(); i++) {
				DateValue dv = dateValues.get(i);
				//String date = dv.getDate();
				Double payment = dv.getValue();

				if (0 == xirr)
					culminateNPV += payment;
				else
					//culminateNPV += (payment / (Math.pow(1 + xirr, days(firstDay, date) / 365.0)));
					culminateNPV += (payment / (Math.pow(1 + xirr, rateList.get(i))));
			}

			if (0 == culminateNPV)
				return xirr;

			if (0 == xirr) {
				if (culminateNPV > 0) {
					minRate = 0;
					upper = 1000.0;//理论上不会超过1000倍的收益
				} else {
					minRate = -0.9999;//理论上最大亏成0，除非加杠杆倒贴钱
					upper = 0;
				}
			} else {

				if (culminateNPV > 0)
					minRate = xirr;
				else
					upper = xirr;
			}

			xirr = (minRate + upper) / 2.0;

			//收益有可能为负
			if (Math.abs(upper - minRate) < ACCURACY)
				return xirr;

			loop++;
		}

		return null;
	}

//	public Map<String, Double> fundXIRR(JdbcTemplate jdbcTemplate, List<String> fundIdList) {
//		Map<String, Double> xirrMap = new LinkedHashMap<>();
////		if (1 == fundIdList.size()) {
////			String fundId = fundIdList.get(0);
////			Double xirr = fundXIRR(jdbcTemplate, fundId);
////
////			xirrMap.put(fundId, xirr);
////			return xirrMap;
////		}
//
//		final String ids = String.join("','", fundIdList);
//
//		final String dateSql = "SELECT a.fund_id,a.date,nav.product_amount,nav.after_accrual_wlevel_capital,nav.before_product_amount,nav.accrual_wlevel_capital_1  "
//				+ " FROM (SELECT n.fund_id,max(n.price_date) AS 'date' FROM cm_gj_nav n WHERE n.isvalid=1 AND n.fund_id IN ('"
//				+ ids
//				+ "') GROUP BY fund_id) a LEFT JOIN cm_gj_nav nav ON a.fund_id=nav.fund_id AND a.date=nav.price_date AND nav.isvalid=1";
//
//		Map<String, Map<String, String>> dateMap = new SqlDao<Map<String, String>>(jdbcTemplate) {
//			@Override
//			protected String getSql() {
//				return dateSql;
//			}
//
//			@Override
//			protected void setParameters(CallableStatement cs) throws SQLException {
//			}
//
//			@Override
//			protected Map<String, String> buildRecord(ResultSet rs) throws SQLException {
//				Map<String, String> m = new HashMap<>();
//				// m.put("fund_id", rs.getString("fund_id"));
//				m.put("date", rs.getString("date"));
//				// 未计提
////				 m.put("marketValue", rs.getString("product_amount"));
//				// 预计提
////				m.put("marketValue", rs.getString("after_accrual_wlevel_capital"));
//
//				String before_product_amount = rs.getString("before_product_amount");
//				String accrual_wlevel_capital_1 = rs.getString("accrual_wlevel_capital_1");
//				if (Utils.isNotEmpty(before_product_amount) && Utils.isNotEmpty(accrual_wlevel_capital_1)) {
//					double before = Double.parseDouble(before_product_amount);
//					double accrual = Double.parseDouble(accrual_wlevel_capital_1);
//					
//					if(before <= accrual)
//						 m.put("marketValue", rs.getString("product_amount"));
//					else
//						m.put("marketValue", rs.getString("after_accrual_wlevel_capital"));
//				}else
//					m.put("marketValue", rs.getString("after_accrual_wlevel_capital"));
//					
//				
//				return m;
//			}
//		}.query("fund_id", "date");
//
//		if (Utils.isEmpty(dateMap)) {
//			for (String fundId : fundIdList)
//				xirrMap.put(fundId, null);
//
//			return xirrMap;
//		}
//
//		List<CashFlow> list = new SqlDao<CashFlow>(jdbcTemplate) {
//			@Override
//			protected String getSql() {
//				String sql = "SELECT fund_id,transaction_date AS 'date',transaction_way AS 'way',IFNull(bonus_method,-1) AS 'bonus_method',sum(abs(transaction_amount)) AS 'money' "
//						+ " FROM cm_fund_transaction_record r WHERE isvalid=1 AND transaction_date IS NOT NULL AND transaction_way IS NOT NULL AND transaction_amount IS NOT NULL "
//						+ " AND fund_id IN ('" + ids
//						+ "') GROUP BY r.fund_id, r.transaction_date,r.accrued_way ORDER BY r.fund_id,transaction_date ASC";
//				return sql;
//			}
//
//			@Override
//			protected void setParameters(CallableStatement cs) throws SQLException {
//			}
//
//			@Override
//			protected CashFlow buildRecord(ResultSet rs) throws SQLException {
//				CashFlow cf = new CashFlow();
//				cf.setFundId(rs.getString("fund_id"));
//				cf.setDate(rs.getString("date"));
//				cf.setTransWay(rs.getInt("way"));
//				cf.setBonusMethod(rs.getInt("bonus_method"));
//				cf.setMoney(rs.getDouble("money"));
//				return cf;
//			}
//		}.query();
//
//		Map<String, List<CashFlow>> cashFlowMap = new HashMap<>();
//		for (CashFlow cf : list) {
//			List<CashFlow> cfList = cashFlowMap.get(cf.getFundId());
//			if (null == cfList)
//				cfList = new ArrayList<>();
//
//			cfList.add(cf);
//			cashFlowMap.put(cf.getFundId(), cfList);
//		}
//
//		for (String fundId : fundIdList) {
//			Map<String, String> map = dateMap.get(fundId);
//
//			if (Utils.isEmpty(map)) {
//				xirrMap.put(fundId, null);
//				continue;
//			}
//
//			String date = map.get("date");
//			if (Utils.isEmpty(date)) {
//				xirrMap.put(fundId, null);
//				continue;
//			}
//
//			String marketValue = map.get("marketValue");
//			if (Utils.isEmpty(marketValue)) {
//				xirrMap.put(fundId, null);
//				continue;
//			}
//			
//			List<CashFlow> cfList = cashFlowMap.get(fundId);
//
//			if (Utils.isEmpty(cfList)) {
//				xirrMap.put(fundId, null);
//				continue;
//			}
//
//			List<DateValue> dateValues = dateValueList(cfList,Double.parseDouble(marketValue), date);
//			Double xirr = xirr(dateValues);
//			xirrMap.put(fundId, xirr);
//		}
//
//		return xirrMap;
//	}

//	public Double fundXIRR(JdbcTemplate jdbcTemplate, String fundId) {
//		Map<String, Double> map = fundXIRR(jdbcTemplate, Arrays.asList(fundId));
//		return map.get(fundId);
//	}
//	public Double fundXIRR(JdbcTemplate jdbcTemplate, String fundId) {
//		String dateSql = "SELECT n.price_date,n.after_accrual_wlevel_capital FROM cm_gj_nav n WHERE n.isvalid=1 AND n.fund_id='" + fundId
//				+ "' ORDER BY n.price_date DESC LIMIT 1";
//		Map<String, String> dateMap = SqlDao.getObject(jdbcTemplate, dateSql,new String[]{"price_date","after_accrual_wlevel_capital"});
//		
//
//		if (Utils.isEmpty(dateMap))
//			return null;
//
//		String latestDate = dateMap.get("price_date");
//		String marketValue = dateMap.get("after_accrual_wlevel_capital");
//		if (Utils.isEmpty(latestDate))
//			return null;
//		if (Utils.isEmpty(marketValue))
//			return null;
//
//		List<CashFlow> list = new SqlDao<CashFlow>(jdbcTemplate) {
//			@Override
//			protected String getSql() {
//				String sql = "SELECT transaction_date AS 'date',transaction_way AS 'way',IFNull(bonus_method,-1) AS 'bonus_method',sum(abs(transaction_amount)) AS 'money' FROM cm_fund_transaction_record r "
//						+ " WHERE isvalid=1 AND transaction_date IS NOT NULL AND transaction_way IS NOT NULL AND transaction_amount IS NOT NULL "
//						+ " AND fund_id='" + fundId
//						+ "' GROUP BY r.fund_id, r.transaction_date,r.accrued_way ORDER BY transaction_date ASC";
//				return sql;
//			}
//
//			@Override
//			protected void setParameters(CallableStatement cs) throws SQLException {
//			}
//
//			@Override
//			protected CashFlow buildRecord(ResultSet rs) throws SQLException {
//				CashFlow cf = new CashFlow();
//				cf.setDate(rs.getString("date"));
//				cf.setTransWay(rs.getInt("way"));
//				cf.setBonusMethod(rs.getInt("bonus_method"));
//				cf.setMoney(rs.getDouble("money"));
//
//				return cf;
//			}
//		}.query();
//
//		if (Utils.isEmpty(list))
//			return null;
//
//		List<DateValue> dateValues = dateValueList(list, Double.parseDouble(marketValue), latestDate);
//
//		return xirr(dateValues);
//	}

//	private List<DateValue> dateValueList(List<CashFlow> list, Double marketValue, String latestDate) {
//		List<DateValue> dvList = new ArrayList<>();
//		// 正负值代表资金流向
//		// 1:申购，2：赎回  3:分红 4：计提
//		for (int i = 0; i < list.size(); i++) {
//			CashFlow cashFlow = list.get(i);
//
//			Double money = Math.abs(cashFlow.getMoney());
//
//			int transWay = cashFlow.getTransWay();
//			if (1 == transWay) {
//				money = -money;
//			} else if (4 == transWay) {
//				// money = -money;
//				//客户要求：业绩计提不算现金流
//				continue;
//			} else if (2 == transWay) {
//				// money = money;
//			} else if (3 == transWay) {
//				// 分红方式 1-现金分红，2-分红再投
//				// 默认现金分红
//				if (1 == cashFlow.getBonusMethod()) {
//					// 现金分红是正现金流
//					// money = money;
//				} else {
//					// 分红再投不产生现金流
//					continue;
//				}
//			}
//
//			DateValue dv = new DateValue(cashFlow.getDate(), money);
//			dvList.add(dv);
//		}
//
//		dvList.add(new DateValue(latestDate, marketValue));
//
//		return dvList;
//	}

	private int days(String date1, String date2) {
		DateTime firstDay = DateTime.parse(date1);
		DateTime secondDay = DateTime.parse(date2);

		long duration = secondDay.getMillis() - firstDay.getMillis();
		duration = duration < 0 ? -duration : duration;

		return (int) (duration / (3600_000 * 24));
	}
}
