package com.utic.center.utic.traf.server.service;

import com.utic.center.common.annotation.ProcessingElapsed;
import com.utic.center.common.service.AbstractProcessService;
import com.utic.center.common.utils.LogUtils;
import com.utic.center.utic.traf.server.config.TraceConfig;
import com.utic.center.utic.traf.server.dto.LinkCollectResultDto;
import com.utic.center.utic.traf.server.dto.LinkDto;
import com.utic.center.utic.traf.server.dto.LinkTrafState;
import com.utic.center.utic.traf.server.dto.utic.TrafficGrade;
import com.utic.center.utic.traf.server.repository.ApplicationRepository;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@Getter
@RequiredArgsConstructor
@Service
public class LinkTrafMissingService implements AbstractProcessService {

    private final TraceConfig trace;
    private final LinkService linkService;
    private final LinkParamService paramService;
    private final LinkTrafCollectService collectService;
    private final LinkPatternService patternService;

    @ProcessingElapsed(type="CORRECT", name="링크 소통정보 결측", starting = false, endLine = true)
    @Override
    public boolean processing() {
        // 모든 결측 알고리즘에 대해 결측 처리를 수행하고
        // 최종 선택된 결측 알고리즘으로 결측을 수행한다.
        
        // 1. PSD 알고리즘(모든 레벨1 구간에 대해 처리)
        missingDataPsd();

        // 2. 레벨1 구간 소통정보를 이용하여 레벨4 구간 소통정보 생성
        calLinkGroupSpeed(4, "P");

        // 3. 레벨4 링크를 이용하여 레벨1 링크 PSD 결측정보 처리
        missingGroupPsd();

        // 4. 결측된 구간에 대하여 결측 알고리즘을 이용하여 속도 계산
        missingDataRevision();

        // 5. 패턴을 이용하여 결측 처리
        missingDataPattern();

        // 6. 국토부 민간소통정보를 이용한 결측
        missingDataMoctPrivate();

        // 7. 결측 처리된 레벨1 소통정보를 이용하여 상위레벨 소통정보를 생성(레벨2,3,4)
        for (int ii = 2; ii <= 4; ii++) {
            calLinkGroupSpeed(ii, "P");
        }
        return true;
    }

    private int missingDataMoctPrivate() {
        final long start = System.currentTimeMillis();
        // 결측처리 방식: 국토부 민간소통정보를 이용하여 결측

        LinkCollectResultDto collectMOCT = Optional.ofNullable(this.collectService.get(LinkTrafCollectService.MOCT))
                .orElseGet(() -> LinkCollectResultDto.builder().clctSystCd(LinkTrafCollectService.MOCT).lists(new ArrayList<>()).build());

        AtomicInteger calCnt = new AtomicInteger();
        AtomicInteger totalCnt = new AtomicInteger();
        collectMOCT.getLists().parallelStream().forEach(dto -> {
            LinkDto link = this.linkService.getLink(dto.getLinkId());
            if (link == null) {
                ApplicationRepository.isLoadingLink = true;
                log.error("[ERR] LinkTrafMissingService.missingDataMoctPrivate: {} link not found.", dto.getLinkId());
                return;
            }

            totalCnt.getAndIncrement(); // 민간 수집 건수
            if (link.getTrafFsn().getState().isState()) {
                // 구간의 소통정보가 생성되었다면 당연히 결측 안함
                return;
            }

            if (dto.getSpeed() > 0) {
                TrafficGrade grade = this.paramService.getTrafficGrade(link.getRoadRank());
                link.getTrafFsn().setTraffic(LinkTrafState.TRAFFIC_CRT_DATA_TYPE.CRT_DATA_MISSING, dto.getCenterId(), dto.getSpeed(), dto.getMissValueYn(), dto.getDataResType());
                link.fsnTrafficCorrect(grade);
                calCnt.getAndIncrement();
            }
        });

        this.linkService.getFusionInfo().setMissMoctCnt(calCnt.get());
        logInfo("레벨1 결측(민간)", totalCnt.get(), calCnt.get(), start);
        return calCnt.get();
    }

