package com.wuban.tron.explore.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.wuban.tron.explore.constant.Constant;
import com.wuban.tron.explore.constant.PageConstant;
import com.wuban.tron.explore.dao.*;
import com.wuban.tron.explore.domain.*;
import com.wuban.tron.explore.entity.*;
import com.wuban.tron.explore.entity.example.BlockDayCensusExample;
import com.wuban.tron.explore.entity.example.TransactionExample;
import com.wuban.tron.explore.service.TransactionService;
import com.wuban.tron.explore.util.BigDecimalUtil;
import com.wuban.tron.explore.util.DateUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.StringRedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.tron.common.utils.ByteArray;
import org.tron.walletserver.WalletApi;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;

/**
 *  <core>区块服务接口方法实现类</core>
 *
 * @author sky
 * @date 2020/11/02
 */
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class TransactionServiceImpl implements TransactionService {

    private final BlockHeaderRepository blockHeaderRepository;

    private final TransactionRepository transactionRepository;

    private final TransactionHexRepository transactionHexRepository;

    private final BlockDayCensusRepository blockDayCensusRepository;

    private final LastBlockRepository lastBlockRepository;

    private final AddressRepository addressRepository;

    private final StringRedisTemplate stringRedisTemplate;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void save(List<TronResponseData> dataList) {

        if (!CollectionUtils.isEmpty(dataList)) {
             List<BlockHeader> headerList = new ArrayList<>(dataList.size());
             List<Transaction> transactionList = new ArrayList<>(dataList.size());
             List<TransactionHex> hexList = new ArrayList<>(dataList.size());

             dataList.forEach(o -> {
                 TronBlockHeader tronBlockHeader = o.getBlock_header();

                 // 区块头设置
                 BlockHeader header = transferBlockHeader(o.getBlockID(), tronBlockHeader);
                 List<Transactions> trList = o.getTransactions();
                 if (CollectionUtils.isEmpty(trList)) {
                     header.setTransactionVolume(0);
                 } else {
                     header.setTransactionVolume(trList.size());

                     List<Transaction> list = transferTransaction(o.getBlockID(), trList, header.getNumber());
                     transactionList.addAll(list);

                     // hex设置
                     List<TransactionHex> tmpList = transferTransactionHex(o.getBlockID(), trList);
                     hexList.addAll(tmpList);
                 }

                 // 区块bytes设置
                 if (!CollectionUtils.isEmpty(transactionList)) {
                     String refBlockBytes = transactionList.get(0).getRefBlockBytes();
                     String blockBytes = new BigInteger(refBlockBytes, 16).toString();
                     header.setBlockBytes(blockBytes);
                 } else {
                     header.setBlockBytes("0");
                 }
                 headerList.add(header);
             });

             String censusBlockSize = headerList.get(0).getBlockBytes();

             /*
                区块头、区块交易、hex持久化
              */
             this.blockHeaderRepository.batchInsert(headerList);

             Integer censusTransNumber = 0;
             Set<String> hexSet = new HashSet<>();
             Set<String> base58Set = new HashSet<>();
             Set<String> conAddrSet = new HashSet<>();
             if (transactionList.size() != 0) {
                 censusTransNumber = transactionList.size();

                 // 数据统计
                 this.censusFlushRedis(censusBlockSize, censusTransNumber);

                 transactionList.forEach(o -> {
                     // hex 数据转换成base58
                     String ownerAddress;
                     String toAddress;
                     String contractAddress;
                     if (!StringUtils.isEmpty(o.getOwnerAddress())) {
                         hexSet.add(o.getOwnerAddress());
                         ownerAddress = WalletApi.encode58Check(ByteArray.fromHexString(o.getOwnerAddress()));
                         o.setOwnerAddress(ownerAddress);
                         base58Set.add(ownerAddress);
                     }
                     if (!StringUtils.isEmpty(o.getToAddress())) {
                         hexSet.add(o.getToAddress());
                         toAddress = WalletApi.encode58Check(ByteArray.fromHexString(o.getToAddress()));
                         o.setToAddress(toAddress);
                         base58Set.add(toAddress);
                     }
                     if (!StringUtils.isEmpty(o.getContractAddress())) {
                         hexSet.add(o.getContractAddress());
                         contractAddress = WalletApi.encode58Check(ByteArray.fromHexString(o.getContractAddress()));
                         o.setContractAddress(contractAddress);
                         base58Set.add(contractAddress);
                         conAddrSet.add(contractAddress);
                     }
                 });
                 this.transactionRepository.batchInsert(transactionList);
                 this.transactionHexRepository.batchInsert(hexList);
             }

             // 更新区块高度
             LastBlock lastBlock = new LastBlock();
             lastBlock.setId(1L);
             lastBlock.setCurBlockNum(headerList.get(0).getNumber());
             this.lastBlockRepository.updateCurBlockNumById(lastBlock);
             log.info("已同步数据区块高度num:{}",headerList.get(0).getNumber());

             // 账户地址push到Redis，未后续抓取账户余额做准备
             List<Address> records = transferAddress(base58Set);
             if (!CollectionUtils.isEmpty(records)) {
                 this.addressRepository.batchInsertOnDuplicateKey(records);
             }

             addressPushRedis(hexSet);
             conAddrPushRedis(conAddrSet);
        }

    }

    /**
     * 合约地址同步到redis
     * @param set
     */
    private void conAddrPushRedis(Set<String> set) {
        if (CollectionUtils.isEmpty(set)) {
            return;
        }

        stringRedisTemplate.executePipelined(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                StringRedisConnection conn = (StringRedisConnection)redisConnection;
                set.forEach(o -> conn.sAdd(Constant.CONTRACT_ADDRESS_KEY, o));
                return null;
            }
        });
    }

    /**
     * 账户地址同步到redis
     * @param set
     */
    private void addressPushRedis(Set<String> set) {
        if (CollectionUtils.isEmpty(set)) {
            return;
        }

        stringRedisTemplate.executePipelined(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                StringRedisConnection conn = (StringRedisConnection)redisConnection;
                set.forEach(o -> conn.sAdd(Constant.ADDRESS_KEY, o));
                return null;
            }
        });
    }

    /**
     * 封装账户地址信息
     *
     * @param set 账户地址
     * @return
     */
    private List<Address> transferAddress(Set<String> set) {
        List<Address> list = new ArrayList<>();

        set.forEach(o -> {
            Address address = Address.builder().address(o).balance(0L).build();
            list.add(address);
        });

        return list;
    }

    /**
     * 封装区块头信息
     *
     * @param blockId 区块ID
     * @param tronBlockHeader 区块头信息
     * @return
     */
    private BlockHeader transferBlockHeader(String blockId, TronBlockHeader tronBlockHeader) {
        BlockHeader header = new BlockHeader();
        header.setBlockId(blockId);

        BlockHeaderRawData rawData = tronBlockHeader.getRaw_data();
        header.setNumber(rawData.getNumber());
        header.setParentHash(rawData.getParentHash());
        header.setTxTrieRoot(rawData.getTxTrieRoot());
        header.setVersion(rawData.getVersion());
        header.setWitnessAddress(rawData.getWitness_address());
        header.setWitnessSignature(tronBlockHeader.getWitness_signature());
        header.setTimestamp(rawData.getTimestamp());
        header.setCreateTime(new Date());
        return header;
    }

    /**
     * 封装区块交易信息
     *
     * @param blockId  区块ID
     * @param transactionsList  交易数据
     * @param number 块高度
     * @return
     */
    private  List<Transaction> transferTransaction(String blockId, List<Transactions> transactionsList, Long number) {
        List<Transaction> retList = new ArrayList<>(transactionsList.size());
        transactionsList.forEach(o -> {
            TransactionsRawData rawData = o.getRaw_data();
            TronContract contract = rawData.getContract().get(0);
            ContractParameter contractParameter = contract.getParameter();
            ContractParameterValue val = contractParameter.getValue();

            Transaction tr = Transaction.builder()
                    .blockId(blockId)
                    .txId(o.getTxID())
                    .contractRet(o.getRet().get(0).getContractRet())
                    .expiration(rawData.getExpiration())
                    .timestamp(rawData.getTimestamp())
                    .refBlockBytes(rawData.getRef_block_bytes())
                    .refBlockHash(rawData.getRef_block_hash())
                    .feeLimit(rawData.getFee_limit())
                    .type(contract.getType())
                    .typeUrl(contractParameter.getType_url())
                    .amount(val.getAmount())
                    .data(val.getData())
                    .contractAddress(val.getContract_address())
                    .ownerAddress(val.getOwner_address())
                    .toAddress(val.getTo_address())
                    .createTime(new Date())
                    .number(number)
                    .build();
            retList.add(tr);
        });

        return retList;
    }

    private   List<TransactionHex> transferTransactionHex(String blockId, List<Transactions> trList) {
        List<TransactionHex> hexList = new ArrayList<>();
        TransactionHex hex = new TransactionHex();
        trList.forEach(obj -> {
            hex.setBlockId(blockId);
            hex.setTxId(obj.getTxID());
            hex.setHex(obj.getRaw_data_hex());
            hex.setSignature(obj.getSignature().get(0));
            hex.setCreateTime(new Date());
            hexList.add(hex);
        });

        return hexList;
    }

    /**
     * 区块大小、区块交易数量push到Redis，未后续统计做准备
     *
     * @param blockSize
     * @param transNumber
     */
    private void censusFlushRedis(String blockSize, Integer transNumber) {
        //this.stringRedisTemplate.opsForList().leftPush(Constant.CENSUS_BLOCK_SIZE, blockSize);
        long res = this.stringRedisTemplate.opsForList().leftPush(Constant.CENSUS_TRANS, transNumber.toString() + "," + blockSize);
        if (res == 0) {
            this.stringRedisTemplate.opsForList().leftPush(Constant.CENSUS_TRANS, transNumber.toString() + "," + blockSize);
        }
    }

    @Override
    public Long getBlockMinTime() {
        return this.blockHeaderRepository.selectBlockMinTime();
    }

    @Override
    public void censusBlockByLastDay() {
        Date nextDate = DateUtil.addDays(new Date(), -1);
        String date = DateUtil.getFormatDate(nextDate,DateUtil.PATTERN_YMD);
        String startDate = date + " 00:00:00";
        String endDate = date + " 23:59:59";
        this.census(date, startDate, endDate);
    }

    @Override
    public void censusBlockByDate(String startDate, String endDate) {
        log.info("按天统计交易数据开始startDate:{} 结束endDate:{}", startDate, endDate);
        if (StringUtils.isEmpty(endDate) || (startDate.equals(endDate))) {
            String _startDate = startDate + " 00:00:00";
            String _endDate = startDate + " 23:59:59";
            this.census(startDate, _startDate, _endDate);
            return;
        }

        String date = startDate;
        while (true) {
            String _startDate = date + " 00:00:00";
            String _endDate = date + " 23:59:59";
            this.census(date, _startDate, _endDate);

            if (date.equals(endDate)) {
                break;
            }

            Date nextDate = DateUtil.addDays(date, 1);
            date = DateUtil.getFormatDate(nextDate, DateUtil.PATTERN_YMD);
        }
    }

    /**
     * 分页查询
     *
     * @param startIndex 页码
     * @param pageSize   每页记录数
     * @param example    检索条件
     * @return 分页列表
     */
    @Override
    public PageInfo<Transaction> getByPageWithCategory(Integer startIndex, Integer pageSize, TransactionExample example) {
        if (startIndex == null) {
            startIndex = PageConstant.DEFAULT_START_INDEX;
        }

        if (pageSize == null) {
            pageSize = PageConstant.DEFAULT_PAGE_SIZE;
        }

        if (pageSize > PageConstant.MAX_PAGE_SIZE) {
            pageSize = PageConstant.MAX_PAGE_SIZE;
        }

        example.setOrderByClause("`timestamp` DESC");
        PageHelper.startPage(startIndex, pageSize);
        List<Transaction> list = this.transactionRepository.selectByPagerEx(example);
        PageInfo<Transaction> pageInfo = new PageInfo<>(list);

        return pageInfo;
    }

    /**
     * 根据条件检索
     *
     * @param example 检索条件
     * @return
     */
    @Override
    public List<Transaction> getByExample(TransactionExample example) {
        return this.transactionRepository.selectByExample(example);
    }

    @Override
    public PageInfo<Transaction> selectListByAddress(String address, Integer type, Integer startIndex, Integer pageSize) {
        Date date;
        switch (type) {
            case 1:
                date = DateUtil.addDays(new Date(), -7);
                break;
            case 2:
                date = DateUtil.addDays(new Date(), -14);
                break;
            case 3:
                date = DateUtil.addDays(new Date(), -30);
                break;
            default:
                date = null;
        }
        String str = DateUtil.getFormatDate(date, DateUtil.PATTERN_YMD_HMS);
        long t = DateUtil.getDateFromDateStr(str, DateUtil.PATTERN_YMD_HMS);

        if (startIndex == null) {
            startIndex = PageConstant.DEFAULT_START_INDEX;
        }

        if (pageSize == null) {
            pageSize = PageConstant.DEFAULT_PAGE_SIZE;
        }

        if (pageSize > PageConstant.MAX_PAGE_SIZE) {
            pageSize = PageConstant.MAX_PAGE_SIZE;
        }

        PageHelper.startPage(startIndex, pageSize);
        List<Transaction> list = this.transactionRepository.selectListByAddress(address, t);
        PageInfo<Transaction> pageInfo = new PageInfo<>(list);

        return pageInfo;
    }

    /**
     * 根据账户地址查询交易信息
     *
     * @param address    账户地址
     * @param startIndex
     * @param pageSize
     * @return
     */
    @Override
    public PageInfo<Transaction> selectPageByAddress(String address, Integer startIndex, Integer pageSize) {
        if (startIndex == null) {
            startIndex = PageConstant.DEFAULT_START_INDEX;
        }

        if (pageSize == null) {
            pageSize = PageConstant.DEFAULT_PAGE_SIZE;
        }

        if (pageSize > PageConstant.MAX_PAGE_SIZE) {
            pageSize = PageConstant.MAX_PAGE_SIZE;
        }

        PageHelper.startPage(startIndex, pageSize);
        List<Transaction> list = this.transactionRepository.selectPageByAddress(address);
        PageInfo<Transaction> pageInfo = new PageInfo<>(list);

        return pageInfo;
    }

    /**
     * 每秒交易tps峰值统计
     *
     * @param startTime 开始时间（毫秒）
     * @param endTime   结束时间（毫秒）
     * @return
     */
    @Override
    public void transTpsCount(long startTime, long endTime) {
        if (startTime == 0L) {
            /*String endDate = DateUtil.getFormatDate(new Date(), DateUtil.PATTERN_YMD_HMS);
            Long endDateSeconds = DateUtil.getDateFromDateStr(endDate, DateUtil.PATTERN_YMD_HMS);
            endTime = endDateSeconds;
            startTime = endTime - 1 * 1000;*/
            startTime = System.currentTimeMillis() - 20 * 1000;
            endTime = startTime + 1000;
        }

        TransactionExample example = new TransactionExample();
        example.createCriteria().andTimestampBetween(startTime, endTime);
        int count = this.transactionRepository.countByExample(example);

        /*
            当前count - (redis：trans_tps_peak value) > 0 重新设置此值
         */
        if (count != 0) {
            String tpsPeakStr = this.stringRedisTemplate.opsForValue().get(Constant.TRANS_TPS_PEAK);
            int tpsPeak = 0;
            if (tpsPeakStr != null) {
                tpsPeak = Integer.valueOf(tpsPeakStr);
            }

            if (count - tpsPeak > 0) {
                this.stringRedisTemplate.opsForValue().set(Constant.TRANS_TPS_PEAK, Integer.valueOf(count).toString());
            }
        }
    }

    private void census(String date, String startDate, String endDate) {
        Long startDateSeconds = DateUtil.getDateFromDateStr(startDate, DateUtil.PATTERN_YMD_HMS);
        Long endDateSeconds =  DateUtil.getDateFromDateStr(endDate, DateUtil.PATTERN_YMD_HMS);
        BlockDayCensus data = this.blockDayCensusRepository.censusBlockByTime(startDateSeconds, endDateSeconds);
        if (data != null) {
            Long totalSeconds = 24 * 60 * 60L;
            BigDecimal aver = BigDecimalUtil.getDevide(new BigDecimal(totalSeconds), new BigDecimal(data.getGenBlockTotalNum()));
            data.setGenBlockAverTime(aver.longValue());
            data.setCensusDate(date);
            data.setCreateTime(new Date());

            BigDecimal averBlockBytes = BigDecimalUtil.getDevide(new BigDecimal(data.getTotalBlockBytes()), new BigDecimal(data.getGenBlockTotalNum()));
            data.setAverBlockBytes(averBlockBytes.intValue());
            this.saveCensusData(data);
            log.info("date:{} 交易数据已统计完成",date);
        } else {
            log.info("date:{} 暂无交易数据统计",date);
        }
    }

    private void saveCensusData(BlockDayCensus data) {
        BlockDayCensusExample example = new BlockDayCensusExample();
        example.createCriteria().andCensusDateEqualTo(data.getCensusDate());
        List<BlockDayCensus> list = this.blockDayCensusRepository.selectByExample(example);
        if (!CollectionUtils.isEmpty(list)) {
            Long id = list.get(0).getId();
            this.blockDayCensusRepository.deleteById(id);
        }
        this.blockDayCensusRepository.insert(data);
    }

}
