# ReservationForecastListenerFileParseByOperaImpl.java

    package cloud.wconnect.rateshop.service.revenue.reservationforecast;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.SystemPlatformEnum;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.rateshop.cons.revenue.RevenueCons;
    import cloud.wconnect.rateshop.entity.revenue.RevenueMarketCode;
    import cloud.wconnect.rateshop.entity.revenue.RevenueSegment;
    import cloud.wconnect.rateshop.pojo.bo.revenue.*;
    import cloud.wconnect.rateshop.service.revenue.IRevenueImportService;
    import cloud.wconnect.rateshop.service.revenue.IRevenueMarketCodeService;
    import cloud.wconnect.rateshop.service.revenue.IRevenueSegmentService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Maps;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.collections.MapUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.dom4j.Document;
    import org.dom4j.Element;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    import java.io.InputStream;
    import java.math.BigDecimal;
    import java.text.MessageFormat;
    import java.time.LocalDate;
    import java.time.ZoneId;
    import java.time.ZonedDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.stream.Collectors;

    /**
     * @author yyt
     * 2023/11/3
     */
    @Service
    @DS("rate_shop")
    public class ReservationForecastListenerFileParseByOperaImpl implements IReservationForecastListenerFileParse {

        @Autowired
        private IRevenueSegmentService revenueSegmentService;
        @Autowired
        private IRevenueMarketCodeService revenueMarketCodeService;
        @Autowired
        private IRevenueImportService revenueImportService;
        @Autowired
        private IRFParseCommonService rfParseCommonService;

        private static final String SOURCE = SystemPlatformEnum.OPERA.getDesc();

        /**
         * 判断支持的平台
         *
         * @param source 平台
         * @return boolean
         * @author yyt
         * @date 2023/11/3
         */
        @Override
        public boolean supports(String source) {
            return SOURCE.equals(source);
        }

        /**
         * 解析报告数据
         *
         * @param propertyId 资产 id
         * @param timeZone   时区
         * @param in         文件流
         * @param date       指定日期
         * @param startDate  开始日期
         * @param endDate    结束日期
         * @return Segment
         * @author yyt
         * @date 2023/11/3
         */
        @Override
        public SegmentBO parseReport(Integer propertyId, String timeZone, InputStream in, String date,
                                           Long startDate, Long endDate) throws Exception {
            List<RevenueSegment> segments = revenueSegmentService.listActiveSegment(propertyId);
            if (CollectionUtils.isEmpty(segments)) {
                return new SegmentBO(date, Collections.emptyList());
            }

            Map<Integer, List<String>> pmsSegmentsById = segments.stream().collect(Collectors.toMap(RevenueSegment::getId,
                    segment -> Lists.newArrayList(StringUtils.split(segment.getPmsSegment(), ","))));

            ZoneId zoneId = ZoneId.of(timeZone);
            int size = segments.size();
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_MM_DD_YY_STRIKE);
            Map<Integer, BigDecimal> revenueBySegmentId = Maps.newHashMapWithExpectedSize(size);
            Map<Integer, Integer> roomsBySegmentId = Maps.newHashMapWithExpectedSize(size);

            // 解析文件数据
            Document document = new SAXReader().read(in);
            List<Node> dateNodes = document.selectNodes("//DAY");
            String detailXpathTmpl = "./LIST_MARKET//MARKET[MARKET_CODE=''{0}'']//DETAIL";
            dateNodes.forEach(dateNode -> {
                String targetDateStr = ((Element) dateNode).elementText("CHAR_RESERVATION_DATE");
                ZonedDateTime targetDate = LocalDate.parse(targetDateStr, dateTimeFormatter).atStartOfDay(zoneId);
                long targetDateSecond = targetDate.toEpochSecond();
                if (targetDateSecond < startDate || targetDateSecond > endDate) {
                    return;
                }

                pmsSegmentsById.forEach((segmentId, operaSegments) -> {
                    BigDecimal totalRevenue = BigDecimal.ZERO;
                    int totalRooms = 0;
                    for (String operaSegment : operaSegments) {
                        String detailXpath = MessageFormat.format(detailXpathTmpl, operaSegment);
                        List<Node> detailNodes = dateNode.selectNodes(detailXpath);
                        for (Node detailNode : detailNodes) {
                            Element element = (Element) detailNode;
                            // 累加每天的Segment Revenue
                            String revenueStr = element.elementTextTrim("TOTAL_REVENUE");
                            if (!revenueStr.isEmpty()) {
                                BigDecimal revenue = new BigDecimal(revenueStr);
                                totalRevenue = totalRevenue.add(revenue);
                            }

                            // 累加每天售出的Segment房间数
                            String roomsStr = element.elementTextTrim("NO_DEFINITE_ROOMS");
                            if (!roomsStr.isEmpty()) {
                                int rooms = Integer.parseInt(roomsStr);
                                totalRooms = totalRooms + rooms;
                            }
                        }
                    }

                    revenueBySegmentId.put(segmentId, revenueBySegmentId.getOrDefault(segmentId, BigDecimal.ZERO).add(totalRevenue));
                    roomsBySegmentId.put(segmentId, roomsBySegmentId.getOrDefault(segmentId, 0) + totalRooms);
                });
            });

            if (MapUtils.isEmpty(revenueBySegmentId) && MapUtils.isEmpty(roomsBySegmentId)) {
                return new SegmentBO(date, Collections.emptyList());
            }

            return new SegmentBO(date, segments.stream().map(item -> {
                Integer id = item.getId();
                return new RoomsAndRevenueBO(id, roomsBySegmentId.get(id), revenueBySegmentId.get(id));
            }).toList());
        }

        /**
         * 解析报告数据
         *
         * @param propertyId 资产 id
         * @param timeZone   时区
         * @param in         文件流
         * @param startDate  开始日期
         * @param endDate    结束日期
         * @return MarketCodeBO
         * @author yyt
         * @date 2024/3/25
         */
        @Override
        public List<MarketCodeBO> parseReportForNext14Days(Integer propertyId, String timeZone, InputStream in,
                                              Long startDate, Long endDate) throws Exception {
            List<RevenueMarketCode> marketCodes = revenueMarketCodeService.listEffiective(propertyId);
            if (CollectionUtils.isEmpty(marketCodes)) {
                return Collections.emptyList();
            }

            Map<Integer, Map<String, Integer>> marketCodeByType = marketCodes.stream().collect(Collectors.groupingBy(RevenueMarketCode::getType,
                    Collectors.toMap(RevenueMarketCode::getMarketCode, RevenueMarketCode::getId)));
            Map<String, Integer> marketCodeByTransient = marketCodeByType.getOrDefault(RevenueCons.TRANSIENT, Collections.emptyMap());
            Map<String, Integer> marketCodeByGroup = marketCodeByType.getOrDefault(RevenueCons.GROUP, Collections.emptyMap());

            List<MarketCodeBO> marketCodeBOs = Lists.newArrayList();

            // 解析文件数据
            Document document = new SAXReader().read(in);
            List<Node> dateNodes = document.selectNodes("//DAY");
            for (Node node : dateNodes) {
                Element element = (Element) node;
                String targetDateStr = element.elementText("CHAR_RESERVATION_DATE");
                long dataDate = DateTimeUtils.dateStrToEpochSecond(targetDateStr, BaseConstant.DATE_FORMAT_MM_DD_YY_STRIKE, timeZone);
                if (dataDate < startDate || dataDate > endDate) {
                    continue;
                }

                List<Node> childNodes = node.selectNodes("./LIST_MARKET/MARKET");
                for (Node childNode : childNodes) {
                    Element elementChild = (Element) childNode;
                    String marketCode = elementChild.elementText("MARKET_CODE");
                    int rooms = 0;
                    Integer transientMarketCodeId = marketCodeByTransient.get(marketCode);
                    Integer groupMarketCodeId = marketCodeByGroup.get(marketCode);
                    if (Objects.nonNull(transientMarketCodeId) || Objects.nonNull(groupMarketCodeId)) {
                        // 查找当前 market code 下的房间数
                        List<Node> roomsNodes = childNode.selectNodes("./LIST_DETAIL/DETAIL");
                        for (Node roomsNode : roomsNodes) {
                            Element roomsElement = (Element) roomsNode;
                            String roomsStr = roomsElement.elementText("ARRIVAL_RMS");
                            rooms += StringUtils.isEmpty(roomsStr) ? 0 : Integer.parseInt(roomsStr);
                        }
                    } else {
                        continue;
                    }

                    if (Objects.nonNull(transientMarketCodeId)) {
                        marketCodeBOs.add(new MarketCodeBO(dataDate, transientMarketCodeId, rooms));
                    }
                    if (Objects.nonNull(groupMarketCodeId)) {
                        marketCodeBOs.add(new MarketCodeBO(dataDate, groupMarketCodeId, rooms));
                    }
                }
            }
            return marketCodeBOs;
        }

        /**
         * 解析报告数据
         *
         * @param propertyId 资产 id
         * @param timeZone   时区
         * @param in         文件流
         * @param startDate  开始日期
         * @param endDate    结束日期
         * @return Segment
         * @author yyt
         * @date 2024/5/21
         */
        @Override
        public List<List<SegmentBO>> parseReportForNext90Days(Integer propertyId, String timeZone, InputStream in, Long startDate, Long endDate) throws Exception {
            List<RevenueSegment> segments = revenueSegmentService.listActiveSegment(propertyId);
            if (CollectionUtils.isEmpty(segments)) {
                return Collections.emptyList();
            }

            return rfParseCommonService.packageSegmentBO(propertyId, segments, startDate, endDate, parseDocument(in, timeZone, startDate, endDate), timeZone);
        }

        /**
         * 解析文件节点数据
         *
         * @param in        文件
         * @param timeZone  时区
         * @param startDate 开始日期
         * @param endDate   结束日期
         * @return ReservationForecastBO
         * @author yyt
         * @date 2024/5/21
         **/
        private List<ReservationForecastBO> parseDocument(InputStream in, String timeZone, Long startDate, Long endDate) throws Exception {
            Document document = new SAXReader().read(in);
            List<Node> dateNodes = document.selectNodes("//DAY");
            List<ReservationForecastBO> reservationForecastBOList = Lists.newArrayListWithExpectedSize(dateNodes.size());

            for (Node node : dateNodes) {
                Element element = (Element) node;
                String targetDateStr = element.elementText("CHAR_RESERVATION_DATE");
                long dataDate = DateTimeUtils.dateStrToEpochSecond(targetDateStr, BaseConstant.DATE_FORMAT_MM_DD_YY_STRIKE, timeZone);
                if (dataDate < startDate || dataDate > endDate) {
                    continue;
                }
                ReservationForecastBO reservationForecastBO = new ReservationForecastBO();
                reservationForecastBO.setDate(dataDate);

                List<Node> childNodes = node.selectNodes("./LIST_MARKET/MARKET");
                List<ReservationForecastMarketCodeBO> reservationForecastMarketCodeBOList = Lists.newArrayListWithExpectedSize(childNodes.size());
                for (Node childNode : childNodes) {
                    Element elementChild = (Element) childNode;
                    String marketCode = elementChild.elementText("MARKET_CODE");

                    ReservationForecastMarketCodeBO reservationForecastMarketCodeBO = new ReservationForecastMarketCodeBO();
                    reservationForecastMarketCodeBO.setMarketCode(marketCode);

                    List<Node> detailNodes = childNode.selectNodes("./LIST_DETAIL/DETAIL");
                    List<ReservationForecastMarketCodesBO> reservationForecastMarketCodesBOList = Lists.newArrayListWithExpectedSize(detailNodes.size());
                    for (Node detailNode : detailNodes) {
                        Element detailElement = (Element) detailNode;

                        BigDecimal revenue = BigDecimal.ZERO;
                        String revenueStr = detailElement.elementTextTrim("TOTAL_REVENUE");
                        if (!revenueStr.isEmpty()) {
                            revenue = new BigDecimal(revenueStr);
                        }

                        int rooms = BaseConstant.ZERO;
                        String roomsStr = detailElement.elementTextTrim("NO_DEFINITE_ROOMS");
                        if (!roomsStr.isEmpty()) {
                            rooms = Integer.parseInt(roomsStr);
                        }
                        reservationForecastMarketCodesBOList.add(new ReservationForecastMarketCodesBO(rooms, revenue));
                    }

                    reservationForecastMarketCodeBO.setDetail(reservationForecastMarketCodesBOList);
                    reservationForecastMarketCodeBOList.add(reservationForecastMarketCodeBO);
                }

                reservationForecastBO.setMarketCodeBOS(reservationForecastMarketCodeBOList);
                reservationForecastBOList.add(reservationForecastBO);
            }

            return reservationForecastBOList;
        }

    }