    private int missingDataPattern() {
        final long start = System.currentTimeMillis();
        this.linkService.getFusionInfo().setColPtnCnt(this.patternService.getPatternCount());

        // 결측처리 방식: 구간의 현재 패턴정보를 이용, DB에 패턴정보 결측 처리를 수행하도록 설정된 링크에 대해서만 처리한다.
        AtomicInteger calCnt = new AtomicInteger();
        AtomicInteger totalCnt = new AtomicInteger();
        this.linkService.getLinkMap().values().parallelStream()
                .filter(link -> link.getLinkLevel() == 1)
                .filter(LinkDto::isPrcsPtrnMissing) // 패턴 결측 처리를 수행하도록 설정된 링크
                .forEach(link -> {
                    totalCnt.incrementAndGet();

                    if (link.getTrafFsn().getState().isState()) {
                        // 구간의 소통정보가 생성되었다면 당연히 결측 안함
                        return;
                    }
                    if (!link.getTrafPtrn().getState().isState()) {
                        // 구간의 패턴정보가 존재하지 않는다면 결측 처리 못함
                        return;
                    }

                    if (link.getTrafPtrn().getSpeed() > 0) {
                        TrafficGrade grade = this.paramService.getTrafficGrade(link.getRoadRank());
                        link.getTrafFsn().setTraffic(LinkTrafState.TRAFFIC_CRT_DATA_TYPE.CRT_DATA_MISSING, "C00", link.getTrafPtrn().getSpeed(), "T", "P");
                        link.fsnTrafficCorrect(grade);
                        calCnt.incrementAndGet();
                    }
                }
            );

        this.linkService.getFusionInfo().setMissPtnCnt(calCnt.get());
        logInfo("레벨1 결측(패턴)", totalCnt.get(), calCnt.get(), start);
        return calCnt.get();
    }

    private int missingDataRevision() {
        final long start = System.currentTimeMillis();
        // 결측된 구간에 대하여 결측 알고리즘으로 생성된 교통정보를 이용하여 속도값 생성
        AtomicInteger fsnCnt = new AtomicInteger();
        AtomicInteger calCnt = new AtomicInteger();
        AtomicInteger totalCnt = new AtomicInteger();
        this.linkService.getLinkMap().values().parallelStream()
                .filter(link -> link.getLinkLevel() == 1)
                .forEach(link -> {
                            if (link.getTrafFsn().getState().isState()) {
                                // 구간의 퓨전소통정보가 존재하면 처리하지 않음
                                fsnCnt.incrementAndGet();
                                return;
                            }

                            totalCnt.incrementAndGet();
                            if (link.getMissPsd().getSpeed() > 0) {
                                TrafficGrade grade = this.paramService.getTrafficGrade(link.getRoadRank());
                                link.getTrafFsn().setTraffic(LinkTrafState.TRAFFIC_CRT_DATA_TYPE.CRT_DATA_MISSING, "C00", link.getMissPsd().getSpeed(), link.getMissPsd().getMissValueYn(), "P");
                                link.fsnTrafficCorrect(grade);
                                calCnt.incrementAndGet();
                            }
                        }
                );

        this.linkService.getFusionInfo().setRealFsnCnt(fsnCnt.get());
        this.linkService.getFusionInfo().setMissPsdCnt(calCnt.get());
        this.linkService.getFusionInfo().setMissKnnCnt(0); // knn 알고리즘 수행하지 않음

        logInfo("결측정보 가공[PSD]", totalCnt.get(), calCnt.get(), start);
        return calCnt.get();
    }

