package com.agileboot.domain.collaboration.record;

import cn.hutool.core.util.StrUtil;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
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.agileboot.infrastructure.user.AuthenticationUtils;
import com.agileboot.infrastructure.user.web.SystemLoginUser;
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.function.Function;
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 static final String TASK_STATUS_COMPLETED = "COMPLETED";
    private static final String TASK_STATUS_INCOMPLETE = "INCOMPLETE";

    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) {
        if (hasComputedFilters(query)) {
            return getComputedFilteredRecordList(query);
        }
        Page<CollaborationRecordEntity> page = recordService.page(query.toPage(), getScopedRecordWrapper(query));
        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 = loadRecordInScope(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.setCompleteDate(getRecordCompleteDate(command.getTasks(), CollaborationTaskCommand::getReleaseDate));
        model.saveRecord();
        saveChildren(model.getRecordId(), command);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateRecord(UpdateCollaborationRecordCommand command) {
        CollaborationRecordModel model = loadRecordInScope(command.getRecordId());
        model.loadFromUpdateCommand(command);
        model.setCompleteDate(getRecordCompleteDate(command.getTasks(), CollaborationTaskCommand::getReleaseDate));
        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;
        }
        checkAllRecordsInScope(ids);
        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 QueryWrapper<CollaborationRecordEntity> getScopedRecordWrapper(CollaborationRecordQuery query) {
        QueryWrapper<CollaborationRecordEntity> wrapper = query.toQueryWrapper();
        applyCurrentCreatorScope(wrapper);
        return wrapper;
    }

    private PageDTO<CollaborationRecordDTO> getComputedFilteredRecordList(CollaborationRecordQuery query) {
        List<CollaborationRecordDTO> records = recordService.list(getScopedRecordWrapper(query)).stream()
            .map(this::buildRecordDTO)
            .filter(record -> matchesComputedFilters(record, query))
            .collect(Collectors.toList());
        return new PageDTO<>(pageRecords(records, query), (long) records.size());
    }

    private List<CollaborationRecordDTO> pageRecords(List<CollaborationRecordDTO> records, CollaborationRecordQuery query) {
        Page<CollaborationRecordEntity> page = query.toPage();
        int start = (int) Math.min((page.getCurrent() - 1) * page.getSize(), records.size());
        int end = (int) Math.min(start + page.getSize(), records.size());
        return records.subList(start, end);
    }

    private boolean hasComputedFilters(CollaborationRecordQuery query) {
        return StrUtil.isNotEmpty(query.getSettlementStatus()) || StrUtil.isNotEmpty(query.getTaskStatus());
    }

    private boolean matchesComputedFilters(CollaborationRecordDTO record, CollaborationRecordQuery query) {
        return matchesSettlementStatus(record, query.getSettlementStatus())
            && matchesTaskStatus(record, query.getTaskStatus());
    }

    private boolean matchesSettlementStatus(CollaborationRecordDTO record, String status) {
        if (StrUtil.isEmpty(status)) {
            return true;
        }
        return status.equals(getRecordSettlementStatus(record));
    }

    private String getRecordSettlementStatus(CollaborationRecordDTO record) {
        return SettlementStatusEnum.aggregate(Arrays.asList(
            getSettlementStatusValue(record.getPurchaseSettlementStatus()),
            getSettlementStatusValue(record.getDeliverySettlementStatus()),
            getSettlementStatusValue(record.getRemunerationSettlementStatus())
        ));
    }

    private String getSettlementStatusValue(SettlementStatusDTO status) {
        return status == null ? SettlementStatusEnum.NONE.getValue() : status.getStatus();
    }

    private boolean matchesTaskStatus(CollaborationRecordDTO record, String status) {
        if (StrUtil.isEmpty(status)) {
            return true;
        }
        boolean isCompleted = isRecordCompleted(record);
        if (TASK_STATUS_COMPLETED.equals(status)) {
            return isCompleted;
        }
        return TASK_STATUS_INCOMPLETE.equals(status) && !isCompleted;
    }

    private boolean isRecordCompleted(CollaborationRecordDTO record) {
        Integer tasksNum = record.getTasksNum();
        Integer completedTasksNum = record.getCompletedTasksNum();
        return tasksNum != null && tasksNum > 0 && tasksNum.equals(completedTasksNum);
    }

    private CollaborationRecordModel loadRecordInScope(Long recordId) {
        CollaborationRecordEntity entity = recordService.getOne(buildScopedRecordIdWrapper(recordId));
        if (entity == null) {
            throw new ApiException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, recordId, "合作记录");
        }
        return new CollaborationRecordModel(entity, recordService);
    }

    private QueryWrapper<CollaborationRecordEntity> buildScopedRecordIdWrapper(Long recordId) {
        QueryWrapper<CollaborationRecordEntity> wrapper = new QueryWrapper<CollaborationRecordEntity>()
            .eq("record_id", recordId);
        applyCurrentCreatorScope(wrapper);
        return wrapper;
    }

    private void checkAllRecordsInScope(Set<Long> ids) {
        QueryWrapper<CollaborationRecordEntity> wrapper = new QueryWrapper<CollaborationRecordEntity>()
            .in("record_id", ids);
        applyCurrentCreatorScope(wrapper);
        if (recordService.count(wrapper) != ids.size()) {
            throw new ApiException(ErrorCode.Business.PERMISSION_NOT_ALLOWED_TO_OPERATE);
        }
    }

    private void applyCurrentCreatorScope(QueryWrapper<CollaborationRecordEntity> wrapper) {
        Long creatorId = getCurrentCreatorScope();
        if (creatorId != null) {
            wrapper.eq("creator_id", creatorId);
        }
    }

    private Long getCurrentCreatorScope() {
        SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
        return loginUser.isAdmin() ? null : loginUser.getUserId();
    }

    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.setCompleteDate(getRecordCompleteDate(tasks, CollaborationTaskEntity::getReleaseDate));
        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 <T> Date getRecordCompleteDate(List<T> tasks, Function<T, Date> getReleaseDate) {
        if (tasks == null || tasks.isEmpty()) {
            return null;
        }
        if (tasks.stream().anyMatch(item -> getReleaseDate.apply(item) == null)) {
            return null;
        }
        return tasks.stream()
            .map(getReleaseDate)
            .max(Date::compareTo)
            .orElse(null);
    }

    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) {
        QueryWrapper<CollaborationRecordEntity> wrapper = new QueryWrapper<CollaborationRecordEntity>()
            .ge("purchase_date", range[0])
            .le("purchase_date", range[1]);
        applyCurrentCreatorScope(wrapper);
        List<CollaborationRecordEntity> records = recordService.list(wrapper);
        return records.stream()
            .map(CollaborationRecordEntity::getPurchasePrice)
            .map(this::defaultAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    private BigDecimal sumExpenditure(Date[] range) {
        QueryWrapper<CollaborationExpenditureEntity> wrapper = new QueryWrapper<CollaborationExpenditureEntity>()
            .ge("spend_date", range[0])
            .le("spend_date", range[1]);
        applyChildRecordScope(wrapper);
        List<CollaborationExpenditureEntity> expenditures = expenditureService.list(wrapper);
        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);
        applyChildRecordScope(wrapper);
        return sumSettlement(settlementService.list(wrapper));
    }

    private void applyChildRecordScope(QueryWrapper<?> wrapper) {
        Long creatorId = getCurrentCreatorScope();
        if (creatorId != null) {
            wrapper.inSql("record_id", buildCreatorRecordSubQuery(creatorId));
        }
    }

    private String buildCreatorRecordSubQuery(Long creatorId) {
        return "select record_id from collaboration_record where deleted = 0 and creator_id = " + creatorId;
    }

    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();
    }

}
