package com.agileboot.domain.collaboration.record;

import com.agileboot.common.core.page.PageDTO;
import com.agileboot.domain.collaboration.record.command.AddCollaborationRecordCommand;
import com.agileboot.domain.collaboration.record.command.CollaborationExpenditureCommand;
import com.agileboot.domain.collaboration.record.command.CollaborationFileCommand;
import com.agileboot.domain.collaboration.record.command.CollaborationSettlementCommand;
import com.agileboot.domain.collaboration.record.command.CollaborationTaskCommand;
import com.agileboot.domain.collaboration.record.command.UpdateCollaborationRecordCommand;
import com.agileboot.domain.collaboration.record.db.CollaborationExpenditureEntity;
import com.agileboot.domain.collaboration.record.db.CollaborationExpenditureService;
import com.agileboot.domain.collaboration.record.db.CollaborationFileEntity;
import com.agileboot.domain.collaboration.record.db.CollaborationFileService;
import com.agileboot.domain.collaboration.record.db.CollaborationRecordEntity;
import com.agileboot.domain.collaboration.record.db.CollaborationRecordService;
import com.agileboot.domain.collaboration.record.db.CollaborationSettlementEntity;
import com.agileboot.domain.collaboration.record.db.CollaborationSettlementService;
import com.agileboot.domain.collaboration.record.db.CollaborationTaskEntity;
import com.agileboot.domain.collaboration.record.db.CollaborationTaskService;
import com.agileboot.domain.collaboration.record.dto.CollaborationExpenditureDTO;
import com.agileboot.domain.collaboration.record.dto.CollaborationFileDTO;
import com.agileboot.domain.collaboration.record.dto.CollaborationMonthlyStatisticsDTO;
import com.agileboot.domain.collaboration.record.dto.CollaborationOptionDTO;
import com.agileboot.domain.collaboration.record.dto.CollaborationRecordDTO;
import com.agileboot.domain.collaboration.record.dto.CollaborationRecordDetailDTO;
import com.agileboot.domain.collaboration.record.dto.CollaborationSettlementDTO;
import com.agileboot.domain.collaboration.record.dto.CollaborationTaskDTO;
import com.agileboot.domain.collaboration.record.dto.SettlementStatusDTO;
import com.agileboot.domain.collaboration.record.enumtype.CollaborationOptionEnum;
import com.agileboot.domain.collaboration.record.enumtype.SettlementStatusEnum;
import com.agileboot.domain.collaboration.record.model.CollaborationRecordModel;
import com.agileboot.domain.collaboration.record.model.CollaborationRecordModelFactory;
import com.agileboot.domain.collaboration.record.query.CollaborationRecordQuery;
import com.agileboot.domain.common.command.BulkOperationCommand;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author codex
 */
@Service
@RequiredArgsConstructor
public class CollaborationRecordApplicationService {

    private static final String PURCHASE_FEE = "拍单费用";
    private static final String DELIVERY_FEE = "快递费用";
    private static final String REMUNERATION_FEE = "稿费";

    private final CollaborationRecordModelFactory recordModelFactory;
    private final CollaborationRecordService recordService;
    private final CollaborationTaskService taskService;
    private final CollaborationExpenditureService expenditureService;
    private final CollaborationSettlementService settlementService;
    private final CollaborationFileService fileService;

    public PageDTO<CollaborationRecordDTO> getRecordList(CollaborationRecordQuery query) {
        Page<CollaborationRecordEntity> page = recordService.page(query.toPage(), query.toQueryWrapper());
        List<CollaborationRecordDTO> records = page.getRecords().stream()
            .map(this::buildRecordDTO)
            .collect(Collectors.toList());
        return new PageDTO<>(records, page.getTotal());
    }

    public CollaborationRecordDetailDTO getRecordInfo(Long recordId) {
        CollaborationRecordModel model = recordModelFactory.loadById(recordId);
        CollaborationRecordDetailDTO dto = new CollaborationRecordDetailDTO(model);
        fillDetailChildren(dto, recordId);
        fillRecordStats(dto, recordId);
        return dto;
    }

