package com.rera.mss.service

import com.rera.mss.exceptions.NoInstrumentWithGivenIdException
import com.rera.mss.exceptions.WrongMeasurementFormatException
import com.rera.mss.measurement.computingBase.Computer
import com.rera.mss.measurement.inputDataParser.DataParserService
import com.rera.mss.model.*
import com.rera.mss.payload.*
import com.rera.mss.repository.*
import com.rera.mss.security.UserPrincipal
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component

@Component
class MeasurementServiceImpl(
        private val dataParser: DataParserService,
        private val instrumentMeasurementRepository: InstrumentMeasurementRepository,
        private val inverterMeasurementRepository: InverterMeasurementRepository,
        private val instrumentRepository: InstrumentRepository,
        private val inverterRepository: InverterRepository,
        private val readingTypeRepository: ReadingTypeRepository,
        private val userRepository: UserRepository,
        private val computer: Computer

) : MeasurementService {
    private val logger = LoggerFactory.getLogger(javaClass)

    override fun logMeasurement(measurement: MssMeasurementDto) {
        logger.info("going to log measurement: $measurement")

        val parsedResult = dataParser.parseInstrumentMeasurement(measurement)


        val instrument = instrumentRepository.findByGuid(parsedResult.guid)
                ?: throw NoInstrumentWithGivenIdException("Could not get instrument with guid '${parsedResult.guid}' from db.")

        if(instrument.state === InstrumentState.MEASURING){
            parsedResult.measurements.map {
                val readingType = readingTypeRepository.findByName(it.key)
                        ?: throw WrongMeasurementFormatException("Could not get reading type with name '${it.key}' from db.")
                instrumentMeasurementRepository.save(
                        InstrumentMeasurement(
                                instrument = instrument,
                                readingType = readingType,
                                readingValue = it.value
                        )
                )
            }
        }
    }

    override fun getMeasurementsOptions(currentUser: UserPrincipal): MeasurementPickerDto {
        val user = userRepository.findUserById(currentUser.getId())
                ?: throw NoSuchElementException()// TODO unify exception handling
        val instruments = instrumentRepository.findByUser(user)
        val inverters = inverterRepository.findByUser(user)

        val connectedInverters = inverters.filter { it.inverterMonitoredAreas.size != 0 }
        val (standaloneInstruments, connectedInstruments) = instruments.partition { it.instrumentMonitoredAreas.size == 0 }

        val (inverterConnectedInstruments, inverterNotConnectedInstruments) = separateConnectedAndNotConnectedInstrumentsInvertersViaMas(
                connectedInverters,
                connectedInstruments
        )

        inverterNotConnectedInstruments.addAll(standaloneInstruments)
        return MeasurementPickerDto(
                withInverter = transformInverterConnectedInstrumentsToDto(inverterConnectedInstruments),
                withoutInverter = transformInverterNotConnectedInstrumentsToDto(inverterNotConnectedInstruments)
        )
    }

    override fun getMeasurementData(currentUser: UserPrincipal, optionsDto: PickedMeasurementsDto): MeasurementDataDto {
        val user = userRepository.findUserById(currentUser.getId()) ?: throw NoSuchElementException()
        return when {
            optionsDto.pickedInverterId != null -> {
                getInverterConnectedData(user, optionsDto.pickedInverterId)
            }
            optionsDto.pickedInstrumentIds != null -> {
                getInverterNotConnectedData(user, optionsDto.pickedInstrumentIds)
            }
            else -> {
                throw NoSuchElementException("Could not load data for picked instrument/inverter ids.")
            }
        }
    }

    private fun transformInverterNotConnectedInstrumentsToDto(inverterNotConnectedInstruments: MutableSet<Instrument>): Set<MeasurementInstrumentDto>? {
        return if (inverterNotConnectedInstruments.isNotEmpty()) {
            inverterNotConnectedInstruments.map { ins ->
                transformMonitoredAreasToDto(ins)
            }.toSet()
        } else {
            return null
        }
    }

    private fun transformInverterConnectedInstrumentsToDto(inverterConnectedInstruments: MutableMap<Inverter, Set<Instrument>>): Set<InverterConnectedInstrumentsDto>? {
        return if (inverterConnectedInstruments.isNotEmpty()) {
            inverterConnectedInstruments.map {
                InverterConnectedInstrumentsDto(
                        inverterId = it.key.id,
                        instruments = it.value.map { ins ->
                            transformMonitoredAreasToDto(ins)
                        }.toSet()
                )
            }.toSet()
        } else {
            return null
        }
    }

    private fun transformMonitoredAreasToDto(ins: Instrument): MeasurementInstrumentDto {
        return MeasurementInstrumentDto(
                id = ins.id,
                monitoredAreas = ins.instrumentMonitoredAreas.map { ma ->
                    MeasurementMonitoredAreaDto(
                            id = ma.id,
                            description = ma.description
                    )
                }.toSet()
        )
    }


    private fun separateConnectedAndNotConnectedInstrumentsInvertersViaMas(connectedInverters: List<Inverter>, connectedInstruments: List<Instrument>): Pair<MutableMap<Inverter, Set<Instrument>>, MutableSet<Instrument>> {
        val inverterConnectedInstruments = mutableMapOf<Inverter, Set<Instrument>>()
        val inverterNotConnectedInstruments = connectedInstruments.toMutableSet()
        connectedInverters.forEach { inv ->
            val inverterMonitoredAreas = inv.inverterMonitoredAreas
            val mappedInstruments = inverterMonitoredAreas.mapNotNull { ma ->
                connectedInstruments.find { ins -> ins.instrumentMonitoredAreas.contains(ma) }
            }.toSet()

            if (mappedInstruments.isNotEmpty() && everyInverterMaIsConnectedToSomeInstrument(mappedInstruments, inverterMonitoredAreas)) inverterConnectedInstruments[inv] = mappedInstruments
                    .also { inverterNotConnectedInstruments.removeAll(it) }
        }
        return inverterConnectedInstruments to inverterNotConnectedInstruments
    }

    private fun everyInverterMaIsConnectedToSomeInstrument(mappedInstruments: Set<Instrument>, inverterMonitoredAreas: MutableSet<MonitoredArea>) =
            inverterMonitoredAreas == mappedInstruments.flatMap { it.instrumentMonitoredAreas }.toSet()


    private fun getInverterNotConnectedData(user: User, instrumentIds: Set<Long>): MeasurementDataDto {
        // TODO user should be able only to list data from his sensors/inverters
        val instruments = instrumentRepository.findAllById(instrumentIds)
        val instrumentIrradianceAndEnergy = computeIrradianceAndEnergy(instruments)

        val theoreticalEnergy = if (instrumentIrradianceAndEnergy != null) {
            computeTheoreticalEnergy(instrumentIrradianceAndEnergy)
        } else null
        return mapComputedValuesToDto(instrumentIrradianceAndEnergy, theoreticalEnergy)
    }

    private fun mapComputedValuesToDto(instrumentIrradianceAndEnergy: Map<Instrument, IrradianceAndEnergy>?, theoreticalEnergy: Map<Long, List<Pair<Long, Float>>>? = null, realEnergy: Pair<Inverter, List<Pair<Long, Float>>>? = null, performanceRatio: Pair<Inverter, List<Pair<Long, Float>>>? = null): MeasurementDataDto {
        return MeasurementDataDto(
                instrumentEnergyAndIrradiance = if (instrumentIrradianceAndEnergy != null) mapInstrumentEnergyAndIrradianceToDto(instrumentIrradianceAndEnergy) else null,
                sumEnergy = if (instrumentIrradianceAndEnergy != null && theoreticalEnergy != null) mapSumEnergyToDto(theoreticalEnergy, realEnergy, instrumentIrradianceAndEnergy) else null,
                performanceRatio = if (instrumentIrradianceAndEnergy != null && performanceRatio != null) mapPerformanceRatioToDto(performanceRatio, instrumentIrradianceAndEnergy) else null
        )
    }

    private fun mapInstrumentEnergyAndIrradianceToDto(instrumentIrradianceAndEnergy: Map<Instrument, IrradianceAndEnergy>): List<EnergyAndIrradianceDataDto> {
        return instrumentIrradianceAndEnergy.map { irradianceAndEnergy ->
            val time = irradianceAndEnergy.value.time
            EnergyAndIrradianceDataDto(
                    instrumentId = irradianceAndEnergy.key.id,
                    minT = time.first(),
                    maxT = time.last(),
                    irradiance = time.mapIndexed { index, t ->
                        DataPointDto(time = t, value = irradianceAndEnergy.value.irradiance[index])
                    },
                    monitoredAreaEnergy = irradianceAndEnergy.value.monitoredAreaEnergies.map { mae ->
                        MonitoredAreaEnergyDto(
                                monitoredAreaId = mae.key.id,
                                graphData = time.mapIndexed { index, t ->
                                    DataPointDto(time = t, value = mae.value[index])
                                }
                        )
                    }
            )
        }
    }

    private fun mapPerformanceRatioToDto(performanceRatio: Pair<Inverter, List<Pair<Long, Float>>>, instrumentIrradianceAndEnergy: Map<Instrument, IrradianceAndEnergy>): PerformanceRatioDataDto {
        return PerformanceRatioDataDto(
                minT = performanceRatio.second.unzip().first.first(),
                maxT = performanceRatio.second.unzip().first.last(),
                inverterId = performanceRatio.first.id,
                instrumentIds = instrumentIrradianceAndEnergy.keys.map { it.id }.toSet(),
                monitoredAreas = instrumentIrradianceAndEnergy.values.flatMap { it.monitoredAreaEnergies.keys.map { MeasurementMonitoredAreaDto(id = it.id, description = it.description) } }.toSet(),
                graphData = mapComputedDataToListOfDataPoints(performanceRatio.second)
        )
    }

    private fun mapSumEnergyToDto(theoreticalEnergy: Map<Long, List<Pair<Long, Float>>>, realEnergy: Pair<Inverter, List<Pair<Long, Float>>>?, instrumentIrradianceAndEnergy: Map<Instrument, IrradianceAndEnergy>): SumEnergyDataDto {
        return SumEnergyDataDto(
                minT = theoreticalEnergy.values.first().first().first,
                maxT = theoreticalEnergy.values.first().last().first,
                realEnergy = if (realEnergy != null) mapRealEnergyToDto(realEnergy) else null,
                theoreticalEnergy = theoreticalEnergy.map { te ->
                    InstrumentEnergyDto(
                            instrumentId = te.key,
                            graphData = mapComputedDataToListOfDataPoints(te.value)
                    )
                }
        )
    }

    private fun mapRealEnergyToDto(realEnergy: Pair<Inverter, List<Pair<Long, Float>>>): InverterEnergyDto {
        return InverterEnergyDto(
                inverterId = realEnergy.first.id,
                graphData = mapComputedDataToListOfDataPoints(realEnergy.second)
        )
    }

    private fun mapComputedDataToListOfDataPoints(computedData: List<Pair<Long, Float>>): List<DataPointDto> {
        return computedData.map {
            DataPointDto(
                    time = it.first,
                    value = it.second
            )
        }
    }

    private fun computeIrradianceAndEnergy(instruments: List<Instrument>): Map<Instrument, IrradianceAndEnergy>? {
        return instruments.associate { instrument ->
            val irradianceInTime = readIrradianceMeasurementData(instrument) ?: return null
            val monitoredAreaEnergies = computeMonitoredAreasEnergies(instrument, irradianceInTime)
            val (time, irradiance) = irradianceInTime.unzip()
            return@associate instrument to IrradianceAndEnergy(
                    time = time,
                    irradiance = irradiance,
                    monitoredAreaEnergies = monitoredAreaEnergies
            )
        }
    }

    private fun computeTheoreticalEnergy(instrumentIrradianceAndEnergy: Map<Instrument, IrradianceAndEnergy>, realEnergyTimeBase: List<Long>? = null): Map<Long, List<Pair<Long, Float>>>? {
        val step = 6 * 600000L // step is equal to timestamp change in ms
        val bounds = computer.computeBoundsOfTimeInterval(instrumentIrradianceAndEnergy, realEnergyTimeBase)
        return if (bounds != null) {
            computer.computeInstrumentsEnergyWithUnifiedTimeBase(instrumentIrradianceAndEnergy, step, bounds)
        } else return null
    }

    private fun readIrradianceMeasurementData(instrument: Instrument): List<Pair<Long, Float>>? {
        val measurements = instrumentMeasurementRepository.findAllByInstrumentAndReadingTypeName(instrument, "mss_si")
        return if (!measurements.isNullOrEmpty()) {
            measurements.map { measurement ->
                (measurement.measuredAt to computeCalibratedValue(measurement.readingValue, instrument))
            }
        } else null
    }

    private fun computeCalibratedValue(rawValue: Float, instrument: Instrument) =
            rawValue * instrument.calibrationValue

    private fun readEnergyMeasurementData(inverter: Inverter): List<Pair<Long, Float>>? {
        val measurements = inverterMeasurementRepository.findAllByInverterAndReadingTypeName(inverter, "inv")
        return if (!measurements.isNullOrEmpty()) {
            measurements.map { measurement ->
                measurement.measuredAt to measurement.readingValue
            }
        } else null
    }

    private fun computeMonitoredAreasEnergies(instrument: Instrument, irradianceInTime: List<Pair<Long, Float>>): Map<MonitoredArea, List<Float>> {
        var storedMaEnergy: List<Float> = emptyList()
        return instrument.instrumentMonitoredAreas.associateWith { ma ->
            storedMaEnergy = computer.computeEnergyFromIrradiance(ma, irradianceInTime, storedMaEnergy)
            storedMaEnergy
        }
    }


    private fun getInverterConnectedData(user: User, inverterId: Long): MeasurementDataDto {
        val instruments = instrumentRepository.findByUser(user)
        val inverters = inverterRepository.findByUser(user)

        val connectedInverters = inverters.filter { it.inverterMonitoredAreas.size != 0 }
        val connectedInstruments = instruments.filter { it.instrumentMonitoredAreas.size != 0 }

        val (inverterConnectedInstruments, _) = separateConnectedAndNotConnectedInstrumentsInvertersViaMas(
                connectedInverters,
                connectedInstruments
        )

        val pickedInverter = inverters.first { it.id == inverterId }
        val instrumentIrradianceAndEnergy = computeIrradianceAndEnergy(inverterConnectedInstruments[pickedInverter]?.toList()
                ?: throw NoSuchElementException("Did not find any instruments for provided inverter id"))

        val realEnergyData = readEnergyMeasurementData(pickedInverter)

        val theoreticalEnergy =
                if (instrumentIrradianceAndEnergy != null) {
                    computeTheoreticalEnergy(instrumentIrradianceAndEnergy, realEnergyData?.unzip()?.first)
                } else null

        val realEnergy = if (theoreticalEnergy != null && realEnergyData != null) {
            pickedInverter to realEnergyData.filter { it.first >= theoreticalEnergy.values.first().first().first && it.first <= theoreticalEnergy.values.first().last().first }
        } else null

        val performanceRatio = if (theoreticalEnergy != null && realEnergy != null) {
            pickedInverter to computer.computePerformanceRatio(theoreticalEnergy, realEnergy.second)
        } else null


        return mapComputedValuesToDto(instrumentIrradianceAndEnergy, theoreticalEnergy, realEnergy, performanceRatio)
    }
}

data class IrradianceAndEnergy(
        val time: List<Long>,
        val irradiance: List<Float>,
        val monitoredAreaEnergies: Map<MonitoredArea, List<Float>>
)