package com.rera.mss.service

import com.rera.mss.model.Instrument
import com.rera.mss.model.InstrumentState
import com.rera.mss.model.MonitoredArea
import com.rera.mss.model.User
import com.rera.mss.payload.InsInvMonitoredAreaDto
import com.rera.mss.payload.InstrumentDto
import com.rera.mss.payload.InstrumentListDto
import com.rera.mss.repository.InstrumentRepository
import com.rera.mss.repository.MonitoredAreaRepository
import com.rera.mss.repository.UserRepository
import com.rera.mss.security.UserPrincipal
import org.springframework.stereotype.Component

@Component
class InstrumentServiceImpl(
        private val instrumentRepository: InstrumentRepository,
        private val userRepository: UserRepository,
        private val monitoredAreaRepository: MonitoredAreaRepository
) : InstrumentService {

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

        if (instrumentRepository.existsByGuid(dto.guid)) {
            return InstrumentServiceOperationResult.Error("Could not add new instrument since instrument with guid: ${dto.guid} already exists.", null)
        }

        user.addInstrument(Instrument(
                gpsCoordinates = dto.gpsCoordinates,
                guid = dto.guid,
                altitude = dto.altitude,
                tilt = dto.tilt,
                direction = dto.direction,
                calibrationValue = 1F,
                state = InstrumentState.CREATED,
                user = user
        ))

        return try {
            userRepository.save(user)
            val persistedInstrument = instrumentRepository.findByGuid(dto.guid)?:throw NoSuchElementException("Could not get instrument with guid '${dto.guid}' from db.")
            InstrumentServiceOperationResult.Success(InstrumentDto(
                    id = persistedInstrument.id,
                    state = InstrumentState.CREATED,
                    guid = persistedInstrument.guid,
                    gpsCoordinates = persistedInstrument.gpsCoordinates,
                    altitude = persistedInstrument.altitude,
                    tilt = persistedInstrument.tilt,
                    direction = persistedInstrument.direction,
                    calibrationValue = persistedInstrument.calibrationValue,
                    monitoredAreas = emptySet()
            ))

        } catch (e: Exception) {
            InstrumentServiceOperationResult.Error("Could not update user data", e)
        }


    }

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

    override fun getInstruments(currentUser: UserPrincipal): InstrumentServiceOperationResult<InstrumentListDto> {
        val user: User = try {
            userRepository.findUserById(currentUser.getId())?:throw NoSuchElementException()
        } catch (e: Exception) {
            return userNotFoundError(e)
        }
        return try {
            val userInstruments = instrumentRepository.findByUser(user)
            val instrumentList = userInstruments.map { i: Instrument ->
                InstrumentDto(
                        id = i.id,
                        state = i.state,
                        guid = i.guid,
                        gpsCoordinates = i.gpsCoordinates,
                        altitude = i.altitude,
                        tilt = i.tilt,
                        direction = i.direction,
                        calibrationValue = i.calibrationValue,
                        monitoredAreas = convertMonitoredAreasToDto(i.instrumentMonitoredAreas)
                )
            }
            val availableMonitoredAreas = getAvailableMonitoredAreas(user)
            InstrumentServiceOperationResult.Success(InstrumentListDto(instruments = instrumentList.toMutableSet(), availableMonitoredAreas = availableMonitoredAreas.toMutableSet()))
        } catch (e: Exception) {
            InstrumentServiceOperationResult.Error("Could not get instruments for user ${user.id}", e)
        }
    }

    override fun updateInstrument(currentUser: UserPrincipal, dto: InstrumentDto): InstrumentServiceOperationResult<InstrumentDto> {
        val instrumentToUpdate = try {
            instrumentRepository.findInstrumentById(dto.id)?:throw NoSuchElementException()  // TODO mely by se vrátit jen instrumenty pro daného uživatele
        } catch (e: Exception) {
            return InstrumentServiceOperationResult.Error("Could not update instrument with id: ${dto.id}, not found.", e)
        }

        // TODO handle if insturment with given guid exists

        try {
            instrumentToUpdate.gpsCoordinates = dto.gpsCoordinates
            instrumentToUpdate.guid = dto.guid
            instrumentToUpdate.altitude = dto.altitude
            instrumentToUpdate.tilt = dto.tilt
            instrumentToUpdate.direction = dto.direction
            instrumentToUpdate.state = dto.state
            instrumentToUpdate.calibrationValue = dto.calibrationValue

            updateMonitoredAreas(dto, instrumentToUpdate)


            val updatedInstrument = instrumentRepository.save(instrumentToUpdate)

            return InstrumentServiceOperationResult.Success(InstrumentDto(
                    id = updatedInstrument.id,
                    state = updatedInstrument.state,
                    guid = updatedInstrument.guid,
                    gpsCoordinates = updatedInstrument.gpsCoordinates,
                    altitude = updatedInstrument.altitude,
                    tilt = updatedInstrument.tilt,
                    direction = updatedInstrument.direction,
                    calibrationValue = updatedInstrument.calibrationValue,
                    monitoredAreas = convertMonitoredAreasToDto(updatedInstrument.instrumentMonitoredAreas)
            ))

        } catch (e: Exception) {
            return InstrumentServiceOperationResult.Error("Could not update instrument with id: ${dto.id}.", e)
        }
    }

    private fun updateMonitoredAreas(dto: InstrumentDto, instrumentToUpdate: Instrument) {
        val maFromDto = monitoredAreaRepository.findAllById(dto.monitoredAreas.map { ma -> ma.id })
        removeMontitoredAreasNotContainedInDto(instrumentToUpdate, maFromDto)
        maFromDto.forEach { ma -> if (!instrumentToUpdate.instrumentMonitoredAreas.contains(ma)) instrumentToUpdate.addMonitoredArea(ma) }
    }

    private fun removeMontitoredAreasNotContainedInDto(instrumentToUpdate: Instrument, maFromDto: List<MonitoredArea>) {
        instrumentToUpdate.instrumentMonitoredAreas.forEach { ma ->
            if (!maFromDto.contains(ma)) {
                val removedArea = instrumentToUpdate.removeMonitoredArea(ma)
                removedArea.instrument = null
                monitoredAreaRepository.save(removedArea)
            }
        }
    }

    private fun convertMonitoredAreasToDto(instrumentMonitoredAreas: Set<MonitoredArea>): MutableSet<InsInvMonitoredAreaDto> {
        return instrumentMonitoredAreas.map { ma: MonitoredArea ->
            InsInvMonitoredAreaDto(ma.id, ma.description)
        }.sortedBy { it.id }.toMutableSet()
    }

    private fun getAvailableMonitoredAreas(user: User): List<InsInvMonitoredAreaDto> {
        return monitoredAreaRepository.findByUser(user).filter { ma: MonitoredArea ->
            ma.instrument == null
        }.map { ma: MonitoredArea -> InsInvMonitoredAreaDto(ma.id, ma.description) }
    }

}