package org.springblade.modules.martial.service.schedule.allocation;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.modules.martial.config.ScheduleConfig;
import org.springblade.modules.martial.pojo.entity.MartialVenue;
import org.springblade.modules.martial.service.schedule.model.ScheduleGroupData;
import org.springblade.modules.martial.service.schedule.model.TimeSlot;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Venue allocation service
 * Allocates groups to venues and time slots
 * 
 * @author Refactored from MartialScheduleArrangeServiceImpl
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class VenueAllocationService {

    private final ScheduleConfig scheduleConfig;

    /**
     * Validate if capacity is sufficient for all groups
     */
    public void validateCapacity(List<ScheduleGroupData> groups,
                                  List<MartialVenue> venues,
                                  List<TimeSlot> timeSlots) {
        int totalDuration = groups.stream()
            .mapToInt(ScheduleGroupData::getEstimatedDuration)
            .sum();

        int totalCapacity = venues.size() * timeSlots.size() * 
            (timeSlots.isEmpty() ? 0 : timeSlots.get(0).getCapacity());

        log.info("=== Capacity Validation ===");
        log.info("Total required duration: {} minutes", totalDuration);
        log.info("Total available capacity: {} minutes ({} venues x {} slots x {} min/slot)",
            totalCapacity, venues.size(), timeSlots.size(),
            timeSlots.isEmpty() ? 0 : timeSlots.get(0).getCapacity());

        if (totalDuration > totalCapacity) {
            String errorMsg = String.format(
                "Insufficient capacity! Need %d minutes, but only %d available (gap: %d minutes)",
                totalDuration, totalCapacity, totalDuration - totalCapacity
            );
            log.error(errorMsg);
            throw new RuntimeException(errorMsg);
        }

        double utilizationRate = totalCapacity > 0 ? (totalDuration * 100.0 / totalCapacity) : 0;
        log.info("Expected utilization rate: {}%", (int)utilizationRate);

        if (utilizationRate > scheduleConfig.getCapacityWarningThreshold()) {
            log.warn("Warning: Utilization rate exceeds 90%, allocation may be difficult");
        }
    }

    /**
     * Allocate groups to venues and time slots
     */
    public void allocate(List<ScheduleGroupData> groups,
                         List<MartialVenue> venues,
                         List<TimeSlot> timeSlots) {
        log.info("=== Starting venue and time slot allocation ===");
        log.info("Venues: {}, Time slots: {}, Groups: {}", venues.size(), timeSlots.size(), groups.size());

        Map<Long, Integer> venueTotalUsed = initVenueTotalUsed(venues);
        Map<String, Map<Long, Integer>> slotVenueUsed = new LinkedHashMap<>();
        Map<String, TimeSlot> slotMap = new LinkedHashMap<>();
        initSlotMaps(timeSlots, venues, slotVenueUsed, slotMap);

        Map<Long, List<ScheduleGroupData>> groupsByProject = groups.stream()
            .collect(Collectors.groupingBy(ScheduleGroupData::getProjectId));
        
        Map<Long, Integer> projectTotalDuration = calculateProjectDurations(groupsByProject);
        List<Long> sortedProjectIds = sortProjectsByPriority(groupsByProject, projectTotalDuration);

        int assignedCount = 0;
        for (Long projectId : sortedProjectIds) {
            assignedCount += allocateProject(projectId, groupsByProject.get(projectId),
                projectTotalDuration.get(projectId), venues, timeSlots,
                venueTotalUsed, slotVenueUsed, slotMap);
        }

        log.info("=== Allocation complete: {}/{} groups assigned ===", assignedCount, groups.size());
        logVenueUsage(venues, venueTotalUsed);
    }

    private Map<Long, Integer> initVenueTotalUsed(List<MartialVenue> venues) {
        Map<Long, Integer> venueTotalUsed = new LinkedHashMap<>();
        for (MartialVenue venue : venues) {
            venueTotalUsed.put(venue.getId(), 0);
        }
        return venueTotalUsed;
    }

    private void initSlotMaps(List<TimeSlot> timeSlots, List<MartialVenue> venues,
                               Map<String, Map<Long, Integer>> slotVenueUsed,
                               Map<String, TimeSlot> slotMap) {
        for (TimeSlot ts : timeSlots) {
            String key = ts.getDate() + "_" + ts.getPeriod();
            if (!slotVenueUsed.containsKey(key)) {
                slotVenueUsed.put(key, new LinkedHashMap<>());
                slotMap.put(key, ts);
                for (MartialVenue venue : venues) {
                    slotVenueUsed.get(key).put(venue.getId(), 0);
                }
            }
        }
    }

    private Map<Long, Integer> calculateProjectDurations(Map<Long, List<ScheduleGroupData>> groupsByProject) {
        Map<Long, Integer> projectTotalDuration = new HashMap<>();
        groupsByProject.forEach((projectId, projectGroups) -> {
            int total = projectGroups.stream()
                .mapToInt(g -> g.getEstimatedDuration() != null ? g.getEstimatedDuration() : 0)
                .sum();
            projectTotalDuration.put(projectId, total);
        });
        return projectTotalDuration;
    }

    private List<Long> sortProjectsByPriority(Map<Long, List<ScheduleGroupData>> groupsByProject,
                                               Map<Long, Integer> projectTotalDuration) {
        List<Long> sortedProjectIds = new ArrayList<>(groupsByProject.keySet());
        sortedProjectIds.sort((a, b) -> {
            List<ScheduleGroupData> groupsA = groupsByProject.get(a);
            List<ScheduleGroupData> groupsB = groupsByProject.get(b);
            int typeA = groupsA.get(0).getProjectType() != null ? groupsA.get(0).getProjectType() : 1;
            int typeB = groupsB.get(0).getProjectType() != null ? groupsB.get(0).getProjectType() : 1;
            if (typeA != typeB) {
                return typeB - typeA;
            }
            return projectTotalDuration.get(b) - projectTotalDuration.get(a);
        });
        return sortedProjectIds;
    }

    private int allocateProject(Long projectId, List<ScheduleGroupData> projectGroups,
                                 int projectDuration, List<MartialVenue> venues,
                                 List<TimeSlot> timeSlots, Map<Long, Integer> venueTotalUsed,
                                 Map<String, Map<Long, Integer>> slotVenueUsed,
                                 Map<String, TimeSlot> slotMap) {
        String projectName = projectGroups.get(0).getGroupName().split(" ")[0];
        int projectMaxParticipants = projectGroups.get(0).getMaxParticipants() != null 
            ? projectGroups.get(0).getMaxParticipants() : 1;

        List<MartialVenue> eligibleVenues = filterEligibleVenues(venues, projectMaxParticipants, projectName);
        Long bestVenueId = findBestVenue(eligibleVenues, venueTotalUsed);
        String bestSlotKey = findBestSlot(bestVenueId, projectDuration, slotVenueUsed, slotMap, projectName);

        TimeSlot targetSlot = slotMap.get(bestSlotKey);
        int slotIndex = timeSlots.indexOf(targetSlot);
        final Long venueId = bestVenueId;
        String venueName = venues.stream()
            .filter(v -> v.getId().equals(venueId))
            .findFirst()
            .map(MartialVenue::getVenueName)
            .orElse("Unknown");

        log.info("Project '{}' assigned to: venue={}, date={}, slot={}", 
            projectName, venueName, targetSlot.getDate(), targetSlot.getStartTime());

        for (ScheduleGroupData group : projectGroups) {
            group.setAssignedVenueId(venueId);
            group.setAssignedVenueName(venueName);
            group.setAssignedDate(targetSlot.getDate());
            group.setAssignedTimeSlot(targetSlot.getStartTime());
            group.setAssignedTimeSlotIndex(slotIndex);
            group.setAssignedTimePeriod(targetSlot.getPeriod());
        }

        slotVenueUsed.get(bestSlotKey).put(venueId, 
            slotVenueUsed.get(bestSlotKey).get(venueId) + projectDuration);
        venueTotalUsed.put(venueId, venueTotalUsed.get(venueId) + projectDuration);

        return projectGroups.size();
    }

    private List<MartialVenue> filterEligibleVenues(List<MartialVenue> venues, 
                                                     int projectMaxParticipants, 
                                                     String projectName) {
        List<MartialVenue> eligibleVenues = venues.stream()
            .filter(v -> {
                Integer capacity = v.getCapacity();
                if (capacity == null || capacity <= 0) return true;
                return capacity >= projectMaxParticipants;
            })
            .collect(Collectors.toList());

        if (eligibleVenues.isEmpty()) {
            log.warn("Project '{}' needs venue for {} people, but no eligible venue found! Using first venue",
                projectName, projectMaxParticipants);
            return venues;
        }
        return eligibleVenues;
    }

    private Long findBestVenue(List<MartialVenue> eligibleVenues, Map<Long, Integer> venueTotalUsed) {
        Long bestVenueId = null;
        int minTotalUsed = Integer.MAX_VALUE;
        for (MartialVenue venue : eligibleVenues) {
            int used = venueTotalUsed.get(venue.getId());
            if (used < minTotalUsed) {
                minTotalUsed = used;
                bestVenueId = venue.getId();
            }
        }
        return bestVenueId;
    }

    private String findBestSlot(Long venueId, int projectDuration,
                                 Map<String, Map<Long, Integer>> slotVenueUsed,
                                 Map<String, TimeSlot> slotMap, String projectName) {
        String bestSlotKey = null;
        for (String slotKey : slotVenueUsed.keySet()) {
            TimeSlot ts = slotMap.get(slotKey);
            int used = slotVenueUsed.get(slotKey).get(venueId);
            int remain = ts.getCapacity() - used;
            if (remain >= projectDuration) {
                bestSlotKey = slotKey;
                break;
            }
        }

        if (bestSlotKey == null) {
            int maxRemain = -1;
            for (String slotKey : slotVenueUsed.keySet()) {
                TimeSlot ts = slotMap.get(slotKey);
                int used = slotVenueUsed.get(slotKey).get(venueId);
                int remain = ts.getCapacity() - used;
                if (remain > maxRemain) {
                    maxRemain = remain;
                    bestSlotKey = slotKey;
                }
            }
            log.warn("Project '{}' duration {} minutes cannot fit in single slot", projectName, projectDuration);
        }
        return bestSlotKey;
    }

    private void logVenueUsage(List<MartialVenue> venues, Map<Long, Integer> venueTotalUsed) {
        log.info("=== Venue Usage Summary ===");
        for (Map.Entry<Long, Integer> entry : venueTotalUsed.entrySet()) {
            final Long vid = entry.getKey();
            String vname = venues.stream()
                .filter(v -> v.getId().equals(vid))
                .findFirst()
                .map(MartialVenue::getVenueName)
                .orElse("Unknown");
            log.info("Venue={}, Total load={} minutes", vname, entry.getValue());
        }
    }
}
