package kr.co.accura.wtm.domain.wbs.service;

import kr.co.accura.wbx.spring.common.BusinessException;
import kr.co.accura.wbx.spring.common.NotFoundException;
import kr.co.accura.wtm.domain.project.entity.Project;
import kr.co.accura.wtm.domain.project.repository.ProjectRepository;
import kr.co.accura.wtm.domain.wbs.dto.*;
import kr.co.accura.wtm.domain.wbs.entity.CanonicalWbs;
import kr.co.accura.wtm.domain.wbs.entity.WbsDisciplineAssignment;
import kr.co.accura.wtm.domain.wbs.entity.WbsNode;
import kr.co.accura.wtm.domain.wbs.entity.WbsVersion;
import kr.co.accura.wtm.domain.wbs.parser.P6WbsParser;
import kr.co.accura.wtm.domain.wbs.repository.CanonicalWbsRepository;
import kr.co.accura.wtm.domain.wbs.repository.WbsDisciplineAssignmentRepository;
import kr.co.accura.wtm.domain.wbs.repository.WbsNodeRepository;
import kr.co.accura.wtm.domain.wbs.repository.WbsVersionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
@Transactional
@RequiredArgsConstructor
public class WbsService {

    private final WbsVersionRepository wbsVersionRepository;
    private final WbsNodeRepository wbsNodeRepository;
    private final CanonicalWbsRepository canonicalWbsRepository;
    private final WbsDisciplineAssignmentRepository wbsDisciplineAssignmentRepository;
    private final ProjectRepository projectRepository;
    private final P6WbsParser p6Parser;

    /**
     * Upload P6 WBS file and create a new version.
     */
    public WbsVersionDto uploadP6Wbs(Long projectId, MultipartFile file,
                                     LocalDate effectiveDate, String description) {
        Project project = projectRepository.findById(projectId)
                .orElseThrow(() -> new NotFoundException("Project not found: " + projectId));

        // 1. Parse file
        WbsParseResult parseResult = p6Parser.parse(file);
        if (parseResult.hasErrors()) {
            throw new BusinessException("WBS parsing error: " + String.join(", ", parseResult.getErrors()));
        }

        // 2. Auto-increment version number
        int nextVersion = wbsVersionRepository
                .findMaxVersionByProjectId(projectId)
                .map(v -> v + 1).orElse(1);

        // 3. Save WBS version
        WbsVersion version = WbsVersion.builder()
                .project(project)
                .versionNumber(nextVersion)
                .effectiveDate(effectiveDate)
                .sourceType("P6_UPLOAD")
                .sourceFilename(file.getOriginalFilename())
                .description(description)
                .status("DRAFT")
                .build();
        wbsVersionRepository.save(version);

        // 4. Bulk save WBS nodes
        List<WbsNode> nodes = parseResult.getNodes().stream()
                .map(dto -> WbsNode.builder()
                        .wbsVersion(version)
                        .wbsCode(dto.getWbsCode())
                        .level(dto.getLevel())
                        .name(dto.getName())
                        .discipline(dto.getDiscipline())
                        .plannedHours(dto.getPlannedHours())
                        .isLeaf(dto.getLevel() != null && dto.getLevel() == 5)
                        .build())
                .toList();
        wbsNodeRepository.saveAll(nodes);

        return WbsVersionDto.from(version, nodes.size());
    }

    /**
     * Activate a WBS version (DRAFT -> ACTIVE).
     * Archives any existing ACTIVE version.
     */
    public void activateVersion(Long versionId) {
        WbsVersion version = wbsVersionRepository.findById(versionId)
                .orElseThrow(() -> new NotFoundException("WBS version not found: " + versionId));

        // Archive existing ACTIVE versions
        wbsVersionRepository.archiveActiveVersions(version.getProjectId());

        version.activate();
        wbsVersionRepository.save(version);

        // Sync canonical WBS
        syncCanonicalWbs(version);
    }

    /**
     * Get WBS versions for a project.
     */
    @Transactional(readOnly = true)
    public List<WbsVersionDto> getVersions(Long projectId) {
        return wbsVersionRepository.findByProject_IdOrderByVersionNumberDesc(projectId).stream()
                .map(v -> {
                    int count = wbsNodeRepository.findByWbsVersion_Id(v.getId()).size();
                    return WbsVersionDto.from(v, count);
                })
                .toList();
    }

