package de.cannamanage.service;

import de.cannamanage.domain.entity.Batch;
import de.cannamanage.domain.entity.Distribution;
import de.cannamanage.domain.entity.Member;
import de.cannamanage.domain.entity.Strain;
import de.cannamanage.domain.enums.MemberStatus;
import de.cannamanage.service.model.report.MemberListReport;
import de.cannamanage.service.model.report.MonthlyReport;
import de.cannamanage.service.model.report.RecallReport;
import de.cannamanage.service.repository.BatchRepository;
import de.cannamanage.service.repository.DistributionRepository;
import de.cannamanage.service.repository.MemberRepository;
import de.cannamanage.service.repository.StrainRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Generates report data for compliance and operational reporting.
 * All methods are read-only transactions scoped to a single tenant.
 */
@Service
@Transactional(readOnly = true)
public class ReportService {

    private static final ZoneId BERLIN = ZoneId.of("Europe/Berlin");

    private final DistributionRepository distributionRepository;
    private final MemberRepository memberRepository;
    private final BatchRepository batchRepository;
    private final StrainRepository strainRepository;

    public ReportService(DistributionRepository distributionRepository,
                         MemberRepository memberRepository,
                         BatchRepository batchRepository,
                         StrainRepository strainRepository) {
        this.distributionRepository = distributionRepository;
        this.memberRepository = memberRepository;
        this.batchRepository = batchRepository;
        this.strainRepository = strainRepository;
    }

    /**
     * Generate a monthly distribution report for the given tenant and month.
     */
    public MonthlyReport generateMonthlyReport(UUID tenantId, YearMonth month) {
        LocalDate startDate = month.atDay(1);
        LocalDate endDate = month.atEndOfMonth().plusDays(1);
        Instant start = startDate.atStartOfDay(BERLIN).toInstant();
        Instant end = endDate.atStartOfDay(BERLIN).toInstant();

        List<Distribution> distributions = distributionRepository.findByTenantIdAndDistributedAtBetween(tenantId, start, end);

        MonthlyReport report = new MonthlyReport();
        report.setMonth(month);
        report.setTotalDistributions(distributions.size());

        BigDecimal totalGrams = distributions.stream()
                .map(Distribution::getQuantityGrams)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        report.setTotalGrams(totalGrams);

        Set<UUID> uniqueMemberIds = distributions.stream()
                .map(Distribution::getMemberId)
                .collect(Collectors.toSet());
        report.setUniqueMembers(uniqueMemberIds.size());

        if (!uniqueMemberIds.isEmpty()) {
            report.setAveragePerMember(totalGrams.divide(
                    BigDecimal.valueOf(uniqueMemberIds.size()), 2, RoundingMode.HALF_UP));
        } else {
            report.setAveragePerMember(BigDecimal.ZERO);
        }

        // Top strains by total grams
        Map<UUID, List<Distribution>> byBatch = distributions.stream()
                .collect(Collectors.groupingBy(Distribution::getBatchId));

        Map<UUID, BigDecimal> gramsByStrain = new HashMap<>();
        Map<UUID, Integer> countByStrain = new HashMap<>();

        for (Map.Entry<UUID, List<Distribution>> entry : byBatch.entrySet()) {
            UUID batchId = entry.getKey();
            Optional<Batch> batchOpt = batchRepository.findById(batchId);
            if (batchOpt.isPresent()) {
                UUID strainId = batchOpt.get().getStrainId();
                BigDecimal batchGrams = entry.getValue().stream()
                        .map(Distribution::getQuantityGrams)
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                gramsByStrain.merge(strainId, batchGrams, BigDecimal::add);
                countByStrain.merge(strainId, entry.getValue().size(), Integer::sum);
            }
        }

        List<MonthlyReport.StrainSummary> topStrains = gramsByStrain.entrySet().stream()
                .sorted(Map.Entry.<UUID, BigDecimal>comparingByValue().reversed())
                .limit(10)
                .map(e -> {
                    String strainName = strainRepository.findById(e.getKey())
                            .map(Strain::getName)
                            .orElse("Unbekannt");
                    return new MonthlyReport.StrainSummary(strainName, e.getValue(),
                            countByStrain.getOrDefault(e.getKey(), 0));
                })
                .toList();
        report.setTopStrains(topStrains);

        // Daily breakdown
        Map<LocalDate, List<Distribution>> byDay = distributions.stream()
                .collect(Collectors.groupingBy(d ->
                        d.getDistributedAt().atZone(BERLIN).toLocalDate()));

        List<MonthlyReport.DailyEntry> dailyEntries = new ArrayList<>();
        LocalDate current = startDate;
        while (current.isBefore(endDate)) {
            List<Distribution> dayDists = byDay.getOrDefault(current, List.of());
            BigDecimal dayGrams = dayDists.stream()
                    .map(Distribution::getQuantityGrams)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            dailyEntries.add(new MonthlyReport.DailyEntry(current, dayGrams, dayDists.size()));
            current = current.plusDays(1);
        }
        report.setDailyBreakdown(dailyEntries);

        return report;
    }