    private int missingDataPsd() {
        final long start = System.currentTimeMillis();
        /* PSD 알고리즘 결측정보 처리(레벨1 모든 구간에 대하여 결측 처리)
         *  1. 레벨1 구간에 대한 처리
         *  2. 결측된 링크의 현재 패턴정보가 존재해야 처리할 수 있음.
         *  3. 상/하류 구간의 현재/패턴 정보가 존재해야함.
         *    3.1 상/하류 구간 정보가 모두 존재하는 경우
         *        결측속도 = 결측구간 패턴속도 * { [AVG(상류부현재속도+하류부현재속도)] / [AVG(상류부패턴속도+하류부패턴속도)]  }
         *    3.2 상/하류 구간 중 하나의 구간 정보 만 존해하는 경우
         *        결측속도 = 결측구간 패턴속도 * { 상/하류구간현재속도 / 상/하류구간패턴속도 }
         *  4. 3항에서 결측한 결측 속도를 포함한 레벨4 구간의 속도정보를 계산
         *  5. 결측이 발생한 레벨1 구간에 대해
         *        결측속도 = 결측구간 패턴속도 * { 레벨4현재속도 / 레벨4패턴속도 }
         */
        AtomicInteger calCnt = new AtomicInteger();
        AtomicInteger totalCnt = new AtomicInteger();
        AtomicInteger upCnt = new AtomicInteger();
        AtomicInteger dnCnt = new AtomicInteger();
        AtomicInteger upDnCnt = new AtomicInteger();
        this.linkService.getLinkMap().values().parallelStream()
                .filter(link -> link.getLinkLevel() == 1)
                .forEach(link -> {
                    totalCnt.incrementAndGet();
                    if (!link.getTrafPtrn().getState().isState()) {
                        // 구간의 패턴정보가 존재하지 않는 다면 결측 처리 못함
                        return;
                    }
                    LinkDto upLink = this.linkService.getLink(link.getUpLinkId());
                    LinkDto dnLink = this.linkService.getLink(link.getDnLinkId());
                    if (upLink == null && dnLink == null) {
                        // 상/하류부 링크가 모두 존재하지 않는 경우 결측처리 못함
                        return;
                    }

                    int step, speed;
                    float ptrnSpeed, cSpeed, pSpeed;
                    step = speed = 0;
                    cSpeed = pSpeed = 0F;
                    ptrnSpeed = (float)link.getTrafPtrn().getSpeed();

                    if (upLink != null) {
                        if (upLink.getTrafFsn().getState().isState() &&
                                upLink.getTrafPtrn().getState().isState()) {
                            // 상류 구간의 현재속도 정보와 패턴정보가 존재한다.
                            step += 1;
                            cSpeed += (float)upLink.getTrafFsn().getSpeed();
                            pSpeed += (float)upLink.getTrafPtrn().getSpeed();
                        }
                    }
                    if (dnLink != null) {
                        if (dnLink.getTrafFsn().getState().isState() &&
                                dnLink.getTrafPtrn().getState().isState()) {
                            // 상류 구간의 현재속도 정보와 패턴정보가 존재한다.
                            step += 2;
                            cSpeed += (float)dnLink.getTrafFsn().getSpeed();
                            pSpeed += (float)dnLink.getTrafPtrn().getSpeed();
                        }
                    }

                    String missValueYn = "";
                    if (step == 1 || step == 2) {
                        // 상류부(H) 또는 하류부(L) 소통정보 만 존재하는 경우
                        speed = Math.round(ptrnSpeed * (cSpeed / pSpeed));
                        if (step == 1) {
                            missValueYn = "H";
                            upCnt.incrementAndGet();
                        }
                        else {
                            missValueYn = "L";
                            dnCnt.incrementAndGet();
                        }
                    }
                    else if (step == 3) {
                        // 상/하류부 소통정보가 모두 존재하는 경우 'Y'
                        speed = Math.round(ptrnSpeed * ( (cSpeed/2) / (pSpeed/2) ));
                        missValueYn = "Y";
                        upDnCnt.incrementAndGet();
                    }
                    if (speed > 0) {
                        speed = link.correctSpeed(speed);
                        link.getMissPsd().setTraffic(missValueYn, speed);
                        calCnt.incrementAndGet();
                    }
                }
            );

        this.linkService.getFusionInfo().setPsdUpCnt(upCnt.get());
        this.linkService.getFusionInfo().setPsdDnCnt(dnCnt.get());
        this.linkService.getFusionInfo().setPsdUpDnCnt(upDnCnt.get());

        if (this.trace.isDebug()) {
            log.info("[INF] {}: 상류부({}), 하류부({}), 상하류부({})",
                    LogUtils.elapsedLog("상-하류부 이용 결측[PSD]"), upCnt.get(), dnCnt.get(), upDnCnt.get());
        }
        logInfo("상-하류부 이용 결측[PSD]", totalCnt.get(), calCnt.get(), start);
        return calCnt.get();
    }

