package com.honey.honey.service;

import com.honey.honey.dto.AdminStatisticsTableRowDto;
import com.honey.honey.repository.AdminStatisticsTableRepository;
import com.honey.honey.repository.AdminStatisticsTableRepository.StatisticsTableBucket;
import com.honey.honey.repository.UserDRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class AdminStatisticsTableService {

    private static final BigDecimal ZERO = new BigDecimal("0.00");
    private static final DateTimeFormatter MONTH_LABEL = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH);

    private final AdminStatisticsTableRepository adminStatisticsTableRepository;
    private final UserDRepository userDRepository;

    public enum TableMode {
        DAY,
        MONTH
    }

    public Page<AdminStatisticsTableRowDto> getStatisticsTable(TableMode mode, int page, int size) {
        LocalDate todayUtc = LocalDate.now(ZoneOffset.UTC);
        LocalDate firstDay = resolveFirstProjectDay(todayUtc);

        if (mode == TableMode.DAY) {
            return buildDayPage(todayUtc, firstDay, page, size);
        }
        return buildMonthPage(todayUtc, firstDay, page, size);
    }

    private LocalDate resolveFirstProjectDay(LocalDate todayUtc) {
        Optional<Integer> minReg = userDRepository.findMinDateReg();
        if (minReg.isEmpty() || minReg.get() == null || minReg.get() <= 0) {
            return todayUtc;
        }
        LocalDate d = Instant.ofEpochSecond(minReg.get()).atZone(ZoneOffset.UTC).toLocalDate();
        if (d.isAfter(todayUtc)) {
            return todayUtc;
        }
        return d;
    }

    private Page<AdminStatisticsTableRowDto> buildDayPage(LocalDate todayUtc, LocalDate firstDay, int page, int size) {
        long totalPeriods = ChronoUnit.DAYS.between(firstDay, todayUtc) + 1;
        if (totalPeriods < 1) {
            totalPeriods = 1;
        }

        int offset = page * size;
        if (offset >= totalPeriods) {
            return new PageImpl<>(List.of(), PageRequest.of(page, size), totalPeriods);
        }

        int pageLen = (int) Math.min(size, totalPeriods - offset);
        LocalDate newestOnPage = todayUtc.minusDays(offset);
        LocalDate oldestOnPage = todayUtc.minusDays(offset + pageLen - 1);

        long startSec = oldestOnPage.atStartOfDay(ZoneOffset.UTC).toEpochSecond();
        long endSec = newestOnPage.plusDays(1).atStartOfDay(ZoneOffset.UTC).toEpochSecond();

        Instant instStart = Instant.ofEpochSecond(startSec);
        Instant instEnd = Instant.ofEpochSecond(endSec);

        Map<LocalDate, StatisticsTableBucket> buckets = new HashMap<>();
        adminStatisticsTableRepository.loadRegistrationBucketsByDay(startSec, endSec, buckets);
        adminStatisticsTableRepository.loadRegistrationOursBucketsByDay(startSec, endSec, buckets);
        adminStatisticsTableRepository.loadPaymentBucketsByDay(instStart, instEnd, buckets);
        adminStatisticsTableRepository.loadPayoutBucketsByDay(instStart, instEnd, buckets);

        List<AdminStatisticsTableRowDto> rows = new ArrayList<>(pageLen);
        for (int i = 0; i < pageLen; i++) {
            int k = offset + i;
            LocalDate day = todayUtc.minusDays(k);
            rows.add(toRowDay(day, buckets.getOrDefault(day, new StatisticsTableBucket())));
        }

        return new PageImpl<>(rows, PageRequest.of(page, size), totalPeriods);
    }

    private Page<AdminStatisticsTableRowDto> buildMonthPage(LocalDate todayUtc, LocalDate firstDay, int page, int size) {
        YearMonth nowYm = YearMonth.from(todayUtc);
        YearMonth firstYm = YearMonth.from(firstDay);

        long totalMonths = (nowYm.getYear() - firstYm.getYear()) * 12L
                + (nowYm.getMonthValue() - firstYm.getMonthValue()) + 1;
        if (totalMonths < 1) {
            totalMonths = 1;
        }

        int offset = page * size;
        if (offset >= totalMonths) {
            return new PageImpl<>(List.of(), PageRequest.of(page, size), totalMonths);
        }

        int pageLen = (int) Math.min(size, totalMonths - offset);
        YearMonth newestOnPage = nowYm.minusMonths(offset);
        YearMonth oldestOnPage = nowYm.minusMonths(offset + pageLen - 1);

        long startSec = oldestOnPage.atDay(1).atStartOfDay(ZoneOffset.UTC).toEpochSecond();
        long endSec = newestOnPage.plusMonths(1).atDay(1).atStartOfDay(ZoneOffset.UTC).toEpochSecond();

        Instant instStart = Instant.ofEpochSecond(startSec);
        Instant instEnd = Instant.ofEpochSecond(endSec);

        Map<String, StatisticsTableBucket> buckets = new HashMap<>();
        adminStatisticsTableRepository.loadRegistrationBucketsByMonth(startSec, endSec, buckets);
        adminStatisticsTableRepository.loadRegistrationOursBucketsByMonth(startSec, endSec, buckets);
        adminStatisticsTableRepository.loadPaymentBucketsByMonth(instStart, instEnd, buckets);
        adminStatisticsTableRepository.loadPayoutBucketsByMonth(instStart, instEnd, buckets);

        List<AdminStatisticsTableRowDto> rows = new ArrayList<>(pageLen);
        for (int i = 0; i < pageLen; i++) {
            int k = offset + i;
            YearMonth ym = nowYm.minusMonths(k);
            rows.add(toRowMonth(ym, buckets.getOrDefault(ym.toString(), new StatisticsTableBucket())));
        }

        return new PageImpl<>(rows, PageRequest.of(page, size), totalMonths);
    }

    private AdminStatisticsTableRowDto toRowDay(LocalDate day, StatisticsTableBucket b) {
        long nonOurs = Math.max(0, b.regs - b.regsOurs);
        BigDecimal profitUsd = b.depositsSumUsd.subtract(b.withdrawalsSumUsd).setScale(2, RoundingMode.HALF_UP);
        BigDecimal profitPct = null;
        if (b.depositsSumUsd.compareTo(ZERO) > 0) {
            BigDecimal ratio = b.withdrawalsSumUsd.divide(b.depositsSumUsd, 8, RoundingMode.HALF_UP);
            profitPct = BigDecimal.ONE.subtract(ratio).multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP);
        }
        return AdminStatisticsTableRowDto.builder()
                .periodKey(day.toString())
                .periodLabel(day.toString())
                .regs(b.regs)
                .regsOurs(b.regsOurs)
                .regsNonOurs(nonOurs)
                .deadOurs(null)
                .deadNonOurs(null)
                .depositsCount(b.depositsCount)
                .depositsSumUsd(b.depositsSumUsd)
                .ftdCount(b.ftdCount)
                .avgPaymentUsd(b.avgPaymentUsd)
                .withdrawalsCount(b.withdrawalsCount)
                .withdrawalsSumUsd(b.withdrawalsSumUsd)
                .profitPercent(profitPct)
                .profitUsd(profitUsd)
                .build();
    }

    private AdminStatisticsTableRowDto toRowMonth(YearMonth ym, StatisticsTableBucket b) {
        long nonOurs = Math.max(0, b.regs - b.regsOurs);
        BigDecimal profitUsd = b.depositsSumUsd.subtract(b.withdrawalsSumUsd).setScale(2, RoundingMode.HALF_UP);
        BigDecimal profitPct = null;
        if (b.depositsSumUsd.compareTo(ZERO) > 0) {
            BigDecimal ratio = b.withdrawalsSumUsd.divide(b.depositsSumUsd, 8, RoundingMode.HALF_UP);
            profitPct = BigDecimal.ONE.subtract(ratio).multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP);
        }
        return AdminStatisticsTableRowDto.builder()
                .periodKey(ym.toString())
                .periodLabel(ym.format(MONTH_LABEL))
                .regs(b.regs)
                .regsOurs(b.regsOurs)
                .regsNonOurs(nonOurs)
                .deadOurs(null)
                .deadNonOurs(null)
                .depositsCount(b.depositsCount)
                .depositsSumUsd(b.depositsSumUsd)
                .ftdCount(b.ftdCount)
                .avgPaymentUsd(b.avgPaymentUsd)
                .withdrawalsCount(b.withdrawalsCount)
                .withdrawalsSumUsd(b.withdrawalsSumUsd)
                .profitPercent(profitPct)
                .profitUsd(profitUsd)
                .build();
    }
}