    /**
     * Get WBS nodes for a specific version.
     */
    @Transactional(readOnly = true)
    public List<WbsNodeDto> getVersionNodes(Long projectId, Integer versionNumber) {
        WbsVersion version = wbsVersionRepository.findByProject_IdAndVersionNumber(projectId, versionNumber)
                .orElseThrow(() -> new NotFoundException("WBS version not found"));
        return wbsNodeRepository.findByWbsVersion_Id(version.getId()).stream()
                .map(WbsNodeDto::from)
                .toList();
    }

    /**
     * Get canonical WBS for a project.
     */
    @Transactional(readOnly = true)
    public List<CanonicalWbsDto> getCanonicalWbs(Long projectId) {
        return canonicalWbsRepository.findByProjectIdAndIsActiveTrue(projectId).stream()
                .map(CanonicalWbsDto::from)
                .toList();
    }

    /**
     * Compare two WBS versions.
     */
    @Transactional(readOnly = true)
    public WbsCompareResult compareVersions(Long projectId, int versionA, int versionB) {
        List<WbsNode> nodesA = wbsNodeRepository.findByProjectIdAndVersion(projectId, versionA);
        List<WbsNode> nodesB = wbsNodeRepository.findByProjectIdAndVersion(projectId, versionB);

        Map<String, WbsNode> mapA = nodesA.stream()
                .collect(Collectors.toMap(WbsNode::getWbsCode, Function.identity()));
        Map<String, WbsNode> mapB = nodesB.stream()
                .collect(Collectors.toMap(WbsNode::getWbsCode, Function.identity()));

        var result = new WbsCompareResult();

        // Added in B
        mapB.keySet().stream()
                .filter(code -> !mapA.containsKey(code))
                .forEach(code -> result.addAdded(mapB.get(code)));

        // Removed from A
        mapA.keySet().stream()
                .filter(code -> !mapB.containsKey(code))
                .forEach(code -> result.addRemoved(mapA.get(code)));

        // Modified
        mapA.keySet().stream()
                .filter(mapB::containsKey)
                .filter(code -> !mapA.get(code).contentEquals(mapB.get(code)))
                .forEach(code -> result.addModified(mapA.get(code), mapB.get(code)));

        return result;
    }

    /**
     * Get WBS-Discipline assignments for a project.
     */
    @Transactional(readOnly = true)
    public List<WbsDisciplineAssignmentDto> getWbsDisciplines(Long projectId) {
        List<CanonicalWbs> canonicalWbsList = canonicalWbsRepository.findByProjectIdAndIsActiveTrue(projectId);
        List<Long> canonicalWbsIds = canonicalWbsList.stream().map(CanonicalWbs::getId).toList();
        return wbsDisciplineAssignmentRepository.findByCanonicalWbs_IdIn(canonicalWbsIds).stream()
                .map(WbsDisciplineAssignmentDto::from)
                .toList();
    }

    /**
     * Save WBS-Discipline assignments for a project.
     */
    public List<WbsDisciplineAssignmentDto> saveWbsDisciplines(Long projectId,
                                                                List<WbsDisciplineAssignmentDto> assignments) {
        projectRepository.findById(projectId)
                .orElseThrow(() -> new NotFoundException("Project not found: " + projectId));

        List<WbsDisciplineAssignment> entities = assignments.stream().map(dto -> {
            CanonicalWbs cw = canonicalWbsRepository.findById(dto.getCanonicalWbsId())
                    .orElseThrow(() -> new NotFoundException("Canonical WBS not found: " + dto.getCanonicalWbsId()));
            return WbsDisciplineAssignment.builder()
                    .id(dto.getId())
                    .canonicalWbs(cw)
                    .discipline(dto.getDiscipline())
                    .isActive(dto.getIsActive() != null ? dto.getIsActive() : true)
                    .build();
        }).toList();

        return wbsDisciplineAssignmentRepository.saveAll(entities).stream()
                .map(WbsDisciplineAssignmentDto::from)
                .toList();
    }

    /**
     * Sync canonical WBS from active WBS version.
     */
    private void syncCanonicalWbs(WbsVersion version) {
        List<WbsNode> nodes = wbsNodeRepository.findByWbsVersion_Id(version.getId());

        for (WbsNode node : nodes) {
            canonicalWbsRepository.findByProjectIdAndWbsCode(
                    version.getProjectId(), node.getWbsCode()
            ).ifPresentOrElse(
                    existing -> {
                        existing.updateFrom(node);
                        existing.setMappedP6Code(node.getWbsCode());
                    },
                    () -> canonicalWbsRepository.save(
                            CanonicalWbs.fromWbsNode(version.getProjectId(), node))
            );
        }
    }
}
