package space.nstart.pcp.slots_service.model.satellite

import ballistics.types.InitialConditions
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotHolderDTO
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
import space.nstart.pcp.slots_service.entity.SlotEntity
import space.nstart.pcp.slots_service.model.BookedSlot
import space.nstart.pcp.slots_service.model.intersection
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import kotlin.math.abs

class TestSatelliteImpl(
    id : Long = 0,
    cycleRevs : Long = 243,
    tnCalc : LocalDateTime = LocalDateTime.of(
        LocalDate.of(2025,10,27),
        LocalTime.of(0,29,23,162630000)),
    durationCalc : Long = 16,
    slotDuration : Long = 10,
    maxChainLength : Int = 3,
    maxSurveyDuration: Int = 300,
    mmi: Long = 10,
    angles : List<Double> = listOf(),
    ic : InitialConditions = InitialConditions()
) : AbstractSatellite(id, cycleRevs, tnCalc, durationCalc, slotDuration, maxChainLength, maxSurveyDuration, mmi, angles, ic) {
    override fun prepareCoverageSlots(
        slots: List<SlotEntity>,
        bookedSlots: List<BookedSlotEntity>,
        timeStart : LocalDateTime,
        timeStop : LocalDateTime,
        sign : RevolutionSign?,
        states : List<SlotStatus>?
    ): List<PreparedCoverageSlot> {
        class BookedInfo(val id : Long, val cycle : Long)
        // Держатель блокирующей брони: сдвинутый на cycle интервал + крен + заявки-держатели.
        // Нужен, чтобы у BUSY-слота показать, чья бронь его блокирует (M3 — обзор конфликтов).
        class HolderInterval(
            val start : LocalDateTime,
            val end : LocalDateTime,
            val roll : Double,
            val requestIds : List<String>
        )
        val selectedSlots = mutableListOf<PreparedCoverageSlot>()
        val cycleBegin = Duration.between(tnCalc, timeStart).toDays() / durationCalc
        val cycleEnd = Duration.between(tnCalc, timeStop).toDays() / durationCalc
        val activeBookedSlots = bookedSlots.filter { it.status in ACTIVE_BOOKED_STATUSES }
        val booked = activeBookedSlots.map { BookedInfo(it.slot.slotId?:0, it.cycle) }
        val holderIntervals = activeBookedSlots.map { bookedSlot ->
            val offsetDays = bookedSlot.cycle * durationCalc
            HolderInterval(
                bookedSlot.slot.tn.plusDays(offsetDays),
                bookedSlot.slot.tk.plusDays(offsetDays),
                bookedSlot.slot.roll,
                bookedSlot.requests.orEmpty().map { it.requestId }.distinct()
            )
        }
        val bookedData = activeBookedSlots.map { bookedSlot ->
            val offsetDays = bookedSlot.cycle * durationCalc
            BookedSlot(
                bookedSlot.slot.tn.plusDays(offsetDays),
                bookedSlot.slot.tk.plusDays(offsetDays),
                bookedSlot.slot.roll,
                0,
                bookedSlot.cycle
            )
        }.toMutableList()
        val bookedIntervals = mergeBookedIntervals(bookedData.map { BookedInterval(it.t, it.tEnd) })
        calculateChainLengths(bookedData)
        for (cycleN in cycleBegin..cycleEnd) {
            val currentSlots = slots.map { ent ->
                SlotEntity(
                    ent.slotId,
                    ent.slotNum,
                    cycleN,
                    ent.satelliteId,
                    ent.coveringType,
                    ent.tn.plusDays(cycleN * durationCalc),
                    ent.tk.plusDays(cycleN * durationCalc),
                    ent.roll,
                    ent.contour,
                    ent.revolution + cycleN * cycleRevs,
                    ent.revolutionSign,
                    latitude = ent.latitude,
                    longitude = ent.longitude
                )
            }
            selectedSlots.addAll(
                currentSlots.filter { slot ->
                    slot.tn >= timeStart && slot.tk <= timeStop &&
                            sign?.let { slot.revolutionSign == it.toString() } ?: true
                }.map {
                    val isBooked = booked.any { booked -> booked.cycle == it.cycle && booked.id == it.slotId }
                    val insideBookedInterval = !isBooked && bookedIntervals.any { booked -> booked.contains(it.tn, it.tk) }
                    val band = if (insideBookedInterval) {
                        true
                    } else {
                        bookedData.any { booked ->
                            val dif = intersection(it.tn.minusSeconds(mmi), it.tk.plusSeconds(mmi), booked.t, booked.tEnd)
                            val gam = abs(it.roll - booked.roll)

                            if (dif) {
                                !(gam < 0.01 && booked.neighbours < maxChainLength)
                            } else {
                                false
                            }
                        }
                    }
                    // Для BUSY-слота собираем держателей: активные брони, чей сдвинутый интервал
                    // пересекается со слотом (с учётом mmi) — та же логика наложения, что и в band.
                    // distinct по requestId: одну бронь могут держать несколько заявок (множ. держатель).
                    val holders = if (band) {
                        holderIntervals
                            .filter { holder ->
                                intersection(it.tn.minusSeconds(mmi), it.tk.plusSeconds(mmi), holder.start, holder.end)
                            }
                            .flatMap { holder -> holder.requestIds }
                            .distinct()
                            .map { reqId -> SlotHolderDTO(reqId) }
                            .takeIf { holders -> holders.isNotEmpty() }
                    } else {
                        null
                    }
                    PreparedCoverageSlot(
                        sourceSlotId = it.slotId ?: 0L,
                        slot = it.toDTO(
                            isBooked,
                            band,
                            holders
                        )
                    )
                })
        }
        if (!states.isNullOrEmpty())
            selectedSlots.removeIf { !states.contains(it.slot.state) }
        return selectedSlots
    }

    private data class BookedInterval(val start: LocalDateTime, val end: LocalDateTime) {
        fun contains(slotStart: LocalDateTime, slotEnd: LocalDateTime): Boolean =
            !slotStart.isBefore(start) && !slotEnd.isAfter(end)
    }

    private fun mergeBookedIntervals(intervals: List<BookedInterval>): List<BookedInterval> {
        if (intervals.isEmpty()) {
            return emptyList()
        }
        val merged = mutableListOf<BookedInterval>()
        intervals.sortedBy { it.start }.forEach { interval ->
            val last = merged.lastOrNull()
            if (last != null && !interval.start.isAfter(last.end)) {
                merged[merged.lastIndex] = BookedInterval(
                    start = minOf(last.start, interval.start),
                    end = maxOf(last.end, interval.end)
                )
            } else {
                merged += interval
            }
        }
        return merged
    }

    private fun calculateChainLengths(slots: MutableList<BookedSlot>) {
        if (slots.isEmpty())
            return
        // 1. Сортируем слоты по времени
        slots.sortBy { it.t }
        var chainStartIndex = 0
        // 2. Проходим по отсортированному списку и определяем границы цепочек
        for (i in 1 until slots.size) {
            val prev = slots[i - 1]
            val current = slots[i]
            // Проверяем критерии соседства:
            // - разница во времени < 1 секунды
            // - разница в roll < 0.01
            val timeDiffSeconds = Duration.between(prev.t, current.t).toMillis() / 1000.0
            val rollDiff = abs(current.roll - prev.roll)
            // Если элементы НЕ являются соседями — завершаем текущую цепочку
            if (timeDiffSeconds >= 30.0 || rollDiff >= 0.01) {
                // Заполняем длину цепочки всем её элементам
                val chainLength = (i - chainStartIndex).toShort()
                for (j in chainStartIndex until i) {
                    slots[j].neighbours = chainLength
                }
                // Начинаем новую цепочку
                chainStartIndex = i
            }
        }
        // 3. Обрабатываем последнюю цепочку
        val lastChainLength = (slots.size - chainStartIndex).toShort()
        for (j in chainStartIndex until slots.size) {
            slots[j].neighbours = lastChainLength
        }
    }


    companion object {
        private val ACTIVE_BOOKED_STATUSES = setOf(
            BookedSlotStatus.BOOKED.name,
            BookedSlotStatus.PROCESSED.name
        )
    }
}
