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.dao.mapper.utic.UticTrafficMapper;
import com.utic.center.utic.traf.server.dao.repository.utic.UticTrafficRepository;
import com.utic.center.utic.traf.server.dto.DbmsBatchJobResult;
import com.utic.center.utic.traf.server.dto.DbmsJobResult;
import com.utic.center.utic.traf.server.dto.LinkDto;
import com.utic.center.utic.traf.server.dto.LinkTrafCenterDto;
import com.utic.center.utic.traf.server.repository.ApplicationRepository;
import com.utic.center.utic.traf.server.service.worker.WorkerUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class LinkTrafSaveUticService implements AbstractProcessService {

    private final TraceConfig trace;
    private final LinkService linkService;
    private final UticTrafficRepository uticRepo;
    private final SqlSessionFactory sqlSessionFactory;

    public LinkTrafSaveUticService(
            TraceConfig trace,
            LinkService linkService,
            UticTrafficRepository uticRepo,
            @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory
    ) {
        this.trace = trace;
        this.linkService = linkService;
        this.uticRepo = uticRepo;
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @ProcessingElapsed(type="SAVE", name="교통정보 UTIC 저장", endLine = true)
    @Override
    public boolean processing() {
        // 데이터 초기화
        initialize();
        // 교통정보 업데이트
        updateTrafficCenter();
        // 뷰테이블 소스 전환
        swapTrafficCenterView();
        return true;
    }

    private void initialize() {
        final long start = System.currentTimeMillis();
        log.info("[INF] {}: Current, Active({}), Next({})",
                LogUtils.elapsedLog("TRAFFIC_CENTER(VIEW)"),
                ApplicationRepository.currTrafficCenter,
                ApplicationRepository.nextTrafficCenter);

        long zeroSpeedCount = this.linkService.getLinkMap().values().parallelStream()
                .filter(link -> link.getLinkLevel() == 1)
                .filter(link -> link.getTrafFsn().getState().isState())
                .filter(link -> link.getTrafFsn().getSpeed() == 0)
                .count();
        // 1레벨 링크 중, 소통상태이면서 속도가 0인 링크 개수
        this.linkService.getFusionInfo().setZeroSpdCnt((int)zeroSpeedCount);

        // 모든 링크에 대해 센터 소통정보 생성
        this.linkService.makeTrafficCenterDto(ApplicationRepository.trafPrcsTime.getPrcsTime());
        this.linkService.makeValidTrafLink();   // 유효한 교통정보 링크 목록 생성
        log.info("[INF] {}", LogUtils.elapsedLog("교통정보 데이터 가공", System.currentTimeMillis() - start));
    }

    private void batchInsertTrafficCenter(DbmsBatchJobResult dbmsJobResult, String jobTable) {
        final long start = System.currentTimeMillis();
        final String mapperName = "insertTrafficCenter";
        final int maxBatchSize = ApplicationRepository.DBMS_BATCH_SIZE;
        int total = 0, target = 0, jobCnt = 0;
        int[] linkCounts = new int[4];

        try (SqlSession sqlSession = this.sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
            UticTrafficMapper mapper = sqlSession.getMapper(UticTrafficMapper.class);

            for (String linkId : dbmsJobResult.getLinkIds()) {
                LinkDto link = this.linkService.getLink(linkId);
                if (link == null) {
                    log.error("[ERR] batchInsertTrafficCenter: {} link not found.", linkId);
                    continue;
                }

                total++;
                if (!link.getTrafFsn().getState().isState()) {
                    continue;
                }
                if (!"O".equals(link.getTrafFsn().getState().getMissValueYn()) && link.getTrafFsn().getSpeed() == 0) {
                    continue;
                }

                target++;
                LinkTrafCenterDto trafDto = link.getTrafCenter();

                try {
                    mapper.insertTrafficCenter(jobTable, trafDto);
                    linkCounts[trafDto.getLinkLevel()-1]++;
                    jobCnt++;

                    if (jobCnt % maxBatchSize == 0) {
                        sqlSession.flushStatements();
                    }
                } catch (Exception e) {
                    log.error("[ERR] batchInsertTrafficCenter({}): jobCnt({}), Exception, [{}], {}.",
                            dbmsJobResult.getJobIndex(), jobCnt, trafDto, e.getMessage());
                    sqlSession.rollback(); // 즉시 롤백
                    break; // 작업 중단
                }
            }
            try {
                sqlSession.flushStatements();
                sqlSession.commit();
            }
            catch (Exception e) {
                log.error("[ERR] batchInsertTrafficCenter({}): commit Exception, [{}], {}.", dbmsJobResult.getJobIndex(), jobTable, e.getMessage());
                sqlSession.rollback(); // 예외 발생 시 롤백
            }
        } catch (Exception e) {
            log.error("[ERR] batchInsertTrafficCenter({}): Exception, [{}], {}.", dbmsJobResult.getJobIndex(), jobTable, e.getMessage());
        }

        dbmsJobResult.setTotal(total);
        dbmsJobResult.setTarget(target);
        dbmsJobResult.setLink1(linkCounts[0]);
        dbmsJobResult.setLink2(linkCounts[1]);
        dbmsJobResult.setLink3(linkCounts[2]);
        dbmsJobResult.setLink4(linkCounts[3]);
        dbmsJobResult.setEffects(jobCnt);
        dbmsJobResult.setElapsedTime(System.currentTimeMillis() - start);

        if (this.trace.isDebug()) {
            log.info("[INS] {} | {}", LogUtils.elapsedLog(mapperName + dbmsJobResult.getJobIndex(), jobCnt, dbmsJobResult.getElapsedTime()), Thread.currentThread().getName());
        }
    }

    private void updateTrafficCenter() {
        final long start = System.currentTimeMillis();
        final String jobTable = ApplicationRepository.nextTrafficCenter;
        final DbmsJobResult jobResult = DbmsJobResult.builder()
                .tableName(ApplicationRepository.nextTrafficCenter)
                .effects(0).elapsedTime(0).build();

        ConcurrentHashMap<Integer, DbmsBatchJobResult> mapData = new ConcurrentHashMap<>();
        final int threadPoolSize = WorkerUtils.allocateWorkerLinkJobs(mapData, ApplicationRepository.PRCS_THREAD_COUNT, this.linkService.getValidTrafLinkList());
        if (threadPoolSize < 0) {
            log.warn("[WAN] updateTrafficCenter: getValidTrafLinkList no data, job return: [{}].", jobTable);
            return;
        }

        log.info("[INF] {}: Links({}), PoolSize({})", LogUtils.elapsedLog("UTIC BATCH SAVE"), this.linkService.getValidTrafLinkList().size(), threadPoolSize);

        final ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);
        mapData.forEach((key, dbmsJobResult) -> {
//            log.info("[INF] {}: Job({}), Link({})", LogUtils.elapsedLog("BatchThread"), dbmsJobResult.getJobIndex(), dbmsJobResult.getLinkIds().size());
            executorService.execute(() -> batchInsertTrafficCenter(dbmsJobResult, jobTable));
        });

        executorService.shutdown();
        try {
            if (!executorService.awaitTermination(120, TimeUnit.SECONDS)) {
                log.warn("[WAN] updateTrafficCenter: Timeout while waiting for tasks to finish, [{}].", jobTable);
                List<Runnable> droppedTasks = executorService.shutdownNow();
                log.warn("[WAN] updateTrafficCenter: {} tasks were dropped during shutdownNow, [{}].", droppedTasks.size(), jobTable);
            }
        } catch (InterruptedException e) {
            log.error("[ERR] updateTrafficCenter: InterruptedException, [{}], {}.", jobTable, e.getMessage());
            List<Runnable> droppedTasks = executorService.shutdownNow();
            log.error("[ERR] updateTrafficCenter: Forced shutdown due to interruption, [{}] tasks dropped.", droppedTasks.size());
            Thread.currentThread().interrupt(); // 현재 스레드의 인터럽트 상태 복원
        }

        long elapsedTime = 0;
        int link1 = 0, link2 = 0, link3 = 0, link4 = 0;

        for (DbmsBatchJobResult result : mapData.values()) {
            elapsedTime = Math.max(elapsedTime, result.getElapsedTime());
            link1 += result.getLink1();
            link2 += result.getLink2();
            link3 += result.getLink3();
            link4 += result.getLink4();
        }

        int resultCount = link1 + link2 + link3 + link4;
        jobResult.setEffects(resultCount);
        jobResult.setElapsedTime(System.currentTimeMillis() - start);

        log.info("[INF] {}", LogUtils.elapsedLog("레벨1 " + jobTable, link1, elapsedTime));
        log.info("[INF] {}", LogUtils.elapsedLog("레벨2 " + jobTable, link2, elapsedTime));
        log.info("[INF] {}", LogUtils.elapsedLog("레벨3 " + jobTable, link3, elapsedTime));
        log.info("[INF] {}", LogUtils.elapsedLog("레벨4 " + jobTable, link4, elapsedTime));
        // 모든 작업이 완료된 후 결과를 출력
        log.info("[INF] {}", LogUtils.elapsedLog(jobTable, jobResult.getEffects(), System.currentTimeMillis() - start));
    }

    private void swapTrafficCenterView() {
        final String nextTable = ApplicationRepository.nextTrafficCenter;
        final String currTable = ApplicationRepository.currTrafficCenter;

        this.uticRepo.rebuildIndexes(nextTable, "1");
        this.uticRepo.rebuildIndexes(nextTable, "2");
        this.uticRepo.enablePrimaryKey(nextTable);

        // VIEW 의 source table을 변경한다.
        this.uticRepo.swapTrafficCenterView(currTable, nextTable);

        // 메모리의 TRAFFIC_CENTER Source Table 정보를 변경한다.
        ApplicationRepository.swapTrafficCenterView();

        log.info("[INF] {}: Changed, Active({}), Next({})", LogUtils.elapsedLog("TRAFFIC_CENTER(VIEW)"), ApplicationRepository.currTrafficCenter, ApplicationRepository.nextTrafficCenter);
    }
}
