package org.octopus.internal.services.impl

import jakarta.transaction.Transactional
import org.octopus.internal.common.enums.EBusinessException
import org.octopus.internal.common.enums.EEntryType
import org.octopus.internal.common.exceptions.OctopusPlanningException
import org.octopus.internal.common.isInTargetMonth
import org.octopus.internal.common.isRecurrentActive
import org.octopus.internal.common.models.DetailedReport
import org.octopus.internal.common.models.Report
import org.octopus.internal.common.toFirstDayOfMonthDate
import org.octopus.internal.common.toYearMonth
import org.octopus.internal.repositories.EntryRepository
import org.octopus.internal.repositories.ReportRepository
import org.octopus.internal.services.ReportService
import org.octopus.internal.web.dtos.ReportDto
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation
import java.time.YearMonth
import java.time.ZoneId
import java.time.temporal.ChronoUnit
import java.util.Date

@Service
class ReportServiceImpl(
    val repo: ReportRepository,
    val entryRepo: EntryRepository,
    private val zoneId: ZoneId = ZoneId.of("UTC") ) : ReportService {

    override fun createReport(reportDto: ReportDto): Report {
        val startingRepo = repo.getStartingReport()
        if (startingRepo != null) {
            throw OctopusPlanningException.create(EBusinessException.STARTING_REPORT_ALREADY_EXISTS, startingRepo)
        }

        return repo.createReport(Report(
            id = 0L,
            start = reportDto.start,
            reportMonth = reportDto.reportMonth,
            totalIncomes = 0.0,
            totalOutcomes = 0.0,
            expectedDelta = 0.0,
            expectedCount = reportDto.realCount,
            corrective = 0.0,
            realCount = reportDto.realCount
        ))
    }

    @Transactional
    override fun generateProjections(endDate: Date): Boolean {
        val allFixedInValidTime = entryRepo.getAllFixedUpToEndDate(endDate)
        val allRecurrent = entryRepo.getAllRecurrentMonthlyToEndDate(endDate)

        val lastRealCountReport = repo.getNonStartingReports(endDate)
            .filter { it.realCount > 0.0 }
            .maxByOrNull { it.reportMonth }


        val loopStartDate: YearMonth
        if (lastRealCountReport != null) {
            loopStartDate = lastRealCountReport.reportMonth.toYearMonth(zoneId).plusMonths(1)
        } else {
            loopStartDate = repo.getStartingReport()!!.reportMonth.toYearMonth(zoneId).plusMonths(1)
        }

        val loopEndDate = endDate.toYearMonth(zoneId)
        if (loopStartDate > loopEndDate) {
            println("Info: All reports are up to date until the end date. No new projections generated.")
            return true
        }

        deleteReportsAfterOrAtWithNewTransaction(loopStartDate.toFirstDayOfMonthDate(zoneId))

        val startingReport = repo.getStartingReport()!!

        var runningProjectedBalance = (lastRealCountReport ?: startingReport).realCount

        var currentMonth = loopStartDate
        while (currentMonth <= loopEndDate) {
            var totalIncomes = 0.0
            var totalOutcomes = 0.0

            allFixedInValidTime.filter {
                it.fixedDate.isInTargetMonth(currentMonth, zoneId)
            }.forEach { entry ->
                when (entry.type) {
                    EEntryType.INCOME -> totalIncomes += entry.amount
                    EEntryType.OUTCOME -> totalOutcomes += entry.amount
                    EEntryType.INVESTMENT -> {} // Ignore investments for direct flow calculation
                }
            }

            allRecurrent.filter { entry ->
                entry.isRecurrentActive(currentMonth, zoneId)
            }.forEach { entry ->
                when (entry.type) {
                    EEntryType.INCOME -> totalIncomes += entry.amount
                    EEntryType.OUTCOME -> totalOutcomes += entry.amount
                    EEntryType.INVESTMENT -> {} // Ignore investments for direct flow calculation
                }
            }

            val expectedDelta = totalIncomes - totalOutcomes
            val expectedCount = runningProjectedBalance + expectedDelta

            val reportDate = currentMonth.toFirstDayOfMonthDate(zoneId)

            val projectionReport = Report(
                id = 0L,
                start = false,
                reportMonth = reportDate,
                totalIncomes = totalIncomes,
                totalOutcomes = totalOutcomes,
                expectedDelta = expectedDelta,
                expectedCount = expectedCount,
                corrective = 0.0,
                realCount = 0.0
            )

            repo.createReport(projectionReport)
            println("Generated and saved projection for $currentMonth. Expected Balance: $expectedCount")

            runningProjectedBalance = expectedCount
            currentMonth = currentMonth.plus(1, ChronoUnit.MONTHS)
        }

        return true
    }

    @Transactional(Transactional.TxType.REQUIRES_NEW)
    private fun deleteReportsAfterOrAtWithNewTransaction(cutoffDate: Date) {
        repo.deleteAllReportsAfterOrAtDate(cutoffDate)
    }

    @Transactional(Transactional.TxType.REQUIRES_NEW)
    private fun deleteReportsAfterNewTransaction(cutoffDate: Date) {
        repo.deleteAllReportsAfter(cutoffDate)
    }


    override fun getAllReports(endDate: Date?): List<Report> {
        val startRepo = repo.getStartingReport()
            ?: throw OctopusPlanningException.create(EBusinessException.STARTING_REPORT_DOESNT_EXIST)
        if (endDate != null && startRepo.reportMonth <= endDate) {
            throw OctopusPlanningException.create(EBusinessException.REPORT_END_DATE_LE_START_REPORT)
        }
        val otherReports = repo.getNonStartingReports(endDate)
        otherReports.addFirst(startRepo)
        return otherReports
    }

    override fun getAllReportsWithAvgCorrection(endDate: Date?): List<Report> {
        val startRepo = repo.getStartingReport()
            ?: throw OctopusPlanningException.create(EBusinessException.STARTING_REPORT_DOESNT_EXIST)
        if (endDate != null && startRepo.reportMonth <= endDate) {
            throw OctopusPlanningException.create(EBusinessException.REPORT_END_DATE_LE_START_REPORT)
        }
        val otherReports = repo.getNonStartingReports(endDate)
        otherReports.addFirst(startRepo)

        val correctedReports = otherReports.filter { it.realCount > 0.0 && !it.start }

        val totalCorrection = correctedReports.sumOf { it.corrective }
        val correctedReportCount = correctedReports.size.toDouble()

        val averageCorrection = if (correctedReportCount > 0) {
            "%.2f".format(totalCorrection / correctedReportCount).toDouble()
        } else {
            0.0
        }

        val finalReports = mutableListOf<Report>()
        var runningProjectedBalance = 0.0

        val lastActualReport = otherReports
            .lastOrNull { it.start || it.realCount > 0.0 }
            ?: startRepo

        runningProjectedBalance = if (lastActualReport.realCount > 0.0) {
            lastActualReport.realCount
        } else {
            lastActualReport.expectedCount
        }

        for (report in otherReports) {
            if (report.start || report.realCount > 0.0) {
                finalReports.add(report)
                runningProjectedBalance = if (report.realCount > 0.0) report.realCount else report.expectedCount
            } else {
                val newExpectedCount = runningProjectedBalance + report.expectedDelta + averageCorrection
                val correctedReport = report.copy(
                    expectedCount = newExpectedCount
                )
                finalReports.add(correctedReport)
                runningProjectedBalance = newExpectedCount
            }
        }

        return finalReports
    }

    override fun getDetailedReport(id: Long): DetailedReport {
        val report = repo.getById(id)
        val reportYearMonth = report.reportMonth.toYearMonth(zoneId)
        val reportDate = report.reportMonth

        val allFixed = entryRepo.getAllFixedUpToEndDate(reportDate)
        val allRecurrent = entryRepo.getAllRecurrentMonthlyToEndDate(reportDate)

        val fixedEntries = allFixed.filter { entry ->
            entry.fixedDate.isInTargetMonth(reportYearMonth, zoneId)
        }

        val recurrentEntries = allRecurrent.filter { entry ->
            entry.isRecurrentActive(reportYearMonth, zoneId)
        }

        val allEntriesForMonth = fixedEntries + recurrentEntries

        return DetailedReport(
            yearMonth = reportYearMonth,
            report = report,
            entries = allEntriesForMonth
        )
    }

    override fun getAllReportsWithDetails(endDate: Date?): List<DetailedReport> {
        val effectiveEndDate = determineEffectiveEndDate(endDate)

        val allReports = repo.getNonStartingReports(effectiveEndDate)
        val allFixed = entryRepo.getAllFixedUpToEndDate(effectiveEndDate)
        val allRecurrent = entryRepo.getAllRecurrentMonthlyToEndDate(effectiveEndDate)

        return allReports.map { report ->
            val reportYearMonth = report.reportMonth.toYearMonth(zoneId)

            val fixedEntries = allFixed.filter { entry ->
                entry.fixedDate.isInTargetMonth(reportYearMonth, zoneId)
            }

            val recurrentEntries = allRecurrent.filter { entry ->
                entry.isRecurrentActive(reportYearMonth, zoneId)
            }

            val allEntriesForMonth = fixedEntries + recurrentEntries

            DetailedReport(
                yearMonth = reportYearMonth,
                report = report,
                entries = allEntriesForMonth
            )
        }
    }

    @Transactional
    override fun updateReport(
        id: Long,
        reportDto: ReportDto
    ): Report {
        val reportToUpdate = repo.getById(id)
        if (reportDto.start != reportToUpdate.start) {
            throw OctopusPlanningException.create(EBusinessException.REPORT_IS_START_NOT_CLEAR)
        }

        if (reportDto.start) {
            val startingReport = repo.createReport(Report(
                id = reportToUpdate.id,
                start = true,
                reportMonth = reportDto.reportMonth,
                totalIncomes = 0.0,
                totalOutcomes = 0.0,
                expectedDelta = 0.0,
                expectedCount = reportDto.realCount,
                corrective = 0.0,
                realCount = reportDto.realCount
            ))

            repo.deleteAllReportsBefore(reportDto.reportMonth)
            generateProjections(determineEffectiveEndDate(null))
            return startingReport
        }

        val correctionAmount = reportDto.realCount - reportToUpdate.expectedCount

        val updatedReport = repo.createReport(Report(
            id = reportToUpdate.id,
            start = false,
            reportMonth = reportToUpdate.reportMonth,
            totalIncomes = reportToUpdate.totalIncomes,
            totalOutcomes = reportToUpdate.totalOutcomes,
            expectedDelta = reportToUpdate.expectedDelta,
            expectedCount = reportToUpdate.expectedCount, // Retain old expectedCount here
            corrective = correctionAmount, // Record the correction
            realCount = reportDto.realCount // Set the new verified amount
        ))

        val cutoffDate = updatedReport.reportMonth
        val subsequentReports = repo.getNonStartingReports(null)
            .filter { it.reportMonth.after(cutoffDate) }
            .sortedBy { it.reportMonth }
            .toMutableList()

        val reportsToUpdateBatch = mutableListOf<Report>()
        for (subReport in subsequentReports) {
            if (subReport.realCount > 0.0) {
                val correctedExpectedCount = subReport.expectedCount + correctionAmount

                reportsToUpdateBatch.add(subReport.copy(
                    expectedCount = correctedExpectedCount,
                    corrective = subReport.realCount - correctedExpectedCount
                ))
            }
        }

        repo.createReports(reportsToUpdateBatch)
        deleteReportsAfterNewTransaction(cutoffDate)
        generateProjections(determineEffectiveEndDate(null))

        return updatedReport
    }

    private fun determineEffectiveEndDate(inputEndDate: Date?): Date {
        if (inputEndDate != null) return inputEndDate

        val allIncome = entryRepo.getAllByType(EEntryType.INCOME)
        val allOutcome = entryRepo.getAllByType(EEntryType.OUTCOME)

        val allDates = (allIncome + allOutcome).mapNotNull { entry -> entry.fixedDate ?: entry.endDate }

        return allDates.maxByOrNull { it } ?: Date()
    }

}