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.model.validation.isNotNullAndNotBlank
import dev.fyloz.trial.colorrecipesexplorer.repository.MaterialTypeRepository
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ResponseStatus
import kotlin.contracts.ExperimentalContracts

interface MaterialTypeService :
    ExternalNamedModelService<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) :
    AbstractExternalNamedModelService<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<AbstractExternalNamedModelService>.save(entity)
    }

    @ExperimentalContracts
    override fun update(entity: MaterialTypeUpdateDto): MaterialType {
        val persistedMaterialType by lazy { getById(entity.id) }

        return update(with(entity) {
            MaterialType(
                id = id,
                name = if (isNotNullAndNotBlank(name)) name else persistedMaterialType.name,
                prefix = if (isNotNullAndNotBlank(prefix)) prefix else persistedMaterialType.prefix,
                systemType = false
            )
        })
    }

    override fun update(entity: MaterialType): MaterialType {
        with(repository.findByPrefix(entity.prefix)) {
            if (this != null && id != entity.id)
                throw EntityAlreadyExistsRestException(entity.prefix)
        }

        return super<AbstractExternalNamedModelService>.update(entity)
    }

    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, newSystemType = true))
            } 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() {}
}