    private int calLinkGroupSpeed(int linkLevel, String dataResType) {
        // 레벨1 링크그룹 소통정보를 이용하여 레벨2,3,4 링크그룹 소통정보를 생성한다.
        final long start = System.currentTimeMillis();
        AtomicInteger calCnt = new AtomicInteger();
        AtomicInteger totalCnt = new AtomicInteger();
        this.linkService.getLinkMap().values().parallelStream()
                .filter(link -> link.getLinkLevel() == linkLevel)
                .forEach(link -> {
                    // 링크그룹에 속한 레벨1 링크의 소통정보를 이용하여 링크그룹 소통정보를 생성한다.
                    if (link.getLinkIds() == null || link.getLinkIds().isEmpty()) {
                        // 그룹내 구간이 존재하지 않으면 정보생성 안함
                        return;
                    }

                    if (link.getTrafFsn().getState().isState()) {
                        // 소통정보가 존재하면 처리하지 않음
                        return;
                    }

                    totalCnt.incrementAndGet();
                    float sumSpeed, sumLength;
                    int   clctCnt;

                    sumSpeed = sumLength = 0.F;
                    clctCnt = 0;
                    for (String linkId : link.getLinkIds()) {
                        LinkDto subLink = this.linkService.getLink(linkId);
                        if (subLink == null) {
                            log.error("[ERR] LinkTrafMissingService.calLinkGroupSpeed[Level:{}]: {} link not found.", linkLevel, linkId);
                            continue;
                        }
                        if (!subLink.getTrafFsn().getState().isState()) {
                            continue; // 퓨전 소통정보가 존재하지 않으면 처리하지 않음
                        }

                        // 퓨전 소통정보 생성
                        if (subLink.getTrafFsn().getSpeed() > 0) {
                            // 운영자가 속도를 0으로 입력한 경우에 대한 처리
                            sumSpeed  += subLink.getTrafFsn().getSpeed() * subLink.getLength();
                            sumLength += subLink.getLength();
                            clctCnt++;
                        }
                    }

                    if (sumSpeed > 0. && sumLength > 0. && clctCnt > 0) {
                        TrafficGrade grade = this.paramService.getTrafficGrade(link.getRoadRank());
                        int speed = Math.round(sumSpeed / sumLength);
                        link.getTrafFsn().setTraffic(LinkTrafState.TRAFFIC_CRT_DATA_TYPE.CRT_DATA_NORMAL, "C00", speed, "M", dataResType);
                        link.fsnTrafficCorrect(grade);
                        calCnt.incrementAndGet();
                    }
                }
            );

        String message = String.format("레벨%d 소통정보 생성", linkLevel);
        logInfo(message, totalCnt.get(), calCnt.get(), start);
        return calCnt.get();
    }

    private int missingGroupPsd() {
        final long start = System.currentTimeMillis();
        AtomicInteger calCnt = new AtomicInteger();
        AtomicInteger totalCnt = new AtomicInteger();
        // 레벨4 구간을 이용한 PSD 결측정보 처리
        // 레벨1은 모든 구간에 대하여 PSD 알고리즘을 수행하고
        // 레벨4 소통정보 다운 알고리즘은 생성되지 않은 레벨1 구간에 대하여 수행함
        // PSD 상/하위 구간으로 처리하지 못한 구간에 대하여 4레벨 다운 소통정보를 생성
        // 레벨1 PSD 속도 = 레벨1 패턴속도 * (레벨4 퓨전속도 / 레벨4 패턴속도)
        this.linkService.getLinkMap().values().parallelStream()
                .filter(link -> link.getLinkLevel() == 4)
                .forEach(link -> {
                if (link.getLinkIds() == null || link.getLinkIds().isEmpty()) {
                    // 레벨4 구간에 속한 레벨1 구간이 존재하지 않음
                    return;
                }

                // 레벨1은 모든 구간에 대하여 PSD 알고리즘을 수행하고
                // 레벨4 소통정보 다운 알고리즘은 생성되지 않은 레벨1 구간에 대하여 수행함
                if (!link.getTrafFsn().getState().isState() ||
                        !link.getTrafPtrn().getState().isState()) {
                    // 레벨4 구간의 퓨전속도 또는 패턴속도가 존재하지 않으면 결측처리를 수행할 수 없다.
                    return;
                }

                totalCnt.incrementAndGet();
                float l4Speed = ((float)link.getTrafFsn().getSpeed() / link.getTrafPtrn().getSpeed());
                for (String linkId : link.getLinkIds()) {
                    LinkDto subLink = this.linkService.getLink(linkId);
                    if (subLink == null) {
                        log.error("[MISSING] LinkTrafMissingService.missingGroupPsd: {} link not found.", linkId);
                        continue;
                    }
                    if (subLink.getMissPsd().getSpeed() > 0) {
                        // 레벨1 구간의 PSD 속도정보가 존재하면 결측처리 수행하지 않음
                        continue;
                    }
                    if (!subLink.getTrafPtrn().getState().isState()) {
                        // 레벨1 구간의 패턴속도가 존재하지 않으면 결측처리 수행하지 않음
                        continue;
                    }

                    int speed = Math.round(subLink.getTrafPtrn().getSpeed() * l4Speed);
                    speed = subLink.correctSpeed(speed);
                    subLink.getMissPsd().setTraffic("K", speed);
                    calCnt.incrementAndGet();
                }
            }
        );
        this.linkService.getFusionInfo().setPsdLv4Cnt(calCnt.get());

        logInfo("레벨4구간 이용 결측[PSD]", totalCnt.get(), calCnt.get(), start);
        return calCnt.get();
    }
    private void logInfo(String message, int totalCnt, int calCnt, long start) {
        log.info("[INF] {}, {} EA.", LogUtils.elapsedLog(message, calCnt, System.currentTimeMillis() - start), LogUtils.numberLog(totalCnt));
    }
}
