package dev.fyloz.trial.colorrecipesexplorer.service

import dev.fyloz.trial.colorrecipesexplorer.config.properties.MaterialTypeProperties
import dev.fyloz.trial.colorrecipesexplorer.exception.RestException
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityAlreadyExistsRestException
import dev.fyloz.trial.colorrecipesexplorer.model.MaterialType
import dev.fyloz.trial.colorrecipesexplorer.model.MaterialTypeSaveDto
import dev.fyloz.trial.colorrecipesexplorer.model.MaterialTypeUpdateDto
import dev.fyloz.trial.colorrecipesexplorer.model.materialType
import dev.fyloz.trial.colorrecipesexplorer.repository.MaterialTypeRepository
import io.jsonwebtoken.lang.Assert
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ResponseStatus

interface MaterialTypeService : NamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeRepository> {
    /** Checks if a material type with the given [prefix] exists. */
    fun existsByPrefix(prefix: String): Boolean

    /** Checks if the given [materialType] is used by one or more materials. */
    fun isUsedByMaterial(materialType: MaterialType): Boolean

    /** Gets all system material types. */
    fun getAllSystemTypes(): Collection<MaterialType>

    /** Gets all material types who are not a system type. */
    fun getAllNonSystemType(): Collection<MaterialType>

    /** Saves and update the system material types. */
    fun saveSystemTypes(systemTypeProperties: Collection<MaterialTypeProperties.MaterialTypeProperty>)
}

@Service
class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val materialService: MaterialService) :
        AbstractNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeRepository>(repository), MaterialTypeService {
    override fun existsByPrefix(prefix: String): Boolean = repository.existsByPrefix(prefix)
    override fun isUsedByMaterial(materialType: MaterialType): Boolean = materialService.existsByMaterialType(materialType)
    override fun getAllSystemTypes(): Collection<MaterialType> = repository.findAllBySystemTypeIs(true)
    override fun getAllNonSystemType(): Collection<MaterialType> = repository.findAllBySystemTypeIs(false)

    override fun save(entity: MaterialType): MaterialType {
        if (existsByPrefix(entity.prefix))
            throw EntityAlreadyExistsRestException(entity.prefix)
        return super.save(entity)
    }

    override fun update(entity: MaterialType): MaterialType {
        Assert.notNull(entity.id, "MaterialTypeService.update() was called with a null identifier")
        val persistedMaterialType = getById(entity.id!!)
        with(repository.findByPrefix(entity.prefix)) {
            if (this != null && id != entity.id)
                throw EntityAlreadyExistsRestException(entity.prefix)
        }

        return super.update(with(entity) {
            MaterialType(
                    id = id,
                    name = if (name.isNotBlank()) name else persistedMaterialType.name,
                    prefix = if (prefix.isNotBlank()) prefix else persistedMaterialType.prefix
            )
        })
    }

    override fun delete(entity: MaterialType) {
        if (isUsedByMaterial(entity))
            throw CannotDeleteUsedMaterialTypeRestException()

        super.delete(entity)
    }

    override fun saveSystemTypes(systemTypeProperties: Collection<MaterialTypeProperties.MaterialTypeProperty>) {
        val systemTypes = systemTypeProperties.map { it.toMaterialType() }
        val oldSystemTypes = getAllSystemTypes().toMutableSet()

        fun saveOrUpdateSystemType(type: MaterialType) {
            if (existsByName(type.name)) {
                val persistedMaterialType = getByName(type.name)
                update(materialType(type, newId = persistedMaterialType.id))
            } else {
                save(type)
            }
        }

        // Save new system types
        systemTypes.forEach {
            saveOrUpdateSystemType(it)
            oldSystemTypes.removeIf { c -> c.name == it.name }
        }

        // Remove old system types
        oldSystemTypes.forEach { update(materialType(it, newSystemType = false)) }
    }
}

@ResponseStatus(HttpStatus.CONFLICT)
class CannotDeleteUsedMaterialTypeRestException : RestException(HttpStatus.CONFLICT) {
    override val exceptionMessage: String = "Cannot delete a used material type"

    override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {}
}
