package com.rera.mss.service

import com.rera.mss.model.MonitoredArea
import com.rera.mss.model.PvPanel
import com.rera.mss.model.PvPanelArea
import com.rera.mss.model.User
import com.rera.mss.payload.MonitoredAreaDto
import com.rera.mss.payload.MonitoredAreaListDto
import com.rera.mss.payload.PvAreaDto
import com.rera.mss.payload.PvPanelDto
import com.rera.mss.repository.*
import com.rera.mss.security.UserPrincipal
import org.springframework.stereotype.Component

@Component
class MonitoredAreaServiceImpl(
        private val userRepository: UserRepository,
        private val instrumentRepository: InstrumentRepository,
        private val inverterRepository: InverterRepository,
        private val monitoredAreaRepository: MonitoredAreaRepository,
        private val pvPanelRepository: PvPanelRepository,
        private val pvAreaRepository: PvAreaRepository
) : MonitoredAreaService {
    override fun addMonitoredArea(currentUser: UserPrincipal, dto: MonitoredAreaDto): MonitoredAreaServiceOperationResult<MonitoredAreaDto> {
        val user: User = try {
            userRepository.findUserById(currentUser.getId())?:throw NoSuchElementException()
        } catch (e: Exception) {
            return userNotFoundError(e)
        }

        val monitoredArea = MonitoredArea(
                description = dto.description,
                instrument = null,
                inverter = null,
                user = user
        )


        return try {
            val savedMa = monitoredAreaRepository.save(monitoredArea)
            MonitoredAreaServiceOperationResult.Success(MonitoredAreaDto(
                    id = savedMa.id,
                    description = savedMa.description,
                    instrumentId = savedMa.instrument?.id,
                    inverterId = savedMa.inverter?.id,
                    pvAreas = emptySet()
            ))
        } catch (e: Exception) {
            MonitoredAreaServiceOperationResult.Error("Could not create new monitored area", e)
        }
    }

    override fun getMonitoredAreas(currentUser: UserPrincipal): MonitoredAreaServiceOperationResult<MonitoredAreaListDto> {
        val user: User = try {
            userRepository.findUserById(currentUser.getId())?:throw NoSuchElementException()
        } catch (e: Exception) {
            return userNotFoundError(e)
        }

        return try {
            val userMas = monitoredAreaRepository.findByUser(user)
            val maList = userMas.map { ma: MonitoredArea ->
                MonitoredAreaDto(
                        id = ma.id,
                        description = ma.description,
                        instrumentId = ma.instrument?.id,
                        inverterId = ma.inverter?.id,
                        pvAreas = convertPvAreasToDto(ma.monitoredAreaPvPanelAreas)
                )
            }.sortedBy { it.id }
            val availablePvPanels = pvPanelRepository.findAll().map { p: PvPanel ->
                PvPanelDto(
                        p.id,
                        p.material + p.efficiency,
                        p.efficiency,
                        p.manufacturer,
                        p.material
                )
            }
            return MonitoredAreaServiceOperationResult.Success(MonitoredAreaListDto(monitoredAreas = maList.toMutableSet(), availablePvPanels = availablePvPanels.toMutableSet()))
        } catch (e: Exception) {
            MonitoredAreaServiceOperationResult.Error("Could not get monitored areas for user ${user.id}", e)
        }
    }

    override fun updateMonitoredArea(currentUser: UserPrincipal, dto: MonitoredAreaDto): MonitoredAreaServiceOperationResult<MonitoredAreaDto> {
        val user: User = try {
            userRepository.findUserById(currentUser.getId())?:throw NoSuchElementException()
        } catch (e: Exception) {
            return userNotFoundError(e)
        }

        val maToUpdate = monitoredAreaRepository.findByIdAndUser(dto.id, user)
                ?: return MonitoredAreaServiceOperationResult.Error("Could not update monitored area with id: ${dto.id}, not found.", null)


        try {
            maToUpdate.description = dto.description
            updatePvAreas(dto, maToUpdate)


            val updatedMa = monitoredAreaRepository.save(maToUpdate)

            return MonitoredAreaServiceOperationResult.Success(
                    MonitoredAreaDto(
                            id = updatedMa.id,
                            description = updatedMa.description,
                            inverterId = updatedMa.inverter?.id,
                            instrumentId = updatedMa.instrument?.id,
                            pvAreas = convertPvAreasToDto(updatedMa.monitoredAreaPvPanelAreas)
                    )
            )
        } catch (e: Exception) {
            return MonitoredAreaServiceOperationResult.Error("Could not update monitored area with id: ${dto.id}.", e)
        }
    }

    private fun updatePvAreas(dto: MonitoredAreaDto, maToUpdate: MonitoredArea) {
        addNewPvAreas(dto, updatePvAreasAccordingToDto(maToUpdate, dto))
    }

    private fun addNewPvAreas(dto: MonitoredAreaDto, maToUpdate: MonitoredArea) {
        dto.pvAreas.filter { it.id == 0L }.forEach { pv ->
            val pvPanel = pv.pvPanel
            maToUpdate.addPvPanelArea(PvPanelArea(
                    area = pv.area,
                    altitude = pv.altitude,
                    tilt = pv.tilt,
                    direction = pv.direction,
                    monitoredArea = maToUpdate,
                    pvPanel = if (pvPanel != null) pvPanelRepository.getOne(pvPanel.id) else null
            ))
        }
    }

    private fun updatePvAreasAccordingToDto(monitoredAreaToUpdate: MonitoredArea, dto: MonitoredAreaDto): MonitoredArea {
        monitoredAreaToUpdate.monitoredAreaPvPanelAreas.forEach { pv ->
            val pvAreaContainedInDto = dto.pvAreas.find { it.id == pv.id }
            if (pvAreaContainedInDto!=null) {
                updateExistingPvAreaInMaToUpdate(pv, pvAreaContainedInDto)
            } else {
                monitoredAreaToUpdate.removePvPanelArea(pv)
            }
        }
        return monitoredAreaToUpdate
    }

    private fun updateExistingPvAreaInMaToUpdate(pv: PvPanelArea, pvAreaContainedInDto:PvAreaDto) {
        pv.altitude = pvAreaContainedInDto.altitude
        pv.area = pvAreaContainedInDto.area
        pv.direction = pvAreaContainedInDto.direction
        pv.pvPanel = if (pvAreaContainedInDto.pvPanel != null) pvPanelRepository.getOne(pvAreaContainedInDto.pvPanel.id) else null
    }


    private fun userNotFoundError(e: Exception) =
            MonitoredAreaServiceOperationResult.Error("Did not find user by id, could not therefore get any user instruments.", e)

    private fun convertPvAreasToDto(monitoredAreasPvAreas: Set<PvPanelArea>): MutableSet<PvAreaDto> {
        return monitoredAreasPvAreas.map { pa: PvPanelArea ->
            val pvPanelDto = pa.pvPanel
            PvAreaDto(
                    id = pa.id,
                    area = pa.area,
                    altitude = pa.altitude,
                    tilt = pa.tilt,
                    direction = pa.direction,
                    pvPanel = if (pvPanelDto == null) null else PvPanelDto(
                            id = pvPanelDto.id,
                            code = pvPanelDto.material + pvPanelDto.efficiency,
                            efficiency = pvPanelDto.efficiency,
                            manufacturer = pvPanelDto.manufacturer,
                            material = pvPanelDto.material

                    )

            )
        }.sortedBy { it.id }.toMutableSet()
    }

}