    /**
     * Generate a member list report, optionally filtered by status.
     */
    public MemberListReport generateMemberListReport(UUID tenantId, MemberStatus filterStatus) {
        List<Member> members;
        if (filterStatus != null) {
            members = memberRepository.findByTenantIdAndStatus(tenantId, filterStatus);
        } else {
            members = memberRepository.findByTenantId(tenantId);
        }

        MemberListReport report = new MemberListReport();
        report.setGeneratedAt(Instant.now());

        List<MemberListReport.MemberEntry> entries = members.stream()
                .map(m -> {
                    MemberListReport.MemberEntry entry = new MemberListReport.MemberEntry();
                    entry.setId(m.getId());
                    entry.setFirstName(m.getFirstName());
                    entry.setLastName(m.getLastName());
                    entry.setMembershipNumber(m.getMembershipNumber());
                    entry.setStatus(m.getStatus());
                    entry.setJoinDate(m.getMembershipDate());
                    entry.setTotalDistributions((int) distributionRepository.countByTenantIdAndMemberId(tenantId, m.getId()));
                    Distribution latest = distributionRepository.findLatestByTenantIdAndMemberId(tenantId, m.getId());
                    entry.setLastDistributionDate(latest != null ? latest.getDistributedAt() : null);
                    return entry;
                })
                .toList();
        report.setMembers(entries);

        return report;
    }

    /**
     * Generate a recall report for a specific batch.
     * Traces all distributions from that batch back to affected members.
     */
    public RecallReport generateRecallReport(UUID tenantId, UUID batchId) {
        Batch batch = batchRepository.findById(batchId)
                .orElseThrow(() -> new de.cannamanage.service.exception.BatchNotFoundException(batchId));

        String strainName = strainRepository.findById(batch.getStrainId())
                .map(Strain::getName)
                .orElse("Unbekannt");

        List<Distribution> distributions = distributionRepository.findByTenantIdAndBatchId(tenantId, batchId);

        BigDecimal totalDistributed = distributions.stream()
                .map(Distribution::getQuantityGrams)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // Resolve member details for each distribution
        List<RecallReport.AffectedMember> affectedMembers = distributions.stream()
                .map(d -> {
                    RecallReport.AffectedMember am = new RecallReport.AffectedMember();
                    am.setMemberId(d.getMemberId());
                    am.setDistributionDate(d.getDistributedAt());
                    am.setGrams(d.getQuantityGrams());
                    memberRepository.findById(d.getMemberId()).ifPresent(member -> {
                        am.setFirstName(member.getFirstName());
                        am.setLastName(member.getLastName());
                    });
                    return am;
                })
                .toList();

        RecallReport report = new RecallReport();
        report.setBatchId(batchId);
        report.setStrainName(strainName);
        report.setBatchNumber(batch.getBatchCode());
        report.setReceivedDate(batch.getHarvestDate());
        report.setTotalGramsDistributed(totalDistributed);
        report.setAffectedMembers(affectedMembers);

        return report;
    }
}