    @Transactional(rollbackFor = Exception.class)
    public void addRecord(AddCollaborationRecordCommand command) {
        CollaborationRecordModel model = recordModelFactory.create();
        model.loadFromAddCommand(command);
        model.saveRecord();
        saveChildren(model.getRecordId(), command);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateRecord(UpdateCollaborationRecordCommand command) {
        CollaborationRecordModel model = recordModelFactory.loadById(command.getRecordId());
        model.loadFromUpdateCommand(command);
        model.updateRecord();
        replaceChildren(command.getRecordId(), command);
    }

    @Transactional(rollbackFor = Exception.class)
    public void deleteRecord(BulkOperationCommand<Long> command) {
        Set<Long> ids = command.getIds();
        if (ids == null || ids.isEmpty()) {
            return;
        }
        taskService.removeByRecordIds(ids);
        expenditureService.removeByRecordIds(ids);
        settlementService.removeByRecordIds(ids);
        fileService.removeByRecordIds(ids);
        recordService.removeBatchByIds(ids);
    }

    public List<CollaborationOptionDTO> getOptions() {
        return Arrays.stream(CollaborationOptionEnum.values())
            .map(item -> new CollaborationOptionDTO(item.getType(), item.getLabel(), Arrays.asList(item.getValues())))
            .collect(Collectors.toList());
    }

    public List<CollaborationMonthlyStatisticsDTO> getMonthlyStatistics(Integer year) {
        List<CollaborationMonthlyStatisticsDTO> statistics = new ArrayList<>();
        for (int month = 1; month <= 12; month++) {
            statistics.add(buildMonthlyStatistics(year, month));
        }
        return statistics;
    }

    private CollaborationRecordDTO buildRecordDTO(CollaborationRecordEntity entity) {
        CollaborationRecordDTO dto = new CollaborationRecordDTO(entity);
        fillRecordStats(dto, entity.getRecordId());
        return dto;
    }

    private void fillDetailChildren(CollaborationRecordDetailDTO dto, Long recordId) {
        dto.setTasks(taskService.listByRecordId(recordId).stream()
            .map(CollaborationTaskDTO::new)
            .collect(Collectors.toList()));
        dto.setExpenditures(expenditureService.listByRecordId(recordId).stream()
            .map(CollaborationExpenditureDTO::new)
            .collect(Collectors.toList()));
        dto.setSettlements(settlementService.listByRecordId(recordId).stream()
            .map(CollaborationSettlementDTO::new)
            .collect(Collectors.toList()));
        dto.setFiles(fileService.listByRecordId(recordId).stream()
            .map(CollaborationFileDTO::new)
            .collect(Collectors.toList()));
    }

    private void fillRecordStats(CollaborationRecordDTO dto, Long recordId) {
        List<CollaborationTaskEntity> tasks = taskService.listByRecordId(recordId);
        List<CollaborationExpenditureEntity> expenditures = expenditureService.listByRecordId(recordId);
        List<CollaborationSettlementEntity> settlements = settlementService.listByRecordId(recordId);
        dto.setTasksNum(tasks.size());
        dto.setCompletedTasksNum((int) tasks.stream().filter(item -> item.getReleaseDate() != null).count());
        dto.setPurchaseSettlementStatus(getStatus(dto.getPurchasePrice(), settlements, PURCHASE_FEE));
        dto.setDeliverySettlementStatus(getStatus(sumExpenditure(expenditures, DELIVERY_FEE), settlements, DELIVERY_FEE));
        dto.setRemunerationSettlementStatus(getStatus(dto.getRemuneration(), settlements, REMUNERATION_FEE));
    }

    private SettlementStatusDTO getStatus(
        BigDecimal expectedAmount, List<CollaborationSettlementEntity> settlements, String purpose) {
        List<CollaborationSettlementEntity> matched = filterSettlements(settlements, purpose);
        if (matched.isEmpty()) {
            return toStatusDTO(SettlementStatusEnum.NONE);
        }
        BigDecimal settledAmount = sumSettlement(matched);
        BigDecimal expected = defaultAmount(expectedAmount);
        if (isAllSettlementPending(matched)) {
            return toStatusDTO(SettlementStatusEnum.UNSETTLED);
        }
        if (isAllSettlementDue(matched) && settledAmount.compareTo(expected) >= 0) {
            return toStatusDTO(SettlementStatusEnum.SETTLED);
        }
        return toStatusDTO(SettlementStatusEnum.PARTIAL);
    }

    private List<CollaborationSettlementEntity> filterSettlements(
        List<CollaborationSettlementEntity> settlements, String purpose) {
        if (settlements == null) {
            return Collections.emptyList();
        }
        return settlements.stream()
            .filter(item -> purpose.equals(item.getPurpose()))
            .collect(Collectors.toList());
    }

    private SettlementStatusDTO toStatusDTO(SettlementStatusEnum status) {
        return new SettlementStatusDTO(status.getValue(), status.getLabel());
    }

    private boolean isAllSettlementDue(List<CollaborationSettlementEntity> settlements) {
        Date today = getTodayStart();
        return settlements.stream()
            .allMatch(item -> item.getSettleDate() != null && !item.getSettleDate().after(today));
    }

    private boolean isAllSettlementPending(List<CollaborationSettlementEntity> settlements) {
        Date today = getTodayStart();
        return settlements.stream()
            .allMatch(item -> item.getSettleDate() == null || item.getSettleDate().after(today));
    }

    private void replaceChildren(Long recordId, AddCollaborationRecordCommand command) {
        List<Long> ids = Collections.singletonList(recordId);
        taskService.removeByRecordIds(ids);
        expenditureService.removeByRecordIds(ids);
        settlementService.removeByRecordIds(ids);
        fileService.removeByRecordIds(ids);
        saveChildren(recordId, command);
    }

    private void saveChildren(Long recordId, AddCollaborationRecordCommand command) {
        saveTasks(recordId, command.getTasks());
        saveExpenditures(recordId, command.getExpenditures());
        saveSettlements(recordId, command.getSettlements());
        saveFiles(recordId, command.getFiles());
    }

    private void saveTasks(Long recordId, List<CollaborationTaskCommand> tasks) {
        List<CollaborationTaskEntity> entities = toTaskEntities(recordId, tasks);
        if (!entities.isEmpty()) {
            taskService.saveBatch(entities);
        }
    }

    private void saveExpenditures(Long recordId, List<CollaborationExpenditureCommand> expenditures) {
        List<CollaborationExpenditureEntity> entities = toExpenditureEntities(recordId, expenditures);
        if (!entities.isEmpty()) {
            expenditureService.saveBatch(entities);
        }
    }

    private void saveSettlements(Long recordId, List<CollaborationSettlementCommand> settlements) {
        List<CollaborationSettlementEntity> entities = toSettlementEntities(recordId, settlements);
        if (!entities.isEmpty()) {
            settlementService.saveBatch(entities);
        }
    }

    private void saveFiles(Long recordId, List<CollaborationFileCommand> files) {
        List<CollaborationFileEntity> entities = toFileEntities(recordId, files);
        if (!entities.isEmpty()) {
            fileService.saveBatch(entities);
        }
    }

    private List<CollaborationTaskEntity> toTaskEntities(Long recordId, List<CollaborationTaskCommand> tasks) {
        List<CollaborationTaskEntity> entities = new ArrayList<>();
        if (tasks == null) {
            return entities;
        }
        for (int i = 0; i < tasks.size(); i++) {
            entities.add(toTaskEntity(recordId, tasks.get(i), i));
        }
        return entities;
    }

    private CollaborationTaskEntity toTaskEntity(Long recordId, CollaborationTaskCommand command, int index) {
        CollaborationTaskEntity entity = new CollaborationTaskEntity();
        entity.setRecordId(recordId);
        entity.setReleaseDate(command.getReleaseDate());
        entity.setSortOrder(index);
        return entity;
    }

    private List<CollaborationExpenditureEntity> toExpenditureEntities(
        Long recordId, List<CollaborationExpenditureCommand> expenditures) {
        if (expenditures == null) {
            return Collections.emptyList();
        }
        return expenditures.stream()
            .map(item -> toExpenditureEntity(recordId, item))
            .collect(Collectors.toList());
    }

    private CollaborationExpenditureEntity toExpenditureEntity(Long recordId, CollaborationExpenditureCommand command) {
        CollaborationExpenditureEntity entity = new CollaborationExpenditureEntity();
        entity.setRecordId(recordId);
        entity.setSpendDate(command.getSpendDate());
        entity.setAmount(command.getAmount());
        entity.setPurpose(command.getPurpose());
        return entity;
    }

    private List<CollaborationSettlementEntity> toSettlementEntities(
        Long recordId, List<CollaborationSettlementCommand> settlements) {
        if (settlements == null) {
            return Collections.emptyList();
        }
        return settlements.stream()
            .map(item -> toSettlementEntity(recordId, item))
            .collect(Collectors.toList());
    }

    private CollaborationSettlementEntity toSettlementEntity(Long recordId, CollaborationSettlementCommand command) {
        CollaborationSettlementEntity entity = new CollaborationSettlementEntity();
        entity.setRecordId(recordId);
        entity.setSettleDate(command.getSettleDate());
        entity.setMethod(command.getMethod());
        entity.setIncome(command.getIncome());
        entity.setPurpose(command.getPurpose());
        return entity;
    }

    private List<CollaborationFileEntity> toFileEntities(Long recordId, List<CollaborationFileCommand> files) {
        List<CollaborationFileEntity> entities = new ArrayList<>();
        if (files == null) {
            return entities;
        }
        for (int i = 0; i < files.size(); i++) {
            entities.add(toFileEntity(recordId, files.get(i), i));
        }
        return entities;
    }

    private CollaborationFileEntity toFileEntity(Long recordId, CollaborationFileCommand command, int index) {
        CollaborationFileEntity entity = new CollaborationFileEntity();
        entity.setRecordId(recordId);
        entity.setFileType(command.getFileType());
        entity.setUrl(command.getUrl());
        entity.setFileName(command.getFileName());
        entity.setNewFileName(command.getNewFileName());
        entity.setOriginalFilename(command.getOriginalFilename());
        entity.setSortOrder(index);
        return entity;
    }

    private CollaborationMonthlyStatisticsDTO buildMonthlyStatistics(Integer year, Integer month) {
        Date[] range = getMonthRange(year, month);
        BigDecimal purchasePrice = sumPurchasePrice(range);
        BigDecimal expenditureAmount = sumExpenditure(range);
        BigDecimal settledRemuneration = sumSettlement(range, REMUNERATION_FEE);
        BigDecimal settledTotal = sumSettlement(range, null);
        return new CollaborationMonthlyStatisticsDTO(month, purchasePrice, expenditureAmount,
            settledRemuneration, settledTotal);
    }

    private BigDecimal sumPurchasePrice(Date[] range) {
        List<CollaborationRecordEntity> records = recordService.list(new QueryWrapper<CollaborationRecordEntity>()
            .ge("purchase_date", range[0])
            .le("purchase_date", range[1]));
        return records.stream()
            .map(CollaborationRecordEntity::getPurchasePrice)
            .map(this::defaultAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    private BigDecimal sumExpenditure(Date[] range) {
        List<CollaborationExpenditureEntity> expenditures = expenditureService.list(
            new QueryWrapper<CollaborationExpenditureEntity>().ge("spend_date", range[0]).le("spend_date", range[1]));
        return expenditures.stream()
            .map(CollaborationExpenditureEntity::getAmount)
            .map(this::defaultAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    private BigDecimal sumSettlement(Date[] range, String purpose) {
        QueryWrapper<CollaborationSettlementEntity> wrapper = new QueryWrapper<CollaborationSettlementEntity>()
            .ge("settle_date", range[0])
            .le("settle_date", range[1])
            .eq(purpose != null, "purpose", purpose);
        return sumSettlement(settlementService.list(wrapper));
    }

    private BigDecimal sumExpenditure(List<CollaborationExpenditureEntity> expenditures, String purpose) {
        return expenditures.stream()
            .filter(item -> purpose.equals(item.getPurpose()))
            .map(CollaborationExpenditureEntity::getAmount)
            .map(this::defaultAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    private BigDecimal sumSettlement(List<CollaborationSettlementEntity> settlements) {
        return settlements.stream()
            .map(CollaborationSettlementEntity::getIncome)
            .map(this::defaultAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    private BigDecimal defaultAmount(BigDecimal amount) {
        return amount == null ? BigDecimal.ZERO : amount;
    }

    private Date[] getMonthRange(Integer year, Integer month) {
        Calendar start = Calendar.getInstance();
        start.set(year, month - 1, 1, 0, 0, 0);
        start.set(Calendar.MILLISECOND, 0);
        Calendar end = (Calendar) start.clone();
        end.add(Calendar.MONTH, 1);
        end.add(Calendar.MILLISECOND, -1);
        return new Date[]{start.getTime(), end.getTime()};
    }

    private Date getTodayStart() {
        Calendar today = Calendar.getInstance();
        today.set(Calendar.HOUR_OF_DAY, 0);
        today.set(Calendar.MINUTE, 0);
        today.set(Calendar.SECOND, 0);
        today.set(Calendar.MILLISECOND, 0);
        return today.getTime();
    }

}
